Чи можете ви пояснити цей план виконання?


20

Я досліджував щось інше, коли натрапляв на цю річ. Я генерував тестові таблиці з деякими даними в ньому і запускав різні запити, щоб дізнатися, як різні способи запиту впливають на план виконання. Ось сценарій, який я використовував для генерації випадкових даних тесту:

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

Тепер, враховуючи ці дані, я викликав такий запит:

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

На моє велике здивування, план виконання, який було сформовано для цього запиту, був таким . (Вибачте за зовнішнє посилання, воно занадто велике для розміщення тут).

Чи може хтось пояснити мені, що відбувається з усіма цими " постійними скануваннями " та " обчислювальними сканерами "? Що відбувається?

План

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

Відповіді:


29

Постійні сканування створюють один рядок пам'яті без стовпців. Верхній скаляр обчислення виводить один рядок з 3 стовпцями

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

Нижній обчислювальний скаляр виводить один рядок із 3 стовпцями

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

Оператор конкатенації об'єднує ці 2 ряди разом і виводить 3 стовпці, але вони тепер перейменовані

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

Expr1012Колонка являє собою набір прапорів , які використовуються всередині визначити деякі шукають властивості для зберігання двигуна .

Наступний обчислювальний скаляр вздовж виводить 2 ряди

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

Останні три стовпці визначаються наступним чином і просто використовуються для сортування з метою перед представленням оператору інтервалу об'єднання

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

Expr1014і Expr1015просто перевірити, чи в прапорі включені певні біти. Expr1013Здається, повертає булевий стовпець істинним, якщо обидва біти для 4ввімкнено і Expr1010є NULL.

Від спроб інших операторів порівняння в запиті я отримую ці результати

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

З чого я підказую, що Біт 4 означає "Має початок діапазону" (на відміну від необмеженого), а Біт 16 означає початок діапазону включно.

Цей набір результатів з 6 стовпців випромінюється від SORTоператора, відсортованого за Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC. Припускаючи , що Trueпредставлено 1і Falseпо 0раніше представленим результуючому вже в цьому порядку.

Виходячи з моїх попередніх припущень, чистий ефект такого роду полягає в поданні діапазонів на інтервал злиття в наступному порядку

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst

Оператор інтервалу злиття видає 2 ряди

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

Для кожного випущеного ряду виконується пошук діапазону

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

Отже, здавалося б, два спроби виконуються. Один, мабуть, > NULL AND < NULLі один > NULL AND < 1048576. Однак прапори, які передаються в них, схоже, модифікують це відповідно IS NULLі < 1048576відповідно. Сподіваємось, @sqlkiwi зможе уточнити це та виправити будь-які неточності!

Якщо ви трохи змінили запит на

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

Тоді план виглядає набагато простіше, якщо шукати індекс з кількома предикатами пошуку.

План показує Seek Keys

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

Пояснення, чому цей простіший план не може бути використаний для випадку в ОП, дає SQLKiwi в коментарях до раніше пов’язаної публікації блогу .

Шукання індексу з кількома предикатами не може поєднувати різні предикати порівняння (наприклад, Isі Eqу випадку в ОП). Це просто обмеження струму продукту (і, по- видимому причина , чому перевірка на рівність в останньому запиті c2 = 0реалізується з допомогою >=і , <=а не тільки просте рівності шукати ви отримуєте для запиту c2 = 0 OR c2 = 1048576.


Я не можу помітити нічого в статті Павла, яка пояснює різницю прапорів для [Expr1012]. Чи можете ви зрозуміти, що означає 60/10?
Марк Сторі-Сміт

@ MarkStorey-Smith - він каже, що 62це для порівняння рівності. Я думаю, 60має означати, що замість того, > AND < як показано в плані, ви насправді отримуєте, >= AND <=якщо це не явний IS NULLпрапор, можливо (?) Або, можливо, біт 2вказує на щось інше, що не пов'язане між собою, і 60все ще є рівність, як коли я це роблю, set ansi_nulls offі змінити його на c2 = nullнього все ще залишається на60
Мартін Сміт

2
@MartinSmith 60 справді для порівняння з NULL. Виразні межі діапазону використовують NULL для представлення "необмеженого" на будь-якому кінці. Шукання завжди є ексклюзивним, тобто шукайте Start:> Expr & End: <Expr, а не включно, використовуючи> = і <=. Дякую за коментар у блозі, я опублікую відповідь чи довший коментар у відповідь вранці (занадто пізно, щоб зробити це справедливістю зараз).
Пол Білий каже, що GoFundMonica

@SQLKiwi - Дякую Що має сенс. Сподіваюся, я до цього часу розібрався з деякими відсутніми бітами.
Мартін Сміт

Дуже дякую, я все ще поглинаю це, але, здається, це добре пояснює, головне питання, що залишилося, - це те, що ви запитуєте @SQLKiwi у своєму блозі. Я ще раз роздумую над вашою відповіддю, щоб переконатися, що у мене немає запитань, і я прийму вашу відповідь. Дякую ще раз, це була величезна допомога.
Андрій Савіних

13

Постійні сканування - це спосіб для SQL Server створити відро, в яке він буде розміщувати щось пізніше в плані виконання. Більш ретельне пояснення цього я опублікував тут . Щоб зрозуміти, для чого потрібне постійне сканування, вам доведеться заглянути далі в план. У цьому випадку оператори Compute Scalar використовуються для заселення простору, створеного постійним скануванням.

Оператори Compute Scalar завантажуються з NULL та значення 1045876, тому вони, очевидно, будуть використовуватися разом з циклом приєднання, намагаючись фільтрувати дані.

Дійсно класна частина полягає в тому, що цей план є тривіальним. Це означає, що він пройшов мінімальний процес оптимізації. Усі операції ведуть до інтервалу злиття. Це використовується для створення мінімального набору операторів порівняння для пошуку індексу ( детальніше про це тут ).

Вся ідея полягає в тому, щоб позбутися значень, що перекриваються, щоб потім можна було витягувати дані з мінімальними пропусками. Хоча це все ще використовується циклічна операція, ви зауважите, що цикл виконується рівно один раз, це означає, що це ефективно сканування.

ДОДАТИ: Це останнє речення вимкнено. Було два прагнення. Я неправильно прочитав план. Решта понять однакові, і мета, мінімальні проходи, однакова.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.