Abfrage 100x langsamer in SQL Server 2014, Zeilenanzahl Spool-Zeile schätzen den Schuldigen?

11

Ich habe eine Abfrage, die in SQL Server 2012 in 800 Millisekunden ausgeführt wird und in SQL Server 2014 etwa 170 Sekunden dauert . Ich denke, ich habe dies auf eine schlechte Kardinalitätsschätzung für den Row Count SpoolBediener eingegrenzt. Ich habe ein wenig über Spool-Operatoren gelesen (z. B. hier und hier ), habe aber immer noch Probleme, einige Dinge zu verstehen:

  • Warum benötigt diese Abfrage einen Row Count SpoolOperator? Ich denke nicht, dass es für die Korrektheit notwendig ist. Welche spezifische Optimierung versucht es also bereitzustellen?
  • Warum schätzt SQL Server, dass der Join zum Row Count SpoolOperator alle Zeilen entfernt?
  • Ist dies ein Fehler in SQL Server 2014? Wenn ja, werde ich in Connect einreichen. Aber ich möchte zuerst ein tieferes Verständnis.

Hinweis: Ich kann die Abfrage als neu schreiben LEFT JOINoder den Tabellen Indizes hinzufügen, um eine akzeptable Leistung sowohl in SQL Server 2012 als auch in SQL Server 2014 zu erzielen. Bei dieser Frage geht es also mehr um das Verständnis dieser spezifischen Abfrage und den Plan und weniger um das Planen wie man die Abfrage anders formuliert.


Die langsame Abfrage

In diesem Pastebin finden Sie ein vollständiges Testskript . Hier ist die spezifische Testabfrage, die ich betrachte:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014: Der geschätzte Abfrageplan

SQL Server glaubt , dass die Left Anti Semi Joinauf die Row Count Spoolwerden die 10.000 Zeilen bis zu 1 Zeile filtern. Aus diesem Grund wird ein LOOP JOINfür den nachfolgenden Join ausgewählt #existingCustomers.

Geben Sie hier die Bildbeschreibung ein


SQL Server 2014: Der eigentliche Abfrageplan

Wie erwartet (von allen außer SQL Server!) Row Count SpoolWurden keine Zeilen entfernt. Wir führen also 10.000 Schleifen durch, wenn SQL Server nur eine Schleife erwartet.

Geben Sie hier die Bildbeschreibung ein


SQL Server 2012: Der geschätzte Abfrageplan

Bei Verwendung von SQL Server 2012 (oder OPTION (QUERYTRACEON 9481)in SQL Server 2014) wird die Row Count Spoolgeschätzte Anzahl der Zeilen nicht reduziert, und es wird ein Hash-Join ausgewählt, was zu einem weitaus besseren Plan führt.

Geben Sie hier die Bildbeschreibung ein

Der LEFT JOIN schreibt neu

Als Referenz ist hier eine Möglichkeit, die Abfrage neu zu schreiben, um eine gute Leistung in allen SQL Server 2012, 2014 und 2016 zu erzielen. Ich bin jedoch immer noch an dem spezifischen Verhalten der obigen Abfrage interessiert und daran, ob dies der Fall ist ist ein Fehler im neuen SQL Server 2014 Cardinality Estimator.

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

Geben Sie hier die Bildbeschreibung ein

Geoff Patterson
quelle

Antworten:

8

Warum benötigt diese Abfrage einen Row Count Spool-Operator? ... welche spezifische Optimierung soll es bieten?

Die cust_nbrSpalte in #existingCustomersist nullbar. Wenn es tatsächlich Nullen enthält, besteht die richtige Antwort darin, Nullzeilen zurückzugeben ( NOT IN (NULL,...) ergibt immer eine leere Ergebnismenge).

Die Abfrage kann also als betrachtet werden

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

Mit der Rowcount-Spool gibt es keine Auswertung der

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

Mehr als einmal.

Dies scheint nur ein Fall zu sein, in dem ein kleiner Unterschied in den Annahmen einen katastrophalen Unterschied in der Leistung bewirken kann.

Nach dem Aktualisieren einer einzelnen Zeile wie unten ...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

... die Abfrage in weniger als einer Sekunde abgeschlossen. Die Zeilenanzahl in tatsächlichen und geschätzten Versionen des Plans ist jetzt nahezu genau richtig.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

Geben Sie hier die Bildbeschreibung ein

Nullzeilen werden wie oben beschrieben ausgegeben.

Die Statistikhistogramme und Schwellenwerte für die automatische Aktualisierung in SQL Server sind nicht detailliert genug, um diese Art der Änderung einzelner Zeilen zu erkennen. Wenn die Spalte nullwertfähig ist, ist es möglicherweise sinnvoll, davon auszugehen, dass sie mindestens eine enthält, NULLauch wenn das Statistikhistogramm derzeit nicht anzeigt, dass es welche gibt.

Martin Smith
quelle
8

Warum benötigt diese Abfrage einen Row Count Spool-Operator? Ich denke nicht, dass es für die Korrektheit notwendig ist. Welche spezifische Optimierung versucht es also bereitzustellen?

Siehe Martins gründliche Antwort auf diese Frage. Der entscheidende Punkt ist, dass, wenn eine einzelne Zeile innerhalb des NOT INis ist NULL, die boolesche Logik so funktioniert, dass "die richtige Antwort darin besteht, null Zeilen zurückzugeben". Der Row Count SpoolBediener optimiert diese (notwendige) Logik.

