Können Sie diesen Ausführungsplan erklären?

20

Ich habe etwas anderes recherchiert, als ich auf dieses Ding gestoßen bin. Ich habe Testtabellen mit einigen Daten generiert und verschiedene Abfragen ausgeführt, um herauszufinden, wie sich die verschiedenen Arten des Schreibens von Abfragen auf den Ausführungsplan auswirken. Hier ist das Skript, mit dem ich zufällige Testdaten generiert habe:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID('t') AND type in (N'U'))
DROP TABLE t
GO

CREATE TABLE t 
(
 c1 int IDENTITY(1,1) NOT NULL 
,c2 int NULL
) 
GO

insert into t
select top 1000000 a from
(select t1.number*2048 + t2.number a, newid() b
from [master]..spt_values t1 
cross join  [master]..spt_values t2
where t1.[type] = 'P' and t2.[type] = 'P') a
order by b
GO

update t set c2 = null
where c2 < 2048 * 2048 / 10
GO


CREATE CLUSTERED INDEX pk ON [t] (c1)
GO

CREATE NONCLUSTERED INDEX i ON t (c2)
GO

Angesichts dieser Daten habe ich nun die folgende Abfrage aufgerufen:

select * 
from t 
where 
      c2 < 1048576 
   or c2 is null
;

Zu meiner großen Überraschung, den Ausführungsplan, der für diese Abfrage generiert wurde, war dies . (Entschuldigung für den externen Link, er ist zu groß, um hierher zu passen.)

Kann mir jemand erklären, was mit all diesen " konstanten Scans " und " Rechenskalaren " los ist? Was ist los?

Planen

  |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1010], [Expr1011], [Expr1012]))
       |--Merge Interval
       |    |--Sort(TOP 2, ORDER BY:([Expr1013] DESC, [Expr1014] ASC, [Expr1010] ASC, [Expr1015] DESC))
       |         |--Compute Scalar(DEFINE:([Expr1013]=((4)&[Expr1012]) = (4) AND NULL = [Expr1010], [Expr1014]=(4)&[Expr1012], [Expr1015]=(16)&[Expr1012]))
       |              |--Concatenation
       |                   |--Compute Scalar(DEFINE:([Expr1005]=NULL, [Expr1006]=NULL, [Expr1004]=(60)))
       |                   |    |--Constant Scan
       |                   |--Compute Scalar(DEFINE:([Expr1008]=NULL, [Expr1009]=(1048576), [Expr1007]=(10)))
       |                        |--Constant Scan
       |--Index Seek(OBJECT:([t].[i]), SEEK:([t].[c2] > [Expr1010] AND [t].[c2] < [Expr1011]) ORDERED FORWARD)
Andrew Savinykh
quelle

Antworten:

29

Die konstanten Scans erzeugen jeweils eine einzelne speicherinterne Zeile ohne Spalten. Der obere Berechnungsskalar gibt eine einzelne Zeile mit 3 Spalten aus

Expr1005    Expr1006    Expr1004
----------- ----------- -----------
NULL        NULL        60

Der untere Berechnungsskalar gibt eine einzelne Zeile mit 3 Spalten aus

Expr1008    Expr1009    Expr1007
----------- ----------- -----------
NULL        1048576        10

Der Verkettungsoperator verbindet diese 2 Zeilen und gibt die 3 Spalten aus, aber sie werden jetzt umbenannt

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

Die Expr1012Spalte besteht aus einer Reihe von Flags, die intern verwendet werden, um bestimmte Sucheigenschaften für die Storage Engine zu definieren .

Der nächste Berechnungsskalar entlang gibt 2 Zeilen aus

Expr1010    Expr1011    Expr1012    Expr1013    Expr1014    Expr1015
----------- ----------- ----------- ----------- ----------- -----------
NULL        NULL        60          True        4           16            
NULL        1048576     10          False       0           0      

Die letzten drei Spalten sind wie folgt definiert und werden nur zu Sortierzwecken verwendet, bevor sie dem Operator "Zusammenführungsintervall" vorgelegt werden

