Database
 sql >> Datenbank >  >> RDS >> Database

Grundlagen von Tabellenausdrücken, Teil 12 – Inline-Tabellenwertfunktionen

Dieser Artikel ist der zwölfte Teil einer Serie über benannte Tabellenausdrücke. Bisher habe ich abgeleitete Tabellen und CTEs behandelt, bei denen es sich um benannte Tabellenausdrücke mit Anweisungsbereich handelt, sowie um Ansichten, bei denen es sich um wiederverwendbare benannte Tabellenausdrücke handelt. Diesen Monat stelle ich Inline-Tabellenwertfunktionen oder iTVFs vor und beschreibe ihre Vorteile im Vergleich zu den anderen benannten Tabellenausdrücken. Ich vergleiche sie auch mit gespeicherten Prozeduren, wobei ich mich hauptsächlich auf Unterschiede in Bezug auf die Standardoptimierungsstrategie konzentriere und das Caching- und Wiederverwendungsverhalten plane. In Bezug auf die Optimierung gibt es viel zu behandeln, also werde ich die Diskussion diesen Monat beginnen und sie nächsten Monat fortsetzen.

In meinen Beispielen verwende ich eine Beispieldatenbank namens TSQLV5. Sie finden das Skript, das es erstellt und füllt, hier und sein ER-Diagramm hier.

Was ist eine Inline-Tabellenwertfunktion?

Im Vergleich zu den zuvor behandelten benannten Tabellenausdrücken ähneln iTVFs hauptsächlich Ansichten. Wie Ansichten werden iTVFs als permanentes Objekt in der Datenbank erstellt und können daher von Benutzern wiederverwendet werden, die über Berechtigungen zur Interaktion mit ihnen verfügen. Der Hauptvorteil von iTVFs gegenüber Ansichten ist die Tatsache, dass sie Eingabeparameter unterstützen. Am einfachsten lässt sich ein iTVF also als parametrisierte Ansicht beschreiben, obwohl Sie es technisch gesehen mit einer CREATE FUNCTION-Anweisung und nicht mit einer CREATE VIEW-Anweisung erstellen.

Es ist wichtig, iTVFs nicht mit Tabellenwertfunktionen mit mehreren Anweisungen (MSTVFs) zu verwechseln. Ersteres ist ein inlinierbarer benannter Tabellenausdruck, der auf einer einzelnen Abfrage ähnlich einer Ansicht basiert, und steht im Mittelpunkt dieses Artikels. Letzteres ist ein programmatisches Modul, das eine Tabellenvariable als Ausgabe zurückgibt, mit einem Fluss mit mehreren Anweisungen in seinem Hauptteil, dessen Zweck es ist, die zurückgegebene Tabellenvariable mit Daten zu füllen.

Syntax

Hier ist die T-SQL-Syntax zum Erstellen eines iTVF:

CREATE [ OR ALTER ] FUNCTION [ . ]

[ () ]

RÜCKGABETABELLE

[ WITH ]

AS

ZURÜCK

[; ]

Beachten Sie in der Syntax die Möglichkeit, Eingabeparameter zu definieren.

Der Zweck des SCHEMABIDNING-Attributs ist derselbe wie bei Ansichten und sollte auf der Grundlage ähnlicher Überlegungen bewertet werden. Einzelheiten finden Sie in Teil 10 der Serie.

Ein Beispiel

Nehmen Sie als Beispiel für ein iTVF an, Sie müssen einen wiederverwendbaren benannten Tabellenausdruck erstellen, der als Eingaben eine Kunden-ID (@custid) und eine Zahl (@n) akzeptiert und die angeforderte Anzahl der letzten Bestellungen aus der Tabelle Sales.Orders zurückgibt für den eingegebenen Kunden.

Sie können diese Aufgabe nicht mit einer Sicht implementieren, da Sichten keine Unterstützung für Eingabeparameter bieten. Wie bereits erwähnt, können Sie sich ein iTVF als parametrisierte Ansicht vorstellen, und als solche ist es das richtige Werkzeug für diese Aufgabe.

Bevor Sie die Funktion selbst implementieren, ist hier der Code zum Erstellen eines unterstützenden Index für die Sales.Orders-Tabelle:

USE TSQLV5;
GO
 
CREATE INDEX idx_nc_cid_odD_oidD_i_eid
  ON Sales.Orders(custid, orderdate DESC, orderid DESC)
  INCLUDE(empid);

