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

Fondamenti di espressioni di tabella, parte 12 – Funzioni con valori di tabella inline

Questo articolo è la dodicesima parte di una serie sulle espressioni di tabelle con nome. Finora ho trattato tabelle derivate e CTE, che sono espressioni di tabelle denominate con ambito istruzione, e viste, che sono espressioni di tabelle denominate riutilizzabili. Questo mese introduco le funzioni inline con valori di tabella, o iTVF, e descrivo i loro vantaggi rispetto alle altre espressioni di tabelle con nome. Li confronto anche con le stored procedure, concentrandomi principalmente sulle differenze in termini di strategia di ottimizzazione predefinita e piano di memorizzazione nella cache e comportamento di riutilizzo. C'è molto da trattare in termini di ottimizzazione, quindi avvierò la discussione questo mese e la proseguirò il mese prossimo.

Nei miei esempi userò un database di esempio chiamato TSQLV5. Puoi trovare lo script che lo crea e lo popola qui e il suo diagramma ER qui.

Che cos'è una funzione inline con valori di tabella?

Rispetto alle espressioni di tabella con nome trattate in precedenza, gli iTVF assomigliano principalmente alle viste. Come le viste, gli iTVF vengono creati come oggetti permanenti nel database e quindi sono riutilizzabili dagli utenti che dispongono delle autorizzazioni per interagire con essi. Il principale vantaggio degli iTVF rispetto alle visualizzazioni è il fatto che supportano i parametri di input. Quindi, il modo più semplice per descrivere un iTVF è come una vista parametrizzata, anche se tecnicamente la crei con un'istruzione CREATE FUNCTION e non con un'istruzione CREATE VIEW.

È importante non confondere gli iTVF con le funzioni multi-statement table-valued (MSTVF). La prima è un'espressione di tabella denominata inlinable basata su una singola query simile a una vista ed è l'argomento principale di questo articolo. Quest'ultimo è un modulo programmatico che restituisce una variabile di tabella come output, con un flusso di istruzioni multiple nel suo corpo il cui scopo è riempire la variabile di tabella restituita con i dati.

Sintassi

Ecco la sintassi T-SQL per la creazione di un iTVF:

CREA [O ALTERA] FUNZIONE [. ]

[ () ]

TABELLA RESI

[ CON ]

COME

RITORNO

[; ]

Osservare nella sintassi la possibilità di definire parametri di input.

Lo scopo dell'attributo SCHEMABIDNING è lo stesso delle viste e deve essere valutato sulla base di considerazioni simili. Per i dettagli, vedere la parte 10 della serie.

Un esempio

Ad esempio per un iTVF, supponiamo di dover creare un'espressione di tabella denominata riutilizzabile che accetti come input un ID cliente (@custid) e un numero (@n) e restituisca il numero richiesto di ordini più recenti dalla tabella Sales.Orders per il cliente di input.

Non è possibile implementare questa attività con una vista poiché le viste non supportano i parametri di input. Come accennato, puoi pensare a un iTVF come a una vista parametrizzata e, in quanto tale, è lo strumento giusto per questo compito.

Prima di implementare la funzione stessa, ecco il codice per creare un indice di supporto nella tabella Sales.Orders:

USE TSQLV5;
GO
 
CREATE INDEX idx_nc_cid_odD_oidD_i_eid
  ON Sales.Orders(custid, orderdate DESC, orderid DESC)
  INCLUDE(empid);

Ed ecco il codice per creare la funzione, denominata Sales.GetTopCustOrders:

CREATE OR ALTER FUNCTION Sales.GetTopCustOrders
  ( @custid AS INT, @n AS BIGINT )
RETURNS TABLE
AS
RETURN
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC;
GO

Proprio come con le tabelle e le viste di base, quando stai recuperando i dati, specifichi gli iTVF nella clausola FROM di un'istruzione SELECT. Ecco un esempio che richiede i tre ordini più recenti per il cliente 1:

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(1, 3);

Farò riferimento a questo esempio come Query 1. Il piano per la Query 1 è mostrato nella Figura 1.

Figura 1:piano per la query 1

Cosa c'è di Inline sugli iTVF?

Se ti stai chiedendo la fonte del termine inline nelle funzioni inline con valori di tabella, ha a che fare con il modo in cui vengono ottimizzate. Il concetto di inlining è applicabile a tutti e quattro i tipi di espressioni di tabelle con nome supportate da T-SQL e in parte riguarda ciò che ho descritto nella parte 4 della serie come disnidificazione/sostituzione. Assicurati di rivedere la sezione pertinente nella Parte 4 se hai bisogno di un aggiornamento.

