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:
- Loops aninhados com uma referência externa (uma aplicação)
- Um carretel de índice lento interno que procura na referência externa
- Um operador de concatenação do lado interno
Por exemplo, a consulta na pergunta produz um plano como o seguinte:
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 PrepRecompute
métodos de interface. No início dessa verificação, os operadores do lado interno leem a CParamBounds:FNeedToReload
propriedade para ver se a referência externa mudou da última vez. Um exemplo de rastreamento de pilha é mostrado abaixo:
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:FNeedToReload
sempre 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 RewindHelper
método e nunca seu ReloadHelper
mé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'.
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;
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:
E o resultado agora está incorreto, apenas três linhas:
Embora esse comportamento não seja garantido, a primeira linha da Verificação de Índice em Cluster tem um c1
valor 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 é:
E, como indicado, são produzidas 8 linhas, todas com c1 = 1
:
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).