Wie kann ich erzwingen, dass eine skalare UDF nur einmal in einer Abfrage ausgewertet wird?

12

Ich habe eine Abfrage, die nach dem Ergebnis einer skalaren UDF filtern muss. Die Abfrage muss als einzelne Anweisung gesendet werden (daher kann ich das UDF-Ergebnis keiner lokalen Variablen zuordnen) und ich kann keine TVF verwenden. Mir sind die Leistungsprobleme bekannt, die durch skalare UDFs verursacht werden. Dazu gehören das Erzwingen der seriellen Ausführung des gesamten Plans, übermäßige Speicherzuweisungen, Probleme bei der Schätzung der Kardinalität und das Fehlen von Inlining. Bei dieser Frage gehe ich davon aus, dass ich eine skalare UDF verwenden muss.

Das Aufrufen der UDF selbst ist ziemlich teuer, aber theoretisch können die Abfragen vom Optimierer so logisch implementiert werden, dass die Funktion nur einmal berechnet werden muss. Ich habe ein stark vereinfachtes Beispiel für diese Frage entworfen. Die folgende Abfrage dauert 6152 ms, um auf meinem Computer ausgeführt zu werden:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

Der Filteroperator im Abfrageplan schlägt vor, dass die Funktion für jede Zeile einmal ausgewertet wurde:

Abfrageplan 1

DDL und Datenvorbereitung:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
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;

GO

DROP TABLE IF EXISTS dbo.X_100_INTEGERS;

CREATE TABLE dbo.X_100_INTEGERS (ID INT NOT NULL);

-- insert 100 integers from 1 - 100
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 dbo.X_100_INTEGERS WITH (TABLOCK)
SELECT n FROM Nums WHERE n <= 100;

Hier ist ein DB Fiddle Link für das obige Beispiel, obwohl der Code ungefähr 18 Sekunden braucht, um dort ausgeführt zu werden.

In einigen Fällen kann ich den Funktionscode möglicherweise nicht bearbeiten, da er von einem Hersteller bereitgestellt wird. In anderen Fällen kann ich Änderungen vornehmen. Wie kann ich erzwingen, dass eine skalare UDF nur einmal in einer Abfrage ausgewertet wird?

Joe Obbish
quelle

Antworten:

17

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 SCHEMABINDINGOption 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:

Abfrageplan 1

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 ALTERgegen die Funktion ausgeben, wird das execution_countfü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 SCHEMABINDINGkann 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 DISTINCTwurde hinzugefügt, um einen Filteroperator zu entfernen. Der Plan sieht vielversprechend aus:

Abfrageplan 2

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_statsder 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 SCHEMABINDINGwie 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:

Abfrageplan 3

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_TABLEerhö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_INTEGERSin der Hauptabfrage verwendet . Dies scheint zu sein , was passiert :

Abfrageplan 4

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 SCHEMABINDINGOption 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 SCHEMABINDINGOption 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.

Joe Obbish
quelle