¿Estamos haciendo algo mal o es un error de SQL Server?
Es un error de resultados incorrectos, que debe informar a través de su canal de soporte habitual. Si no tiene un acuerdo de soporte, puede ser útil saber que los incidentes pagados normalmente se reembolsan si Microsoft confirma el comportamiento como un error.
El error requiere tres ingredientes:
- Bucles anidados con una referencia externa (una aplicación)
- Un carrete de índice perezoso del lado interno que busca en la referencia externa
- Un operador de concatenación del lado interno
Por ejemplo, la consulta en la pregunta produce un plan como el siguiente:
Hay muchas formas de eliminar uno de estos elementos, por lo que el error ya no se reproduce.
Por ejemplo, uno podría crear índices o estadísticas que significan que el optimizador elige no utilizar un carrete de índice diferido. O bien, uno podría usar sugerencias para forzar un hash o fusionar unión en lugar de usar Concatenación. También se podría reescribir la consulta para expresar la misma semántica, pero lo que da como resultado una forma de plan diferente donde faltan uno o más de los elementos necesarios.
Más detalles
Un carrete de índice diferido almacena en caché las filas de resultados del lado interno, en una tabla de trabajo indexada por valores de referencia externa (parámetro correlacionado). Si se le pide a un carrete de índice diferido una referencia externa que ha visto antes, obtiene la fila de resultados en caché de su tabla de trabajo (un "rebobinado"). Si se solicita al spool un valor de referencia externo que no haya visto antes, ejecuta su subárbol con el valor de referencia externo actual y almacena en caché el resultado (un "reenlace"). El predicado de búsqueda en el carrete de índice diferido indica la (s) clave (s) para su tabla de trabajo.
El problema ocurre en esta forma de plan específico cuando el carrete verifica si una nueva referencia externa es la misma que ha visto antes. La unión de bucles anidados actualiza sus referencias externas correctamente y notifica a los operadores en su entrada interna a través de sus PrepRecompute
métodos de interfaz. Al comienzo de esta comprobación, los operadores del lado interno leen la CParamBounds:FNeedToReload
propiedad para ver si la referencia externa ha cambiado desde la última vez. A continuación se muestra un ejemplo de seguimiento de pila:
Cuando existe el subárbol que se muestra arriba, específicamente donde se usa la concatenación, algo sale mal (quizás un problema de ByVal / ByRef / Copy) con los enlaces de manera que CParamBounds:FNeedToReload
siempre devuelve falso, independientemente de si la referencia externa realmente cambió o no.
Cuando existe el mismo subárbol, pero se usa una Unión de combinación o Unión de hash, esta propiedad esencial se establece correctamente en cada iteración, y el Spool de índice diferido se rebobina o rebobina cada vez según corresponda. El Distinct Sort y Stream Aggregate son irreprensibles, por cierto. Mi sospecha es que Merge y Hash Union hacen una copia del valor anterior, mientras que Concatenation usa una referencia. Desafortunadamente, es casi imposible verificar esto sin acceso al código fuente de SQL Server.
El resultado neto es que el carrete de índice diferido en la forma del plan problemático siempre piensa que ya ha visto la referencia externa actual, rebobina buscando en su tabla de trabajo, generalmente no encuentra nada, por lo que no se devuelve ninguna fila para esa referencia externa. Pasando por la ejecución en un depurador, el spool solo ejecuta su RewindHelper
método, y nunca su ReloadHelper
método (reload = rebind en este contexto). Esto es evidente en el plan de ejecución porque todos los operadores bajo el spool tienen 'Número de ejecuciones = 1'.
La excepción, por supuesto, es para la primera referencia externa que se da el Lazy Index Spool. Esto siempre ejecuta el subárbol y almacena en caché una fila de resultados en la tabla de trabajo. Todas las iteraciones posteriores dan como resultado un rebobinado, que solo producirá una fila (la única fila en caché) cuando la iteración actual tenga el mismo valor para la referencia externa que la primera vez.
Entonces, para cualquier conjunto de entrada dado en el lado externo de la unión de bucles anidados, la consulta devolverá tantas filas como duplicados de la primera fila procesada (más una para la primera fila, por supuesto).
Manifestación
Tabla y datos de muestra:
CREATE TABLE #T1
(
pk integer IDENTITY NOT NULL,
c1 integer NOT NULL,
CONSTRAINT PK_T1
PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6);
La siguiente consulta (trivial) produce un recuento correcto de dos para cada fila (18 en total) utilizando una Unión de combinación:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C;
Si ahora agregamos una sugerencia de consulta para forzar una Concatenación:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C
OPTION (CONCAT UNION);
El plan de ejecución tiene la forma problemática:
Y el resultado ahora es incorrecto, solo tres filas:
Aunque este comportamiento no está garantizado, la primera fila del Análisis de índice agrupado tiene un c1
valor de 1. Hay otras dos filas con este valor, por lo que se generan tres filas en total.
Ahora trunca la tabla de datos y cárgala con más duplicados de la "primera" fila:
TRUNCATE TABLE #T1;
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (1), (1), (1), (1), (1);
Ahora el plan de concatenación es:
Y, como se indica, se producen 8 filas, todas con, c1 = 1
por supuesto:
Noté que ha abierto un elemento Connect para este error, pero realmente ese no es el lugar para informar problemas que están teniendo un impacto en la producción. Si ese es el caso, realmente debe comunicarse con el Soporte técnico de Microsoft.
Este error de resultados incorrectos se corrigió en algún momento. Ya no se reproduce en ninguna versión de SQL Server desde 2012 en adelante. Reproduce en SQL Server 2008 R2 SP3-GDR build 10.50.6560.0 (X64).