Warum führt SQL Server für jede qualifizierende Zeile der Tabelle eine Unterabfrage aus?

7

Diese Abfrage wird in ~ 21 Sekunden ausgeführt ( Ausführungsplan ):

select 
    a.month
    , count(*) 
from SubqueryTest a 
where a.year = (select max(b.year) from SubqueryTest b)
group by a.month

Wenn die Unterabfrage durch eine Variable ersetzt wird, wird sie in <1 Sekunde ausgeführt ( Ausführungsplan ):

declare @year float
select @year = max(b.year) from SubqueryTest b
select 
    month
    , count(*) 
from SubqueryTest where year = @year group by month

Nach dem Ausführungsplan zu urteilen, wird die Unterauswahl "select max ..." für jede der Millionen Zeilen in "SubqueryTest a:" ausgeführt, weshalb es so lange dauert.

Meine Frage: Da die Unterauswahl skalar, deterministisch und nicht korreliert ist, warum macht der Abfrageoptimierer nicht das, was ich in meinem zweiten Beispiel getan habe, und führt die Unterabfrage einmal aus, speichert das Ergebnis und verwendet es dann für die Hauptabfrage? Ich bin mir sicher, dass mein Verständnis von SQL Server nur eine Lücke aufweist, aber ich würde wirklich gerne beim Ausfüllen helfen - ein paar Stunden mit Google haben nicht geholfen.

Die Tabelle ist etwas mehr als 1 GB mit fast 28 Millionen Datensätzen:

CREATE TABLE SubqueryTest(
  [pk_id] [int] IDENTITY(1,1) NOT NULL
  , [Year] [float] NULL
  , [Month] [float] NULL PRIMARY KEY CLUSTERED ([pk_id] ASC))

CREATE NONCLUSTERED INDEX idxSubqueryTest ON SubqueryTest ([Year] ASC)
CaptainSlock
quelle
6
Mein Wunder ist, warum Sie Yearals Schwimmer haben. Sorry, nein, das macht für Stardates Sinn . Aber Monthals Schwimmer? Wirft mich wirklich auf.
Ypercubeᵀᴹ
5
Können Sie die Ausführungspläne liefern?
Martin Smith
@ypercube :) Diese Daten dahinter stammen von Access (das immer noch das Front-End ist). Es wurde von der Verwendung des Access-to-SQL-Server-Migrationsassistenten migriert, der Floats mag.
CaptainSlock
@MartinSmith Ausführungspläne hinzugefügt.
CaptainSlock
Was ist das Ergebnis des Verschiebens Ihrer Unterabfrage aus der where-Klausel in den Hauptteil der Abfrage als innerer Join? Wählen Sie a.month, count (*) von SubQueryTest a join (wählen Sie max (year) als [year] von SubQueryTest b) als b für a.year = b.year group by a.month
World Wide DBA

Antworten:

6

Der langsame Plan berechnet nicht die MAXfür jede Zeile in der äußeren Abfrage.

Tatsächlich berechnet es es niemals explizit.

Es gibt einen ähnlichen Plan wie

WITH CTE
     AS (SELECT TOP(1) WITH TIES *
         FROM   SubqueryTest
         WHERE year IS NOT NULL
         ORDER  BY year desc)
SELECT month,
       count(*)
FROM   CTE
GROUP  BY month 

Langsamer Plan (geschätzte Zeilenanzahl)

Geben Sie hier die Bildbeschreibung ein

Sie haben einen nicht abdeckenden Index, year ascsodass dieser rückwärts gescannt wird, um die Zeilen im ersten Jahr abzurufen (wird aufgrund des impliziten IS NOT NULLPrädikats als Suche angezeigt).

Leider scheint es nicht zwischen TOP 1und TOP 1 WITH TIESbei der Schätzung der Zeilenanzahl zu unterscheiden .

In diesem Fall macht es einen großen Unterschied. (geschätzte 2-Schlüssel-Suche im Vergleich zu tatsächlichen 4.424.803), sodass Sie einen unangemessenen Plan erhalten.

Langsamer Plan (tatsächliche Zeilenanzahl)

Geben Sie hier die Bildbeschreibung ein

Sie können monthin den Index yearentweder als Schlüssel oder als eingeschlossene Spalte hinzufügen , um den Index abzudecken. Der Vorteil des Hinzufügens als Sekundärschlüsselspalte besteht darin, dass es dann ohne zusätzliche Sortierung in ein Stream-Aggregat eingespeist werden kann (obwohl ein Hash-Aggregat für nur 12 verschiedene Werte ohnehin in Ordnung wäre).

Ein nicht abdeckender Index für eine solche nicht selektive Spalte ist für die überwiegende Mehrheit der Abfragen wirklich ziemlich nutzlos. Der Index wird vom "schnellen" Plan völlig ignoriert, der einen parallelen Scan der gesamten Tabelle durchführt und das Prädikat für alle 27.445.400 Zeilen auswertet (anstatt die große Anzahl von Suchvorgängen durchzuführen).

Geben Sie hier die Bildbeschreibung ein

Martin Smith
quelle
Ehrlich gesagt scheint mir dies ein Performance- / Optimierungsfehler zu sein. Es ist zulässig, ein statisches, stabiles Ergebnis aus der Unterabfrage anzunehmen und zwischenzuspeichern. Warum wird dies nicht immer getan, unabhängig von der geschätzten Anzahl der Zeilen? Wann ist der gewählte Plan immer besser?
RBarryYoung
@RBarryYoung - Nun, der Plan mit der Variablen ist auch nicht großartig! Wenn es nur eine Handvoll Duplikate TOP 1dafür gäbe , wäre dies der beste Plan. Der Fehler für mich ist, dass die durchschnittliche Selektivität für diese Spalte bei der Schätzung der Zeilen fürTOP 1 WITH TIES
Martin Smith am
Hmmm, das ist immer noch verwirrend / komisch. Warum ich mir den langsamen Plan anschaue und einige der "geschätzten Teilbaumkosten" viel geringer sind als die einzelnen E / A- und CPU-Kosten. Vielleicht funktioniert mein Gehirn heute einfach nicht, aber das scheint mir unmöglich ...?
RBarryYoung
@RBarryYoung - Weil es unter einem ist TOP 1, werden sie für ein Reihenziel verkleinert. SQL Server schätzt, dass das TOPAnfordern von Zeilen nach dem Empfang der ersten Zeile beendet wird. Da die ersten 4.424.803 Zeilen des Index-Scans dasselbe Jahr haben, dauert es sogar viel länger.
Martin Smith
1
@RBarryYoung - Paul White geht in seiner Antwort hier speziell darauf ein . Hilft das?
Martin Smith