Configurar
DROP TABLE IF EXISTS #EmptyTable, #BigTable
CREATE TABLE #EmptyTable(A int);
CREATE TABLE #BigTable(A int);
INSERT INTO #BigTable
SELECT TOP 10000000 CRYPT_GEN_RANDOM(3)
FROM sys.all_objects o1,
sys.all_objects o2,
sys.all_objects o3;
Consulta
WITH agg
AS (SELECT DISTINCT a
FROM #BigTable)
SELECT *
FROM #EmptyTable E
INNER HASH JOIN agg B
ON B.A = E.A;
Plano de execução
Problema
Esta é uma reprodução simplificada de um fenômeno que eu não havia notado antes de hoje. Minha expectativa para uma junção de hash interna seria que, se a entrada de compilação estiver vazia, o lado do teste não deve ser executado, pois a junção não pode retornar nenhuma linha. O exemplo acima contradiz isso e lê as 10 milhões de linhas da tabela. Isso adiciona 2,196 segundos ao tempo de execução da consulta (99,9%).
Observações Adicionais
- Com
OPTION (MAXDOP 1)
o plano de execução não lê nenhuma linha de#BigTable
. OActualExecutions
é0
para todos os operadores dentro da junção de hash. - Para a consulta
SELECT * FROM #EmptyTable E INNER HASH JOIN #BigTable B ON B.A = E.A
- recebo um plano paralelo, o operador de varredura no interior da junção de hash temActualExecutions
DOP, mas ainda nenhuma linha é lida. Este plano não tem operador de repartição de fluxos (ou agregado)
Pergunta
O que está acontecendo aqui? Por que o plano original apresenta o problema e os outros casos não?
Não executar o lado do probe da junção quando a compilação está vazia é uma otimização. Não está disponível para junção de hash no modo de linha paralela quando o lado do teste tem uma ramificação filho, ou seja, quando há um operador de troca.
Houve um relatório semelhante há muitos anos por Adam Machanic no agora extinto site de comentários Connect. O cenário era um Filtro de inicialização no lado do probe, que executava seus operadores filho inesperadamente. A resposta da Microsoft foi que o mecanismo exige uma garantia de que certas estruturas são inicializadas, e a única maneira sensata de impor isso era garantir que os operadores do lado do probe fossem abertos.
Minha própria lembrança dos detalhes é que não inicializar uma subárvore levou a erros de tempo paralelo difíceis de corrigir. Garantir que a ramificação filha fosse iniciada era uma solução para esses problemas.
A junção de hash no modo de lote não tem esse efeito colateral porque a maneira como os encadeamentos são gerenciados é diferente.
No seu caso particular, o efeito é mais pronunciado porque o agregado de hash está bloqueando; ele consome toda a sua entrada durante a chamada Open() do iterador. Quando há apenas operadores de streaming no lado do probe, o impacto no desempenho costuma ser mais limitado, dependendo de quanto trabalho é necessário para retornar a primeira linha ao lado do probe da junção de hash.
Não é uma resposta, mas se a junção de hash não for forçada, essa consulta não receberá a junção de hash como um plano. Uma solução alternativa é definir uma variável de bit como 1 se existirem linhas na tabela e 0 se não existirem e em vez de #Emptytable use (Selecione * de #Emptytable onde @bit =1)
E adicione uma opção recompile ao final, nenhuma execução acontecerá.
Eu acho que essa condição nunca deve acontecer se o forçamento não for usado e se for necessário uma solução alternativa.