Come puoi vedere nella Figura 1, grazie al fatto che la funzione è stata inline, SQL Server è stato in grado di creare un piano ottimale che interagisce direttamente con gli indici della tabella di base sottostante. Nel nostro caso, il piano esegue una ricerca nell'indice di supporto creato in precedenza.

Gli iTVF portano il concetto di inlining un ulteriore passo avanti applicando l'ottimizzazione dell'incorporamento dei parametri per impostazione predefinita. Paul White descrive l'ottimizzazione dell'incorporamento dei parametri nel suo eccellente articolo Parametro Sniffing, Embedding e RECOMPILE Options. Con l'ottimizzazione dell'incorporamento dei parametri, i riferimenti ai parametri di query vengono sostituiti con i valori delle costanti letterali dell'esecuzione corrente, quindi il codice con le costanti viene ottimizzato.

Osservare nel piano in Figura 1 che sia il predicato di ricerca dell'operatore Index Seek che l'espressione superiore dell'operatore Top mostrano i valori costanti letterali incorporati 1 e 3 dall'esecuzione della query corrente. Non mostrano rispettivamente i parametri @custid e @n.

Con iTVF, l'ottimizzazione dell'incorporamento dei parametri viene utilizzata per impostazione predefinita. Con le stored procedure, le query con parametri sono ottimizzate per impostazione predefinita. È necessario aggiungere OPTION(RECOMPILE) alla query di una stored procedure per richiedere l'ottimizzazione dell'incorporamento dei parametri. Maggiori dettagli sull'ottimizzazione di iTVF rispetto alle stored procedure, comprese le implicazioni, a breve.

Modifica dei dati tramite iTVF

Ricordiamo dalla parte 11 della serie che, purché vengano soddisfatti determinati requisiti, le espressioni di tabelle con nome possono essere oggetto di istruzioni di modifica. Questa capacità si applica agli iTVF in modo simile al modo in cui si applica alle visualizzazioni. Ad esempio, ecco il codice che potresti utilizzare per eliminare i tre ordini più recenti del cliente 1 (non eseguirlo effettivamente):

DELETE FROM Sales.GetTopCustOrders(1, 3);

In particolare nel nostro database, il tentativo di eseguire questo codice fallirebbe a causa dell'applicazione dell'integrità referenziale (gli ordini interessati hanno righe ordine correlate nella tabella Sales.OrderDetails), ma è un codice valido e supportato.

iTVF e stored procedure

Come accennato in precedenza, la strategia di ottimizzazione delle query predefinita per iTVF è diversa da quella per le stored procedure. Con iTVF, l'impostazione predefinita prevede l'utilizzo dell'ottimizzazione dell'incorporamento dei parametri. Con le stored procedure, l'impostazione predefinita consiste nell'ottimizzare le query con parametri durante l'applicazione dello sniffing dei parametri. Per ottenere l'incorporamento dei parametri per una query di stored procedure, è necessario aggiungere OPTION(RICIMPILA).

Come con molte strategie e tecniche di ottimizzazione, l'inclusione dei parametri ha i suoi vantaggi e svantaggi.

Il vantaggio principale è che consente semplificazioni delle query che a volte possono portare a piani più efficienti. Alcune di queste semplificazioni sono davvero affascinanti. Paul lo dimostra con le stored procedure nel suo articolo e lo dimostrerò con iTVF il mese prossimo.

Lo svantaggio principale dell'ottimizzazione dell'incorporamento dei parametri è che non si ottiene una memorizzazione nella cache dei piani efficiente e un comportamento di riutilizzo come si fa per i piani parametrizzati. Con ogni combinazione distinta di valori di parametro, ottieni una stringa di query distinta e quindi una compilazione separata che risulta in un piano separato nella cache. Con iTVF con input costanti, puoi ottenere il comportamento di riutilizzo del piano, ma solo se vengono ripetuti gli stessi valori dei parametri. Ovviamente, una query di stored procedure con OPTION(RECOMPILE) non riutilizzerà un piano anche ripetendo gli stessi valori di parametro, su richiesta.

Dimostrerò tre casi:

  1. Piani riutilizzabili con costanti risultanti dall'ottimizzazione predefinita dell'incorporamento dei parametri per le query iTVF con costanti
  2. Piani parametrizzati riutilizzabili risultanti dall'ottimizzazione predefinita delle query di stored procedure con parametri
  3. Piani non riutilizzabili con costanti risultanti dall'ottimizzazione dell'incorporamento dei parametri per le query di stored procedure con OPTION(RECOMPILE)

