Eine Tabellenwertfunktion mit mehreren Anweisungen gibt das Ergebnis in einer Tabellenvariablen zurück.
Werden diese Ergebnisse jemals wiederverwendet oder wird die Funktion bei jedem Aufruf vollständig ausgewertet?
Eine Tabellenwertfunktion mit mehreren Anweisungen gibt das Ergebnis in einer Tabellenvariablen zurück.
Werden diese Ergebnisse jemals wiederverwendet oder wird die Funktion bei jedem Aufruf vollständig ausgewertet?
Antworten:
Die Ergebnisse einer Funktion mit mehreren Anweisungen und Tabellenwerten (msTVF) werden niemals zwischengespeichert oder über Anweisungen (oder Verbindungen) hinweg wiederverwendet. Es gibt jedoch verschiedene Möglichkeiten, wie ein msTVF-Ergebnis in derselben Anweisung wiederverwendet werden kann . Insofern wird eine msTVF nicht unbedingt bei jedem Aufruf neu aufgefüllt.
Diese (absichtlich ineffiziente) msTVF gibt einen bestimmten Bereich von Ganzzahlen mit einem Zeitstempel in jeder Zeile zurück:
IF OBJECT_ID(N'dbo.IntegerRange', 'TF') IS NOT NULL
DROP FUNCTION dbo.IntegerRange;
GO
CREATE FUNCTION dbo.IntegerRange (@From integer, @To integer)
RETURNS @T table
(
n integer PRIMARY KEY,
ts datetime DEFAULT CURRENT_TIMESTAMP
)
WITH SCHEMABINDING
AS
BEGIN
WHILE @From <= @To
BEGIN
INSERT @T (n)
VALUES (@From);
SET @From = @From + 1;
END;
RETURN;
END;
Wenn alle Parameter des Funktionsaufrufs Konstanten (oder Laufzeitkonstanten) sind, füllt der Ausführungsplan das Ergebnis der Tabellenvariablen einmal aus. Der Rest des Plans kann mehrmals auf die Tabellenvariable zugreifen. Die statische Natur der Tabellenvariablen ist aus dem Ausführungsplan ersichtlich. Beispielsweise:
SELECT
IR.n,
IR.ts
FROM dbo.IntegerRange(1, 5) AS IR
ORDER BY
IR.n;
Gibt ein Ergebnis ähnlich dem folgenden zurück:
Der Ausführungsplan lautet:
Der Operator Sequence ruft zuerst den Operator Table Valued Function auf, der die Tabellenvariable auffüllt (beachten Sie, dass dieser Operator keine Zeilen zurückgibt). Als Nächstes ruft die Sequenz ihre zweite Eingabe auf, die den Inhalt der Tabellenvariablen zurückgibt (in diesem Fall unter Verwendung eines Clustered-Index-Scans).
Das Werbegeschenk, dass der Plan ein "statisches" Tabellenvariablenergebnis verwendet, ist der Operator "Tabellenwertfunktion" unter einer Sequenz. Die Tabellenvariable muss einmal vollständig gefüllt werden, bevor der Rest des Plans ausgeführt werden kann.
Um zu zeigen, dass auf das Ergebnis der Tabellenvariablen mehrmals zugegriffen wird, verwenden wir eine zweite Tabelle mit Zeilen, die von 1 bis 5 nummeriert sind:
IF OBJECT_ID(N'dbo.T', 'U') IS NOT NULL
DROP TABLE dbo.T;
CREATE TABLE dbo.T (i integer NOT NULL);
INSERT dbo.T (i)
VALUES (1), (2), (3), (4), (5);
Und eine neue Abfrage, die diese Tabelle mit unserer Funktion verknüpft (dies könnte auch als eine geschrieben werden APPLY
):
SELECT T.i,
IR.n,
IR.ts
FROM dbo.T AS T
JOIN dbo.IntegerRange(1, 5) AS IR
ON IR.n = T.i;
Das Ergebnis ist:
Der Ausführungsplan:
Wie zuvor füllt die Sequenz zuerst die Tabellenvariable msTVF result. Als nächstes werden verschachtelte Schleifen verwendet, um jede Zeile aus der Tabelle zu verknüpfenT
mit einer Zeile aus dem msTVF-Ergebnis zu verbinden. Da die Funktionsdefinition einen hilfreichen Index für die Tabellenvariable enthielt, kann eine Indexsuche verwendet werden.
Der entscheidende Punkt ist, dass der Plan zwei separate Operatoren für das Ergebnis der msTVF-Tabellenvariablen enthält, wenn die Parameter der msTVF Konstanten sind (einschließlich Variablen und Parameter) oder als Laufzeitkonstanten für die Anweisung von der Ausführungsengine behandelt werden: einen zum Auffüllen der Tabelle; eine andere, um auf die Ergebnisse zuzugreifen, möglicherweise mehrmals auf die Tabelle zuzugreifen und möglicherweise Indizes zu verwenden, die in der Funktionsdefinition deklariert sind.
Um die Unterschiede hervorzuheben, wenn korrelierte Parameter (äußere Referenzen) oder nicht konstante Funktionsparameter verwendet werden, werden wir den Inhalt der Tabelle ändern, T
damit die Funktion viel mehr zu tun hat:
TRUNCATE TABLE dbo.T;
INSERT dbo.T (i)
VALUES (50001), (50002), (50003), (50004), (50005);
Die folgende geänderte Abfrage verwendet jetzt einen äußeren Verweis auf eine Tabelle T
in einem der Funktionsparameter:
SELECT T.i,
IR.n,
IR.ts
FROM dbo.T AS T
CROSS APPLY dbo.IntegerRange(1, T.i) AS IR
WHERE IR.n = T.i;
Diese Abfrage dauert ungefähr 8 Sekunden , um folgende Ergebnisse zurückzugeben:
Beachten Sie den Zeitunterschied zwischen den Zeilen in der Spalte ts
. Die WHERE
Klausel begrenzt das Endergebnis für eine Ausgabe mit vernünftiger Größe, aber die ineffiziente Funktion benötigt noch eine Weile, um die Tabellenvariable mit ungeraden 50.000 Zeilen zu füllen (abhängig vom korrelierten Wert von i
from table T
).
Der Ausführungsplan lautet:
Beachten Sie das Fehlen eines Sequenzoperators. Jetzt gibt es einen einzelnen Tabellenwertfunktionsoperator, der die Tabellenvariable auffüllt und ihre Zeilen bei jeder Iteration zurückgibt des Joins mit verschachtelten Schleifen .
Um es klar auszudrücken: Mit nur 5 Zeilen in Tabelle T wird der Operator "Tabellenwertfunktion" fünfmal ausgeführt. Es generiert 50.001 Zeilen in der ersten Iteration, 50.002 in der zweiten ... und so weiter. Die Tabellenvariable wird zwischen den Iterationen "weggeworfen" (abgeschnitten), sodass jeder der fünf Aufrufe eine vollständige Grundgesamtheit darstellt. Aus diesem Grund ist es so langsam und es dauert ungefähr dieselbe Zeit, bis jede Zeile im Ergebnis angezeigt wird.
Randnotizen:
Das obige Szenario wurde natürlich absichtlich entworfen, um zu zeigen, wie schlecht die Leistung sein kann, wenn die msTVF bei jeder Iteration viele Zeilen auffüllt.
Eine sinnvolle Implementierung des obigen Codes würde beide msTVF-Parameter auf setzen i
und die redundante WHERE
Klausel entfernen . Die Tabellenvariable wird bei jeder Iteration immer noch abgeschnitten und neu aufgefüllt, jedoch nur mit jeweils einer Zeile.
Wir könnten auch die Minimal- und Maximalwerte i
abrufen T
und sie in einem vorherigen Schritt in Variablen speichern. Das Aufrufen der Funktion mit Variablen anstelle von korrelierten Parametern würde die Verwendung des 'statischen' Tabellenvariablenmusters ermöglichen, wie bereits erwähnt.
Wenn Sie noch einmal auf die ursprüngliche Frage zurückkommen, bei der das statische Sequenzmuster nicht verwendet werden kann, kann SQL Server dies vermeiden Abschneiden und erneute Auffüllen der msTVF-Tabellenvariablen , wenn sich keiner der korrelierten Parameter seit der vorherigen Iteration eines Nested-Loop-Joins geändert hat.
Um dies zu demonstrieren, werden wir den Inhalt von T
durch fünf identische i
Werte ersetzen :
TRUNCATE TABLE dbo.T;
INSERT dbo.T (i)
VALUES (50005), (50005), (50005), (50005), (50005);
Die Abfrage mit einem korrelierten Parameter noch einmal:
SELECT T.i,
IR.n,
IR.ts
FROM dbo.T AS T
CROSS APPLY dbo.IntegerRange(1, T.i) AS IR
WHERE IR.n = T.i;
Diesmal erscheinen die Ergebnisse in ungefähr 1,5 Sekunden :
Beachten Sie die identischen Zeitstempel in jeder Zeile. Das zwischengespeicherte Ergebnis in der Tabellenvariablen wird für nachfolgende Iterationen wiederverwendet, bei denen der korrelierte Wert i
unverändert bleibt. Die Wiederverwendung des Ergebnisses ist viel schneller als das Einfügen von jeweils 50.005 Zeilen.
Der Ausführungsplan sieht sehr ähnlich aus:
Der Hauptunterschied liegt in den Eigenschaften " Tatsächliche Rückspulungen" und " Tatsächliche Rückspulungen" des Operators "Tabellenwertfunktion":
Wenn sich die korrelierten Parameter nicht ändern, kann SQL Server die aktuellen Ergebnisse in der Tabellenvariablen wiedergeben (zurückspulen). Wenn sich die Korrelation ändert, muss SQL Server die Tabellenvariable abschneiden und neu füllen (erneut binden). Der eine Rebind erfolgt bei der ersten Iteration; Die vier nachfolgenden Iterationen sind alle Rückläufe, da der Wert von T.i
unverändert bleibt.