Я пытаюсь создать примерный план запроса, чтобы показать, почему UNIONing двух наборов результатов может быть лучше, чем использование OR в предложении JOIN. План запроса, который я написал, поставил меня в тупик. Я использую базу данных StackOverflow с некластеризованным индексом Users.Reputation.
CREATE NONCLUSTERED INDEX IX_NC_REPUTATION ON dbo.USERS(Reputation)
SELECT DISTINCT Users.Id
FROM dbo.Users
INNER JOIN dbo.Posts
ON Users.Id = Posts.OwnerUserId
OR Users.Id = Posts.LastEditorUserId
WHERE Users.Reputation = 5
План запроса находится по адресу https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE , продолжительность запроса для меня составляет 4:37 мин, возвращено 26612 строк.
Я не видел такого стиля постоянного сканирования, созданного ранее из существующей таблицы - я не знаю, почему постоянное сканирование выполняется для каждой отдельной строки, когда постоянное сканирование обычно используется для одной строки, введенной пользователем например SELECT GETDATE (). Почему это используется здесь? Я был бы очень признателен за руководство по прочтению этого плана запроса.
Если я разделю это ИЛИ на UNION, то получится стандартный план, выполняемый за 12 секунд с возвращенными теми же 26612 строками.
SELECT Users.Id
FROM dbo.Users
INNER JOIN dbo.Posts
ON Users.Id = Posts.OwnerUserId
WHERE Users.Reputation = 5
UNION
SELECT Users.Id
FROM dbo.Users
INNER JOIN dbo.Posts
ON Users.Id = Posts.LastEditorUserId
WHERE Users.Reputation = 5
Я интерпретирую этот план как делающий это:
- Получить все 41782500 строк из сообщений (фактическое количество строк соответствует сканированию CI в сообщениях)
- Для каждого 41782500 строк в сообщениях:
- Производим скаляры:
- Expr1005: OwnerUserId
- Expr1006: OwnerUserId
- Expr1004: статическое значение 62
- Expr1008: LastEditorUserId
- Expr1009: LastEditorUserId
- Expr1007: статическое значение 62
- В конкатенации:
- Exp1010: Если Expr1005 (OwnerUserId) не равен NULL, используйте это, иначе используйте Expr1008 (LastEditorUserID)
- Expr1011: если Expr1006 (OwnerUserId) не равен NULL, используйте его, иначе используйте Expr1009 (LastEditorUserId)
- Expr1012: Если Expr1004 (62) является нулевым, используйте его, иначе используйте Expr1007 (62)
- В скаляре Compute: я не знаю, что делает амперсанд.
- Expr1013: 4 [и?] 62 (Expr1012) = 4 и OwnerUserId IS NULL (NULL = Expr1010)
- Expr1014: 4 [и?] 62 (Expr1012)
- Expr1015: 16 и 62 (Expr1012)
- В порядке сортировки по:
- Expr1013 Desc
- Expr1014 Asc
- Expr1010 Asc
- Expr1015 Desc
- В интервале слияния были удалены Expr1013 и Expr1015 (это входы, но не выходы)
- В поиске по индексу под объединением вложенных циклов он использует Expr1010 и Expr1011 в качестве предикатов поиска, но я не понимаю, каким образом он имеет к ним доступ, если не выполнил соединение с вложенным циклом из IX_NC_REPUTATION к поддереву, содержащему Expr1010 и Expr1011 ,
- Объединение Nested Loops возвращает только те Users.ID, которые соответствуют предыдущему поддереву. Из-за предиката pushdown возвращаются все строки, возвращаемые при поиске индекса по IX_NC_REPUTATION.
- Последнее объединение вложенных циклов: для каждой записи сообщения выведите Users.Id, где в указанном ниже наборе данных найдено совпадение.
SELECT Users.Id FROM dbo.Users WHERE Users.Reputation = 5 AND ( EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id = Posts.OwnerUserId) OR EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id = Posts.LastEditorUserId) ) ;
SELECT Users.Id FROM dbo.Users WHERE Users.Reputation = 5 AND EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id IN (Posts.OwnerUserId, Posts.LastEditorUserId) ) ;
Ответы:
План похож на тот, который я подробно расскажу здесь .
Posts
Таблица сканируется.Для каждого ряда он извлекает
OwnerUserId
иLastEditorUserId
. Это похоже на способUNPIVOT
работы. Вы видите единственный оператор постоянного сканирования в плане для ниже, создающего две выходные строки для каждой входной строки.В этом случае план немного сложнее, так как семантика заключается в том
or
, что если значения обоих столбцов одинаковы, то при соединении должна быть выдана только одна строкаUsers
(не две)Затем они проходят через интервал слияния, так что в случае, если значения совпадают, диапазон сворачивается вниз и выполняется только один поиск
Users
- в противном случае выполняется два поиска против него.Значение
62
- это флаг, означающий, что поиск должен быть равенством.относительно
Они определены в выделенном желтым цветом операторе конкатенации. Это на внешней стороне желтых выделенных вложенных циклов. Так что это происходит до того, как выделенный желтым цветом поиск будет внутри этих вложенных циклов.
Переписывание, которое дает аналогичный план (хотя интервал слияния заменяется объединением слияний) ниже, в случае, если это помогает.
В зависимости от того, какие индексы имеются в
Posts
таблице, вариант этого запроса может быть более эффективным, чем предложенное вамиUNION ALL
решение. (У копии базы данных, которая у меня есть, нет для этого полезного индекса, и предлагаемое решение выполняет два полных сканированияPosts
. Ниже приведено одно сканирование)источник