Ich habe eine Klasse von Abfragen, die die Existenz eines von zwei Dingen testen. Es ist von der Form
SELECT CASE
WHEN EXISTS (SELECT 1 FROM ...)
OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;
Die eigentliche Anweisung wird in C generiert und als Ad-hoc-Abfrage über eine ODBC-Verbindung ausgeführt.
Kürzlich hat sich herausgestellt, dass das zweite SELECT in den meisten Fällen wahrscheinlich schneller sein wird als das erste SELECT, und dass das Ändern der Reihenfolge der beiden EXISTS-Klauseln in mindestens einem missbräuchlichen Testfall, den wir gerade erstellt hatten, zu einer drastischen Beschleunigung führte.
Das Offensichtliche ist, einfach die beiden Klauseln zu wechseln, aber ich wollte sehen, ob jemand, der mit SQL Server besser vertraut ist, dies abwägen möchte. Es fühlt sich so an, als würde ich mich auf Zufall und ein "Implementierungsdetail" verlassen.
(Es scheint auch so, als würde SQL Server, wenn es intelligenter wäre, beide EXISTS-Klauseln parallel ausführen und den jeweils ersten zuerst den anderen kurzschließen lassen.)
Gibt es eine bessere Möglichkeit, SQL Server dazu zu bringen, die Laufzeit einer solchen Abfrage konsistent zu verbessern?
Aktualisieren
Vielen Dank für Ihre Zeit und Ihr Interesse an meiner Frage. Ich hatte keine Fragen zu den tatsächlichen Abfrageplänen erwartet, bin aber bereit, sie zu teilen.
Dies gilt für eine Softwarekomponente, die SQL Server 2008R2 und höher unterstützt. Die Form der Daten kann je nach Konfiguration und Verwendung sehr unterschiedlich sein. Mein Kollege hat darüber nachgedacht, diese Änderung an der Abfrage vorzunehmen, da die (im Beispiel) dbf_1162761$z$rv$1257927703
Tabelle immer mehr oder gleich der Anzahl der Zeilen enthält als die dbf_1162761$z$dd$1257927703
Tabelle - manchmal deutlich mehr (Größenordnungen).
Hier ist der missbräuchliche Fall, den ich erwähnt habe. Die erste Abfrage ist die langsame und dauert etwa 20 Sekunden. Die zweite Abfrage wird sofort abgeschlossen.
Für das, was es wert ist, wurde kürzlich auch das Bit "OPTIMIZE FOR UNKNOWN" hinzugefügt, da das Parameter-Sniffing bestimmte Fälle verwarf.
Ursprüngliche Abfrage:
SELECT CASE
WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Ursprünglicher Plan:
|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
|--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
|--Constant Scan
|--Concatenation
|--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
| |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
| |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
|--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
|--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]), WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
|--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]), WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
Feste Abfrage:
SELECT CASE
WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Fester Plan:
|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
|--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
|--Constant Scan
|--Concatenation
|--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
| |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]), WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
| |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]), WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
|--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
|--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
|--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
quelle
Antworten:
Als allgemeine Faustregel führt SQL Server die Teile einer
CASE
Anweisung in der angegebenen Reihenfolge aus, kann jedoch dieOR
Bedingungen neu anordnen . Bei einigen Abfragen können Sie eine durchweg bessere Leistung erzielen, indem Sie die Reihenfolge derWHEN
Ausdrücke in einerCASE
Anweisung ändern . Manchmal können Sie auch eine bessere Leistung erzielen, wenn Sie die Reihenfolge der Bedingungen in einerOR
Anweisung ändern , aber das Verhalten ist nicht garantiert.Es ist wahrscheinlich am besten, mit einem einfachen Beispiel durchzugehen. Ich teste gegen SQL Server 2016, daher ist es möglich, dass Sie auf Ihrem Computer nicht genau die gleichen Ergebnisse erzielen, aber meines Wissens gelten dieselben Prinzipien. Zuerst füge ich eine Million Ganzzahlen von 1 bis 1000000 in zwei Tabellen ein, eine mit einem Clustered-Index und eine als Heap:
Betrachten Sie die folgende Abfrage:
Wir wissen, dass die Bewertung der Unterabfrage
X_CI
anhand der Unterabfrage viel billiger istX_HEAP
, insbesondere wenn keine übereinstimmende Zeile vorhanden ist. Wenn es keine übereinstimmende Zeile gibt, müssen wir nur einige logische Lesevorgänge für die Tabelle mit einem Clustered-Index durchführen. Wir müssten jedoch alle Zeilen des Heaps scannen, um zu wissen, dass es keine übereinstimmende Zeile gibt. Das weiß auch der Optimierer. Im Allgemeinen ist die Verwendung eines Clustered-Index zum Nachschlagen einer Zeile im Vergleich zum Scannen einer Tabelle sehr kostengünstig.Für diese Beispieldaten würde ich die Abfrage folgendermaßen schreiben:
Dadurch wird SQL Server effektiv gezwungen, die Unterabfrage zuerst mit einem Clustered-Index für die Tabelle auszuführen. Hier sind die Ergebnisse von
SET STATISTICS IO, TIME ON
:Wenn beim Suchen im Abfrageplan bei der Suche auf Etikett 1 Daten zurückgegeben werden, ist der Scan auf Etikett 2 nicht erforderlich und wird nicht durchgeführt:
Die folgende Abfrage ist viel weniger effizient:
Wenn wir uns den Abfrageplan ansehen, sehen wir, dass der Scan auf Etikett 2 immer stattfindet. Wenn eine Zeile gefunden wird, wird die Suche bei Label 1 übersprungen. Das ist nicht die Reihenfolge, die wir wollten:
Die Leistung bestätigt dies:
Zurück zur ursprünglichen Abfrage: Bei dieser Abfrage werden die Suche und der Scan in der Reihenfolge ausgewertet, die für die Leistung gut ist:
Und in dieser Abfrage werden sie in umgekehrter Reihenfolge ausgewertet:
Im Gegensatz zu den vorherigen Abfragepaaren zwingt das SQL Server-Abfrageoptimierungsprogramm jedoch nichts dazu, eine vor der anderen auszuwerten. Sie sollten sich bei nichts Wichtigem auf dieses Verhalten verlassen.
Wenn eine Unterabfrage vor der anderen ausgewertet werden muss, verwenden Sie abschließend eine
CASE
Anweisung oder eine andere Methode, um die Reihenfolge zu erzwingen. Andernfalls können Sie Unterabfragen in einer vonOR
Ihnen gewünschten Bedingung bestellen. Sie können jedoch nicht garantieren, dass das Optimierungsprogramm sie in der angegebenen Reihenfolge ausführt.Nachtrag:
Eine natürliche Folgefrage ist, was Sie tun können, wenn SQL Server entscheiden soll, welche Abfrage billiger ist, und diese zuerst ausführen soll. Alle bisherigen Methoden scheinen von SQL Server in der Reihenfolge implementiert zu sein, in der die Abfrage geschrieben wird, auch wenn für einige von ihnen kein garantiertes Verhalten vorliegt.
Hier ist eine Option, die für die einfachen Demo-Tabellen zu funktionieren scheint:
Eine db fiddle Demo finden Sie hier . Durch Ändern der Reihenfolge der abgeleiteten Tabellen wird der Abfrageplan nicht geändert. In beiden Abfragen wird die
X_HEAP
Tabelle nicht berührt. Mit anderen Worten, der Abfrageoptimierer scheint zuerst die billigere Abfrage auszuführen. Ich kann nicht empfehlen, so etwas in der Produktion zu verwenden, daher ist es hauptsächlich aus Neugierde gedacht. Es kann einen viel einfacheren Weg geben, dasselbe zu erreichen.quelle
CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 END
könnte eine Alternative sein, obwohl dies immer noch darauf beruht, manuell zu entscheiden, welche Abfrage schneller ist, und diese zuerst zu setzen. Ich bin mir nicht sicher, ob es eine Möglichkeit gibt, dies auszudrücken, damit SQL Server automatisch neu anordnet, sodass der billige zuerst automatisch ausgewertet wird.