Estoy tratando de ver si hay una manera de engañar a SQL Server para que use un cierto plan para la consulta.
1. Medio ambiente
Imagine que tiene algunos datos que se comparten entre diferentes procesos. Entonces, supongamos que tenemos algunos resultados de experimentos que ocupan mucho espacio. Luego, para cada proceso, sabemos qué año / mes de resultado del 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
Ahora, para cada proceso tenemos parámetros guardados en la tabla
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. Datos de prueba
Agreguemos algunos datos de prueba:
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. Obteniendo resultados
Ahora, es muy fácil obtener resultados de experimentos al @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
El plan es agradable y paralelo:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
consulta 0 plan
4. problema
Pero, para hacer uso de los datos un poco más genérico, quiero tener otra función - dbo.f_GetSharedDataBySession(@session_id int)
. Entonces, la forma más sencilla sería crear funciones escalares, traduciendo @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
Y ahora podemos crear nuestra función:
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
consulta 1 plan
El plan es el mismo, excepto que, por supuesto, no es paralelo, porque las funciones escalares que realizan el acceso a datos hacen que todo el plan sea serial .
Así que probé varios enfoques diferentes, como usar subconsultas en lugar de funciones 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
consulta 2 plan
O 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
consulta 3 plan
Pero no puedo encontrar una manera de escribir esta consulta para que sea tan buena como la que usa funciones escalares.
Par de pensamientos:
- Básicamente, lo que quiero es poder decirle de alguna manera a SQL Server que precalcule ciertos valores y luego los pase más como constantes.
- Lo que podría ser útil es si tuviéramos alguna pista de materialización intermedia . He comprobado un par de variantes (TVF multi-declaración o cte con top), pero hasta ahora ningún plan es tan bueno como el que tiene funciones escalares
- Sé acerca de la próxima mejora de SQL Server 2017 - Froid: optimización de programas imperativos en una base de datos relacional. Sin embargo, no estoy seguro de que ayude. Sin embargo, hubiera sido bueno que se demuestre lo contrario aquí.
Información Adicional
Estoy usando una función (en lugar de seleccionar datos directamente de las tablas) porque es mucho más fácil de usar en muchas consultas diferentes, que generalmente tienen @session_id
como parámetro.
Me pidieron que comparara los tiempos de ejecución reales. En este caso particular
- la consulta 0 se ejecuta durante ~ 500 ms
- la consulta 1 se ejecuta durante ~ 1500 ms
- la consulta 2 se ejecuta durante ~ 1500 ms
- la consulta 3 se ejecuta durante ~ 2000 ms.
El plan n. ° 2 tiene una exploración de índice en lugar de una búsqueda, que luego se filtra por predicados en bucles anidados. El plan n. ° 3 no es tan malo, pero aún así trabaja más y funciona más lentamente que el plan n. ° 0.
Supongamos que dbo.Params
se cambia raramente, y generalmente tiene alrededor de 1-200 filas, no más de, digamos, que se espera 2000. Ahora son alrededor de 10 columnas y no espero agregar columnas con demasiada frecuencia.
El número de filas en los parámetros no es fijo, por lo que por cada @session_id
habrá una fila. El número de columnas no está fijo, es una de las razones por las que no quiero llamar dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
desde todas partes, por lo que puedo agregar una nueva columna a esta consulta internamente. Me alegraría escuchar cualquier opinión / sugerencia sobre esto, incluso si tiene algunas restricciones.