Можете ли вы объяснить этот план выполнения?


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По-видимому, возвращает логический столбец true, если бит включен 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

Объяснение того, почему этот более простой план не может быть использован для случая в OP, дано SQLKiwi в комментариях к предыдущему сообщению в блоге .

Поиск по индексу с несколькими предикатами не может смешивать разные типы предикатов сравнения (т. IsЕ. EqВ случае с OP). Это просто ограничение тока продукта (и, по- видимому причина , почему проверка на равенство в последнем запросе 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, поэтому они явно будут использоваться с Loop Join в целях фильтрации данных.

Действительно крутая часть в том, что этот план тривиален. Это означает, что он прошел минимальный процесс оптимизации. Все операции ведут к интервалу слияния. Это используется для создания минимального набора операторов сравнения для поиска по индексу ( подробности здесь ).

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

ADDENDUM: последнее предложение не подходит. Было два поиска. Я неправильно понял план. Остальные понятия одинаковы, а цель, минимальные пропуски, одинакова.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.