CROSS APPLY produz junção externa


17

Em resposta à contagem de SQL distinta na partição, Erik Darling postou esse código para solucionar a falta de COUNT(DISTINCT) OVER ():

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

A consulta usa CROSS APPLY(não OUTER APPLY); por que há uma associação externa no plano de execução em vez de uma associação interna ?

insira a descrição da imagem aqui

Além disso, por que descomentar o grupo por cláusula resulta em uma junção interna?

insira a descrição da imagem aqui

Eu não acho que os dados sejam importantes, mas copiando os dados de kevinwhat na outra questão:

create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)

Respostas:


23

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 BYcláusula correspondente é um agregado escalar .
  • Um agregado com uma GROUP BYclá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 COUNTagregado 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 BYna tabela derivada (caso contrário, não poderá haver Acoluna na qual participar). A junção deve ser uma junção externa para que cada linha da tabela @Acontinue produzindo uma linha na saída. A junção esquerda produzirá uma NULLcoluna for cquando o predicado da junção não for avaliado como verdadeiro. Isso NULLprecisa ser convertido em zero COALESCEpara concluir uma transformação correta de aplicar .

A demonstração abaixo mostra como a junção externa e COALESCEsã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 APPLYnaturalmente 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 ApplyHandleretc.) 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):

Spool de índice aplicar plano

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 BYclá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


-3

A aplicação cruzada é uma operação lógica nos dados. Ao decidir como obter esses dados, o SQL Server escolhe o operador físico apropriado para obter os dados desejados.

Não há operador de aplicação física e o SQL Server o converte no operador de junção apropriado e esperançosamente eficiente.

Você pode encontrar uma lista dos operadores físicos no link abaixo.

https://docs.microsoft.com/en-us/sql/relational-databases/showplan-logical-and-physical-operators-reference?view=sql-server-2017

O otimizador de consulta cria um plano de consulta como uma árvore que consiste em operadores lógicos. Depois que o otimizador de consulta cria o plano, ele escolhe o operador físico mais eficiente para cada operador lógico. O otimizador de consulta usa uma abordagem baseada em custo para determinar qual operador físico implementará um operador lógico.

Geralmente, uma operação lógica pode ser implementada por vários operadores físicos. No entanto, em casos raros, um operador físico também pode implementar várias operações lógicas.

edit / Parece que entendi sua pergunta errada. O servidor SQL normalmente escolhe o operador mais apropriado. Sua consulta não precisa retornar valores para todas as combinações de ambas as tabelas, quando é usada uma junção cruzada. Basta calcular o valor que você deseja para cada linha é o que é feito aqui.

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.