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.
la source
Réponses:
Vous ne pouvez pas vraiment atteindre en toute sécurité exactement ce que vous voulez dans SQL Server aujourd'hui, c'est-à-dire dans une seule instruction et avec une exécution parallèle, dans les limites définies dans la question (comme je les perçois).
Donc ma réponse simple est non . Le reste de cette réponse est principalement une discussion de la raison pour laquelle cela est, au cas où cela serait intéressant.
Il est possible d'obtenir un plan parallèle, comme indiqué dans la question, mais il existe deux variétés principales, dont aucune ne convient à vos besoins:
Une jointure de boucles imbriquées corrélées, avec un round-robin répartit les flux au niveau supérieur. Étant donné qu'une seule ligne est garantie pour provenir d'
Params
unesession_id
valeur spécifique , la face intérieure fonctionnera sur un seul thread, même si elle est marquée avec l'icône de parallélisme. C'est pourquoi le plan apparemment parallèle 3 ne fonctionne pas aussi bien; il s'agit en fait d'un feuilleton.L'autre alternative est pour le parallélisme indépendant du côté intérieur de la jointure des boucles imbriquées. Indépendant ici signifie que les threads sont démarrés du côté interne, et pas simplement les mêmes threads que ceux exécutant le côté externe de la jointure des boucles imbriquées. SQL Server prend uniquement en charge le parallélisme de boucles imbriquées côté intérieur indépendant lorsqu'il est garanti qu'il y a une ligne côté extérieur et qu'il n'y a pas de paramètres de jointure corrélés ( plan 2 ).
Ainsi, nous avons le choix d'un plan parallèle qui est en série (en raison d'un thread) avec les valeurs corrélées souhaitées; ou un plan parallèle intérieur qui doit balayer car il n'a pas de paramètres à rechercher. (À part: Il devrait vraiment être autorisé à conduire le parallélisme interne en utilisant exactement un ensemble de paramètres corrélés, mais il n'a jamais été implémenté, probablement pour une bonne raison).
Une question naturelle est alors: pourquoi avons-nous besoin de paramètres corrélés? Pourquoi SQL Server ne peut-il pas simplement rechercher directement les valeurs scalaires fournies par exemple par une sous-requête?
Eh bien, SQL Server ne peut «rechercher d'index» qu'en utilisant des références scalaires simples, par exemple une constante, une variable, une colonne ou une référence d'expression (de sorte qu'un résultat de fonction scalaire peut également être qualifié). Une sous-requête (ou une autre construction similaire) est tout simplement trop complexe (et potentiellement dangereuse) pour être poussée dans le moteur de stockage dans son ensemble. Par conséquent, des opérateurs de plan de requête distincts sont requis. C'est à son tour nécessite une corrélation, ce qui signifie pas de parallélisme du type que vous souhaitez.
Dans l'ensemble, il n'y a vraiment pas de meilleure solution actuellement que des méthodes telles que l'attribution des valeurs de recherche aux variables, puis l'utilisation de celles des paramètres de fonction dans une instruction distincte.
Maintenant, vous pouvez avoir des considérations locales spécifiques qui signifient que la mise en cache des valeurs actuelles de l'année et du mois en
SESSION_CONTEXT
vaut la peine, c'est-à-dire:Mais cela tombe dans la catégorie des solutions de contournement.
D'un autre côté, si les performances d'agrégation sont primordiales, vous pouvez envisager de vous en tenir aux fonctions en ligne et de créer un index columnstore (principal ou secondaire) sur la table. Vous pouvez constater que les avantages du stockage columnstore, du traitement en mode batch et du pushdown agrégé offrent de plus grands avantages qu'une recherche parallèle en mode ligne de toute façon.
Mais méfiez-vous des fonctions scalaires T-SQL, en particulier avec le stockage columnstore, car il est facile de se retrouver avec la fonction évaluée par ligne dans un filtre en mode ligne séparé. Il est généralement assez difficile de garantir le nombre de fois que SQL Server choisira d'évaluer les scalaires, et mieux de ne pas essayer.
la source
session_context
mais je décide que c'est une idée un peu trop folle pour moi et je ne sais pas comment cela va s'adapter à mon architecture actuelle. Ce qui serait utile cependant, c'est peut-être un indice que je pourrais utiliser pour faire savoir à l'optimiseur qu'il devrait traiter le résultat de la sous-requête comme une simple référence scalaire.Autant que je sache, la forme de plan que vous voulez n'est pas possible avec seulement T-SQL. Il semble que vous souhaitiez que la forme du plan d'origine (plan de requête 0) avec les sous-requêtes de vos fonctions soit appliquées directement en tant que filtres par rapport à l'analyse d'index clusterisé. Vous n'obtiendrez jamais un plan de requête comme celui-ci si vous n'utilisez pas de variables locales pour conserver les valeurs de retour des fonctions scalaires. Le filtrage sera à la place implémenté comme jointure de boucle imbriquée. Il existe trois façons différentes (du point de vue du parallélisme) que la jointure de boucle peut être implémentée:
Ce sont les seules formes de plan possibles que je connaisse. Vous pouvez en obtenir d'autres si vous utilisez une table temporaire, mais aucune d'entre elles ne résout votre problème fondamental si vous souhaitez que les performances des requêtes soient aussi bonnes que pour la requête 0.
Vous pouvez obtenir des performances de requête équivalentes en utilisant les FDU scalaires pour affecter des valeurs de retour aux variables locales et en utilisant ces variables locales dans votre requête. Vous pouvez encapsuler ce code dans une procédure stockée ou une UDF à instructions multiples pour éviter les problèmes de maintenabilité. Par exemple:
Les FDU scalaires ont été déplacés en dehors de la requête pour laquelle vous souhaitez être éligible au parallélisme. Le plan de requête que j'obtiens semble être celui que vous souhaitez:
Les deux approches présentent des inconvénients si vous devez utiliser cet ensemble de résultats dans d'autres requêtes. Vous ne pouvez pas rejoindre directement une procédure stockée. Vous devez enregistrer les résultats dans une table temporaire qui a son propre ensemble de problèmes. Vous pouvez rejoindre un MS-TVF, mais dans SQL Server 2016, vous pouvez voir des problèmes d'estimation de cardinalité. SQL Server 2017 offre une exécution entrelacée pour MS-TVF qui pourrait résoudre entièrement le problème.
Juste pour clarifier quelques points: les FDU scalaires T-SQL interdisent toujours le parallélisme et Microsoft n'a pas dit que FROID sera disponible dans SQL Server 2017.
la source
Cela peut très probablement être fait en utilisant SQLCLR. L'un des avantages des FDU scalaires SQLCLR est qu'ils n'empêchent pas le parallélisme s'ils n'accèdent pas aux données (et doivent parfois également être marqués comme «déterministes»). Alors, comment utilisez-vous quelque chose qui ne nécessite aucun accès aux données lorsque l'opération elle-même nécessite un accès aux données?
Eh bien, parce que le
dbo.Params
tableau devrait:INT
colonnesil est possible de mettre en cache les trois colonnes -
session_id, experiment_year int, experiment_month
- dans une collection statique (par exemple un dictionnaire, peut-être) qui est remplie hors processus et lue par les FDU Scalar qui obtiennent les valeursexperiment_year int
etexperiment_month
. Ce que j'entends par «hors processus» est: vous pouvez avoir une UDF scalaire SQLCLR complètement distincte ou une procédure stockée qui peut accéder aux données et lire à partir de ladbo.Params
table pour remplir la collection statique. Cette UDF ou procédure stockée serait exécutée avant d'utiliser les UDF qui obtiennent les valeurs "année" et "mois", de cette façon les UDF qui obtiennent les valeurs "année" et "mois" n'accèdent pas aux données de base de données.L'UDF ou la procédure stockée qui lit les données peut d'abord vérifier si la collection a 0 entrées et si oui, remplir, sinon ignorer. Vous pouvez même garder une trace de l'heure à laquelle il a été rempli et s'il a dépassé X minutes (ou quelque chose comme ça), puis effacer et re-remplir même s'il y a des entrées dans la collection. Mais sauter la population sera utile car elle devra être exécutée fréquemment pour s'assurer qu'elle est toujours remplie pour que les deux principaux FDU obtiennent les valeurs.
La principale préoccupation est lorsque SQL Server décide de décharger le domaine d'application pour une raison quelconque (ou s'il est déclenché par quelque chose qui utilise
DBCC FREESYSTEMCACHE('ALL');
). Vous ne voulez pas risquer que la collecte soit effacée entre l'exécution de l'UDF "populate" ou de la procédure stockée et des UDF pour obtenir les valeurs "year" et "month". Dans ce cas, vous pouvez avoir une vérification au tout début de ces deux UDF pour lever une exception si la collection est vide, car il vaut mieux faire une erreur que de fournir avec succès de faux résultats.Bien entendu, la préoccupation susmentionnée suppose que le souhait est de faire marquer l'Assemblée par
SAFE
. Si l'assembly peut être marqué commeEXTERNAL_ACCESS
, il est possible qu'un constructeur statique exécute la méthode qui lit les données et remplit la collection, de sorte que vous n'ayez besoin de l'exécuter manuellement que pour actualiser les lignes, mais elles seront toujours renseignées (car le constructeur de classe statique s'exécute toujours lorsque la classe est chargée, ce qui se produit chaque fois qu'une méthode de cette classe est exécutée après un redémarrage ou que le domaine d'application est déchargé). Cela nécessite l'utilisation d'une connexion régulière et non la connexion de contexte en cours (qui n'est pas disponible pour les constructeurs statiques, d'où la nécessitéEXTERNAL_ACCESS
).Remarque: pour ne pas être tenu de marquer l'assembly comme
UNSAFE
, vous devez marquer toutes les variables de classe statiques commereadonly
. Cela signifie, à tout le moins, la collection. Ce n'est pas un problème car les collections en lecture seule peuvent avoir des éléments ajoutés ou supprimés, elles ne peuvent tout simplement pas être initialisées en dehors du constructeur ou de la charge initiale. Le suivi de l'heure à laquelle la collection a été chargée dans le but de l'expiration après X minutes est plus délicat car unestatic readonly DateTime
variable de classe ne peut pas être modifiée en dehors du constructeur ou de la charge initiale. Pour contourner cette restriction, vous devez utiliser une collection statique en lecture seule qui contient un seul élément qui est laDateTime
valeur afin qu'il puisse être supprimé et rajouté lors d'une actualisation.la source
readonly statics
est sûre ou sage dans le SQLCLR. Je suis encore moins convaincu de continuer à tromper le système en en faisantreadonly
un type de référence, que vous allez ensuite changer . Me donne les volontés absolues tbh.static
objets) dans SQL Server, oui, il existe un risque de conditions de concurrence. C'est pourquoi j'ai d'abord déterminé à partir de l'OP que ces données sont minimes et stables, et pourquoi j'ai qualifié cette approche comme nécessitant "rarement de changement", et donné un moyen de rafraîchir en cas de besoin. Dans ce cas d'utilisation, je ne vois pas grand-chose, voire aucun risque. Il y a quelques années, j'ai trouvé un article sur la possibilité de mettre à jour les collections en lecture seule comme étant de conception (en C #, pas de discussion sur SQLCLR). Va essayer de le trouver.