Database
 sql >> Database >  >> RDS >> Database

Parametrizzazione semplice e piani banali — Parte 2

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.