Exemplo da vida real, quando usar OUTER / CROSS APPLY no SQL


124

Eu estive olhando CROSS / OUTER APPLYcom um colega e estamos lutando para encontrar exemplos da vida real de onde usá-los.

Passei bastante tempo analisando Quando devo usar o Cross Apply sobre a Junção interna? e pesquisando no Google, mas o exemplo principal (apenas) parece bastante bizarro (usando o número de linhas de uma tabela para determinar quantas linhas selecionar em outra tabela).

Eu pensei que este cenário pode se beneficiar de OUTER APPLY:

Tabela de contatos (contém 1 registro para cada contato) Tabela de entradas de comunicação (pode conter n telefone, fax, e-mail para cada contato)

Porém, usando subconsultas, expressões comuns de tabela, OUTER JOINcom RANK()e OUTER APPLYtodos parecem ter o mesmo desempenho. Acho que isso significa que o cenário não é aplicável APPLY.

Compartilhe alguns exemplos da vida real e ajude a explicar o recurso!


5
"top n per group" ou XML de análise é comum. Veja algumas das minhas respostas stackoverflow.com/…
gbn




Respostas:


174

Alguns usos para APPLYsão ...

1) N principais consultas por grupo (pode ser mais eficiente para algumas cardinalidades)

SELECT pr.name,
       pa.name
FROM   sys.procedures pr
       OUTER APPLY (SELECT TOP 2 *
                    FROM   sys.parameters pa
                    WHERE  pa.object_id = pr.object_id
                    ORDER  BY pr.name) pa
ORDER  BY pr.name,
          pa.name 

2) Chamando uma função com valor de tabela para cada linha na consulta externa

SELECT *
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle)

3) Reutilizando um Alias ​​de Coluna

SELECT number,
       doubled_number,
       doubled_number_plus_one
FROM master..spt_values
CROSS APPLY (SELECT 2 * CAST(number AS BIGINT)) CA1(doubled_number)  
CROSS APPLY (SELECT doubled_number + 1) CA2(doubled_number_plus_one)  

4) Desvinculando mais de um grupo de colunas

Assume a estrutura da tabela que viola 1NF ....

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

     Foo1 INT, Foo2 INT, Foo3 INT,
     Bar1 INT, Bar2 INT, Bar3 INT
  ); 

Exemplo usando a VALUESsintaxe 2008+ .

SELECT Id,
       Foo,
       Bar
FROM   T
       CROSS APPLY (VALUES(Foo1, Bar1),
                          (Foo2, Bar2),
                          (Foo3, Bar3)) V(Foo, Bar); 

Em 2005 UNION ALLpode ser usado em seu lugar.

SELECT Id,
       Foo,
       Bar
FROM   T
       CROSS APPLY (SELECT Foo1, Bar1 
                    UNION ALL
                    SELECT Foo2, Bar2 
                    UNION ALL
                    SELECT Foo3, Bar3) V(Foo, Bar);

1
Uma boa lista de usos lá, mas a chave são os exemplos da vida real - eu adoraria ver um para cada um.
Lee Tickett

Para o nº 1, isso pode ser alcançado igualmente usando classificação, subconsultas ou expressões comuns de tabela? Você pode dar um exemplo quando isso não for verdade?
Lee Tickett

@LeeTickett - Por favor, leia o link. Tem uma discussão de 4 páginas sobre quando você prefere um ao outro.
Martin Smith

1
Certifique-se de visitar o link incluído no exemplo # 1. Eu usei essas duas abordagens (ROW OVER e CROSS APPLY) com ambas com bom desempenho em vários cenários, mas nunca entendi por que elas têm desempenho diferente. Esse artigo foi enviado dos céus !! O foco na indexação adequada, que corresponde à ordem pelas direções, ajudou em grande parte as consultas que possuem estrutura "adequada", mas que apresentam problemas de desempenho quando consultadas. Obrigado por incluí-lo !!
Chris Porter

1
@mr_eclair parece que agora está em itprotoday.com/software-development/…
Martin Smith

87

Existem várias situações em que você não pode evitar CROSS APPLYou OUTER APPLY.

Considere que você tem duas tabelas.

TABELA MESTRE

x------x--------------------x
| Id   |        Name        |
x------x--------------------x
|  1   |          A         |
|  2   |          B         |
|  3   |          C         |
x------x--------------------x

TABELA DE DETALHES

x------x--------------------x-------x
| Id   |      PERIOD        |   QTY |
x------x--------------------x-------x
|  1   |   2014-01-13       |   10  |
|  1   |   2014-01-11       |   15  |
|  1   |   2014-01-12       |   20  |
|  2   |   2014-01-06       |   30  |
|  2   |   2014-01-08       |   40  |
x------x--------------------x-------x                                       



                                                            CROSS APPLY

Há muitas situações em que precisamos substituir INNER JOINcom CROSS APPLY.

1. Se queremos juntar 2 tabelas de TOP nresultados com INNER JOINfuncionalidade

Considere se precisamos selecionar Ide Namede Mastere últimas duas datas para cada Idpartir Details table.

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
INNER JOIN
(
    SELECT TOP 2 ID, PERIOD,QTY 
    FROM DETAILS D      
    ORDER BY CAST(PERIOD AS DATE)DESC
)D
ON M.ID=D.ID

