Sto cercando di vedere se c'è un modo per ingannare SQL Server per utilizzare un determinato piano per la query.
1. Ambiente
Immagina di avere alcuni dati condivisi tra diversi processi. Supponiamo quindi di avere alcuni risultati dell'esperimento che occupano molto spazio. Quindi, per ogni processo sappiamo quale anno / mese di risultato dell'esperimento vogliamo usare.
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
Ora, per ogni processo abbiamo i parametri salvati nella tabella
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. Dati di prova
Aggiungiamo alcuni dati di test:
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. Recupero dei risultati
Ora, è molto facile ottenere risultati dell'esperimento @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
Il piano è carino e parallelo:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
query 0 piano
4. Problema
Ma, per rendere l'uso dei dati un po 'più generico, voglio avere un'altra funzione - dbo.f_GetSharedDataBySession(@session_id int)
. Quindi, il modo più semplice sarebbe quello di creare funzioni scalari, traducendo @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 ora possiamo creare la nostra funzione:
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
query 1 piano
Il piano è lo stesso tranne che, ovviamente, non è parallelo, perché le funzioni scalari che eseguono l'accesso ai dati rendono l'intero piano seriale .
Quindi ho provato diversi approcci, ad esempio usando subquery invece di funzioni scalari:
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
piano query 2
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
query 3 piano
Ma non riesco a trovare un modo per scrivere questa query per essere buono come quello che utilizza le funzioni scalari.
Paio di pensieri:
- Fondamentalmente quello che vorrei è poter dire in qualche modo a SQL Server di pre-calcolare determinati valori e poi passarli ulteriormente come costanti.
- Ciò che potrebbe essere utile è se avessimo qualche suggerimento sulla materializzazione intermedia . Ho controllato un paio di varianti (TVF multi-statement o cte con top), ma finora nessun piano è buono come quello con funzioni scalari
- Sono a conoscenza del prossimo miglioramento di SQL Server 2017 - Froid: ottimizzazione dei programmi imperativi in un database relazionale. Non sono sicuro che possa aiutare, però. Sarebbe stato bello essere smentito qui, però.
Informazioni aggiuntive
Sto usando una funzione (piuttosto che selezionare i dati direttamente dalle tabelle) perché è molto più facile da usare in molte query diverse, che di solito hanno @session_id
come parametro.
Mi è stato chiesto di confrontare i tempi di esecuzione effettivi. In questo caso particolare
- la query 0 viene eseguita per ~ 500 ms
- la query 1 viene eseguita per ~ 1500ms
- la query 2 viene eseguita per ~ 1500ms
- la query 3 viene eseguita per ~ 2000 ms.
Il piano n. 2 ha una scansione dell'indice anziché una ricerca, che viene quindi filtrata dai predicati sui cicli nidificati. Il piano n. 3 non è poi così male, ma fa ancora più lavoro e rallenta il piano n. 0.
Supponiamo che dbo.Params
sia cambiato raramente e di solito hanno circa 1-200 righe, non più di, supponiamo che 2000 sia mai previsto. Sono circa 10 colonne e non mi aspetto di aggiungere colonne troppo spesso.
Il numero di righe in Params non è fisso, quindi per ogni @session_id
ci sarà una riga. Il numero di colonne non è stato risolto, è uno dei motivi per cui non voglio chiamare dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
da nessuna parte, quindi posso aggiungere una nuova colonna a questa query internamente. Sarei felice di sentire qualsiasi opinione / suggerimento su questo, anche se ha alcune restrizioni.