[Expr1013] = Scalar Operator(((4)&[Expr1012]) = (4) AND NULL = [Expr1010]), 
[Expr1014] = Scalar Operator((4)&[Expr1012]), 
[Expr1015] = Scalar Operator((16)&[Expr1012])

Expr1014und Expr1015testen Sie einfach, ob bestimmte Bits im Flag aktiviert sind. Expr1013erscheint für beide eine boolean Spalte true , wenn das Bit zurück 4auf und Expr1010ist NULL.

Wenn ich andere Vergleichsoperatoren in der Abfrage ausprobiere, erhalte ich diese Ergebnisse

+----------+----------+----------+-------------+----+----+---+---+---+---+
| Operator | Expr1010 | Expr1011 | Flags (Dec) |       Flags (Bin)       |
|          |          |          |             | 32 | 16 | 8 | 4 | 2 | 1 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
| >        | 1048576  | NULL     |           6 |  0 |  0 | 0 | 1 | 1 | 0 |
| >=       | 1048576  | NULL     |          22 |  0 |  1 | 0 | 1 | 1 | 0 |
| <=       | NULL     | 1048576  |          42 |  1 |  0 | 1 | 0 | 1 | 0 |
| <        | NULL     | 1048576  |          10 |  0 |  0 | 1 | 0 | 1 | 0 |
| =        | 1048576  | 1048576  |          62 |  1 |  1 | 1 | 1 | 1 | 0 |
| IS NULL  | NULL     | NULL     |          60 |  1 |  1 | 1 | 1 | 0 | 0 |
+----------+----------+----------+-------------+----+----+---+---+---+---+

Daraus schließe ich, dass Bit 4 "Bereichsanfang" bedeutet (im Gegensatz zu "unbegrenzt") und Bit 16 bedeutet, dass der Bereichsanfang inklusive ist.

Diese 6-Spalten-Ergebnismenge wird vom SORTOperator sortiert nach ausgegeben Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC. Unter der Annahme , Truedargestellt durch 1und Falsedurch 0die zuvor dargestellten resultset ist bereits in dieser Reihenfolge.

Basierend auf meinen vorherigen Annahmen besteht der Nettoeffekt dieser Art darin, die Bereiche für das Zusammenführungsintervall in der folgenden Reihenfolge darzustellen

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst

Der Operator für das Zusammenführungsintervall gibt 2 Zeilen aus

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

Für jede ausgegebene Zeile wird eine Bereichssuche durchgeführt

Seek Keys[1]: Start:[dbo].[t].c2 > Scalar Operator([Expr1010]), 
               End: [dbo].[t].c2 < Scalar Operator([Expr1011])

Es sieht also so aus, als würden zwei Suchvorgänge ausgeführt. Eins anscheinend > NULL AND < NULLund eins > NULL AND < 1048576. Allerdings sind die Fahnen , die in scheinen dies zu ändern , übergeben bekommen IS NULLund < 1048576jeweils. Hoffentlich kann @sqlkiwi dies klären und eventuelle Ungenauigkeiten korrigieren!

Wenn Sie die Abfrage leicht in ändern

select *
from t 
where 
      c2 > 1048576 
   or c2 = 0
;

Dann sieht der Plan mit einer Indexsuche mit mehreren Suchprädikaten viel einfacher aus.

Der Plan zeigt Seek Keys

Start: c2 >= 0, End: c2 <= 0, 
Start: c2 > 1048576

Die Erklärung, warum dieser einfachere Plan für den Fall im OP nicht verwendet werden kann, wird von SQLKiwi in den Kommentaren zum zuvor verlinkten Blog-Beitrag gegeben .

