Я намагаюся зрозуміти, чи існує спосіб, як обдурити SQL Server, щоб використовувати певний план для запиту.
1. Навколишнє середовище
Уявіть, що у вас є деякі дані, якими можна ділитися між різними процесами. Отже, припустимо, у нас є деякі результати експериментів, які займають багато місця. Потім для кожного процесу ми знаємо, який рік / місяць результату експерименту ми хочемо використати.
if object_id('dbo.SharedData') is not null
drop table SharedData
create table dbo.SharedData (
experiment_year int,
experiment_month int,
rn int,
calculated_number int,
primary key (experiment_year, experiment_month, rn)
)
go
Тепер для кожного процесу ми маємо параметри, збережені в таблиці
if object_id('dbo.Params') is not null
drop table dbo.Params
create table dbo.Params (
session_id int,
experiment_year int,
experiment_month int,
primary key (session_id)
)
go
2. Дані тесту
Додамо кілька тестових даних:
insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4
go
insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
cross join master.dbo.spt_values as v2
go
insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
cross join master.dbo.spt_values as v2
go
3. Отримати результати
Зараз отримати результати експерименту дуже просто @experiment_year/@experiment_month
:
create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.SharedData as d
where
d.experiment_year = @experiment_year and
d.experiment_month = @experiment_month
)
go
План хороший і паралельний:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
запит 0 план
4. Проблема
Але, щоб зробити використання даних трохи більш загальним, я хочу мати ще одну функцію - dbo.f_GetSharedDataBySession(@session_id int)
. Отже, прямим способом було б створення скалярних функцій, перекладаючи @session_id
-> @experiment_year/@experiment_month
:
create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
return (
select
p.experiment_year
from dbo.Params as p
where
p.session_id = @session_id
)
end
go
create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
return (
select
p.experiment_month
from dbo.Params as p
where
p.session_id = @session_id
)
end
go
І тепер ми можемо створити свою функцію:
create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.f_GetSharedData(
dbo.fn_GetExperimentYear(@session_id),
dbo.fn_GetExperimentMonth(@session_id)
) as d
)
go
запит 1 план
План такий же, за винятком того, що він, звичайно, не паралельний, оскільки скалярні функції, що виконують доступ до даних, роблять весь план послідовним .
Тому я спробував кілька різних підходів, наприклад, використовуючи підзапити замість скалярних функцій:
create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.f_GetSharedData(
(select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
(select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
) as d
)
go
запит 2 план
Або за допомогою cross apply
create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.Params as p
cross apply dbo.f_GetSharedData(
p.experiment_year,
p.experiment_month
) as d
where
p.session_id = @session_id
)
go
запит 3 плану
Але я не можу знайти спосіб написати цей запит настільки ж хорошим, як той, що використовує скалярні функції.
Пара думок:
- В основному, я хотів би мати можливість якось сказати SQL Server попередньо обчислити певні значення, а потім передати їх далі як константи.
- Що може бути корисним, якби у нас був якийсь проміжний натяк на матеріалізацію . Я перевірив декілька варіантів (багатосказаний TVF або cte with top), але жоден план не такий хороший, як той, який має скалярні функції досі
- Я знаю про майбутнє вдосконалення SQL Server 2017 - Froid: Оптимізація імперативних програм у реляційній базі даних. Я не впевнений, що це допоможе. Хоча було б, якби тут було доведено неправильно.
Додаткова інформація
Я використовую функцію (замість того, щоб вибирати дані безпосередньо з таблиць), оскільки це набагато простіше використовувати у багатьох різних запитах, які зазвичай мають @session_id
параметр.
Мене попросили порівняти фактичний час виконання. У цьому конкретному випадку
- запит 0 працює за ~ 500 мс
- запит 1 працює на ~ 1500 мс
- запит 2 працює за ~ 1500 мс
- запит 3 працює для ~ 2000ms.
План №2 замість пошуку шукає індексне сканування, яке потім фільтрується предикатами в вкладені петлі. План №3 не так вже й поганий, але все ж робить більше роботи і працює повільніше, ніж план №0.
Припустимо, що dbo.Params
вона змінюється рідко і зазвичай має близько 1-200 рядів, не більше ніж, скажімо, 2000 року колись очікується. Зараз це близько 10 стовпців, і я не очікую занадто часто додавати стовпці.
Кількість рядків у Парамах не фіксована, тому для кожного @session_id
буде ряд. Кількість стовпців там не виправлена, це одна з причин, я не хочу телефонувати dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
звідусіль, тому я можу внутрішньо додати новий стовпець до цього запиту. Буду радий почути будь-яку думку / пропозицію з цього приводу, навіть якщо це має деякі обмеження.