Warum schätzt SQL Server, dass der Join zum Row Count Spool-Operator alle Zeilen entfernt?

Microsoft bietet ein hervorragendes Whitepaper zum SQL 2014 Cardinality Estimator . In diesem Dokument habe ich folgende Informationen gefunden:

Das neue CE geht davon aus, dass die abgefragten Werte im Datensatz vorhanden sind, auch wenn der Wert außerhalb des Bereichs des Histogramms liegt. Das neue CE in diesem Beispiel verwendet eine Durchschnittsfrequenz, die durch Multiplizieren der Tabellenkardinalität mit der Dichte berechnet wird.

Oft ist eine solche Änderung sehr gut; Dies verringert das aufsteigende Schlüsselproblem erheblich und liefert in der Regel einen konservativeren Abfrageplan (höhere Zeilenschätzung) für Werte, die basierend auf dem Statistikhistogramm außerhalb des Bereichs liegen.

In diesem speziellen Fall NULLführt die Annahme, dass ein Wert gefunden wird, zu der Annahme, dass durch das Row Count SpoolVerknüpfen mit dem alle Zeilen herausgefiltert werden #potentialNewCustomers. In dem Fall, in dem es tatsächlich eine NULLZeile gibt, ist dies eine korrekte Schätzung (wie in Martins Antwort zu sehen). In dem Fall, dass keine NULLZeile vorhanden ist, kann der Effekt jedoch verheerend sein, da SQL Server eine Schätzung nach dem Join von 1 Zeile erstellt, unabhängig davon, wie viele Eingabezeilen angezeigt werden. Dies kann im Rest des Abfrageplans zu sehr schlechten Join-Optionen führen.

Ist das ein Fehler in SQL 2014? Wenn ja, werde ich in Connect einreichen. Aber ich möchte zuerst ein tieferes Verständnis.

Ich denke, es liegt in der Grauzone zwischen einem Fehler und einer leistungsbeeinträchtigenden Annahme oder Einschränkung des neuen Kardinalitätsschätzers von SQL Server. Diese Eigenart kann jedoch im speziellen Fall einer nullbaren NOT INKlausel, die zufällig keine NULLWerte enthält, zu erheblichen Leistungseinbußen im Vergleich zu SQL 2012 führen .

Aus diesem Grund habe ich ein Connect-Problem eingereicht , damit das SQL-Team die möglichen Auswirkungen dieser Änderung auf den Kardinalitätsschätzer kennt.

Update: Wir sind jetzt auf CTP3 für SQL16 und ich habe bestätigt, dass das Problem dort nicht auftritt.

Geoff Patterson
quelle
4

Die Antwort von Martin Smith und Ihre Selbstantwort haben alle wichtigen Punkte richtig angesprochen. Ich möchte nur einen Bereich für zukünftige Leser hervorheben:

Bei dieser Frage geht es also mehr darum, diese spezifische Abfrage zu verstehen und gründlich zu planen, und weniger darum, wie die Abfrage anders formuliert werden kann.

Der angegebene Zweck der Abfrage ist:

-- Prune any existing customers from the set of potential new customers

Diese Anforderung lässt sich in SQL auf verschiedene Weise leicht ausdrücken. Welche ausgewählt wird, ist ebenso eine Frage des Stils wie alles andere, aber die Abfragespezifikation sollte trotzdem geschrieben werden, um in allen Fällen korrekte Ergebnisse zu liefern. Dies beinhaltet die Berücksichtigung von Nullen.

Die logische Anforderung vollständig ausdrücken:

  • Geben Sie potenzielle Kunden zurück, die noch keine Kunden sind
  • Listen Sie jeden potenziellen Kunden höchstens einmal auf
  • Schließen Sie null potenzielle und bestehende Kunden aus (was auch immer ein null Kunde bedeutet)

Wir können dann eine Abfrage schreiben, die diesen Anforderungen entspricht, und zwar unter Verwendung der von uns bevorzugten Syntax. Beispielsweise:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Dies führt zu einem effizienten Ausführungsplan, der korrekte Ergebnisse liefert:

Ausführungsplan

Wir können das NOT INals <> ALLoder NOT = ANYohne Einfluss auf den Plan oder die Ergebnisse ausdrücken :

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Oder mit NOT EXISTS:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

Daran ist nichts Magisches oder etwas besonders Unangenehmes an der Verwendung von IN, ANYoder ALL- wir müssen die Abfrage nur richtig schreiben, damit immer die richtigen Ergebnisse erzielt werden.

Die kompakteste Form verwendet EXCEPT:

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

Dies führt auch zu korrekten Ergebnissen, obwohl der Ausführungsplan aufgrund fehlender Bitmap-Filterung möglicherweise weniger effizient ist:

Nicht-Bitmap-Ausführungsplan

Die ursprüngliche Frage ist interessant, da sie ein leistungsbeeinträchtigendes Problem mit der erforderlichen Nullprüfungsimplementierung aufdeckt. Der Punkt dieser Antwort ist, dass das korrekte Schreiben der Abfrage das Problem ebenfalls vermeidet.

Paul White 9
quelle