Eine Indexsuche mit mehreren Prädikaten kann nicht verschiedene Arten von Vergleichsprädikaten (dh Isund Eqim Fall des OP) mischen . Dies ist nur eine aktuelle Einschränkung des Produkts (und vermutlich der Grund, warum der Gleichheitstest in der letzten Abfrage c2 = 0unter Verwendung von implementiert wird >=und <=nicht nur die einfache Gleichheitssuche, die Sie für die Abfrage erhalten c2 = 0 OR c2 = 1048576.

Martin Smith
quelle
Ich kann in Pauls Artikel nichts finden, das den Unterschied in den Flags für [Expr1012] erklärt. Können Sie ableiten, was die 60/10 hier bedeutet?
Mark Storey-Smith
@ MarkStorey-Smith - er sagt 62ist für einen Gleichstellungsvergleich. Ich denke, das 60muss bedeuten, dass anstatt > AND < wie im Plan gezeigt, Sie tatsächlich erhalten, es >= AND <=sei denn, es ist ein explizites IS NULLFlag, vielleicht (?) Oder vielleicht zeigt das Bit 2etwas anderes an, das nichts damit zu 60tun hat, set ansi_nulls offund ist immer noch gleich, als wenn ich es ändere und c2 = nulles bleibt dabei60
Martin Smith
2
@MartinSmith 60 ist in der Tat für einen Vergleich mit NULL. Die Bereichsgrenzenausdrücke verwenden NULL, um "unbegrenzt" an beiden Enden darzustellen. Die Suche ist immer exklusiv, dh suche Start:> Ausdruck & Ende: <Ausdruck statt inklusive mit> = und <=. Vielen Dank für den Blog-Kommentar. Ich werde morgen früh eine Antwort oder einen längeren Kommentar als Antwort posten (zu spät, um es jetzt richtig zu machen).
Paul White sagt GoFundMonica
@ SQLKiwi - Danke. Das macht Sinn. Hoffentlich habe ich vorher einige der fehlenden Teile herausgefunden.
Martin Smith
Vielen Dank, ich nehme das immer noch in mich auf, aber es scheint die Dinge gut zu erklären. Die Hauptfrage, die noch offen ist, ist die, die Sie @SQLKiwi in seinem Blog gestellt haben. Ich werde noch ein paar Tage über Ihre Antwort nachdenken, um sicherzustellen, dass ich keine weiteren Fragen habe, und ich werde Ihre Antwort akzeptieren. Nochmals vielen Dank, es war eine große Hilfe.
Andrew Savinykh
13

Die konstanten Scans sind eine Möglichkeit für SQL Server, einen Bucket zu erstellen, in den etwas später im Ausführungsplan eingefügt wird. Ich habe hier eine ausführlichere Erklärung dazu veröffentlicht . Um zu verstehen, wofür der ständige Scan gedacht ist, müssen Sie sich den Plan genauer ansehen. In diesem Fall werden die Compute Scalar-Operatoren verwendet, um den durch den konstanten Scan erstellten Speicherplatz aufzufüllen.

Die Compute Scalar-Operatoren werden mit NULL und dem Wert 1045876 geladen, daher werden sie eindeutig mit dem Loop Join verwendet, um die Daten zu filtern.

Der wirklich coole Teil ist, dass dieser Plan Trivial ist. Dies bedeutet, dass ein minimaler Optimierungsprozess durchgeführt wurde. Alle Vorgänge führen zum Zusammenführungsintervall. Dies wird verwendet, um einen minimalen Satz von Vergleichsoperatoren für eine Indexsuche zu erstellen ( Details dazu hier ).

Die ganze Idee ist, überlappende Werte loszuwerden, damit die Daten dann mit minimalen Durchläufen herausgezogen werden können. Obwohl es sich immer noch um eine Schleifenoperation handelt, werden Sie feststellen, dass die Schleife genau einmal ausgeführt wird, was bedeutet, dass es sich tatsächlich um einen Scan handelt.

ADDENDUM: Dieser letzte Satz ist aus. Es gab zwei Suchanfragen. Ich habe den Plan falsch verstanden. Der Rest der Konzepte ist das gleiche und das Ziel, minimale Pässe, ist das gleiche.

Grant Fritchey
quelle