Resultados de selección impredecibles de SQL Server (¿error de dbms?)

37

A continuación se muestra un ejemplo simple, que arroja resultados extraños, que son impredecibles y no podemos explicarlo en nuestro equipo. ¿Estamos haciendo algo mal o es un error de SQL Server?

Después de una investigación, redujimos el área de búsqueda a una cláusula sindical en la subconsulta , que selecciona un registro de la tabla "hombres"

Funciona como se esperaba en SQL Server 2000 (devuelve 12 filas), pero en 2008 y 2012 solo devuelve una fila.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Esto devuelve solo una fila: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Descomenta la última línea y da: 2 2 2

Hay muchos comportamientos extraños:

  • Después de una serie de caídas, crea, trunca e inserta en la tabla "hombres" , a veces funciona (devuelve 12 filas)
  • Cuando cambia "union select men.wife_id" a "union all select men.wife_id" o "union select isnull (men.wife_id, null)" (!!!) devuelve 12 filas (como se esperaba).
  • El comportamiento extraño parece no estar relacionado con el tipo de datos de la columna "wife_id". Lo observamos en el sistema de desarrollo con conjuntos de datos mucho mayores.
  • "where wife_id> 0" devuelve 6 filas
  • También observamos comportamientos extraños de puntos de vista con este tipo de declaraciones. SELECT * devuelve un subconjunto de filas, SELECT TOP 1000 devuelve todos
Ryszard Bocian
fuente

Respuestas:

35

¿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:

  1. Bucles anidados con una referencia externa (una aplicación)
  2. Un carrete de índice perezoso del lado interno que busca en la referencia externa
  3. Un operador de concatenación del lado interno

Por ejemplo, la consulta en la pregunta produce un plan como el siguiente:

Plan anotado

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 PrepRecomputemétodos de interfaz. Al comienzo de esta comprobación, los operadores del lado interno leen la CParamBounds:FNeedToReloadpropiedad para ver si la referencia externa ha cambiado desde la última vez. A continuación se muestra un ejemplo de seguimiento de pila:

CParamBounds: FNeedToReload

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:FNeedToReloadsiempre 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 RewindHelpermétodo, y nunca su ReloadHelpermé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'.

RewindHelper

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;

Plan de unión de unión

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:

Plan de concatenación

Y el resultado ahora es incorrecto, solo tres filas:

Resultado de tres filas

Aunque este comportamiento no está garantizado, la primera fila del Análisis de índice agrupado tiene un c1valor 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:

Plan de concatenación de 8 filas

Y, como se indica, se producen 8 filas, todas con, c1 = 1por supuesto:

Resultado de 8 filas

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).

Paul White dice GoFundMonica
fuente
-3

¿Por qué usa una subconsulta sin la declaración from? Creo que eso puede causar la diferencia en los servidores de 2005 y 2008. ¿Tal vez podrías ir con una unión explícita?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

fuente
3
Sí, esto funciona, pero mi versión también debería funcionar. El ejemplo abstracto anterior es una versión mucho más simplificada de nuestra consulta de producción, lo que tiene mucho más sentido.