A consulta acima gera o seguinte resultado.

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-12   |  20   |
x------x---------x--------------x-------x

Veja, ele gerou resultados para as duas últimas datas e as duas últimas Ide uniu esses registros apenas na consulta externa ativada Id, o que está errado. Para conseguir isso, precisamos usar CROSS APPLY.

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
CROSS APPLY
(
    SELECT TOP 2 ID, PERIOD,QTY 
    FROM DETAILS D  
    WHERE M.ID=D.ID
    ORDER BY CAST(PERIOD AS DATE)DESC
)D

e forma ele resultado seguinte.

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     | 2014-01-08   |  40   |
|   2  |   B     | 2014-01-06   |  30   |
x------x---------x--------------x-------x

Aqui está o trabalho. A consulta interna CROSS APPLYpode fazer referência à tabela externa, onde INNER JOINnão pode fazer isso (gera erro de compilação). Ao encontrar as duas últimas datas, a união é feita dentro CROSS APPLY, ou seja WHERE M.ID=D.ID,.

2. Quando precisamos de INNER JOINfuncionalidade usando funções.

CROSS APPLYpode ser usado como um substituto INNER JOINquando precisamos obter resultados da Mastertabela e a function.

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
CROSS APPLY dbo.FnGetQty(M.ID) C

E aqui está a função

CREATE FUNCTION FnGetQty 
(   
    @Id INT 
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT ID,PERIOD,QTY 
    FROM DETAILS
    WHERE ID=@Id
)

que gerou o seguinte resultado

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-11   |  15   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     | 2014-01-06   |  30   |
|   2  |   B     | 2014-01-08   |  40   |
x------x---------x--------------x-------x



                                                            APLICAÇÃO EXTERNA

1. Se queremos juntar 2 tabelas de TOP nresultados com LEFT JOINfuncionalidade

Considere se precisamos selecionar ID e Nome Mastere as duas últimas datas para cada ID da Detailstabela.

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
LEFT JOIN
(
    SELECT TOP 2 ID, PERIOD,QTY 
    FROM DETAILS D  
    ORDER BY CAST(PERIOD AS DATE)DESC
)D
ON M.ID=D.ID

que forma o seguinte resultado

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     |   NULL       |  NULL |
|   3  |   C     |   NULL       |  NULL |
x------x---------x--------------x-------x

Isso trará resultados errados, ou seja, trará apenas os dados das duas datas mais recentes da Detailstabela, independentemente de Idse juntar a nós Id. Portanto, a solução adequada está sendo usada OUTER APPLY.

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
OUTER APPLY
(
    SELECT TOP 2 ID, PERIOD,QTY 
    FROM DETAILS D  
    WHERE M.ID=D.ID
    ORDER BY CAST(PERIOD AS DATE)DESC
)D

que forma o seguinte resultado desejado

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     | 2014-01-08   |  40   |
|   2  |   B     | 2014-01-06   |  30   |
|   3  |   C     |   NULL       |  NULL |
x------x---------x--------------x-------x

2. Quando precisamos de LEFT JOINfuncionalidade usando functions.

OUTER APPLYpode ser usado como um substituto LEFT JOINquando precisamos obter resultados da Mastertabela e a function.

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
OUTER APPLY dbo.FnGetQty(M.ID) C

E a função vai aqui.

CREATE FUNCTION FnGetQty 
(   
    @Id INT 
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT ID,PERIOD,QTY 
    FROM DETAILS
    WHERE ID=@Id
)

que gerou o seguinte resultado

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-11   |  15   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     | 2014-01-06   |  30   |
|   2  |   B     | 2014-01-08   |  40   |
|   3  |   C     |   NULL       |  NULL |
x------x---------x--------------x-------x



                             Característica comum de CROSS APPLYeOUTER APPLY

CROSS APPLYou OUTER APPLYpode ser usado para reter NULLvalores ao girar, que são intercambiáveis.

Considere que você tem a tabela abaixo

x------x-------------x--------------x
|  Id  |   FROMDATE  |   TODATE     |
x------x-------------x--------------x
|   1  |  2014-01-11 | 2014-01-13   | 
|   1  |  2014-02-23 | 2014-02-27   | 
|   2  |  2014-05-06 | 2014-05-30   |    
|   3  |   NULL      |   NULL       | 
x------x-------------x--------------x

Quando você usa UNPIVOTtrazer FROMDATEAND TODATEpara uma coluna, ele elimina os NULLvalores por padrão.

SELECT ID,DATES
FROM MYTABLE
UNPIVOT (DATES FOR COLS IN (FROMDATE,TODATE)) P

o que gera o resultado abaixo. Observe que perdemos o registro do Idnúmero3

  x------x-------------x
  | Id   |    DATES    |
  x------x-------------x
  |  1   |  2014-01-11 |
  |  1   |  2014-01-13 |
  |  1   |  2014-02-23 |
  |  1   |  2014-02-27 |
  |  2   |  2014-05-06 |
  |  2   |  2014-05-30 |
  x------x-------------x

