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

Das Problem mit Fensterfunktionen und -ansichten

Einführung

Seit ihrer Einführung in SQL Server 2005 funktionieren Fenster wie ROW_NUMBER und RANK haben sich als äußerst nützlich bei der Lösung einer Vielzahl gängiger T-SQL-Probleme erwiesen. Bei dem Versuch, solche Lösungen zu verallgemeinern, versuchen Datenbankdesigner oft, sie in Ansichten zu integrieren, um die Kapselung und Wiederverwendung von Code zu fördern. Leider führt eine Einschränkung im SQL Server-Abfrageoptimierer häufig dazu, dass Ansichten, die Fensterfunktionen enthalten, nicht die erwartete Leistung erbringen. Dieser Beitrag geht ein veranschaulichendes Beispiel des Problems durch, erläutert die Gründe und bietet eine Reihe von Problemumgehungen.

Dieses Problem kann auch in abgeleiteten Tabellen, allgemeinen Tabellenausdrücken und Inline-Funktionen auftreten, aber ich sehe es am häufigsten bei Ansichten, weil sie absichtlich allgemeiner geschrieben sind.

Fensterfunktionen

Fensterfunktionen werden durch das Vorhandensein eines OVER() unterschieden -Klausel und gibt es in drei Varianten:

  • Ranking-Fensterfunktionen
    • ROW_NUMBER
    • RANK
    • DENSE_RANK
    • NTILE
  • Aggregierte Fensterfunktionen
    • MIN , MAX , AVG , SUM
    • COUNT , COUNT_BIG
    • CHECKSUM_AGG
    • STDEV , STDEVP , VAR , VARP
  • Analysefensterfunktionen
    • LAG , LEAD
    • FIRST_VALUE , LAST_VALUE
    • PERCENT_RANK , PERCENTILE_CONT , PERCENTILE_DISC , CUME_DIST

Die Ranking- und Aggregat-Fensterfunktionen wurden in SQL Server 2005 eingeführt und in SQL Server 2012 erheblich erweitert. Die analytischen Fensterfunktionen sind neu für SQL Server 2012.

Alle oben aufgeführten Fensterfunktionen sind anfällig für die in diesem Artikel beschriebene Einschränkung des Optimierers.

Beispiel

Unter Verwendung der AdventureWorks-Beispieldatenbank besteht die vorliegende Aufgabe darin, eine Abfrage zu schreiben, die alle Transaktionen für Produkt Nr. 878 zurückgibt, die am letzten verfügbaren Datum stattgefunden haben. Es gibt alle möglichen Möglichkeiten, diese Anforderung in T-SQL auszudrücken, aber wir werden uns dafür entscheiden, eine Abfrage zu schreiben, die eine Fensterfunktion verwendet. Der erste Schritt besteht darin, Transaktionsdatensätze für Produkt Nr. 878 zu finden und sie in absteigender Datumsreihenfolge zu ordnen:

SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY rnk; 

Die Ergebnisse der Abfrage sind wie erwartet, mit sechs Transaktionen, die am letzten verfügbaren Datum stattgefunden haben. Der Ausführungsplan enthält ein Warndreieck, das uns auf einen fehlenden Index hinweist:

Wie bei fehlenden Indexvorschlägen üblich, müssen wir bedenken, dass die Empfehlung nicht das Ergebnis einer gründlichen Analyse der Anfrage ist – es ist eher ein Hinweis darauf, dass wir ein wenig darüber nachdenken müssen, wie diese Anfrage auf die benötigten Daten zugreift.

Der vorgeschlagene Index wäre sicherlich effizienter, als die Tabelle vollständig zu scannen, da er eine Indexsuche nach dem bestimmten Produkt, an dem wir interessiert sind, ermöglichen würde. Der Index würde auch alle erforderlichen Spalten abdecken, aber er würde die Sortierung nicht vermeiden (nach TransactionDate absteigend). Der ideale Index für diese Abfrage würde eine Suche nach ProductID zulassen , geben Sie die ausgewählten Datensätze in umgekehrter Reihenfolge TransactionDate zurück sortieren und die anderen zurückgegebenen Spalten abdecken:

NICHT EINGESCHLOSSENEN INDEX ERSTELLEN ixON Production.TransactionHistory (ProductID, TransactionDate DESC)INCLUDE (ReferenceOrderID, Quantity);

Mit diesem Index ist der Ausführungsplan viel effizienter. Der Clustered-Index-Scan wurde durch eine Bereichssuche ersetzt, und eine explizite Sortierung ist nicht mehr erforderlich:

Der letzte Schritt für diese Abfrage besteht darin, die Ergebnisse auf die Zeilen mit Rang #1 zu beschränken. Wir können nicht direkt im WHERE filtern -Klausel unserer Abfrage, da Fensterfunktionen nur im SELECT vorkommen dürfen und ORDER BY Klauseln.