Iniziamo con il caso n. 1.

Usa il codice seguente per interrogare il nostro iTVF con @custid =1 e @n =3:

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(1, 3);

Come promemoria, questa sarebbe la seconda esecuzione dello stesso codice poiché l'hai già eseguita una volta con gli stessi valori di parametro in precedenza, risultando nel piano mostrato nella Figura 1.

Utilizzare il codice seguente per interrogare l'iTVF con @custid =2 e @n =3 una volta:

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(2, 3);

Farò riferimento a questo codice come Query 2. Il piano per la Query 2 è mostrato nella Figura 2.

Figura 2:piano per la query 2

Ricordiamo che il piano nella Figura 1 per la query 1 si riferiva all'ID cliente costante 1 nel predicato di ricerca, mentre questo piano si riferisce all'ID cliente costante 2.

Utilizzare il codice seguente per esaminare le statistiche di esecuzione delle query:

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders(%';

Questo codice genera il seguente output:

plan_handle         execution_count text                                           query_plan
------------------- --------------- ---------------------------------------------- ----------------
0x06000B00FD9A1...  1               SELECT ... FROM Sales.GetTopCustOrders(2, 3);  <ShowPlanXML...>
0x06000B00F5C34...  2               SELECT ... FROM Sales.GetTopCustOrders(1, 3);  <ShowPlanXML...>

(2 rows affected)

Sono stati creati due piani separati qui:uno per la query con ID cliente 1, che è stata utilizzata due volte, e un altro per la query con ID cliente 2, che è stata utilizzata una volta. Con un numero molto elevato di combinazioni distinte di valori di parametro, ti ritroverai con un gran numero di compilazioni e piani memorizzati nella cache.

Procediamo con il caso n. 2:la strategia di ottimizzazione predefinita delle query di stored procedure con parametri. Utilizzare il codice seguente per incapsulare la nostra query in una stored procedure denominata Sales.GetTopCustOrders2:

CREATE OR ALTER PROC Sales.GetTopCustOrders2
  ( @custid AS INT, @n AS BIGINT )
AS
  SET NOCOUNT ON;
 
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC;
GO

Utilizzare il codice seguente per eseguire la stored procedure con @custid =1 e @n =3 due volte:

EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;

La prima esecuzione attiva l'ottimizzazione della query, risultando nel piano parametrizzato mostrato in Figura 3:

Figura 3:Piano per Sales.GetTopCustOrders2 proc

Osservare il riferimento al parametro @custid nel predicato di ricerca e al parametro @n nell'espressione in alto.

Utilizzare il codice seguente per eseguire la stored procedure con @custid =2 e @n =3 una volta:

EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;

Il piano parametrizzato memorizzato nella cache mostrato nella Figura 3 viene riutilizzato di nuovo.

Utilizzare il codice seguente per esaminare le statistiche di esecuzione delle query:

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';

Questo codice genera il seguente output:

plan_handle         execution_count text                                            query_plan
------------------- --------------- ----------------------------------------------- ----------------
0x05000B00F1604...  3               ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...>

(1 row affected)

È stato creato e memorizzato nella cache un solo piano parametrizzato, che è stato utilizzato tre volte, nonostante i valori dell'ID cliente cambiassero.

Procediamo al caso n. 3. Come accennato, con le query di stored procedure è possibile ottenere l'ottimizzazione dell'incorporamento dei parametri quando si utilizza OPTION(RECOMPILE). Utilizzare il codice seguente per modificare la query della procedura per includere questa opzione:

CREATE OR ALTER PROC Sales.GetTopCustOrders2
  ( @custid AS INT, @n AS BIGINT )
AS
  SET NOCOUNT ON;
 
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC
  OPTION(RECOMPILE);
GO

Esegui il processo con @custid =1 e @n =3 due volte:

EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;

Ottieni lo stesso piano mostrato in precedenza nella Figura 1 con le costanti incorporate.

Esegui il processo con @custid =2 e @n =3 una volta:

EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;

Ottieni lo stesso piano mostrato in precedenza nella Figura 2 con le costanti incorporate.

Esamina le statistiche di esecuzione della query:

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';

Questo codice genera il seguente output:

plan_handle         execution_count text                                            query_plan
------------------- --------------- ----------------------------------------------- ----------------
0x05000B00F1604...  1               ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...>

(1 row affected)

Il conteggio delle esecuzioni mostra 1, che riflette solo l'ultima esecuzione. SQL Server memorizza nella cache l'ultimo piano eseguito, quindi può mostrare le statistiche per tale esecuzione, ma su richiesta non riutilizza il piano. Se controlli il piano mostrato sotto l'attributo query_plan, scoprirai che è quello creato per le costanti nell'ultima esecuzione, mostrato in precedenza nella Figura 2.

Se stai cercando un minor numero di compilazioni e un comportamento efficiente della memorizzazione nella cache e del riutilizzo, l'approccio di ottimizzazione della stored procedure predefinita delle query con parametri è la strada da percorrere.

C'è un grande vantaggio che un'implementazione basata su iTVF ha rispetto a un'implementazione basata su stored procedure, quando è necessario applicare la funzione a ciascuna riga in una tabella e passare colonne dalla tabella come input. Si supponga, ad esempio, di dover restituire i tre ordini più recenti per ciascun cliente nella tabella Sales.Customers. Nessun costrutto di query consente di applicare una stored procedure per riga in una tabella. Potresti implementare una soluzione iterativa con un cursore, ma è sempre un buon giorno quando puoi evitare i cursori. Combinando l'operatore APPLY con una chiamata iTVF, puoi svolgere l'attività in modo piacevole e pulito, in questo modo:

SELECT C.custid, O.orderid, O.orderdate, O.empid
FROM Sales.Customers AS C
  CROSS APPLY Sales.GetTopCustOrders( C.custid, 3 ) AS O;

Questo codice genera il seguente output (abbreviato):

custid      orderid     orderdate  empid
----------- ----------- ---------- -----------
1           11011       2019-04-09 3
1           10952       2019-03-16 1
1           10835       2019-01-15 1
2           10926       2019-03-04 4
2           10759       2018-11-28 3
2           10625       2018-08-08 3
...

(263 rows affected)

La chiamata alla funzione viene incorporata e il riferimento al parametro @custid viene sostituito con la correlazione C.custid. Ciò si traduce nel piano mostrato nella Figura 4.

Figura 4:piano per query con APPLY e Sales.GetTopCustOrders iTVF

Il piano esegue la scansione di alcuni indici nella tabella Sales.Customers per ottenere il set di ID cliente e applica una ricerca nell'indice di supporto creato in precedenza in Sales.Orders per cliente. C'è solo un piano poiché la funzione è stata incorporata nella query esterna, trasformandosi in un join correlato o laterale. Questo piano è altamente efficiente, soprattutto quando la colonna custid in Sales.Orders è molto densa, ovvero quando c'è un numero limitato di ID cliente distinti.

Naturalmente, ci sono altri modi per implementare questa attività, come l'utilizzo di un CTE con la funzione ROW_NUMBER. Tale soluzione tende a funzionare meglio di quella basata su APPLY quando la colonna custid nella tabella Sales.Orders ha una densità bassa. Ad ogni modo, il compito specifico che ho usato nei miei esempi non è così importante ai fini della nostra discussione. Il mio punto era spiegare le diverse strategie di ottimizzazione impiegate da SQL Server con i diversi strumenti.

Quando hai finito, usa il seguente codice per la pulizia:

DROP INDEX IF EXISTS idx_nc_cid_odD_oidD_i_eid ON Sales.Orders;

Riepilogo e novità

Allora, cosa abbiamo imparato da questo?

Un iTVF è un'espressione di tabella denominata con parametri riutilizzabile.

SQL Server utilizza una strategia di ottimizzazione dell'incorporamento dei parametri con iTVF per impostazione predefinita e una strategia di ottimizzazione delle query con parametri con query di stored procedure. L'aggiunta di OPTION(RECOMPILE) a una query di stored procedure può comportare l'ottimizzazione dell'incorporamento dei parametri.

Se desideri ottenere un minor numero di compilazioni e un comportamento efficiente di memorizzazione nella cache e riutilizzo dei piani, i piani di query delle procedure con parametri sono la strada da percorrere.

I piani per le query iTVF vengono memorizzati nella cache e possono essere riutilizzati, purché vengano ripetuti gli stessi valori dei parametri.

Puoi combinare comodamente l'uso dell'operatore APPLY e di un iTVF per applicare l'iTVF a ciascuna riga della tabella di sinistra, passando le colonne della tabella di sinistra come input all'iTVF.

Come accennato, c'è molto da coprire sull'ottimizzazione di iTVF. Questo mese ho confrontato iTVF e stored procedure in termini di strategia di ottimizzazione predefinita e pianificazione del comportamento di memorizzazione nella cache e riutilizzo. Il mese prossimo approfondirò le semplificazioni derivanti dall'ottimizzazione dell'incorporamento dei parametri.


No