Estou tentando ver se há uma maneira de enganar o SQL Server para usar um determinado plano para a consulta.
1. Ambiente
Imagine que você tem alguns dados que são compartilhados entre diferentes processos. Então, suponha que tenhamos alguns resultados de experimentos que ocupem muito espaço. Então, para cada processo, sabemos qual ano / mês de resultado do experimento queremos usar.
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
Agora, para cada processo, temos parâmetros salvos na tabela
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. Dados de teste
Vamos adicionar alguns dados de teste:
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. Buscando resultados
Agora, é muito fácil obter resultados da experiência @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
O plano é bom e paralelo:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
consulta 0 plano
4. Problema
Mas, para tornar o uso dos dados um pouco mais genérico, quero ter outra função - dbo.f_GetSharedDataBySession(@session_id int)
. Portanto, a maneira direta seria criar funções escalares, traduzindo @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
E agora podemos criar nossa função:
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
plano de consulta 1
O plano é o mesmo, exceto que, é claro, não é paralelo, porque funções escalares que executam acesso a dados tornam todo o plano serial .
Então, eu tentei várias abordagens diferentes, como usar subconsultas em vez de funções escalares:
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
plano de consulta 2
Ou usando 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
plano de consulta 3
Mas não consigo encontrar uma maneira de escrever essa consulta tão boa quanto a que utiliza funções escalares.
Par de pensamentos:
- Basicamente, o que eu gostaria é poder, de alguma forma, dizer ao SQL Server para pré-calcular certos valores e depois passá-los como constantes.
- O que poderia ser útil é se tivéssemos alguma dica de materialização intermediária . Eu verifiquei algumas variantes (TVF de múltiplas instruções ou cte com top), mas nenhum plano é tão bom quanto o que tem funções escalares até agora
- Eu sei sobre as próximas melhorias do SQL Server 2017 - Froid: Otimização de programas imperativos em um banco de dados relacional . Não tenho certeza se isso ajudará. Seria bom se provar errado aqui, no entanto.
Informação adicional
Estou usando uma função (em vez de selecionar dados diretamente das tabelas) porque é muito mais fácil usar em muitas consultas diferentes, que geralmente têm @session_id
como parâmetro.
Me pediram para comparar os tempos de execução reais. Nesse caso em particular
- a consulta 0 é executada por ~ 500ms
- a consulta 1 é executada por ~ 1500ms
- a consulta 2 é executada por ~ 1500ms
- a consulta 3 é executada por ~ 2000ms.
O plano 2 tem uma varredura de índice em vez de uma busca, que é filtrada por predicados em loops aninhados. O plano 3 não é tão ruim assim, mas ainda faz mais trabalho e funciona mais devagar que o plano 0.
Vamos supor que isso dbo.Params
seja alterado raramente e geralmente tenha de 1 a 200 linhas, não mais do que, digamos, 2000. Agora são 10 colunas e não espero adicionar colunas com muita frequência.
O número de linhas no Params não é fixo, portanto, para cada @session_id
uma delas, haverá uma linha. O número de colunas não é fixo, é um dos motivos pelos quais não desejo ligar dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
de todos os lugares, para que eu possa adicionar nova coluna a essa consulta internamente. Eu ficaria feliz em ouvir quaisquer opiniões / sugestões sobre isso, mesmo que haja algumas restrições.