Und hier ist der Code zum Erstellen der Funktion namens Sales.GetTopCustOrders:

CREATE OR ALTER FUNCTION Sales.GetTopCustOrders
  ( @custid AS INT, @n AS BIGINT )
RETURNS TABLE
AS
RETURN
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC;
GO

Genau wie bei Basistabellen und Ansichten geben Sie beim Abrufen von Daten iTVFs in der FROM-Klausel einer SELECT-Anweisung an. Hier ist ein Beispiel, in dem die drei letzten Bestellungen für Kunde 1 angefordert werden:

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(1, 3);

Ich bezeichne dieses Beispiel als Abfrage 1. Der Plan für Abfrage 1 ist in Abbildung 1 dargestellt.

Abbildung 1:Plan für Abfrage 1

Was ist Inline an iTVFs?

Falls Sie sich fragen, woher der Begriff inline kommt Bei Inline-Tabellenwertfunktionen hat es damit zu tun, wie sie optimiert werden. Das Inlining-Konzept ist auf alle vier Arten benannter Tabellenausdrücke anwendbar, die T-SQL unterstützt, und beinhaltet teilweise das, was ich in Teil 4 der Serie als Entschachtelung/Ersetzung beschrieben habe. Stellen Sie sicher, dass Sie den entsprechenden Abschnitt in Teil 4 erneut besuchen, wenn Sie eine Auffrischung benötigen.

Wie Sie in Abbildung 1 sehen können, war SQL Server dank der Inline-Funktion in der Lage, einen optimalen Plan zu erstellen, der direkt mit den Indizes der zugrunde liegenden Basistabelle interagiert. In unserem Fall führt der Plan eine Suche im unterstützenden Index durch, den Sie zuvor erstellt haben.

iTVFs gehen mit dem Inlining-Konzept noch einen Schritt weiter, indem sie standardmäßig die Optimierung der Parametereinbettung anwenden. Paul White beschreibt die Optimierung der Parametereinbettung in seinem ausgezeichneten Artikel Parameter Sniffing, Embedding, and the RECOMPILE Options. Bei der Parametereinbettungsoptimierung werden Abfrageparameterreferenzen durch die wörtlichen Konstantenwerte aus der aktuellen Ausführung ersetzt, und dann wird der Code mit den Konstanten optimiert.

Beachten Sie im Plan in Abbildung 1, dass sowohl das Suchprädikat des Index Seek-Operators als auch der Top-Ausdruck des Top-Operators die eingebetteten Literalkonstantenwerte 1 und 3 aus der aktuellen Abfrageausführung anzeigen. Sie zeigen die Parameter @custid bzw. @n nicht an.

Bei iTVFs wird standardmäßig die Optimierung der Parametereinbettung verwendet. Bei gespeicherten Prozeduren werden parametrisierte Abfragen standardmäßig optimiert. Sie müssen OPTION(RECOMPILE) zur Abfrage einer gespeicherten Prozedur hinzufügen, um die Optimierung der Parametereinbettung anzufordern. Weitere Einzelheiten zur Optimierung von iTVFs im Vergleich zu gespeicherten Prozeduren, einschließlich Auswirkungen, in Kürze.

Ändern von Daten über iTVFs

Erinnern Sie sich an Teil 11 der Serie, dass benannte Tabellenausdrücke das Ziel von Änderungsanweisungen sein können, solange bestimmte Anforderungen erfüllt sind. Diese Fähigkeit gilt für iTVFs ähnlich wie für Ansichten. Hier ist zum Beispiel Code, den Sie verwenden könnten, um die drei letzten Bestellungen von Kunde 1 zu löschen (führen Sie diesen nicht wirklich aus):

DELETE FROM Sales.GetTopCustOrders(1, 3);

Insbesondere in unserer Datenbank würde der Versuch, diesen Code auszuführen, aufgrund der Durchsetzung der referenziellen Integrität fehlschlagen (die betroffenen Bestellungen haben zufällig zugehörige Bestellpositionen in der Tabelle „Sales.OrderDetails“), aber es handelt sich um gültigen und unterstützten Code.

iTVFs vs. Stored Procedures

