Czy możesz wyjaśnić ten plan wykonania?

20

Badając coś innego, szukałem czegoś innego. Wygenerowałem tabele testowe z pewnymi danymi i uruchomiłem różne zapytania, aby dowiedzieć się, w jaki sposób różne sposoby pisania zapytań wpływają na plan wykonania. Oto skrypt, którego użyłem do wygenerowania losowych danych testowych:

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

Biorąc pod uwagę te dane, wywołałem następujące zapytanie:

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

Ku mojemu wielkiemu zaskoczeniu, plan wykonania, który został wygenerowany dla tego zapytania, był następujący . (Przepraszamy za link zewnętrzny, jest za duży, aby zmieścić się tutaj).

Czy ktoś może mi wyjaśnić, co słychać w tych „ stałych skanach ” i „ skalarach obliczeniowych ”? Co się dzieje?

Plan

  |--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
źródło

Odpowiedzi:

29

Każde skanowanie ciągłe tworzy pojedynczy wiersz w pamięci bez kolumn. Górny obliczeniowy skalar wyprowadza pojedynczy wiersz z 3 kolumnami

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

Dolny obliczeniowy skalar wyprowadza pojedynczy wiersz z 3 kolumnami

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

Operator konkatenacji Łączy te 2 rzędy razem i wyprowadza 3 kolumny, ale ich nazwy zostały zmienione

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

Expr1012Kolumna jest zestaw flag używanych wewnętrznie definiować pewne poszukiwania właściwości Storage Engine .

Następny oblicz skalar wzdłuż wyjść 2 wiersze

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

Ostatnie trzy kolumny są zdefiniowane w następujący sposób i są używane do celów sortowania przed przedstawieniem ich operatorowi interwału scalania

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

Expr1014i Expr1015po prostu sprawdź, czy niektóre bity są włączone na fladze. Expr1013wydaje się zwracać wartość logiczną true, jeśli oba bity for 4są włączone i Expr1010NULL.

Po wypróbowaniu innych operatorów porównania w zapytaniu uzyskuję te wyniki

+----------+----------+----------+-------------+----+----+---+---+---+---+
| 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 |
+----------+----------+----------+-------------+----+----+---+---+---+---+

Z czego wywnioskowałem, że bit 4 oznacza „Ma początek zakresu” (w przeciwieństwie do braku ograniczeń), a bit 16 oznacza, że ​​początek zakresu jest włącznie.

Ten 6-kolumnowy zestaw wyników jest emitowany przez SORToperatora posortowanego według Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC. Zakładając, że Truejest reprezentowany przez 1i Falseprzez 0poprzednio przedstawionego wynikowego jest już w tej kolejności.

Opierając się na moich wcześniejszych założeniach, efektem netto tego rodzaju jest przedstawienie zakresów dla interwału scalania w następującej kolejności

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst

Operator interwału scalania wyprowadza 2 rzędy

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

Dla każdego emitowanego wiersza przeprowadzane jest wyszukiwanie zakresu

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

Wygląda na to, że wykonywane są dwa wyszukiwania. Jeden najwyraźniej > NULL AND < NULLjeden > NULL AND < 1048576. Jednak przekazywane flagi wydają się odpowiednio modyfikować to IS NULLi < 1048576. Ufnie @sqlkiwi może to wyjaśnić i naprawić wszelkie nieścisłości!

Jeśli nieznacznie zmienisz zapytanie na

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

Wówczas plan wygląda na znacznie prostszy z wyszukiwaniem indeksu z wieloma predykatami wyszukiwania.

Plan pokazuje Seek Keys

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

Wyjaśnienie, dlaczego ten prostszy plan nie może być użyty w sprawie w OP, zostało podane przez SQLKiwi w komentarzach do wcześniejszego linku na blogu .

Indeks szukać z wieloma predykaty nie można mieszać różne rodzaje porównania orzecznika (tzn. Is, A Eqw przypadku, w PO). Jest to po prostu ograniczenie prądu produktu (i jest przypuszczalnie dlatego test równości w ostatnim zapytaniu c2 = 0jest realizowany przy użyciu >=i <=raczej niż tylko prostym równości szukać masz dla zapytania c2 = 0 OR c2 = 1048576.

Martin Smith
źródło
Nie widzę niczego w artykule Paula, który wyjaśnia różnicę w flagach dla [Expr1012]. Czy możesz wywnioskować, co oznacza 60/10 tutaj?
Mark Storey-Smith
@ MarkStorey-Smith - mówi, że 62służy do porównania równości. Myślę, że 60musi to oznaczać, że zamiast > AND < jak pokazano w planie, w rzeczywistości dostajesz, >= AND <=chyba że jest to wyraźna IS NULLflaga, może (?), A może bit 2wskazuje na coś innego niezwiązanego i 60nadal jest równy, jak wtedy, gdy to robię set ansi_nulls offi zmieniam na c2 = nullto, pozostaje na60
Martin Smith
2
@MartinSmith 60 jest rzeczywiście dla porównania z NULL. Wyrażenia granic zakresu używają NULL do reprezentowania „nieograniczonego” na obu końcach. Poszukiwanie jest zawsze wyłączne, tzn. Poszukiwanie Start:> Wyrażenie i zakończenie: <Wyrażenie zamiast włączenia za pomocą> = i <=. Dzięki za komentarz na blogu, rano opublikuję odpowiedź lub dłuższy komentarz w odpowiedzi (za późno, aby zrobić to teraz sprawiedliwie).
Paul White mówi GoFundMonica
@SQLKiwi - Dzięki. To ma sens. Mam nadzieję, że do tego czasu odkryję niektóre brakujące elementy.
Martin Smith
Dziękuję bardzo, nadal to wchłaniam, ale wydaje się, że dobrze to wyjaśnia, główne pytanie, które pozostaje, to pytanie, które zadajesz @SQLKiwi na swoim blogu. Zastanowię się jeszcze kilka dni nad twoją odpowiedzią, aby upewnić się, że nie będę mieć dalszych pytań i zaakceptuję twoją odpowiedź. Jeszcze raz dziękuję, to była ogromna pomoc.
Andrew Savinykh,
13

Ciągłe skanowanie to sposób, w jaki SQL Server tworzy wiadro, w którym umieści coś później w planie wykonania. Tutaj zamieściłem dokładniejsze wyjaśnienie . Aby zrozumieć, do czego służy ciągłe skanowanie, musisz dokładniej przyjrzeć się planowi. W tym przypadku do wypełnienia przestrzeni utworzonej przez ciągłe skanowanie używane są operatory obliczania skalarnego.

Operatory obliczeń skalarnych są ładowane z wartością NULL i wartością 1045876, więc z pewnością będą one używane z łączeniem pętli w celu filtrowania danych.

Naprawdę fajne jest to, że ten plan jest Trywialny. Oznacza to, że przeszedł minimalny proces optymalizacji. Wszystkie operacje prowadzą do interwału scalania. Służy to do utworzenia minimalnego zestawu operatorów porównania dla wyszukiwania indeksu ( szczegóły na ten temat tutaj ).

Chodzi o to, aby pozbyć się nakładających się wartości, aby móc następnie wyciągać dane przy minimalnej liczbie przejść. Mimo że nadal używa operacji pętli, zauważysz, że pętla wykonuje się dokładnie raz, co oznacza, że ​​jest to skutecznie skanowanie.

DODATEK: Ostatnie zdanie jest wyłączone. Były dwie próby. Źle odczytałem plan. Reszta pojęć jest taka sama, a cel, minimalne podanie, jest taki sam.

Grant Fritchey
źródło