Resultados imprevisíveis de seleção do SQL Server (erro de dbms?)


37

Abaixo está um exemplo simples, que retorna resultados estranhos, imprevisíveis e não podemos explicá-lo em nossa equipe. Estamos fazendo algo errado ou é um erro do SQL Server?

Após alguma investigação, reduzimos a área de pesquisa à cláusula union na subconsulta , que seleciona um registro da tabela "men"

Funciona como esperado no SQL Server 2000 (retorna 12 linhas), mas em 2008 e 2012 retorna apenas uma linha.

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)

Isso retorna apenas uma linha: 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

Remova o comentário da última linha e ela mostra: 2 2 2

Existem muitos comportamentos estranhos:

  • Após séries de descargas, cria, trunca e insere na tabela "men" , às vezes funciona (retorna 12 linhas)
  • Quando você altera "union select men.wife_id" para "union all select men.wife_id" ou "union select isnull (men.wife_id, null)" (!!!), ele retorna 12 linhas (conforme o esperado).
  • O comportamento estranho parece não ter relação com o tipo de dados da coluna "wife_id". Observamos isso no sistema de desenvolvimento com conjuntos de dados muito maiores.
  • "where wife_id> 0" retorna 6 linhas
  • também observamos um comportamento estranho de visões com esse tipo de afirmação. SELECT * retorna subconjunto de linhas, SELECT TOP 1000 retorna todos

Respostas:


35

Estamos fazendo algo errado ou é um erro do SQL Server?

É um erro de resultados errados, que você deve relatar através do seu canal de suporte habitual. Se você não tiver um contrato de suporte, pode ser útil saber que os incidentes pagos normalmente são reembolsados ​​se a Microsoft confirmar o comportamento como um bug.

O bug requer três ingredientes:

  1. Loops aninhados com uma referência externa (uma aplicação)
  2. Um carretel de índice lento interno que procura na referência externa
  3. Um operador de concatenação do lado interno

Por exemplo, a consulta na pergunta produz um plano como o seguinte:

Plano anotado

Existem várias maneiras de remover um desses elementos, para que o bug não seja mais reproduzido.

Por exemplo, é possível criar índices ou estatísticas que significam que o otimizador escolhe não utilizar um spool de índice lento. Ou, pode-se usar dicas para forçar uma união de hash ou mesclagem em vez de usar concatenação. Também se pode reescrever a consulta para expressar a mesma semântica, mas que resulta em um formato de plano diferente, onde um ou mais dos elementos necessários estão ausentes.

Mais detalhes

Um Spool de Índice Preguiçoso armazena em cache preguiçosamente as linhas de resultados do lado interno, em uma tabela de trabalho indexada pelos valores de referência externa (parâmetro correlacionado). Se um Spool de Índice Preguiçoso for solicitado para uma referência externa que ele já viu antes, ele buscará a linha de resultados em cache de sua tabela de trabalho (um "rebobinar"). Se for solicitado ao spool um valor de referência externo que ele nunca viu antes, ele executará sua subárvore com o valor de referência externo atual e armazenará em cache o resultado (uma "religação"). O predicado de busca no spool de índice lento indica as chaves para sua tabela de trabalho.

O problema ocorre nessa forma de plano específico quando o spool verifica se uma nova referência externa é a mesma que a anterior. A junção de loops aninhados atualiza suas referências externas corretamente e notifica os operadores sobre sua entrada interna por meio de seus PrepRecomputemétodos de interface. No início dessa verificação, os operadores do lado interno leem a CParamBounds:FNeedToReloadpropriedade para ver se a referência externa mudou da última vez. Um exemplo de rastreamento de pilha é mostrado abaixo:

CParamBounds: FNeedToReload

Quando a subárvore mostrada acima existe, especificamente onde a Concatenação é usada, algo dá errado (talvez um problema de ByVal / ByRef / Copy) nas ligações, de modo que CParamBounds:FNeedToReloadsempre retorne falso, independentemente de a referência externa realmente ter sido alterada ou não.

Quando a mesma subárvore existe, mas uma União de mesclagem ou União de hash é usada, essa propriedade essencial é configurada corretamente em cada iteração, e o Spool de Índice Preguiçoso retrocede ou religa sempre que apropriado. A Distinct Sort e Stream Aggregate são sem culpa, a propósito. Minha suspeita é que Merge e Hash Union façam uma cópia do valor anterior, enquanto Concatenação usa uma referência. É praticamente impossível verificar isso sem acesso ao código-fonte do SQL Server, infelizmente.

O resultado líquido é que o Lazy Index Spool na forma problemática do plano sempre pensa que já viu a referência externa atual, retrocede ao procurar em sua tabela de trabalho, geralmente não encontra nada; portanto, nenhuma linha é retornada para essa referência externa. Percorrendo a execução em um depurador, o spool apenas executa seu RewindHelpermétodo e nunca seu ReloadHelpermétodo (reload = rebind neste contexto). Isso é evidente no plano de execução porque todos os operadores no spool têm 'Número de execuções = 1'.

RewindHelper

A exceção, é claro, é a primeira referência externa fornecida pelo Lazy Index Spool. Isso sempre executa a subárvore e armazena em cache uma linha de resultado na tabela de trabalho. Todas as iterações subsequentes resultam em um retrocesso, que produzirá apenas uma linha (a única linha em cache) quando a iteração atual tiver o mesmo valor para a referência externa da primeira vez.

Portanto, para qualquer entrada definida no lado externo da junção de loops aninhados, a consulta retornará quantas linhas houver duplicatas da primeira linha processada (mais uma, é claro, para a primeira linha).

Demo

Dados de tabela e amostra:

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

A seguinte consulta (trivial) produz uma contagem correta de duas para cada linha (18 no total) usando uma União de mesclagem:

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;

Mesclar plano de união

Se agora adicionarmos uma dica de consulta para forçar uma concatenação:

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

O plano de execução tem a forma problemática:

Plano de concatenação

E o resultado agora está incorreto, apenas três linhas:

Resultado de três linhas

Embora esse comportamento não seja garantido, a primeira linha da Verificação de Índice em Cluster tem um c1valor de 1. Há duas outras linhas com esse valor, portanto, três linhas são produzidas no total.

Agora trunque a tabela de dados e carregue-a com mais duplicatas da 'primeira' linha:

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

Agora, o plano de concatenação é:

Plano de concatenação de 8 linhas

E, como indicado, são produzidas 8 linhas, todas com c1 = 1:

Resultado de 8 linhas

Percebo que você abriu um item do Connect para esse bug, mas na verdade esse não é o lugar para relatar problemas com impacto na produção. Se for esse o caso, você realmente deve entrar em contato com o suporte da Microsoft.


Este erro de resultados errados foi corrigido em algum momento. Ele não é mais reproduzido para mim em nenhuma versão do SQL Server a partir de 2012. Ele é reproduzido novamente no SQL Server 2008 R2 SP3-GDR build 10.50.6560.0 (X64).


-3

Por que você usa uma subconsulta sem a instrução from? Eu acho que isso pode causar a diferença nos servidores de 2005 e 2008. Talvez você possa ir com junção 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

3
Sim, isso funciona, mas minha versão também deve funcionar. O exemplo abstrato acima é uma versão muito simplificada da nossa consulta de produção, o que faz muito mais sentido.
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.