Wie bereits erwähnt, unterscheidet sich die standardmäßige Abfrageoptimierungsstrategie für iTVFs von der für gespeicherte Prozeduren. Bei iTVFs wird standardmäßig die Parametereinbettungsoptimierung verwendet. Bei gespeicherten Prozeduren werden parametrisierte Abfragen standardmäßig optimiert, während Parameter-Sniffing angewendet wird. Um die Parametereinbettung für eine Abfrage einer gespeicherten Prozedur zu erhalten, müssen Sie OPTION(RECOMPILE).

hinzufügen

Wie bei vielen Optimierungsstrategien und -techniken hat die Parametereinbettung ihre Vor- und Nachteile.

Das Hauptplus besteht darin, dass es Abfragevereinfachungen ermöglicht, die manchmal zu effizienteren Plänen führen können. Einige dieser Vereinfachungen sind wirklich faszinierend. Paul demonstriert dies mit gespeicherten Prozeduren in seinem Artikel, und ich werde dies nächsten Monat mit iTVFs demonstrieren.

Der Hauptnachteil der Parametereinbettungsoptimierung ist, dass Sie kein effizientes Plan-Caching und kein Wiederverwendungsverhalten erhalten, wie Sie es bei parametrisierten Plänen tun. Mit jeder eindeutigen Kombination von Parameterwerten erhalten Sie eine eindeutige Abfragezeichenfolge und somit eine separate Kompilierung, die zu einem separaten zwischengespeicherten Plan führt. Mit iTVFs mit konstanten Eingaben können Sie das Wiederverwendungsverhalten von Plänen erreichen, aber nur, wenn dieselben Parameterwerte wiederholt werden. Offensichtlich wird eine Abfrage einer gespeicherten Prozedur mit OPTION(RECOMPILE) einen Plan nicht wiederverwenden, selbst wenn dieselben Parameterwerte auf Anfrage wiederholt werden.

Ich werde drei Fälle demonstrieren:

  1. Wiederverwendbare Pläne mit Konstanten, die sich aus der standardmäßigen Parametereinbettungsoptimierung für iTVF-Abfragen mit Konstanten ergeben
  2. Wiederverwendbare parametrisierte Pläne, die aus der Standardoptimierung parametrisierter gespeicherter Prozedurabfragen resultieren
  3. Nicht wiederverwendbare Pläne mit Konstanten, die aus der Optimierung der Parametereinbettung für Abfragen gespeicherter Prozeduren mit OPTION(RECOMPILE) resultieren

Beginnen wir mit Fall Nr. 1.

Verwenden Sie den folgenden Code, um unser iTVF mit @custid =1 und @n =3 abzufragen:

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(1, 3);

Zur Erinnerung:Dies wäre die zweite Ausführung desselben Codes, da Sie ihn zuvor bereits einmal mit denselben Parameterwerten ausgeführt haben, was zu dem in Abbildung 1 gezeigten Plan geführt hat.

Verwenden Sie den folgenden Code, um das iTVF einmal mit @custid =2 und @n =3 abzufragen:

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(2, 3);

Ich bezeichne diesen Code als Abfrage 2. Der Plan für Abfrage 2 ist in Abbildung 2 dargestellt.

Abbildung 2:Plan für Abfrage 2

Denken Sie daran, dass der Plan in Abbildung 1 für Abfrage 1 auf die konstante Kunden-ID 1 im Suchprädikat verwies, während dieser Plan auf die konstante Kunden-ID 2 verweist.

