In definitiva, non è possibile forzare SQL Server a valutare un UDF scalare solo una volta in una query. Tuttavia, ci sono alcuni passaggi che possono essere presi per incoraggiarlo. Con i test credo che sia possibile ottenere qualcosa che funzioni con la versione corrente di SQL Server, ma è possibile che le modifiche future richiedano la revisione del codice.
Se è possibile modificare il codice, una buona cosa da provare è se possibile rendere deterministica la funzione. Paul White sottolinea qui che la funzione deve essere creata con l' SCHEMABINDING
opzione e che il codice funzione stesso deve essere deterministico.
Dopo aver apportato la seguente modifica:
CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE @tbl TABLE (VAL VARCHAR(5));
-- make the function expensive to call
INSERT INTO @tbl
SELECT [VALUE]
FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');
RETURN 1;
END;
La query dalla domanda viene eseguita in 64 ms:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
Il piano di query non ha più l'operatore filtro:
Per essere sicuri che sia stato eseguito solo una volta, possiamo usare il nuovo DMV sys.dm_exec_function_stats rilasciato in SQL Server 2016:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
Emettere un ALTER
contro la funzione reimposterà il execution_count
per quell'oggetto. La query sopra restituisce 1, il che significa che la funzione è stata eseguita una sola volta.
Si noti che solo perché la funzione è deterministica non significa che verrà valutata una sola volta per qualsiasi query. In effetti, per alcune query l'aggiunta SCHEMABINDING
può peggiorare le prestazioni. Considera la seguente query:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Il superfluo è DISTINCT
stato aggiunto per sbarazzarsi di un operatore di filtro. Il piano sembra promettente:
Sulla base di ciò, ci si aspetterebbe che l'UDF venga valutato una volta e utilizzato come tabella esterna nel join del ciclo nidificato. Tuttavia, l'esecuzione della query richiede 6446 ms sul mio computer. Secondo sys.dm_exec_function_stats
la funzione è stata eseguita 100 volte. Com'è possibile? In " Calcoli scalari, espressioni ed esecuzione del piano di esecuzione ", Paul White sottolinea che l'operatore di calcolo scalare può essere rinviato:
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.
Per questa query sembra che la chiamata UDF sia stata rinviata fino a quando non fosse necessaria, a quel punto è stata valutata 100 volte.
È interessante notare che l'esempio CTE viene eseguito in 71 ms sulla mia macchina quando l'UDF non è definito con SCHEMABINDING
, come nella domanda originale. La funzione viene eseguita una sola volta quando viene eseguita la query. Ecco il piano di query per questo:
Non è chiaro perché lo scalare di calcolo non sia differito. Potrebbe essere perché il non determinismo della funzione limita la riorganizzazione degli operatori che Query Optimizer può fare.
Un approccio alternativo consiste nell'aggiungere una piccola tabella al CTE e interrogare l'unica riga in quella tabella. Qualsiasi tavolino farà, ma usiamo quanto segue:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
La query diventa quindi:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
L'aggiunta dbo.X_ONE_ROW_TABLE
dell'incertezza aggiunge per l'ottimizzatore. Se la tabella ha zero righe, il CTE restituirà 0 righe. In ogni caso, l'ottimizzatore non può garantire che il CTE restituisca una riga se l'UDF non è deterministico, quindi sembra probabile che l'UDF verrà valutato prima del join. Mi aspetto che l'ottimizzatore esegua la scansione dbo.X_ONE_ROW_TABLE
, utilizzi un aggregato di flusso per ottenere il valore massimo di una riga restituita (che richiede la valutazione della funzione) e utilizzarlo come tabella esterna per un loop nidificato da unire dbo.X_100_INTEGERS
nella query principale . Questo sembra essere ciò che accade :
La query viene eseguita in circa 110 ms sulla mia macchina e l'UDF viene valutato una sola volta in base a sys.dm_exec_function_stats
. Sarebbe errato dire che Query Optimizer è costretto a valutare l'UDF solo una volta. Tuttavia, è difficile immaginare una riscrittura dell'ottimizzatore che porterebbe a una query a costi inferiori, anche con le limitazioni relative a UDF e al calcolo dei costi scalari.
In breve, per le funzioni deterministiche (che devono includere l' SCHEMABINDING
opzione) prova a scrivere la query nel modo più semplice possibile. Se su SQL Server 2016 o versione successiva, confermare che la funzione è stata eseguita solo una volta utilizzando sys.dm_exec_function_stats
. I piani di esecuzione possono essere fuorvianti al riguardo.
Affinché le funzioni non considerate da SQL Server siano deterministiche, incluso tutto ciò che manca SCHEMABINDING
dell'opzione, un approccio è quello di inserire l'UDF in un CTE o tabella derivata attentamente predisposti. Ciò richiede un po 'di attenzione, ma lo stesso CTE può funzionare sia per le funzioni deterministiche che non deterministiche.