Überprüfen Sie die Existenz mit EXISTS, um COUNT zu übertreffen! ... nicht?


35

Ich habe oft gelesen, dass man die Existenz einer Zeile immer mit EXISTS anstatt mit COUNT überprüfen muss.

In einigen neuen Szenarien habe ich jedoch eine Leistungsverbesserung bei der Verwendung von count gemessen.
Das Muster sieht folgendermaßen aus:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

Ich bin nicht mit Methoden vertraut, mit denen ich feststellen kann, was in SQL Server "vor sich geht". Daher habe ich mich gefragt, ob bei EXISTS ein nicht angezeigter Fehler aufgetreten ist, der den von mir durchgeführten Messungen durchaus Sinn machte (könnte EXISTS RBAR sein ?!).

Haben Sie eine Erklärung für dieses Phänomen?

BEARBEITEN:

Hier ist ein vollständiges Skript, das Sie ausführen können:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

Auf SQL Server 2008R2 (sieben 64 Bit) erhalte ich dieses Ergebnis

COUNT Ausführung:

Tabelle '# 455F344D'. Scananzahl 1, logische Lesevorgänge 8, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '# 492FC531'. Scananzahl 1, logische Lesevorgänge 30, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

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

EXISTS Ausführung:

Tabelle '# 492FC531'. Scananzahl 1, logische Lesevorgänge 96, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '# 455F344D'. Scananzahl 1, logische Lesevorgänge 8, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

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

Antworten:


43

Ich habe oft gelesen, dass man die Existenz einer Zeile immer mit EXISTS anstatt mit COUNT überprüfen muss.

Es ist sehr selten, dass etwas immer wahr ist, besonders wenn es um Datenbanken geht. Es gibt eine beliebige Anzahl von Möglichkeiten, dieselbe Semantik in SQL auszudrücken. Wenn es eine nützliche Faustregel gibt, kann es sein, dass Abfragen mit der natürlichsten verfügbaren Syntax (und ja, das ist subjektiv) geschrieben werden und nur Überschreibungen in Betracht gezogen werden, wenn der Abfrageplan oder die Leistung, die Sie erhalten, nicht akzeptabel sind.

Ich gehe davon aus, dass Existenzfragen am natürlichsten ausgedrückt werden, wenn man sie verwendet EXISTS. Es ist auch meine Erfahrung, die EXISTS dazu neigt, besser zu optimieren als die OUTER JOINAusschussalternative NULL. Verwenden COUNT(*)und Filtern nach =0ist eine weitere Alternative, die im SQL Server-Abfrageoptimierungsprogramm Unterstützung bietet. Ich persönlich habe jedoch festgestellt, dass dies bei komplexeren Abfragen unzuverlässig ist. In jedem Fall EXISTSscheint mir das viel natürlicher zu sein als jede dieser Alternativen.

Ich habe mich gefragt, ob es einen nicht angekündigten Fehler bei EXISTS gibt, der den Messungen, die ich durchgeführt habe, vollkommen Sinn macht

Ihr spezielles Beispiel ist interessant, da es die Art und Weise hervorhebt, wie das Optimierungsprogramm mit Unterabfragen in CASEAusdrücken (und EXISTSinsbesondere Tests) umgeht .

Unterabfragen in CASE-Ausdrücken

Betrachten Sie die folgende (vollkommen legale) Abfrage:

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

Die Semantik vonCASE ist, dass WHEN/ELSEKlauseln im Allgemeinen in Textreihenfolge ausgewertet werden. In der obigen Abfrage wäre es falsch ELSE, wenn SQL Server einen Fehler zurückgeben würde, wenn die Unterabfrage mehr als eine Zeile zurückgeben würde, wenn die WHENKlausel erfüllt wäre. Um diese Semantik zu berücksichtigen, erstellt das Optimierungsprogramm einen Plan, der Pass-Through-Prädikate verwendet:

Pass-Through-Prädikate

Die Innenseite der Joins mit verschachtelten Schleifen wird nur ausgewertet, wenn das Pass-Through-Prädikat false zurückgibt. Der Gesamteffekt besteht darin, dass CASEAusdrücke der Reihe nach getestet werden und Unterabfragen nur ausgewertet werden, wenn kein vorheriger Ausdruck erfüllt wurde.

CASE-Ausdrücke mit einer EXISTS-Unterabfrage

Wenn eine CASEUnterabfrage verwendet wird EXISTS, wird der logische Existenztest als Semi-Join implementiert. Zeilen, die normalerweise vom Semi-Join zurückgewiesen werden, müssen jedoch beibehalten werden, falls sie in einer späteren Klausel benötigt werden. Zeilen, die diese spezielle Art von Semi-Join durchlaufen, erhalten ein Flag, das angibt, ob der Semi-Join eine Übereinstimmung gefunden hat oder nicht. Dieses Flag wird als Sondenspalte bezeichnet .

