Letztendlich ist es nicht möglich, SQL Server zu zwingen, eine skalare UDF nur einmal in einer Abfrage auszuwerten. Es gibt jedoch einige Schritte, die unternommen werden können, um dies zu fördern. Ich bin der Meinung, dass Sie mit dem Testen etwas bekommen können, das mit der aktuellen Version von SQL Server funktioniert, aber es ist möglich, dass zukünftige Änderungen eine erneute Überprüfung Ihres Codes erfordern.
Wenn es möglich ist, den Code zu bearbeiten, sollten Sie zunächst versuchen, die Funktion möglichst deterministisch zu machen. Paul White weist hier darauf hin, dass die Funktion mit der SCHEMABINDING
Option erstellt werden muss und der Funktionscode selbst deterministisch sein muss.
Nach der folgenden Änderung:
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;
Die Abfrage aus der Frage wird in 64 ms ausgeführt:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
Der Abfrageplan verfügt nicht mehr über den Filteroperator:
Um sicherzugehen, dass es nur einmal ausgeführt wird, können wir die neue DMV sys.dm_exec_function_stats verwenden, die in SQL Server 2016 veröffentlicht wurde:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
Wenn Sie ein ALTER
gegen die Funktion ausgeben, wird das execution_count
für dieses Objekt zurückgesetzt. Die obige Abfrage gibt 1 zurück, was bedeutet, dass die Funktion nur einmal ausgeführt wurde.
Nur weil die Funktion deterministisch ist, bedeutet dies nicht, dass sie für jede Abfrage nur einmal ausgewertet wird. Bei einigen Abfragen SCHEMABINDING
kann das Hinzufügen sogar die Leistung beeinträchtigen. Betrachten Sie die folgende Abfrage:
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;
Das Überflüssige DISTINCT
wurde hinzugefügt, um einen Filteroperator zu entfernen. Der Plan sieht vielversprechend aus:
Basierend darauf würde man erwarten, dass die UDF einmal ausgewertet wird und als äußere Tabelle in dem Join mit verschachtelten Schleifen verwendet wird. Es dauert jedoch 6446 ms, bis die Abfrage auf meinem Computer ausgeführt wird. Laut sys.dm_exec_function_stats
der Funktion wurde 100 mal ausgeführt. Wie ist das möglich? In "Leistung von Skalaren, Ausdrücken und Ausführungsplänen berechnen " weist Paul White darauf hin, dass der Operator "Skalar berechnen" verschoben werden kann:
In den meisten Fällen definiert ein Compute Scalar einfach einen Ausdruck. Die eigentliche Berechnung wird verschoben, bis etwas später im Ausführungsplan das Ergebnis benötigt.
Bei dieser Abfrage wurde der UDF-Aufruf anscheinend so lange zurückgestellt, bis er benötigt wurde. Zu diesem Zeitpunkt wurde er 100 Mal ausgewertet.
Interessanterweise wird das CTE-Beispiel in 71 ms auf meinem Computer ausgeführt, wenn die UDF nicht SCHEMABINDING
wie in der ursprünglichen Frage mit definiert ist. Die Funktion wird nur einmal ausgeführt, wenn die Abfrage ausgeführt wird. Hier ist der Abfrageplan dafür:
Es ist nicht klar, warum der Compute Scalar nicht zurückgestellt wird. Dies kann daran liegen, dass der Nichtdeterminismus der Funktion die Neuanordnung der Operatoren, die der Abfrageoptimierer ausführen kann, einschränkt.
Ein alternativer Ansatz besteht darin, dem CTE eine kleine Tabelle hinzuzufügen und die einzige Zeile in dieser Tabelle abzufragen. Jeder kleine Tisch reicht aus, aber verwenden wir Folgendes:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
Die Abfrage lautet dann:
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;
Das Hinzufügen von dbo.X_ONE_ROW_TABLE
erhöht die Unsicherheit für den Optimierer. Wenn die Tabelle keine Zeilen enthält, gibt der CTE 0 Zeilen zurück. In jedem Fall kann das Optimierungsprogramm nicht garantieren, dass der CTE eine Zeile zurückgibt, wenn die UDF nicht deterministisch ist. Daher ist es wahrscheinlich, dass die UDF vor dem Join ausgewertet wird. Ich würde erwarten, dass der Optimierer scannt dbo.X_ONE_ROW_TABLE
, ein Stream-Aggregat verwendet, um den Maximalwert der einen zurückgegebenen Zeile zu erhalten (für die die Funktion ausgewertet werden muss) und dies als äußere Tabelle für einen Join mit einer verschachtelten Schleife dbo.X_100_INTEGERS
in der Hauptabfrage verwendet . Dies scheint zu sein , was passiert :
Die Abfrage läuft in ca. 110 ms auf meinem Rechner ab und die UDF wird laut nur einmal ausgewertet sys.dm_exec_function_stats
. Es wäre falsch zu sagen, dass das Abfrageoptimierungsprogramm gezwungen ist, die UDF nur einmal auszuwerten. Es ist jedoch schwer vorstellbar, dass ein Optimierungsprogramm zu einer kostengünstigeren Abfrage führt, selbst wenn die Einschränkungen in Bezug auf UDF und die Berechnung der skalaren Kosten bestehen.
Zusammenfassend kann gesagt werden, dass Sie für deterministische Funktionen (die die SCHEMABINDING
Option enthalten müssen) versuchen, die Abfrage so einfach wie möglich zu schreiben. Stellen Sie sicher, dass die Funktion unter SQL Server 2016 oder einer späteren Version nur einmal ausgeführt wurde sys.dm_exec_function_stats
. Ausführungspläne können in dieser Hinsicht irreführend sein.
Für Funktionen, die von SQL Server nicht als deterministisch angesehen werden, einschließlich aller Funktionen, für die die SCHEMABINDING
Option fehlt , besteht ein Ansatz darin, die UDF in einem sorgfältig gestalteten CTE oder einer abgeleiteten Tabelle abzulegen. Dies erfordert ein wenig Sorgfalt, aber der gleiche CTE kann sowohl für deterministische als auch für nicht deterministische Funktionen funktionieren.