Plano de consulta estranho ao usar OR na cláusula JOIN - Varredura constante para todas as linhas da tabela

10

Estou tentando produzir um plano de consulta de exemplo para mostrar por que UNIAR dois conjuntos de resultados pode ser melhor do que usar OR em uma cláusula JOIN. Um plano de consulta que escrevi me deixou perplexo. Estou usando o banco de dados StackOverflow com um índice não clusterizado em Users.Reputation.

Imagem do plano de consulta A consulta é

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

O plano de consulta está em https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE , a duração da consulta para mim é de 4:37 min, com retorno de 26612 linhas.

Eu nunca vi esse estilo de varredura constante sendo criado a partir de uma tabela existente antes - não estou familiarizado com o motivo pelo qual uma varredura constante é executada para cada linha, quando uma varredura constante geralmente é usada para uma única linha inserida pelo usuário por exemplo, SELECT GETDATE (). Por que é usado aqui? Eu realmente aprecio algumas orientações ao ler este plano de consulta.

Se eu dividir esse OR em um UNION, ele produzirá um plano padrão em execução em 12 segundos com as mesmas 26612 linhas retornadas.

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

Eu interpreto esse plano da seguinte maneira:

  • Obter todas as 41782500 linhas de Postagens (o número real de linhas corresponde à verificação do IC nas Postagens)
  • Para cada 41782500 linhas em Postagens:
    • Produza escalares:
    • Expr1005: OwnerUserId
    • Expr1006: OwnerUserId
    • Expr1004: O valor estático 62
    • Expr1008: LastEditorUserId
    • Expr1009: LastEditorUserId
    • Expr1007: O valor estático 62
  • No concatenar:
    • Exp1010: Se Expr1005 (OwnerUserId) não for nulo, use o resto Expr1008 (LastEditorUserID)
    • Expr1011: Se Expr1006 (OwnerUserId) não for nulo, use isso; caso contrário, use Expr1009 (LastEditorUserId)
    • Expr1012: Se Expr1004 (62) for nulo, use-o; caso contrário, use Expr1007 (62)
  • No escalar de computação: não sei o que um e comercial faz.
    • Expr1013: 4 [e?] 62 (Expr1012) = 4 e OwnerUserId É NULL (NULL = Expr1010)
    • Expr1014: 4 [e?] 62 (Expr1012)
    • Expr1015: 16 e 62 (Expr1012)
  • Na ordem Classificar por:
    • Expr1013 Desc
    • Expr1014 Asc
    • Expr1010 Asc
    • Expr1015 Desc
  • No intervalo de mesclagem, ele removeu o Expr1013 e o Expr1015 (são entradas, mas não saídas)
  • Na busca de índice abaixo da junção de loops aninhados, ele está usando Expr1010 e Expr1011 como predicados de busca, mas não entendo como ele tem acesso a eles quando não fez a junção de loop aninhado de IX_NC_REPUTATION à subárvore que contém Expr1010 e Expr1011 .
  • A associação Loops aninhados retorna apenas os Users.IDs que têm uma correspondência na subárvore anterior. Devido ao empilhamento de predicado, todas as linhas retornadas da busca de índice em IX_NC_REPUTATION são retornadas.
  • Os últimos Loops aninhados ingressam: Para cada registro de Postagens, produza Users.Id onde uma correspondência é encontrada no conjunto de dados abaixo.
Andrew
fonte
Você tentou com uma subconsulta EXISTS ou subconsultas? 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ᵀᴹ
Uma subconsulta: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ᵀᴹ

Respostas:

10

O plano é semelhante ao que eu faço em mais detalhes aqui .

A Poststabela é digitalizada.

Para cada linha extrai o OwnerUserIde LastEditorUserId. Isso é semelhante ao modo como UNPIVOTfunciona. Você vê um único operador de varredura constante no plano abaixo, criando as duas linhas de saída para cada linha de entrada.

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

Nesse caso, o plano é um pouco mais complexo, pois a semântica oré que, se os dois valores de coluna forem os mesmos, apenas uma linha deverá ser emitida a partir da junção Users(não duas)

Eles são então colocados no intervalo de mesclagem para que, no caso de os valores serem os mesmos, o intervalo seja reduzido e apenas uma busca seja executada Users- caso contrário, duas buscas serão executadas contra ele.

O valor 62é um sinalizador, o que significa que a busca deve ser uma busca de igualdade.

A respeito de

Eu não entendo como ele tem acesso a eles quando não fez a junção de loop aninhado de IX_NC_REPUTATION para a subárvore que contém Expr1010 e Expr1011

Eles são definidos no operador de concatenação destacado em amarelo. Está no lado externo dos loops aninhados destacados em amarelo. Portanto, isso é executado antes da busca destacada em amarelo no interior dos loops aninhados.

insira a descrição da imagem aqui

Uma reescrita que fornece um plano semelhante (embora com o intervalo de mesclagem substituído por uma união de mesclagem) está abaixo, caso isso ajude.

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) 

insira a descrição da imagem aqui

Dependendo de quais índices estão disponíveis na Poststabela, uma variante dessa consulta pode ser mais eficiente que a UNION ALLsolução proposta . (a cópia do banco de dados que possuo não possui um índice útil para isso e a solução proposta faz duas varreduras completas Posts. A seguir, em uma varredura)

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

insira a descrição da imagem aqui

Martin Smith
fonte