Sumário
O SQL Server usa a junção correta (interna ou externa) e adiciona projeções quando necessário para honrar toda a semântica da consulta original ao executar traduções internas entre aplicar e ingressar .
As diferenças nos planos podem ser explicadas pelas diferentes semânticas de agregados com e sem um grupo por cláusula no SQL Server.
Detalhes
Join vs Apply
Precisamos ser capazes de distinguir entre uma aplicação e uma associação :
Aplique
A entrada interna (inferior) da aplicação é executada para cada linha da entrada externa (superior), com um ou mais valores de parâmetros laterais internos fornecidos pela linha externa atual. O resultado geral da aplicação é a combinação (união de todas) de todas as linhas produzidas pelas execuções laterais internas parametrizadas. A presença de parâmetros que significa aplicar às vezes é chamada de junção correlacionada.
Uma aplicação é sempre implementada nos planos de execução pelo operador Nested Loops . O operador terá uma propriedade Referências externas, em vez de unir predicados. As referências externas são os parâmetros passados do lado externo para o lado interno em cada iteração do loop.
Junte-se
Uma junção avalia seu predicado de junção no operador de junção. A junção geralmente pode ser implementada por Hash Match , Merge ou Nested Loops operadores no SQL Server.
Quando Loops aninhados é escolhido, ele pode ser diferenciado de uma aplicação pela falta de referências externas (e geralmente pela presença de um predicado de junção). A entrada interna de uma junção nunca faz referência a valores da entrada externa - o lado interno ainda é executado uma vez para cada linha externa, mas as execuções do lado interno não dependem de nenhum valor da linha externa atual.
Para obter mais detalhes, consulte meu post. Inscreva-se contra a associação de loops aninhados .
... por que existe uma junção externa no plano de execução em vez de uma junção interna ?
A junção externa surge quando o otimizador transforma uma aplicação em uma junção (usando uma regra chamada ApplyHandler
) para verificar se ele pode encontrar um plano mais barato baseado em junção. A junção é necessária para ser uma junção externa para correção quando a aplicação contém um agregado escalar . Uma junção interna não teria garantia de produzir os mesmos resultados que o original se aplica, como veremos.
Agregados escalares e vetoriais
- Um agregado sem uma
GROUP BY
cláusula correspondente é um agregado escalar .
- Um agregado com uma
GROUP BY
cláusula correspondente é um vetor agregado.
No SQL Server, um agregado escalar sempre produzirá uma linha, mesmo se não houver linhas a serem agregadas. Por exemplo, o COUNT
agregado escalar de nenhuma linha é zero. Um vetor COUNT
agregado sem linhas é o conjunto vazio (nenhuma linha).
As seguintes consultas de brinquedos ilustram a diferença. Você também pode ler mais sobre agregados escalares e vetoriais no meu artigo Diversão com agregados escalares e vetoriais .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
db <> demo de violino
Transformando aplicar para participar
I mencionados antes que a união é requerida para ter uma junção externa para correcção quando o original aplicar contém um agregado escalar . Para mostrar por que esse é o caso em detalhes, usarei um exemplo simplificado da consulta de pergunta:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
O resultado correto para a coluna c
é zero , porque COUNT_BIG
é um agregado escalar . Ao converter esta consulta de aplicação para ingressar no formulário, o SQL Server gera uma alternativa interna que seria semelhante à seguinte se fosse expressa em T-SQL:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Para reescrever o apply como uma junção não correlacionada, precisamos introduzir a GROUP BY
na tabela derivada (caso contrário, não poderá haver A
coluna na qual participar). A junção deve ser uma junção externa para que cada linha da tabela @A
continue produzindo uma linha na saída. A junção esquerda produzirá uma NULL
coluna for c
quando o predicado da junção não for avaliado como verdadeiro. Isso NULL
precisa ser convertido em zero COALESCE
para concluir uma transformação correta de aplicar .
A demonstração abaixo mostra como a junção externa e COALESCE
são necessárias para produzir os mesmos resultados usando a junção que a consulta de aplicação original :
db <> demo de violino
Com o GROUP BY
... por que descomentar o grupo por cláusula resulta em uma junção interna?
Continuando o exemplo simplificado, mas adicionando um GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
A COUNT_BIG
é agora um vector de agregado, de modo que o resultado correcto para um conjunto de entrada vazio já não é zero, é nenhuma linha de todo . Em outras palavras, executar as instruções acima não produz saída.
Essa semântica é muito mais fácil de honrar ao converter de aplicar para juntar , pois CROSS APPLY
naturalmente rejeita qualquer linha externa que não gera linhas laterais internas. Portanto, podemos usar com segurança uma junção interna agora, sem projeção de expressão extra:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
A demonstração abaixo mostra que a reescrita da junção interna produz os mesmos resultados que o original se aplica ao agregado de vetor:
db <> demo de violino
O otimizador escolhe uma junção interna de mesclagem com a pequena tabela porque encontra uma junção barata plano de rapidamente (plano suficientemente bom encontrado). O otimizador baseado em custos pode reescrever a junção de volta para uma aplicação - talvez encontrando um plano de aplicação mais barato, como será aqui se uma junção de loop ou dica for forçada - mas não vale a pena o esforço nesse caso.
Notas
Os exemplos simplificados usam tabelas diferentes com conteúdos diferentes para mostrar as diferenças semânticas mais claramente.
Pode-se argumentar que o otimizador deve ser capaz de raciocinar sobre uma auto-junção que não é capaz de gerar linhas incompatíveis (sem junção), mas hoje não contém essa lógica. O acesso à mesma tabela várias vezes em uma consulta não garante os mesmos resultados em geral, de qualquer maneira, dependendo do nível de isolamento e da atividade simultânea.
O otimizador se preocupa com essas semânticas e casos extremos, para que você não precise.
Bônus: Plano de Aplicação Interna
O SQL Server pode produzir um plano de aplicação interno (não um plano de associação interno !) Para a consulta de exemplo, mas escolhe não por motivos de custo. O custo do plano de associação externa mostrado na pergunta é de 0,02898 unidades na instância do SQL Server 2017 do meu laptop.
Você pode forçar um plano de aplicação (junção correlacionada) usando o sinalizador de rastreio não documentado e não suportado 9114 (que desativa ApplyHandler
etc.) apenas para ilustração:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Isso produz um plano de aplicação de loops aninhados com um spool de índice lento. O custo total estimado é 0,0463983 (superior ao plano selecionado):
Observe que o plano de execução usando loops aninhados de aplicação produz resultados corretos usando a semântica "junção interna", independentemente da presença da GROUP BY
cláusula.
No mundo real, normalmente teríamos um índice para dar suporte a uma busca no lado interno da aplicação para incentivar o SQL Server a escolher essa opção naturalmente, por exemplo:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
db <> demo de violino