Resumen
SQL Server utiliza la combinación correcta (interna o externa) y agrega proyecciones cuando sea necesario para respetar toda la semántica de la consulta original al realizar traducciones internas entre aplicar y unir .
Las diferencias en los planes pueden explicarse por la semántica diferente de los agregados con y sin un grupo por cláusula en SQL Server.
Detalles
Unirse vs Aplicar
Tendremos que poder distinguir entre una solicitud y una unión :
Aplicar
La entrada interna (inferior) de la aplicación se ejecuta para cada fila de la entrada externa (superior), con uno o más valores de parámetros del lado interno proporcionados por la fila externa actual. El resultado general de la aplicación es la combinación (unión de todos) de todas las filas producidas por las ejecuciones parametrizadas del lado interno. La presencia de parámetros significa aplicar a veces se denomina unión correlacionada.
El operador Nested Loops siempre implementa una solicitud en los planes de ejecución . El operador tendrá una propiedad de referencias externas en lugar de unir predicados. Las referencias externas son los parámetros pasados del lado externo al interno en cada iteración del bucle.
Unirse
Una unión evalúa su predicado de unión en el operador de unión. La unión generalmente puede ser implementada por operadores de Hash Match , Merge o Nested Loops en SQL Server.
Cuando se elige Nested Loops , se puede distinguir de una aplicación por la falta de referencias externas (y generalmente la presencia de un predicado de unión). La entrada interna de una unión nunca hace referencia a valores de la entrada externa: el lado interno todavía se ejecuta una vez para cada fila externa, pero las ejecuciones del lado interno no dependen de ningún valor de la fila externa actual.
Para obtener más detalles, consulte mi publicación Aplicar frente a Nested Loops Join .
... ¿por qué hay una combinación externa en el plan de ejecución en lugar de una combinación interna ?
La combinación externa surge cuando el optimizador transforma una aplicación a una combinación (usando una regla llamada ApplyHandler
) para ver si puede encontrar un plan más económico basado en la combinación. Se requiere que la unión sea una unión externa para la corrección cuando la aplicación contiene un agregado escalar . No se garantizaría que una unión interna produzca los mismos resultados que la aplicación original como veremos.
Agregados escalares y vectoriales
- Un agregado sin una
GROUP BY
cláusula correspondiente es un agregado escalar .
- Un agregado con una
GROUP BY
cláusula correspondiente es un agregado vectorial .
En SQL Server, un agregado escalar siempre producirá una fila, incluso si no tiene filas para agregar. Por ejemplo, el COUNT
agregado escalar sin filas es cero. Un conjunto de vectores COUNT
sin filas es el conjunto vacío (sin filas).
Las siguientes consultas sobre juguetes ilustran la diferencia. También puede leer más sobre los agregados escalares y vectoriales en mi artículo Diversión con agregados escalares y vectoriales .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
demostración de violín db <>
Transformar solicitar unirse
Mencioné antes que la unión debe ser una unión externa para la corrección cuando la aplicación original contiene un agregado escalar . Para mostrar por qué este es el caso en detalle, utilizaré un ejemplo simplificado de la consulta de preguntas:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
El resultado correcto para la columna c
es cero , porque COUNT_BIG
es un agregado escalar . Al traducir esta consulta de solicitud al formulario de unión, SQL Server genera una alternativa interna que se vería similar a la siguiente si se expresara en T-SQL:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Para reescribir la aplicación como una unión no correlacionada, tenemos que introducir una GROUP BY
en la tabla derivada (de lo contrario, no podría haber una A
columna para unir). La unión tiene que ser una unión externa para que cada fila de la tabla @A
continúe produciendo una fila en la salida. La combinación izquierda producirá una NULL
columna for c
cuando el predicado de combinación no se evalúe como verdadero. Eso NULL
necesita ser traducido a cero COALESCE
para completar una transformación correcta de aplicar .
La demostración a continuación muestra cómo COALESCE
se requieren tanto la combinación externa como la combinación para producir los mismos resultados utilizando la combinación como la consulta de aplicación original :
demostración de violín db <>
Con el GROUP BY
... ¿por qué descomentar el grupo por la cláusula resulta en una unión interna?
Continuando con el ejemplo simplificado, pero agregando un GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
El COUNT_BIG
es ahora un vector agregado, por lo que el resultado correcto para un conjunto de entrada vacío ya no es cero, es ninguna fila en absoluto . En otras palabras, ejecutar las declaraciones anteriores no produce ningún resultado.
Esta semántica es mucho más fácil de cumplir cuando se traduce de aplicar a unir , ya CROSS APPLY
que rechaza naturalmente cualquier fila externa que no genere filas laterales internas. Por lo tanto, ahora podemos usar de forma segura una combinación interna, sin proyección de expresión adicional:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
La demostración a continuación muestra que la reescritura de unión interna produce los mismos resultados que la aplicación original con agregado vectorial:
demostración de violín db <>
El optimizador elige una combinación interna de combinación con la tabla pequeña porque encuentra un plan de combinación barato rápidamente ( se encontró un plan lo suficientemente bueno). El optimizador basado en el costo puede continuar para reescribir la unión de nuevo a una solicitud, tal vez encontrar un plan de solicitud más barato, como lo hará aquí si se usa una unión de bucle o una sugerencia de fuerza de búsqueda, pero no vale la pena el esfuerzo en este caso.
Notas
Los ejemplos simplificados usan diferentes tablas con diferentes contenidos para mostrar las diferencias semánticas más claramente.
Se podría argumentar que el optimizador debería ser capaz de razonar acerca de que una autounión no sea capaz de generar filas no coincidentes (no unidas), pero hoy no contiene esa lógica. No se garantiza que acceder a la misma tabla varias veces en una consulta produzca los mismos resultados en general de todos modos, dependiendo del nivel de aislamiento y la actividad concurrente.
El optimizador se preocupa por estas semánticas y casos extremos para que no tenga que hacerlo.
Bonificación: Plan de aplicación interna
SQL Server puede producir un plan de aplicación interno (¡no un plan de combinación interno !) Para la consulta de ejemplo, simplemente elige no hacerlo por razones de costo. El costo del plan de combinación externa que se muestra en la pregunta es de 0.02898 unidades en la instancia de SQL Server 2017 de mi computadora portátil.
Puede forzar un plan de aplicación (unión correlacionada) utilizando el indicador de traza 9114 no documentado y no admitido (que deshabilita, ApplyHandler
etc.) solo como ilustración:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Esto produce un plan de bucles anidados de aplicación con un carrete de índice diferido. El costo total estimado es 0.0463983 (más alto que el plan seleccionado):
Tenga en cuenta que el plan de ejecución que utiliza bucles anidados aplica produce resultados correctos utilizando la semántica de "unión interna" independientemente de la presencia de la GROUP BY
cláusula.
En el mundo real, normalmente tendríamos un índice para admitir una búsqueda en el lado interno de la solicitud para alentar a SQL Server a elegir esta opción de forma natural, por ejemplo:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
demostración de violín db <>