Plan de consulta extraño cuando se utiliza OR en la cláusula JOIN: exploración constante para cada fila de la tabla

10

Estoy tratando de producir un plan de consulta de muestra para mostrar por qué UNIONAR dos conjuntos de resultados puede ser mejor que usar OR en una cláusula JOIN. Un plan de consulta que he escrito me ha dejado perplejo. Estoy usando la base de datos StackOverflow con un índice no agrupado en Users.Reputation.

Imagen del plan de consulta La consulta es

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

El plan de consulta se encuentra en https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE , la duración de la consulta para mí es 4:37 min, 26612 filas devueltas.

No he visto este estilo de escaneo constante creado a partir de una tabla existente antes; no estoy familiarizado con por qué hay un escaneo constante ejecutado para cada fila, cuando un escaneo constante se usa generalmente para una sola fila ingresada por el usuario por ejemplo SELECT GETDATE (). ¿Por qué se usa aquí? Realmente agradecería alguna orientación al leer este plan de consulta.

Si divido ese OR en una UNIÓN, produce un plan estándar que se ejecuta en 12 segundos con las mismas 26612 filas devueltas.

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

Interpreto este plan como hacer esto:

  • Obtenga todas las 41782500 filas de las publicaciones (el número real de filas coincide con el escaneo de CI en las publicaciones)
  • Por cada 41782500 filas en Publicaciones:
    • Producir escalares:
    • Expr1005: OwnerUserId
    • Expr1006: OwnerUserId
    • Expr1004: el valor estático 62
    • Expr1008: LastEditorUserId
    • Expr1009: LastEditorUserId
    • Expr1007: el valor estático 62
  • En la concatenada:
    • Exp1010: Si Expr1005 (OwnerUserId) no es nulo, use ese otro uso Expr1008 (LastEditorUserID)
    • Expr1011: si Expr1006 (OwnerUserId) no es nulo, úselo, de lo contrario use Expr1009 (LastEditorUserId)
    • Expr1012: Si Expr1004 (62) es nulo, use eso, de lo contrario use Expr1007 (62)
  • En el escalar Compute: no sé qué hace un ampersand.
    • Expr1013: 4 [y?] 62 (Expr1012) = 4 y OwnerUserId IS NULL (NULL = Expr1010)
    • Expr1014: 4 [y?] 62 (Expr1012)
    • Expr1015: 16 y 62 (Expr1012)
  • En Ordenar por ordenar por:
    • Expr1013 Desc
    • Expr1014 Asc
    • Expr1010 Asc
    • Expr1015 Desc
  • En el intervalo de fusión, eliminó Expr1013 y Expr1015 (estas son entradas pero no salidas)
  • En el índice de búsqueda debajo de los bucles anidados, use Expr1010 y Expr1011 como predicados de búsqueda, pero no entiendo cómo tiene acceso a estos cuando no ha hecho la unión de bucle anidado desde IX_NC_REPUTATION al subárbol que contiene Expr1010 y Expr1011 .
  • La unión de bucles anidados devuelve solo los usuarios. ID que coinciden en el subárbol anterior. Debido al pushdown del predicado, se devuelven todas las filas devueltas de la búsqueda de índice en IX_NC_REPUTATION.
  • Se unen los últimos Nested Loops: para cada registro de Publicaciones, genera Users.Id donde se encuentra una coincidencia en el conjunto de datos a continuación.
Andrés
fuente
¿Intentó con una subconsulta EXISTA o 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ᵀᴹ
una 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ᵀᴹ

Respuestas:

10

El plan es similar al que entro con más detalle aquí .

La Poststabla está escaneada.

Para cada fila extrae el OwnerUserIdy LastEditorUserId. Esto es similar a la forma en que UNPIVOTfunciona. Verá un único operador de exploración constante en el plan para el siguiente, creando las dos filas de salida para cada fila de entrada.

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

En este caso, el plan es un poco más complejo ya que la semántica ores que si los dos valores de columna son iguales, solo se debe emitir una fila desde la unión Users(no dos)

Luego, se pasan por el intervalo de fusión para que, en el caso de que los valores sean los mismos, el rango se contraiga y solo se ejecute una búsqueda Users, de lo contrario, se ejecutarán dos búsquedas.

El valor 62es una bandera que significa que la búsqueda debe ser una búsqueda de igualdad.

Respecto a

No entiendo cómo tiene acceso a estos cuando no ha hecho la unión de bucle anidado desde IX_NC_REPUTATION al subárbol que contiene Expr1010 y Expr1011

Estos se definen en el operador de concatenación resaltado en amarillo. Esto está en el lado exterior de los bucles anidados resaltados en amarillo. Entonces esto se ejecuta antes de la búsqueda resaltada en amarillo en el interior de esos bucles anidados.

ingrese la descripción de la imagen aquí

Una reescritura que da un plan similar (aunque con el intervalo de fusión reemplazado por una unión de fusión) está debajo en caso de que esto ayude.

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) 

ingrese la descripción de la imagen aquí

Dependiendo de qué índices estén disponibles en la Poststabla, una variante de esta consulta puede ser más eficiente que su UNION ALLsolución propuesta . (la copia de la base de datos que tengo no tiene un índice útil para esto y la solución propuesta realiza dos escaneos completos Posts. El siguiente lo hace en un escaneo)

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

ingrese la descripción de la imagen aquí

Martin Smith
fuente