Wir können diese Einschränkung umgehen, indem wir eine abgeleitete Tabelle, einen gemeinsamen Tabellenausdruck, eine Funktion oder eine Ansicht verwenden. Bei dieser Gelegenheit verwenden wir einen allgemeinen Tabellenausdruck (auch bekannt als Inline-Ansicht):

WITH RatedTransactions AS( SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( ORDER BY th.TransactionDate DESC) FROM Production.TransactionHistory AS th WHERE th.ProductID =878 )SELECT TransactionID, ReferenceOrderID, TransactionDate, QuantityFROM RatedTransactionsWHERE rnk =1;

Der Ausführungsplan ist derselbe wie zuvor, mit einem zusätzlichen Filter, um nur Zeilen mit Rang #1 zurückzugeben:

Die Abfrage gibt die sechs gleichrangigen Zeilen zurück, die wir erwarten:

Verallgemeinern der Abfrage

Es stellt sich heraus, dass unsere Abfrage sehr nützlich ist, also wird die Entscheidung getroffen, sie zu verallgemeinern und die Definition in einer Ansicht zu speichern. Damit dies für jedes Produkt funktioniert, müssen wir zwei Dinge tun:die ProductID zurückgeben aus der Ansicht und partitionieren Sie die Ranking-Funktion nach Produkt:

CREATE VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.QuantityFROM ( SELECT th.ProductID, th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER (PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) FROM Production.TransactionHistory AS th) AS sq1WHERE sq1.rnk =1;

Das Auswählen aller Zeilen aus der Ansicht führt zu folgendem Ausführungsplan und korrekten Ergebnissen:

Wir können jetzt die letzten Transaktionen für Produkt 878 mit einer viel einfacheren Abfrage in der Ansicht finden:

SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878;

Wir erwarten, dass der Ausführungsplan für diese neue Abfrage genau derselbe ist wie vor dem Erstellen der Ansicht. Der Abfrageoptimierer sollte in der Lage sein, den in WHERE angegebenen Filter zu pushen -Klausel nach unten in die Ansicht, was zu einer Indexsuche führt.

Wir müssen an dieser Stelle jedoch innehalten und ein wenig nachdenken. Der Abfrageoptimierer kann nur Ausführungspläne erstellen, die garantiert die gleichen Ergebnisse liefern wie die logische Abfragespezifikation – ist es sicher, unseren WHERE zu pushen -Klausel in die Ansicht?PARTITION BY erscheint -Klausel der Fensterfunktion in der Ansicht. Der Grund dafür ist, dass das Entfernen vollständiger Gruppen (Partitionen) aus der Fensterfunktion die Rangfolge der von der Abfrage zurückgegebenen Zeilen nicht beeinflusst. Die Frage ist, weiß der SQL Server-Abfrageoptimierer das? Die Antwort hängt davon ab, welche Version von SQL Server wir ausführen.

SQL Server 2005-Ausführungsplan

Ein Blick auf die Filtereigenschaften in diesem Plan zeigt, dass zwei Prädikate angewendet werden:

Die ProductID = 878 Das Prädikat wurde nicht nach unten in die Ansicht verschoben, was zu einem Plan führt, der unseren Index scannt und jede Zeile in der Tabelle einordnet, bevor nach Produkt Nr. 878 und den Zeilen Nr. 1 gefiltert wird.

Der SQL Server 2005-Abfrageoptimierer kann keine geeigneten Prädikate über eine Fensterfunktion in einem niedrigeren Abfragebereich hinausschieben (Ansicht, allgemeiner Tabellenausdruck, Inline-Funktion oder abgeleitete Tabelle). Diese Einschränkung gilt für alle SQL Server 2005-Builds.

Ausführungsplan für SQL Server 2008+

Dies ist der Ausführungsplan für dieselbe Abfrage auf SQL Server 2008 oder höher:

Die ProductID Das Prädikat wurde erfolgreich an den Ranking-Operatoren vorbeigeschoben und ersetzte den Index-Scan durch die effiziente Index-Suche.

Der Abfrageoptimierer von 2008 enthält eine neue Vereinfachungsregel SelOnSeqPrj (auf Sequenzprojekt auswählen), das in der Lage ist, sichere äußere Prädikate über Fensterfunktionen hinaus zu verschieben. Um den weniger effizienten Plan für diese Abfrage in SQL Server 2008 oder höher zu erstellen, müssen wir diese Funktion des Abfrageoptimierers vorübergehend deaktivieren:

SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878OPTION (QUERYRULEOFF SelOnSeqPrj);

Leider ist der SelOnSeqPrj Vereinfachungsregel funktioniert nur wenn das Prädikat einen Vergleich mit einer Konstanten durchführt . Aus diesem Grund erzeugt die folgende Abfrage den suboptimalen Plan auf SQL Server 2008 und höher:

DECLARE @ProductID INT =878; SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;

Das Problem kann auch dann noch auftreten, wenn das Prädikat einen konstanten Wert verwendet. SQL Server kann beschließen, triviale Abfragen automatisch zu parametrisieren (eine Abfrage, für die es einen offensichtlich besten Plan gibt). Wenn die automatische Parametrisierung erfolgreich ist, sieht der Optimierer einen Parameter anstelle einer Konstante und den SelOnSeqPrj Regel wird nicht angewendet.

Bei Abfragen, bei denen keine automatische Parametrisierung versucht wird (oder bei denen festgestellt wird, dass sie unsicher ist), kann die Optimierung dennoch fehlschlagen, wenn die Datenbankoption für FORCED PARAMETERIZATION ist an. Unsere Testabfrage (mit dem konstanten Wert 878) ist für die automatische Parametrisierung nicht sicher, aber die erzwungene Parametrisierungseinstellung setzt dies außer Kraft, was zu dem ineffizienten Plan führt:

ALTER DATABASE AdventureWorksSET PARAMETERIZATION FORCED;GOSELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878;GOALTER DATABASE AdventureWorksSET PARAMETERISATION SIMPLE;

Problemumgehung für SQL Server 2008+

Damit der Optimierer einen konstanten Wert für eine Abfrage „sehen“ kann, die auf eine lokale Variable oder einen Parameter verweist, können wir eine OPTION (RECOMPILE) hinzufügen Abfragehinweis:

DECLARE @ProductID INT =878; SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductIDOPTION (RECOMPILE);

Hinweis: Der Ausführungsplan vor der Ausführung („geschätzt“) zeigt immer noch einen Index-Scan, da der Wert der Variablen noch nicht tatsächlich festgelegt ist. Wenn die Abfrage ausgeführt wird , der Ausführungsplan zeigt jedoch den gewünschten Indexsuchplan:

Der SelOnSeqPrj Regel existiert in SQL Server 2005 nicht, also OPTION (RECOMPILE) kann da nicht helfen. Falls Sie sich fragen, die OPTION (RECOMPILE) Problemumgehung führt zu einer Suche, selbst wenn die Datenbankoption für erzwungene Parametrisierung aktiviert ist.

Problemumgehung Nr. 1 für alle Versionen

In einigen Fällen ist es möglich, die problematische Ansicht, den allgemeinen Tabellenausdruck oder die abgeleitete Tabelle durch eine parametrisierte Inline-Tabellenwertfunktion zu ersetzen:

CREATE FUNCTION dbo.MostRecentTransactionsForProduct( @ProductID integer) RETURN TABLEWITH SCHEMABINDING ASRETURN SELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.Quantity FROM ( SELECT th.ProductID, th.TransactionID, th. ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) FROM Production.TransactionHistory AS th WHERE th.ProductID =@ProductID ) AS sq1 WHERE sq1.rnk =1;

Diese Funktion platziert explizit die ProductID Prädikat im selben Bereich wie die Fensterfunktion, wodurch die Optimierungseinschränkung vermieden wird. Unsere Beispielabfrage, die zur Verwendung der Inline-Funktion geschrieben wurde, wird zu:

SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsForProduct(878) AS mrt;

Dadurch wird der gewünschte Indexsuchplan auf allen Versionen von SQL Server erstellt, die Fensterfunktionen unterstützen. Diese Problemumgehung erzeugt eine Suche, selbst wenn das Prädikat auf einen Parameter oder eine lokale Variable verweist – OPTION (RECOMPILE) ist nicht erforderlich.PARTITION BY zu entfernen -Klausel zu verwenden und die ProductID nicht mehr zurückzugeben Säule. Ich habe die Definition so belassen wie die Ansicht, die sie ersetzt hat, um die Ursache der Ausführungsplanunterschiede klarer darzustellen.

Problemumgehung Nr. 2 für alle Versionen

Die zweite Problemumgehung gilt nur für Ranking-Fensterfunktionen, die gefiltert werden, um Zeilen mit der Nummer oder Rangnummer 1 zurückzugeben (unter Verwendung von ROW_NUMBER , RANK , oder DENSE_RANK ). Dies ist jedoch eine sehr häufige Verwendung, daher ist es erwähnenswert.

Ein zusätzlicher Vorteil besteht darin, dass diese Problemumgehung zu noch effizienteren Plänen führen kann als die zuvor gesehenen Indexsuchpläne. Zur Erinnerung:Der bisherige beste Plan sah so aus:

Dieser Ausführungsplan hat den Rang 1.918 Zeilen, obwohl es letztendlich nur 6 zurückgibt . Wir können diesen Ausführungsplan verbessern, indem wir die Fensterfunktion in einem ORDER BY verwenden -Klausel, anstatt Zeilen zu ordnen und dann nach Rang #1 zu filtern:

