SQL Server memorizza nella cache i valori calcolati in una query?


10

Ogni volta che mi imbatto in questo tipo di query, mi chiedo sempre come funzionerebbe SQL Server. Se eseguo qualsiasi tipo di query che richiede un calcolo e quindi utilizzo quel valore in più posizioni, ad esempio in selecte il order by, SQL Server lo calcolerà due volte per ogni riga o verrà memorizzato nella cache? Inoltre, come funziona con le funzioni definite dall'utente?

Esempi:

SELECT CompanyId, Count(*)
FROM Sales
ORDER BY Count(*) desc

SELECT Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STX, Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STY
FROM Table

SELECT Id, udf.MyFunction(Id)
FROM Table
ORDER BY udf.MyFunction(Id)

C'è un modo per renderlo più efficiente o SQL Server è abbastanza intelligente da gestirlo per me?


"dipende" ecco una mostra rextester.com/DXOB90032
Martin Smith

Che puoi confrontare con rextester.com/ARSO25902
Martin Smith

@MartinSmith non stai usando una funzione non deterministica? Se è così, mi aspetterei che SQL lo eseguisse due volte.
Jonas Stawski

c'è sempre un'eccezione! Puoi provare SELECT RAND() FROM Sales order by RAND(): questo viene valutato solo una volta in quanto è sia non deterministico che costante di runtime.
Martin Smith

Risposte:


11

Query Optimizer di SQL Server può combinare valori calcolati ripetuti in un singolo operatore di calcolo scalare. Se lo farà o meno dipenderà dal costo del piano di query e dalle proprietà del valore calcolato. Come previsto, non lo farà per valori calcolati non deterministici, quali alcune eccezioni come RAND(). Inoltre non lo farà per le funzioni definite dall'utente.

Inizierò con un esempio di funzione definita dall'utente. Ecco un esempio eccellente di una funzione definita dall'utente:

CREATE OR ALTER FUNCTION dbo.NULL_FUNCTION (@N BIGINT) RETURNS BIGINT
WITH SCHEMABINDING
AS
BEGIN
RETURN NULL;
END;

Voglio anche creare una tabella e inserire 100 righe al suo interno:

CREATE TABLE X_100 (N BIGINT NOT NULL);

WITH
L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO X_100 WITH (TABLOCK)
SELECT n
FROM Nums WHERE n <= 100;

La dbo.NULL_FUNCTIONfunzione è deterministica. Quante volte verrà eseguito per la seguente query?

SELECT n, dbo.NULL_FUNCTION(n)
FROM X_100;

In base al piano di query, questo verrà eseguito una volta per ogni riga o 100 volte:

piano di query 1

SQL Server 2016 ha introdotto il DMV sys.dm_exec_function_stats . Possiamo fare delle istantanee di quel DMV per vedere quante volte un UDF viene eseguito da una query.

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('NULL_FUNCTION');

Il risultato è 100, quindi la funzione è stata eseguita 100 volte.

Proviamo un'altra semplice query:

SELECT n, dbo.NULL_FUNCTION(n), dbo.NULL_FUNCTION(n) 
FROM X_100;

Il piano di query suggerisce che la funzione verrà eseguita 200 volte:

piano di query 2

I risultati di sys.dm_exec_function_statssuggeriscono che la funzione è stata eseguita 200 volte.

Si noti che non è sempre possibile utilizzare il piano di query per capire quante volte viene eseguito uno scalare di calcolo. La seguente citazione è tratta da " Calcola scalari, espressioni ed esecuzione del piano di esecuzione ":

Questo porta le persone a pensare che Compute Scalar si comporti come la maggior parte degli altri operatori: man mano che le righe lo attraversano, i risultati di qualsiasi calcolo contenuto nel Compute Scalar vengono aggiunti allo stream. Questo non è generalmente vero. Nonostante il nome, Calcola scalare non calcola sempre nulla e non contiene sempre un singolo valore scalare (ad esempio può essere un vettore, un alias o persino un predicato booleano). Più spesso, uno scalare di calcolo definisce semplicemente un'espressione; il calcolo effettivo viene rinviato fino a quando qualcosa di più tardi nel piano di esecuzione necessita del risultato.

Proviamo un altro esempio. Per la seguente domanda spero che l'UDF venga calcolato una volta:

WITH NULL_FUNCTION_CTE (NULL_VALUE) AS
(
SELECT DISTINCT dbo.NULL_FUNCTION(0)
)
SELECT n , cte.NULL_VALUE
FROM X_100
CROSS JOIN NULL_FUNCTION_CTE cte;

Il piano di query suggerisce che verrà calcolato una volta:

piano di query

Tuttavia, il DMV rivela la verità. Lo scalare di calcolo viene rinviato fino a quando non è necessario, che si trova nell'operatore di join. È valutato 100 volte.

Hai anche chiesto cosa puoi fare per incoraggiare l'ottimizzatore per evitare di ricalcolare la stessa espressione più volte. La cosa migliore che puoi fare è evitare di usare UDF scalari nel tuo codice. Esse presentano una serie di problemi di prestazioni al di fuori di questa domanda, tra cui l'espansione della memoria, la forzatura dell'intera query MAXDOP 1, stime errate della cardinalità e un ulteriore utilizzo della CPU. Se è necessario utilizzare un UDF e il valore di tale UDF è una costante, è possibile calcolarlo al di fuori della query e inserirlo in una variabile locale.

Per le query senza UDF, puoi provare a evitare di scrivere espressioni che restituiscono lo stesso risultato ma non vengono digitate esattamente allo stesso modo. Per questo prossimo esempio sto usando il database AdventureworksDW2016CTP3 pubblicamente disponibile, ma davvero qualsiasi database lo farà. Quante volte verranno COUNT(*)calcolate per questa query?

SELECT OrderDateKey, COUNT(*) 
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

Per questa query possiamo capirlo osservando l'operatore Hash Match (aggregato).

hash match

La COUNT(*)viene calcolato una volta per ogni valore unico di OrderDateKey. L'inclusione della ORDER BYclausola non comporta che venga calcolata due volte. Puoi vedere il piano di esecuzione qui .

Ora, considera una query che restituirà gli stessi esatti risultati ma è scritta in un modo diverso:

SELECT OrderDateKey, SUM(1)
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

Query Optimizer non è abbastanza intelligente da combinarli, quindi verrà svolto un ulteriore lavoro:

hash match 2

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.