Tipi di dati parametro
Come accennato nella prima parte di questa serie, uno dei motivi per cui è meglio parametrizzare in modo esplicito è che si ha il pieno controllo sui tipi di dati dei parametri. La semplice parametrizzazione presenta una serie di stranezze in quest'area, che possono comportare la memorizzazione nella cache di più piani parametrizzati del previsto o la ricerca di risultati diversi rispetto alla versione non parametrizzata.
Quando SQL Server applica la parametrizzazione semplice a un'istruzione ad hoc, fa un'ipotesi sul tipo di dati del parametro di sostituzione. Tratterò i motivi dell'ipotesi più avanti nella serie.
Per il momento, diamo un'occhiata ad alcuni esempi che utilizzano il database Stack Overflow 2010 su SQL Server 2019 CU 14. La compatibilità del database è impostata su 150 e la soglia di costo per il parallelismo è impostata su 50 per evitare il parallelismo per ora:
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;
Queste dichiarazioni risultano in sei piani memorizzati nella cache, tre Adhoc e tre Preparati :
Diversi tipi ipotizzati
Notare i diversi tipi di dati dei parametri in Preparato piani.
Inferenza tipo di dati
I dettagli su come viene indovinato ciascun tipo di dati sono complessi e documentati in modo incompleto. Come punto di partenza, SQL Server deduce un tipo di base dalla rappresentazione testuale del valore, quindi utilizza il sottotipo compatibile più piccolo.
Per una stringa di numeri senza virgolette o punto decimale, SQL Server sceglie da tinyint
, smallint
e integer
. Per tali numeri oltre l'intervallo di un integer
, SQL Server utilizza numeric
con la minor precisione possibile. Ad esempio, il numero 2,147,483,648 viene digitato come numeric(10,0)
. Il bigint
type non viene utilizzato per la parametrizzazione lato server. Questo paragrafo spiega i tipi di dati selezionati negli esempi precedenti.
Stringhe di numeri con un punto decimale viene interpretato come numeric
, con una precisione e una scala sufficientemente grandi da contenere il valore fornito. Le stringhe precedute da un simbolo di valuta vengono interpretate come money
. Le stringhe in notazione scientifica si traducono in float
. Il smallmoney
e real
tipi non sono impiegati.
Il datetime
e uniqueidentifer
i tipi non possono essere dedotti dai formati di stringhe naturali. Per ottenere un datetime
o uniqueidentifier
tipo di parametro, il valore letterale deve essere fornito in formato escape ODBC. Ad esempio {d '1901-01-01'}
, {ts '1900-01-01 12:34:56.790'}
o {guid 'F85C72AB-15F7-49E9-A949-273C55A6C393'}
. In caso contrario, la data prevista o il valore letterale UUID viene digitato come stringa. Tipi di data e ora diversi da datetime
non vengono utilizzati.
La stringa generale e i letterali binari sono digitati come varchar(8000)
, nvarchar(4000)
o varbinary(8000)
a seconda dei casi, a meno che il valore letterale non superi 8000 byte, nel qual caso il max
viene utilizzata la variante. Questo schema aiuta a evitare l'inquinamento della cache e il basso livello di riutilizzo che risulterebbe dall'utilizzo di lunghezze specifiche.
Non è possibile utilizzare CAST
o CONVERT
per impostare il tipo di dati per i parametri per motivi che descriverò in dettaglio più avanti in questa serie. C'è un esempio di questo nella prossima sezione.
Non tratterò la parametrizzazione forzata in questa serie, ma voglio menzionare le regole per l'inferenza del tipo di dati in tal caso presentano alcune differenze importanti rispetto alla parametrizzazione semplice . La parametrizzazione forzata non è stata aggiunta fino a SQL Server 2005, quindi Microsoft ha avuto l'opportunità di incorporare alcune lezioni dalla semplice parametrizzazione esperienza e non dovevo preoccuparmi molto dei problemi di compatibilità con le versioni precedenti.
Tipi numerici
Per i numeri con una virgola decimale e numeri interi oltre l'intervallo di integer
, le regole del tipo dedotto presentano problemi speciali per il riutilizzo del piano e l'inquinamento della cache.
Considera la seguente query utilizzando i decimali:
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;
Questa query è idonea per una parametrizzazione semplice . SQL Server sceglie la precisione e la scala più piccole per i parametri in grado di contenere i valori forniti. Ciò significa che sceglie numeric(8,5)
per 987.65432
e numeric(9,3)
per 123456.789
:
Tipi di dati numerici dedotti
Questi tipi dedotti non corrispondono a decimal(19,8)
tipo della colonna, in modo che venga visualizzata una conversione attorno al parametro nel piano di esecuzione:
Conversione in tipo di colonna
Queste conversioni rappresentano solo una piccola inefficienza di runtime in questo caso particolare. In altre situazioni, una mancata corrispondenza tra il tipo di dati della colonna e il tipo dedotto di un parametro potrebbe impedire la ricerca di un indice o richiedere a SQL Server di eseguire ulteriori operazioni per produrre una ricerca dinamica.
Anche quando il piano di esecuzione risultante sembra ragionevole, una mancata corrispondenza di tipo può facilmente influire sulla qualità del piano a causa dell'effetto della mancata corrispondenza di tipo sulla stima della cardinalità. È sempre meglio utilizzare tipi di dati corrispondenti e prestare particolare attenzione ai tipi derivati risultanti dalle espressioni.
Pianifica il riutilizzo
Il problema principale con il piano corrente sono i tipi dedotti specifici che influiscono sulla corrispondenza del piano memorizzato nella cache e quindi sul riutilizzo. Eseguiamo un altro paio di query della stessa forma generale:
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
Ora guarda la cache del piano:
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;
Mostra un AdHoc e Preparato dichiarazione per ogni domanda che abbiamo inviato:
Dichiarazioni preparate separate
Il testo parametrizzato è lo stesso, ma i tipi di dati dei parametri sono diversi, quindi i piani separati vengono memorizzati nella cache e non si verifica alcun riutilizzo del piano.
Se continuiamo a inviare query con diverse combinazioni di scala o precisione, un nuovo Preparato il piano verrà creato e memorizzato nella cache ogni volta. Ricorda che il tipo dedotto di ciascun parametro non è limitato dal tipo di dati della colonna, quindi potremmo ritrovarci con un numero enorme di piani memorizzati nella cache, a seconda dei valori letterali numerici inviati. Il numero di combinazioni da numeric(1,0)
a numeric(38,38)
è già grande prima di pensare a più parametri.
Parametrizzazione esplicita
Questo problema non si pone quando utilizziamo la parametrizzazione esplicita, scegliendo idealmente lo stesso tipo di dati della colonna con cui viene confrontato il parametro:
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;
Con la parametrizzazione esplicita, la query della cache del piano mostra un solo piano memorizzato nella cache, utilizzato tre volte e non sono necessarie conversioni di tipo:
Parametrizzazione esplicita
Come nota finale, ho usato decimal
e numeric
in modo intercambiabile in questa sezione. Sono tecnicamente tipi diversi, sebbene documentati come sinonimi e si comportino in modo equivalente. Di solito è così, ma non sempre:
-- 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);
Probabilmente è un piccolo bug del parser, ma vale comunque la pena essere coerenti (a meno che tu non stia scrivendo un articolo e desideri segnalare un'eccezione interessante).
Operatori aritmetici
C'è un altro caso limite che voglio affrontare, sulla base di un esempio fornito nella documentazione, ma in modo un po' più dettagliato (e forse accuratezza):
-- 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;
I risultati sono diversi, come documentato:
Risultati diversi
Con parametrizzazione semplice
Quando semplice parametrizzazione si verifica, SQL Server parametrizza entrambi i valori letterali. Il 1.
il valore è digitato come numeric(1,0)
come previsto. In modo alquanto incoerente, il 7
è digitato come integer
(non tinyint
). Le regole dell'inferenza del tipo sono state costruite nel tempo da diversi team. I comportamenti vengono mantenuti per evitare di violare il codice legacy.
Il passaggio successivo riguarda il /
operatore aritmetico. SQL Server richiede tipi compatibili prima di eseguire la divisione. Dato numeric
(decimal
) ha una precedenza del tipo di dati maggiore rispetto a integer
, il integer
verrà convertito in numeric
.
SQL Server deve convertire in modo implicito il integer
in numeric
. Ma quale precisione e scala utilizzare? La risposta potrebbe essere basata sul letterale originale, come fa SQL Server in altre circostanze, ma usa sempre numeric(10)
qui.
Il tipo di dati del risultato della divisione di un numeric(1,0)
da un numeric(10,0)
è determinato da un altro insieme di regole, fornite nella documentazione per precisione, scala e lunghezza. Inserendo i numeri nelle formule per la precisione del risultato e la scala fornite, abbiamo:
- Precisione del risultato:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + max(6, 0 + 10 + 1)
- =1 + max(6, 11)
- =1 + 11
- =12
- Scala dei risultati:
- max(6, s1 + p2 + 1)
- =massimo(6, 0 + 10 + 1)
- =massimo(6, 11)
- =11
Il tipo di dati di 1. / 7
è, quindi, numeric(12, 11)
. Questo valore viene quindi convertito in float
come richiesto e visualizzato come 0.14285714285
(con 11 cifre dopo la virgola).
Senza parametrizzazione semplice
Quando non viene eseguita una semplice parametrizzazione, il 1.
literal è digitato come numeric(1,0)
come prima. Il 7
è inizialmente digitato come integer
anche come visto in precedenza. La differenza fondamentale è il integer
viene convertito in numeric(1,0)
, quindi l'operatore di divisione ha tipi comuni con cui lavorare. Questa è la più piccola precisione e scala in grado di contenere il valore 7
. Ricorda la semplice parametrizzazione utilizzata numeric(10,0)
qui.
Le formule di precisione e scala per dividere numeric(1,0)
per numeric(1,0)
fornire un tipo di dati di risultato di numeric(7,6)
:
- Precisione del risultato:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + max(6, 0 + 1 + 1)
- =1 + max(6, 2)
- =1 + 6
- =7
- Scala dei risultati:
- max(6, s1 + p2 + 1)
- =massimo(6, 0 + 1 + 1)
- =massimo(6, 2)
- =6
Dopo la conversione finale in float
, il risultato visualizzato è 0.142857
(con sei cifre dopo la virgola).
La differenza osservata nei risultati è quindi dovuta alla derivazione del tipo provvisorio (numeric(12,11)
rispetto a numeric(7,6)
) anziché la conversione finale in float
.
Se hai bisogno di ulteriori prove la conversione in float
non è responsabile, considera:
-- 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);
Risultato con decimale
I risultati differiscono per valore e scala come prima.
Questa sezione non copre tutte le stranezze dell'inferenza e della conversione del tipo di dati con la semplice parametrizzazione con qualsiasi mezzo. Come detto in precedenza, è meglio utilizzare parametri espliciti con tipi di dati noti ove possibile.
Fine della parte 2
La parte successiva di questa serie descrive come semplice parametrizzazione influisce sui piani di esecuzione.