Verwenden Sie den folgenden Code, um die Statistiken zur Abfrageausführung zu untersuchen:

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders(%';

Dieser Code generiert die folgende Ausgabe:

plan_handle         execution_count text                                           query_plan
------------------- --------------- ---------------------------------------------- ----------------
0x06000B00FD9A1...  1               SELECT ... FROM Sales.GetTopCustOrders(2, 3);  <ShowPlanXML...>
0x06000B00F5C34...  2               SELECT ... FROM Sales.GetTopCustOrders(1, 3);  <ShowPlanXML...>

(2 rows affected)

Hier werden zwei separate Pläne erstellt:einer für die Abfrage mit der Kunden-ID 1, der zweimal verwendet wurde, und ein weiterer für die Abfrage mit der Kunden-ID 2, die einmal verwendet wurde. Bei einer sehr großen Anzahl unterschiedlicher Kombinationen von Parameterwerten erhalten Sie am Ende eine große Anzahl von Zusammenstellungen und zwischengespeicherten Plänen.

Fahren wir mit Fall Nr. 2 fort:der standardmäßigen Optimierungsstrategie parametrisierter Abfragen gespeicherter Prozeduren. Verwenden Sie den folgenden Code, um unsere Abfrage in einer gespeicherten Prozedur namens Sales.GetTopCustOrders2 zu kapseln:

CREATE OR ALTER PROC Sales.GetTopCustOrders2
  ( @custid AS INT, @n AS BIGINT )
AS
  SET NOCOUNT ON;
 
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC;
GO

Verwenden Sie den folgenden Code, um die gespeicherte Prozedur zweimal mit @custid =1 und @n =3 auszuführen:

EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;

Die erste Ausführung löst die Optimierung der Abfrage aus, was zu dem in Abbildung 3 gezeigten parametrisierten Plan führt:

Abbildung 3:Plan für Sales.GetTopCustOrders2-Prozess

Beachten Sie den Verweis auf den Parameter @custid im Suchprädikat und auf den Parameter @n im obersten Ausdruck.

Verwenden Sie den folgenden Code, um die gespeicherte Prozedur mit @custid =2 und @n =3 einmal auszuführen:

EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;

Der in Abbildung 3 gezeigte zwischengespeicherte parametrisierte Plan wird erneut verwendet.

Verwenden Sie den folgenden Code, um die Statistiken zur Abfrageausführung zu untersuchen:

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';

Dieser Code generiert die folgende Ausgabe:

plan_handle         execution_count text                                            query_plan
------------------- --------------- ----------------------------------------------- ----------------
0x05000B00F1604...  3               ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...>

(1 row affected)

Trotz der sich ändernden Kunden-ID-Werte wurde nur ein parametrisierter Plan erstellt und zwischengespeichert und dreimal verwendet.

Fahren wir mit Fall Nr. 3 fort. Wie bereits erwähnt, erhalten Sie bei Abfragen gespeicherter Prozeduren möglicherweise eine Parametereinbettungsoptimierung, wenn Sie OPTION(RECOMPILE) verwenden. Verwenden Sie den folgenden Code, um die Prozedurabfrage so zu ändern, dass sie diese Option enthält:

CREATE OR ALTER PROC Sales.GetTopCustOrders2
  ( @custid AS INT, @n AS BIGINT )
AS
  SET NOCOUNT ON;
 
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC
  OPTION(RECOMPILE);
GO

Führen Sie die Prozedur zweimal mit @custid =1 und @n =3 aus:

EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;

Sie erhalten denselben Plan wie zuvor in Abbildung 1 mit den eingebetteten Konstanten.

Führen Sie die Prozedur mit @custid =2 und @n =3 einmal aus:

EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;

Sie erhalten denselben Plan wie zuvor in Abbildung 2 mit den eingebetteten Konstanten.

Untersuchen Sie die Statistiken zur Abfrageausführung:

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';

Dieser Code generiert die folgende Ausgabe:

plan_handle         execution_count text                                            query_plan
------------------- --------------- ----------------------------------------------- ----------------
0x05000B00F1604...  1               ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...>

(1 row affected)

Der Ausführungszähler zeigt 1 an, was nur die letzte Ausführung widerspiegelt. SQL Server speichert den zuletzt ausgeführten Plan zwischen, sodass Statistiken für diese Ausführung angezeigt werden können, aber auf Anfrage wird der Plan nicht wiederverwendet. Wenn Sie den unter dem Attribut query_plan angezeigten Plan überprüfen, werden Sie feststellen, dass es sich um den Plan handelt, der bei der letzten Ausführung für die Konstanten erstellt wurde, wie zuvor in Abbildung 2 gezeigt.

Wenn Sie nach weniger Kompilierungen und effizientem Plan-Caching und Wiederverwendungsverhalten suchen, ist der standardmäßige Optimierungsansatz für gespeicherte Prozeduren von parametrisierten Abfragen der richtige Weg.

Es gibt einen großen Vorteil, den eine iTVF-basierte Implementierung gegenüber einer auf gespeicherten Prozeduren basierenden Implementierung hat – wenn Sie die Funktion auf jede Zeile in einer Tabelle anwenden und Spalten aus der Tabelle als Eingaben übergeben müssen. Angenommen, Sie müssen die drei letzten Bestellungen für jeden Kunden in der Tabelle „Sales.Customers“ zurückgeben. Kein Abfragekonstrukt ermöglicht es Ihnen, eine gespeicherte Prozedur pro Zeile in einer Tabelle anzuwenden. Sie könnten eine iterative Lösung mit einem Cursor implementieren, aber es ist immer ein guter Tag, an dem Sie Cursor vermeiden können. Wenn Sie den APPLY-Operator mit einem iTVF-Aufruf kombinieren, können Sie die Aufgabe schön und sauber wie folgt ausführen:

SELECT C.custid, O.orderid, O.orderdate, O.empid
FROM Sales.Customers AS C
  CROSS APPLY Sales.GetTopCustOrders( C.custid, 3 ) AS O;

Dieser Code generiert die folgende Ausgabe (abgekürzt):

custid      orderid     orderdate  empid
----------- ----------- ---------- -----------
1           11011       2019-04-09 3
1           10952       2019-03-16 1
1           10835       2019-01-15 1
2           10926       2019-03-04 4
2           10759       2018-11-28 3
2           10625       2018-08-08 3
...

(263 rows affected)

Der Funktionsaufruf wird inline gesetzt und die Referenz auf den Parameter @custid wird durch die Korrelation C.custid ersetzt. Daraus ergibt sich der in Abbildung 4 dargestellte Plan.

Abbildung 4:Planen Sie die Abfrage mit APPLY und Sales.GetTopCustOrders iTVF

Der Plan scannt einen Index in der Sales.Customers-Tabelle, um den Satz von Kunden-IDs abzurufen, und wendet eine Suche im unterstützenden Index an, den Sie zuvor für Sales.Orders pro Kunde erstellt haben. Es gibt nur einen Plan, da die Funktion in die äußere Abfrage eingebettet wurde und sich in einen korrelierten oder lateralen Join verwandelt. Dieser Plan ist sehr effizient, insbesondere wenn die Spalte custid in Sales.Orders sehr dicht ist, d. h. wenn es eine kleine Anzahl unterschiedlicher Kunden-IDs gibt.

Natürlich gibt es andere Möglichkeiten, diese Aufgabe zu implementieren, z. B. die Verwendung eines CTE mit der Funktion ROW_NUMBER. Eine solche Lösung funktioniert tendenziell besser als die APPLY-basierte Lösung, wenn die Spalte custid in der Tabelle Sales.Orders eine geringe Dichte aufweist. Wie auch immer, die spezifische Aufgabe, die ich in meinen Beispielen verwendet habe, ist für die Zwecke unserer Diskussion nicht so wichtig. Mir ging es darum, die verschiedenen Optimierungsstrategien zu erklären, die SQL Server mit den verschiedenen Tools anwendet.

Wenn Sie fertig sind, verwenden Sie den folgenden Code für die Bereinigung:

DROP INDEX IF EXISTS idx_nc_cid_odD_oidD_i_eid ON Sales.Orders;

Zusammenfassung und nächste Schritte

Was haben wir daraus gelernt?

Ein iTVF ist ein wiederverwendbarer parametrisierter benannter Tabellenausdruck.

SQL Server verwendet standardmäßig eine Parametereinbettungsoptimierungsstrategie mit iTVFs und eine parametrisierte Abfrageoptimierungsstrategie mit gespeicherten Prozedurabfragen. Das Hinzufügen von OPTION(RECOMPILE) zu einer Abfrage einer gespeicherten Prozedur kann zu einer Optimierung der Parametereinbettung führen.

Wenn Sie weniger Kompilierungen und ein effizientes Plan-Caching und Wiederverwendungsverhalten erhalten möchten, sind Abfragepläne für parametrisierte Prozeduren der richtige Weg.

Pläne für iTVF-Abfragen werden zwischengespeichert und können wiederverwendet werden, solange dieselben Parameterwerte wiederholt werden.

Sie können die Verwendung des APPLY-Operators und eines iTVF bequem kombinieren, um das iTVF auf jede Zeile aus der linken Tabelle anzuwenden und Spalten aus der linken Tabelle als Eingaben an das iTVF zu übergeben.

Wie bereits erwähnt, gibt es viel über die Optimierung von iTVFs zu berichten. Diesen Monat habe ich iTVFs und gespeicherte Prozeduren im Hinblick auf die Standardoptimierungsstrategie und das geplante Caching- und Wiederverwendungsverhalten verglichen. Nächsten Monat werde ich mich eingehender mit Vereinfachungen befassen, die sich aus der Optimierung der Parametereinbettung ergeben.


No