SELECT TOP (1) WITH TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY RANK() OVER ( ORDER BY th.TransactionDate DESC);

Diese Abfrage veranschaulicht schön die Verwendung einer Fensterfunktion in ORDER BY -Klausel, aber wir können es noch besser machen, indem wir die Fensterfunktion vollständig eliminieren:

SELECT TOP (1) WITH TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY th.TransactionDate DESC;

Dieser Plan liest nur 7 Zeilen aus der Tabelle, um dieselbe 6-zeilige Ergebnismenge zurückzugeben. Warum 7 Reihen? Der Top-Operator läuft in WITH TIES Modus:

Es fordert weiterhin jeweils eine Zeile von seinem Teilbaum an, bis sich das TransactionDate ändert. Die siebte Reihe ist für den Top erforderlich, um sicherzustellen, dass keine weiteren Reihen mit gebundenem Wert qualifiziert werden.

Wir können die Logik der obigen Abfrage erweitern, um die problematische Ansichtsdefinition zu ersetzen:

ALTER VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT p.ProductID, Ranking1.TransactionID, Ranking1.ReferenceOrderID, Ranking1.TransactionDate, Ranking1.QuantityFROM – Liste der Produkt-IDs (SELECT ProductID FROM Production.Product) AS pCROSS APPLY( – Gibt den Rang zurück #1 Ergebnisse für jede Produkt-ID SELECT TOP (1) WITH TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity FROM Production.TransactionHistory AS WHERE th.ProductID =p.ProductID ORDER BY th.TransactionDate DESC) AS Rang 1;

Die Ansicht verwendet jetzt ein CROSS APPLY um die Ergebnisse unseres optimierten ORDER BY zu kombinieren Abfrage für jedes Produkt. Unsere Testabfrage ist unverändert:

DECLARE @ProductID integer;SET @ProductID =878; SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;

Sowohl Vor- als auch Nachausführungspläne zeigen eine Indexsuche, ohne dass eine OPTION (RECOMPILE) erforderlich ist Abfragehinweis. Das Folgende ist ein Plan nach der Ausführung („tatsächlich“):

Wenn die Ansicht ROW_NUMBER verwendet hatte statt RANK , hätte die Ersetzungsansicht einfach das WITH TIES weggelassen -Klausel auf TOP (1) . Die neue Ansicht könnte natürlich auch als parametrisierte Inline-Tabellenwertfunktion geschrieben werden.

Man könnte argumentieren, dass der ursprüngliche Index-Suchplan mit dem rnk = 1 Prädikat könnte auch optimiert werden, um nur 7 Zeilen zu testen. Schließlich sollte der Optimierer wissen, dass Rankings vom Sequence Project-Operator in streng aufsteigender Reihenfolge erstellt werden, sodass die Ausführung enden könnte, sobald eine Zeile mit einem Rang größer als eins gesehen wird. Der Optimierer enthält diese Logik heute jedoch nicht.

Abschließende Gedanken

Benutzer sind oft von der Leistung von Ansichten enttäuscht, die Fensterfunktionen enthalten. Der Grund kann oft auf die in diesem Beitrag beschriebene Einschränkung des Optimierers zurückgeführt werden (oder vielleicht weil der View-Designer nicht erkannt hat, dass Prädikate, die auf die View angewendet werden, in PARTITION BY erscheinen müssen Klausel sicher nach unten gedrückt werden).

Ich möchte betonen, dass diese Einschränkung nicht nur für Aufrufe gilt und auch nicht auf ROW_NUMBER beschränkt ist , RANK und DENSE_RANK . Sie sollten sich dieser Einschränkung bewusst sein, wenn Sie eine Funktion mit einem OVER verwenden -Klausel in einer Ansicht, allgemeiner Tabellenausdruck, abgeleitete Tabelle oder Inline-Tabellenwertfunktion.

Benutzer von SQL Server 2005, die auf dieses Problem stoßen, stehen vor der Wahl, die Ansicht als parametrisierte Inline-Tabellenwertfunktion umzuschreiben oder APPLY zu verwenden Technik (falls zutreffend).

Benutzer von SQL Server 2008 haben die zusätzliche Option, eine OPTION (RECOMPILE) zu verwenden Abfragehinweis, ob das Problem gelöst werden kann, indem dem Optimierer erlaubt wird, eine Konstante anstelle einer Variablen- oder Parameterreferenz zu sehen. Denken Sie jedoch daran, die Pläne nach der Ausführung zu überprüfen, wenn Sie diesen Hinweis verwenden:Der Plan vor der Ausführung kann im Allgemeinen nicht den optimalen Plan anzeigen.