Nesses casos, um CROSS APPLYou OUTER APPLYserá útil

SELECT DISTINCT ID,DATES
FROM MYTABLE 
OUTER APPLY(VALUES (FROMDATE),(TODATE))
COLUMNNAMES(DATES)

que forma o resultado a seguir e mantém Idonde seu valor é3

  x------x-------------x
  | Id   |    DATES    |
  x------x-------------x
  |  1   |  2014-01-11 |
  |  1   |  2014-01-13 |
  |  1   |  2014-02-23 |
  |  1   |  2014-02-27 |
  |  2   |  2014-05-06 |
  |  2   |  2014-05-30 |
  |  3   |     NULL    |
  x------x-------------x

Em vez de postar exatamente a mesma resposta em duas perguntas, por que não sinalizar uma como duplicada?
Tab Alleman

2
Considero que esta resposta é mais aplicável à resposta à pergunta original. Seus exemplos mostram cenários da "vida real".
FRANKO

Então, para esclarecer. O cenário "top n"; isso poderia ser feito com junção esquerda / interna, mas usando um "número de linha sobre partição por id" e selecionando "WHERE M.RowNumber <3" ou algo parecido?
Chaitanya

1
Ótima resposta em geral! Com certeza, essa é uma resposta melhor que a aceita, porque é: simples, com exemplos visuais e explicações úteis.
Arsen Khachaturyan

8

Um exemplo da vida real seria se você tivesse um agendador e quisesse ver qual era a entrada de log mais recente para cada tarefa agendada.

select t.taskName, lg.logResult, lg.lastUpdateDate
from task t
cross apply (select top 1 taskID, logResult, lastUpdateDate
             from taskLog l
             where l.taskID = t.taskID
             order by lastUpdateDate desc) lg

em nossos testes, sempre achamos que a função join with window é a mais eficiente para o top n (achei que isso sempre seria verdade, pois aplicar e subconsulta são cursivas / exigem loops aninhados). Embora eu ache que já o tenha quebrado ... graças ao link de Martin, que sugere que se você não está retornando a tabela inteira e não há índices ideais na tabela, o número de leituras seria muito menor usando a aplicação cruzada (ou uma subconsulta se top n onde n = 1)
Lee Tickett 14/02

Eu tenho essencialmente essa consulta aqui e certamente não está executando nenhuma subconsulta com loops aninhados. Dada a tabela de log possui uma PK de taskID e lastUpdateDate, é uma operação muito rápida. Como você reformaria essa consulta para usar uma função de janela?
BJury

2
select * from task t join interior (select taskid, logresult, lastupdatedate, rank () over (partição por ordem de taskid por lastupdatedate desc) _rank) lg em lg.taskid = t.taskid e lg._rank = 1
Lee Tickett

5

Para responder ao ponto acima, dê um exemplo:

create table #task (taskID int identity primary key not null, taskName varchar(50) not null)
create table #log (taskID int not null, reportDate datetime not null, result varchar(50) not null, primary key(reportDate, taskId))

insert #task select 'Task 1'
insert #task select 'Task 2'
insert #task select 'Task 3'
insert #task select 'Task 4'
insert #task select 'Task 5'
insert #task select 'Task 6'

insert  #log
select  taskID, 39951 + number, 'Result text...'
from    #task
        cross join (
            select top 1000 row_number() over (order by a.id) as number from syscolumns a cross join syscolumns b cross join syscolumns c) n

E agora execute as duas consultas com um plano de execução.

select  t.taskID, t.taskName, lg.reportDate, lg.result
from    #task t
        left join (select taskID, reportDate, result, rank() over (partition by taskID order by reportDate desc) rnk from #log) lg
            on lg.taskID = t.taskID and lg.rnk = 1

select  t.taskID, t.taskName, lg.reportDate, lg.result
from    #task t
        outer apply (   select  top 1 l.*
                        from    #log l
                        where   l.taskID = t.taskID
                        order   by reportDate desc) lg

Você pode ver que a consulta de aplicação externa é mais eficiente. (Não foi possível anexar o plano porque sou um novo usuário ... Doh.)


o plano de execução me interessa - você sabe por que a solução rank () faz uma varredura de índice e uma classificação cara em oposição à aplicação externa, que um índice procura e não parece fazer uma classificação (embora seja necessário porque você pode ' t fazer um top sem uma espécie)?
Lee Tickett

1
A aplicação externa não precisa executar uma classificação, pois pode usar o índice na tabela subjacente. Presumivelmente, a consulta com a função rank () precisa processar a tabela inteira para garantir que suas classificações estejam corretas.
BJury

você não pode fazer um top sem uma classificação. embora o seu ponto sobre o processamento de toda a tabela pode ser verdade que me surpreenderia (eu sei o sql otimizador / compilador pode decepcionar ao longo do tempo, mas isso seria um comportamento louco)
Lee Tickett

2
Você pode obter um topo sem classificação, quando os dados agrupados estão contra um índice, pois o otimizador sabe que já está classificado, portanto, literalmente, só precisa extrair a primeira (ou a última) entrada do índice.
BJury
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.