Я пытаюсь увидеть, есть ли способ обмануть 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 с верхом), но ни один план пока не так хорош, как план со скалярными функциями.
- Я знаю о предстоящем улучшении SQL Server 2017 - Froid: оптимизация императивных программ в реляционной базе данных. Однако я не уверен, что это поможет. Хотя было бы неплохо оказаться здесь неправым.
Дополнительная информация
Я использую функцию (а не выбираю данные непосредственно из таблиц), потому что ее гораздо проще использовать во многих различных запросах, которые обычно имеют @session_id
параметр.
Меня попросили сравнить фактическое время выполнения. В этом конкретном случае
- запрос 0 выполняется в течение ~ 500 мс
- запрос 1 выполняется в течение ~ 1500 мс
- запрос 2 выполняется в течение ~ 1500 мс
- запрос 3 выполняется в течение ~ 2000 мс.
План № 2 имеет поиск индекса вместо поиска, который затем фильтруется по предикатам во вложенных циклах. План № 3 не так уж и плох, но все же выполняет больше работы и работает медленнее, чем план № 0.
Давайте предположим, что dbo.Params
это изменяется редко, и обычно имеет около 1-200 строк, не больше, скажем, 2000 когда-либо ожидается. Сейчас это около 10 столбцов, и я не собираюсь добавлять столбцы слишком часто.
Количество строк в Params не фиксировано, поэтому для каждого @session_id
будет ряд. Количество столбцов там не фиксировано, это одна из причин, по которой я не хочу звонить dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
откуда угодно , поэтому я могу добавить новый столбец к этому запросу внутри страны. Я был бы рад услышать любые мнения / предложения по этому вопросу, даже если у него есть некоторые ограничения.