J'essaie de voir s'il existe un moyen de tromper SQL Server pour utiliser un certain plan pour la requête.
1. Environnement
Imaginez que vous ayez des données qui sont partagées entre différents processus. Supposons donc que nous ayons des résultats d'expérience qui prennent beaucoup de place. Ensuite, pour chaque processus, nous savons quelle année / mois de résultat d'expérience nous voulons utiliser.
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
Maintenant, pour chaque processus, nous avons des paramètres enregistrés dans le tableau
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. Données d'essai
Ajoutons quelques données de 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. Récupération des résultats
Maintenant, il est très facile d'obtenir les résultats de l'expérience en @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
Le plan est sympa et parallèle:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
plan de requête 0
4. Problème
Mais, pour rendre l'utilisation des données un peu plus générique, je veux avoir une autre fonction - dbo.f_GetSharedDataBySession(@session_id int)
. Donc, la manière la plus simple serait de créer des fonctions scalaires, en traduisant @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
Et maintenant, nous pouvons créer notre fonction:
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
plan de requête 1
Le plan est le même, sauf qu'il n'est bien sûr pas parallèle, car les fonctions scalaires assurant l'accès aux données rendent l'ensemble du plan série .
J'ai donc essayé plusieurs approches différentes, comme utiliser des sous-requêtes au lieu de fonctions scalaires:
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
plan de requête 2
Ou en utilisant 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
plan de requête 3
Mais je ne peux pas trouver un moyen d'écrire cette requête aussi bonne que celle qui utilise les fonctions scalaires.
Quelques pensées:
- Fondamentalement, ce que je voudrais, c'est pouvoir en quelque sorte dire à SQL Server de précalculer certaines valeurs, puis de les transmettre sous forme de constantes.
- Ce qui pourrait être utile, c'est si nous avions un indice de matérialisation intermédiaire . J'ai vérifié quelques variantes (TVF multi-déclarations ou cte avec top), mais aucun plan n'est aussi bon que celui avec des fonctions scalaires jusqu'à présent
- Je suis au courant de l'amélioration à venir de SQL Server 2017 - Froid: optimisation des programmes impératifs dans une base de données relationnelle. Je ne suis pas sûr que cela aidera, cependant. Cela aurait été bien de se tromper ici, cependant.
Information additionnelle
J'utilise une fonction (plutôt que de sélectionner des données directement dans les tables) car elle est beaucoup plus facile à utiliser dans de nombreuses requêtes différentes, qui ont généralement @session_id
comme paramètre.
On m'a demandé de comparer les temps d'exécution réels. Dans ce cas particulier
- la requête 0 s'exécute pendant environ 500 ms
- la requête 1 s'exécute pendant ~ 1500 ms
- la requête 2 s'exécute pendant ~ 1500 ms
- la requête 3 s'exécute pendant environ 2000 ms.
Le plan n ° 2 a un balayage d'index au lieu d'une recherche, qui est ensuite filtré par les prédicats sur les boucles imbriquées. Le plan n ° 3 n'est pas si mal, mais fait encore plus de travail et fonctionne plus lentement que le plan n ° 0.
Supposons que cela dbo.Params
soit rarement modifié, et comportent généralement environ 1 à 200 lignes, pas plus que, disons, 2000 est attendu. C'est environ 10 colonnes maintenant et je ne m'attends pas à ajouter trop de colonnes.
Le nombre de lignes dans Params n'est pas fixe, donc pour chaque @session_id
il y aura une ligne. Le nombre de colonnes n'est pas fixe, c'est l'une des raisons pour lesquelles je ne veux pas appeler dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
de partout, donc je peux ajouter une nouvelle colonne à cette requête en interne. Je serais heureux d'entendre des opinions / suggestions à ce sujet, même si cela comporte certaines restrictions.