Reihenfolge der Klauseln in „EXISTS (…) OR EXISTS (…)“

11

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$1257927703Tabelle immer mehr oder gleich der Anzahl der Zeilen enthält als die dbf_1162761$z$dd$1257927703Tabelle - 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)
jr
quelle
1
Verwandte Fragen und Antworten
Paul White 9

Antworten:

11

Als allgemeine Faustregel führt SQL Server die Teile einer CASEAnweisung in der angegebenen Reihenfolge aus, kann jedoch die ORBedingungen neu anordnen . Bei einigen Abfragen können Sie eine durchweg bessere Leistung erzielen, indem Sie die Reihenfolge der WHENAusdrücke in einer CASEAnweisung ändern . Manchmal können Sie auch eine bessere Leistung erzielen, wenn Sie die Reihenfolge der Bedingungen in einer ORAnweisung ä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:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Betrachten Sie die folgende Abfrage:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Wir wissen, dass die Bewertung der Unterabfrage X_CIanhand der Unterabfrage viel billiger ist X_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:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

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:

Tabelle 'X_CI'. Scananzahl 0, logische Lesevorgänge 3, physische Lesevorgänge 0

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms.

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:

gute Abfrage

Die folgende Abfrage ist viel weniger effizient:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

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:

schlechter Abfrageplan

Die Leistung bestätigt dies:

Tabelle 'X_HEAP'. Scananzahl 1, logische Lesevorgänge 7247

SQL Server-Ausführungszeiten: CPU-Zeit = 15 ms, verstrichene Zeit = 22 ms.

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:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Und in dieser Abfrage werden sie in umgekehrter Reihenfolge ausgewertet:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

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 CASEAnweisung oder eine andere Methode, um die Reihenfolge zu erzwingen. Andernfalls können Sie Unterabfragen in einer von ORIhnen 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:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

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

Joe Obbish
quelle
4
Oder 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 ENDkö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.
Martin Smith