Types de données de paramètres
Comme mentionné dans la première partie de cette série, l'une des raisons pour lesquelles il est préférable de paramétrer explicitement est que vous ayez un contrôle total sur les types de données de paramètre. Le paramétrage simple présente un certain nombre de bizarreries dans ce domaine, ce qui peut entraîner la mise en cache d'un plus grand nombre de plans paramétrés que prévu ou la recherche de résultats différents par rapport à la version non paramétrée.
Lorsque SQL Server applique le paramétrage simple à une instruction ad hoc, il fait une supposition sur le type de données du paramètre de remplacement. Je couvrirai les raisons de la supposition plus tard dans la série.
Pour le moment, examinons quelques exemples utilisant la base de données Stack Overflow 2010 sur SQL Server 2019 CU 14. La compatibilité de la base de données est définie sur 150 et le seuil de coût pour le parallélisme est défini sur 50 pour éviter le parallélisme pour le moment :
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 252; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 25221; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 252552;
Ces déclarations se traduisent par six plans en cache, trois Adhoc et trois Préparé :
Différents types devinés
Notez les différents types de données de paramètre dans le Préparé plans.
Inférence de type de données
Les détails de la façon dont chaque type de données est deviné sont complexes et incomplètement documentés. Comme point de départ, SQL Server déduit un type de base à partir de la représentation textuelle de la valeur, puis utilise le plus petit sous-type compatible.
Pour une chaîne de nombres sans guillemets ni point décimal, SQL Server choisit parmi tinyint
, smallint
, et integer
. Pour de tels nombres au-delà de la plage d'un integer
, SQL Server utilise numeric
avec la plus petite précision possible. Par exemple, le nombre 2 147 483 648 est saisi sous la forme numeric(10,0)
. Le bigint
type n'est pas utilisé pour le paramétrage côté serveur. Ce paragraphe explique les types de données sélectionnés dans les exemples précédents.
Chaînes de nombres avec un point décimal est interprété comme numeric
, avec une précision et une échelle juste assez grandes pour contenir la valeur fournie. Les chaînes précédées d'un symbole monétaire sont interprétées comme money
. Les chaînes en notation scientifique se traduisent par float
. Le smallmoney
et real
les types ne sont pas employés.
Le datetime
et uniqueidentifer
les types ne peuvent pas être déduits des formats de chaîne naturels. Pour obtenir un datetime
ou uniqueidentifier
type de paramètre, la valeur littérale doit être fournie au format d'échappement ODBC. Par exemple {d '1901-01-01'}
, {ts '1900-01-01 12:34:56.790'}
, ou {guid 'F85C72AB-15F7-49E9-A949-273C55A6C393'}
. Sinon, la date prévue ou le littéral UUID est saisi sous forme de chaîne. Types de date et d'heure autres que datetime
ne sont pas utilisés.
Les chaînes générales et les littéraux binaires sont saisis sous la forme varchar(8000)
, nvarchar(4000)
, ou varbinary(8000)
le cas échéant, sauf si le littéral dépasse 8 000 octets, auquel cas le max
variante est utilisée. Ce schéma permet d'éviter la pollution du cache et le faible niveau de réutilisation qui résulteraient de l'utilisation de longueurs spécifiques.
Il n'est pas possible d'utiliser CAST
ou CONVERT
pour définir le type de données des paramètres pour des raisons que je détaillerai plus tard dans cette série. Vous en trouverez un exemple dans la section suivante.
Je ne couvrirai pas le paramétrage forcé dans cette série, mais je tiens à mentionner que les règles d'inférence de type de données dans ce cas présentent des différences importantes par rapport au paramétrage simple . Le paramétrage forcé n'a pas été ajouté avant SQL Server 2005, Microsoft a donc eu l'opportunité d'incorporer quelques leçons du paramétrage simple expérience, et n'a pas eu à se soucier des problèmes de rétrocompatibilité.
Types numériques
Pour les nombres avec un point décimal et les nombres entiers au-delà de la plage de integer
, les règles de type déduites présentent des problèmes particuliers de réutilisation du plan et de pollution du cache.
Considérez la requête suivante utilisant des décimales :
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO DROP TABLE IF EXISTS dbo.Test; GO CREATE TABLE dbo.Test ( SomeValue decimal(19,8) NOT NULL ); GO SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 987.65432 AND T.SomeValue < 123456.789;
Cette requête se qualifie pour le paramétrage simple . SQL Server choisit la précision et l'échelle les plus petites pour les paramètres pouvant contenir les valeurs fournies. Cela signifie qu'il choisit numeric(8,5)
pour 987.65432
et numeric(9,3)
pour 123456.789
:
Types de données numériques inférées
Ces types déduits ne correspondent pas au decimal(19,8)
type de la colonne, donc une conversion autour du paramètre apparaît dans le plan d'exécution :
Conversion en type de colonne
Ces conversions ne représentent qu'une petite inefficacité d'exécution dans ce cas particulier. Dans d'autres situations, une incompatibilité entre le type de données de la colonne et le type déduit d'un paramètre peut empêcher une recherche d'index ou obliger SQL Server à effectuer un travail supplémentaire pour fabriquer une recherche dynamique.
Même lorsque le plan d'exécution résultant semble raisonnable, une incompatibilité de type peut facilement affecter la qualité du plan en raison de l'effet de l'incompatibilité de type sur l'estimation de la cardinalité. Il est toujours préférable d'utiliser des types de données correspondants et de porter une attention particulière aux types dérivés résultant d'expressions.
Planifier la réutilisation
Le principal problème avec le plan actuel est les types inférés spécifiques affectant la correspondance du plan mis en cache et donc la réutilisation. Exécutons quelques requêtes supplémentaires de la même forme générale :
SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 98.76 AND T.SomeValue < 123.4567; GO SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 1.2 AND T.SomeValue < 1234.56789; GO
Regardez maintenant le cache du plan :
SELECT CP.usecounts, CP.objtype, ST.[text] FROM sys.dm_exec_cached_plans AS CP CROSS APPLY sys.dm_exec_sql_text (CP.plan_handle) AS ST WHERE ST.[text] NOT LIKE '%dm_exec_cached_plans%' AND ST.[text] LIKE '%SomeValue%Test%' ORDER BY CP.objtype ASC;
Il affiche un AdHoc et Préparé déclaration pour chaque requête que nous avons soumise :
Déclarations préparées séparées
Le texte paramétré est le même, mais les types de données de paramètre sont différents, donc des plans séparés sont mis en cache et aucune réutilisation de plan ne se produit.
Si nous continuons à soumettre des requêtes avec différentes combinaisons d'échelle ou de précision, un nouveau préparé plan sera créé et mis en cache à chaque fois. N'oubliez pas que le type déduit de chaque paramètre n'est pas limité par le type de données de la colonne, nous pourrions donc nous retrouver avec un nombre considérable de plans en cache, en fonction des littéraux numériques soumis. Le nombre de combinaisons de numeric(1,0)
à numeric(38,38)
est déjà grand avant de penser à plusieurs paramètres.
Paramétrage explicite
Ce problème ne se pose pas lorsque nous utilisons un paramétrage explicite, en choisissant idéalement le même type de données que la colonne à laquelle le paramètre est comparé :
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO DECLARE @stmt nvarchar(4000) = N'SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= @P1 AND T.SomeValue < @P2;', @params nvarchar(4000) = N'@P1 numeric(19,8), @P2 numeric(19,8)'; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 987.65432, @P2 = 123456.789; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 98.76, @P2 = 123.4567; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 1.2, @P2 = 1234.56789;
Avec un paramétrage explicite, la requête de cache de plan affiche un seul plan mis en cache, utilisé trois fois et aucune conversion de type nécessaire :
Paramétrage explicite
Comme note finale, j'ai utilisé decimal
et numeric
indifféremment dans cette section. Ils sont techniquement différents types, bien que documentés comme étant des synonymes et se comportant de manière équivalente. C'est généralement le cas, mais pas toujours :
-- Raises error 8120: -- Column 'dbo.Test.SomeValue' is invalid in the select list -- because it is not contained in either an aggregate function -- or the GROUP BY clause. SELECT CONVERT(decimal(19,8), T.SomeValue) FROM dbo.Test AS T GROUP BY CONVERT(numeric(19,8), T.SomeValue);
C'est probablement un petit bogue de l'analyseur, mais il vaut toujours la peine d'être cohérent (sauf si vous écrivez un article et que vous souhaitez signaler une exception intéressante).
Opérateurs arithmétiques
Il y a un autre cas marginal que je veux aborder, basé sur un exemple donné dans la documentation, mais avec un peu plus de détails (et peut-être de précision) :
-- The dbo.LinkTypes table contains two rows -- Uses simple parameterization SELECT r = CONVERT(float, 1./ 7) FROM dbo.LinkTypes AS LT; -- No simple parameterization due to -- constant-constant comparison SELECT r = CONVERT(float, 1./ 7) FROM dbo.LinkTypes AS LT WHERE 1 = 1;
Les résultats sont différents, comme documenté :
Résultats différents
Avec paramétrage simple
Quand le paramétrage simple se produit, SQL Server paramètre les deux valeurs littérales. Le 1.
la valeur est saisie sous la forme numeric(1,0)
comme prévu. De manière quelque peu incohérente, le 7
est de type integer
(pas tinyint
). Les règles d'inférence de type ont été construites au fil du temps, par différentes équipes. Les comportements sont conservés pour éviter de casser le code hérité.
L'étape suivante implique le /
opérateur arithmétique. SQL Server requiert des types compatibles avant d'effectuer la division. Donné numeric
(decimal
) a une priorité de type de données supérieure à integer
, le integer
sera converti en numeric
.
SQL Server doit implicitement convertir le integer
en numeric
. Mais quelle précision et échelle utiliser ? La réponse peut être basée sur le littéral d'origine, comme le fait SQL Server dans d'autres circonstances, mais il utilise toujours numeric(10)
ici.
Le type de données du résultat de la division d'un numeric(1,0)
par un numeric(10,0)
est déterminé par un autre ensemble de règles, données dans la documentation pour la précision, l'échelle et la longueur. En insérant les nombres dans les formules pour la précision et l'échelle des résultats qui y sont données, nous avons :
- Précision du résultat :
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + max(6, 0 + 10 + 1)
- =1 + max(6, 11)
- =1 + 11
- =12
- Échelle de résultat :
- max(6, s1 + p2 + 1)
- =max(6, 0 + 10 + 1)
- =max(6, 11)
- =11
Le type de données de 1. / 7
est donc numeric(12, 11)
. Cette valeur est ensuite convertie en float
comme demandé et affiché sous la forme 0.14285714285
(avec 11 chiffres après la virgule).
Sans paramétrage simple
Lorsque le paramétrage simple n'est pas effectué, le 1.
le littéral est saisi sous la forme numeric(1,0)
comme avant. Le 7
est initialement tapé comme integer
également comme vu précédemment. La principale différence est le integer
est converti en numeric(1,0)
, ainsi l'opérateur de division a des types communs avec lesquels travailler. Il s'agit de la plus petite précision et échelle pouvant contenir la valeur 7
. Rappelez-vous le paramétrage simple utilisé numeric(10,0)
ici.
Les formules de précision et d'échelle pour diviser numeric(1,0)
par numeric(1,0)
donner un type de données de résultat numeric(7,6)
:
- Précision du résultat :
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + max(6, 0 + 1 + 1)
- =1 + max(6, 2)
- =1 + 6
- =7
- Échelle de résultat :
- max(6, s1 + p2 + 1)
- =max(6, 0 + 1 + 1)
- =max(6, 2)
- =6
Après la conversion finale en float
, le résultat affiché est 0.142857
(avec six chiffres après la virgule).
La différence observée dans les résultats est donc due à la dérivation de type intermédiaire (numeric(12,11)
vs numeric(7,6)
) plutôt que la conversion finale en float
.
Si vous avez besoin de preuves supplémentaires de la conversion en float
n'est pas responsable, considérez :
-- Simple parameterization SELECT r = CONVERT(decimal(13,12), 1. / 7) FROM dbo.LinkTypes AS LT; -- No simple parameterization SELECT r = CONVERT(decimal(13,12), 1. / 7) FROM dbo.LinkTypes AS LT OPTION (MAXDOP 1);
Résultat avec décimal
Les résultats diffèrent en valeur et en échelle comme auparavant.
Cette section ne couvre pas toutes les bizarreries de l'inférence et de la conversion des types de données avec un paramétrage simple n'importe comment. Comme indiqué précédemment, il est préférable d'utiliser des paramètres explicites avec des types de données connus dans la mesure du possible.
Fin de la partie 2
La prochaine partie de cette série décrit comment le paramétrage simple affecte les plans d'exécution.