Странный план запроса при использовании OR в предложении JOIN - постоянное сканирование для каждой строки в таблице

10

Я пытаюсь создать примерный план запроса, чтобы показать, почему 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, где в указанном ниже наборе данных найдено совпадение.
Андрей
источник
Вы пробовали с подзапросом EXISTS или подзапросами? 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) ) ;
ypercubeᵀᴹ
один подзапрос: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) ) ;
ypercubeᵀᴹ

Ответы:

10

План похож на тот, который я подробно расскажу здесь .

PostsТаблица сканируется.

Для каждого ряда он извлекает OwnerUserIdи LastEditorUserId. Это похоже на способ UNPIVOTработы. Вы видите единственный оператор постоянного сканирования в плане для ниже, создающего две выходные строки для каждой входной строки.

SELECT *
FROM dbo.Posts
UNPIVOT (X FOR U IN (OwnerUserId,LastEditorUserId)) Unpvt

В этом случае план немного сложнее, так как семантика заключается в том or, что если значения обоих столбцов одинаковы, то при соединении должна быть выдана только одна строка Users(не две)

Затем они проходят через интервал слияния, так что в случае, если значения совпадают, диапазон сворачивается вниз и выполняется только один поиск Users- в противном случае выполняется два поиска против него.

Значение 62- это флаг, означающий, что поиск должен быть равенством.

относительно

Я не понимаю, как у него есть доступ к ним, когда он не сделал объединение вложенного цикла из IX_NC_REPUTATION к поддереву, содержащему Expr1010 и Expr1011

Они определены в выделенном желтым цветом операторе конкатенации. Это на внешней стороне желтых выделенных вложенных циклов. Так что это происходит до того, как выделенный желтым цветом поиск будет внутри этих вложенных циклов.

введите описание изображения здесь

Переписывание, которое дает аналогичный план (хотя интервал слияния заменяется объединением слияний) ниже, в случае, если это помогает.

SELECT DISTINCT D2.UserId
FROM   dbo.Posts p
       CROSS APPLY (SELECT Users.Id AS UserId
                    FROM   (SELECT p.OwnerUserId
                            UNION /*collapse duplicate to single row*/
                            SELECT p.LastEditorUserId) D1(UserId)
                           JOIN Users
                             ON Users.Id = D1.UserId) D2
OPTION (FORCE ORDER) 

введите описание изображения здесь

В зависимости от того, какие индексы имеются в Postsтаблице, вариант этого запроса может быть более эффективным, чем предложенное вами UNION ALLрешение. (У копии базы данных, которая у меня есть, нет для этого полезного индекса, и предлагаемое решение выполняет два полных сканирования Posts. Ниже приведено одно сканирование)

WITH Unpivoted AS
(
SELECT UserId
FROM dbo.Posts
UNPIVOT (UserId FOR U IN (OwnerUserId,LastEditorUserId)) Unpivoted
)
SELECT DISTINCT Users.Id
FROM dbo.Users INNER HASH JOIN Unpivoted
       ON  Users.Id = Unpivoted.UserId
WHERE Users.Reputation = 5

введите описание изображения здесь

Мартин Смит
источник