Die Details der Implementierung bestehen darin, dass die logische Unterabfrage durch einen korrelierten Join ('apply') mit einer Testspalte ersetzt wird. Die Arbeit wird durch eine Vereinfachungsregel im Abfrageoptimierer mit dem Namen RemoveSubqInPrj(Unterabfrage in Projektion entfernen) ausgeführt. Wir können die Details mit dem Trace-Flag 8606 sehen:

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

Ein Teil des Eingabebaums, der den EXISTSTest zeigt, ist unten dargestellt:

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

Dies verwandelt sich RemoveSubqInPrjin eine Struktur, die geleitet wird von:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

Dies ist die linke Halbverbindung, die mit dem zuvor beschriebenen Prüfpunkt angewendet wird. Diese erste Umwandlung ist die einzige, die derzeit in SQL Server-Abfrageoptimierungsprogrammen verfügbar ist, und die Kompilierung schlägt einfach fehl, wenn diese Umwandlung deaktiviert ist.

Eine der möglichen Ausführungsplanformen für diese Abfrage ist eine direkte Implementierung dieser logischen Struktur:

NLJ Semi Join mit Probe

Der endgültige Berechnungsskalar wertet das Ergebnis des CASEAusdrucks anhand des Werts der Prüfspalte aus:

Skalarausdruck berechnen

Die Grundform des Planbaums bleibt erhalten, wenn die Optimierung andere physische Verknüpfungstypen für die Halbverknüpfung berücksichtigt. Nur der Merge-Join unterstützt eine Prüfspalte, sodass ein Hash-Semi-Join, obwohl logisch möglich, nicht berücksichtigt wird:

Mit der Sondensäule verschmelzen

Beachten Sie, dass die Zusammenführung einen Ausdruck mit Expr1008der Bezeichnung ausgibt (der Name ist derselbe wie zuvor, ein Zufall), obwohl für keinen Operator im Plan eine Definition dafür angezeigt wird. Dies ist wieder nur die Sondensäule. Nach wie vor verwendet der endgültige Berechnungsskalar diesen Prüfpunktwert zur Auswertung der CASE.

Das Problem ist, dass das Optimierungsprogramm Alternativen nicht vollständig untersucht, die sich erst durch das Zusammenführen (oder Hash) von Semi-Joins lohnen. Im Plan mit verschachtelten Schleifen besteht kein Vorteil darin, bei T2jeder Iteration zu überprüfen, ob die Zeilen im Bereich übereinstimmen. Bei einem Zusammenführungs- oder Hash-Plan kann dies eine nützliche Optimierung sein.

Wenn wir der Abfrage ein übereinstimmendes BETWEENPrädikat hinzufügen T2, wird diese Prüfung lediglich für jede Zeile als Residuum des Merge-Semi-Joins ausgeführt (im Ausführungsplan schwer zu erkennen, aber vorhanden):

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

Verbleibendes Prädikat

Wir würden hoffen, dass das BETWEENPrädikat stattdessen zu T2einer Suche zurückgedrängt wird. Normalerweise würde das Optimierungsprogramm dies in Betracht ziehen (auch ohne das zusätzliche Prädikat in der Abfrage). Implizierte Prädikate ( BETWEENon T1und das Join-Prädikat zwischen T1und T2zusammen implizieren das BETWEENon T2) werden erkannt , ohne dass sie im ursprünglichen Abfragetext vorhanden sind. Leider bedeutet das Muster "Probe anwenden", dass dies nicht untersucht wird.

Es gibt Möglichkeiten, die Abfrage zu schreiben, um Suchvorgänge für beide Eingaben in einen Merge-Semi-Join zu erzeugen. Eine Möglichkeit besteht darin, die Abfrage auf eine ziemlich unnatürliche Weise zu schreiben (und dabei den Grund zu verneinen, den ich im Allgemeinen bevorzuge EXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

TOP Trickplan

Es würde mich nicht freuen, diese Abfrage in einer Produktionsumgebung zu schreiben. Sie soll nur zeigen, dass die gewünschte Planform möglich ist. Wenn die tatsächliche Abfrage, die Sie schreiben müssen, CASEauf diese Weise verwendet wird und die Leistung darunter leidet, dass auf der Testseite eines Merge-Semi-Joins keine Suche erfolgt, können Sie die Abfrage mit einer anderen Syntax schreiben, die die richtigen Ergebnisse liefert, und a effizienter Ausführungsplan.


6

Das Argument "COUNT (*) vs EXISTS" prüft, ob ein Datensatz vorhanden ist. Beispielsweise:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

vs

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

Ihr SQL-Skript wird nicht COUNT(*)als Datensatz verwendet, und daher würde ich nicht sagen, dass es in Ihrem Szenario anwendbar ist.


Irgendwelche Gedanken / Schlussfolgerungen, die auf dem Skript basieren, das ich gepostet habe?
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.