Exemple réel, quand utiliser OUTER / CROSS APPLY dans SQL

124

J'ai regardé CROSS / OUTER APPLYavec un collègue et nous avons du mal à trouver des exemples concrets où les utiliser.

J'ai passé beaucoup de temps à regarder Quand devrais-je utiliser Cross Apply sur Inner Join? et googler mais l'exemple principal (seul) semble assez bizarre (en utilisant le nombre de lignes d'une table pour déterminer le nombre de lignes à sélectionner dans une autre table).

Je pensais que ce scénario pourrait bénéficier de OUTER APPLY:

Tableau des contacts (contient 1 enregistrement pour chaque contact) Tableau des entrées de communication (peut contenir n téléphone, fax, e-mail pour chaque contact)

Mais en utilisant des sous-requêtes, des expressions de table communes, OUTER JOINavec RANK()et OUTER APPLYtous semblent fonctionner de la même manière. Je suppose que cela signifie que le scénario n'est pas applicable APPLY.

Veuillez partager quelques exemples réels et aider à expliquer la fonctionnalité!

Lee Tickett
la source
5
"top n par groupe" ou l'analyse XML est courante. Voir certaines de mes réponses stackoverflow.com/...
gbn
duplicata possible de Quand devrais-je utiliser Cross Apply sur Inner Join?
Tab Alleman du

Réponses:

174

Certaines utilisations APPLYsont ...

1) Top N par requêtes de groupe (peut être plus efficace pour certaines cardinalités)

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) Appel d'une fonction de table pour chaque ligne de la requête externe

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

3) Réutilisation d'un alias de colonne

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) Annulation du pivotement de plusieurs groupes de colonnes

Suppose que 1NF viole la structure de table ...

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

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

Exemple utilisant la VALUESsyntaxe 2008+ .

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

En 2005 UNION ALLpeut être utilisé à la place.

SELECT Id,
       Foo,
       Bar
FROM   T
       CROSS APPLY (SELECT Foo1, Bar1 
                    UNION ALL
                    SELECT Foo2, Bar2 
                    UNION ALL
                    SELECT Foo3, Bar3) V(Foo, Bar);
Martin Smith
la source
1
Une belle liste d'utilisations, mais la clé est les exemples de la vie réelle - j'aimerais en voir un pour chacun.
Lee Tickett
Pour le n ° 1, cela peut être réalisé également en utilisant le rang, les sous-requêtes ou les expressions de table courantes? Pouvez-vous donner un exemple lorsque ce n'est pas vrai?
Lee Tickett
@LeeTickett - Veuillez lire le lien. Il contient une discussion de 4 pages sur le moment où vous préférez l'un à l'autre.
Martin Smith
1
Assurez-vous de visiter le lien inclus dans l'exemple n ° 1. J'ai utilisé ces deux approches (ROW OVER et CROSS APPLY), les deux fonctionnant bien dans divers scénarios, mais je n'ai jamais compris pourquoi elles fonctionnent différemment. Cet article a été envoyé du ciel !! L'accent mis sur l'indexation appropriée correspondant à l'ordre par directions a grandement aidé pour les requêtes qui ont une structure «appropriée» mais qui ont des problèmes de performances lorsqu'elles sont interrogées. Merci de l'avoir inclus !!
Chris Porter
1
@mr_eclair semble être maintenant à itprotoday.com/software-development
Martin Smith
87

Il existe diverses situations où vous ne pouvez pas éviter CROSS APPLYou OUTER APPLY.

Considérez que vous avez deux tables.

TABLE DE MAÎTRE

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

TABLEAU DE DÉTAILS

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                                       



                                                            APPLIQUER CROISÉ

Il y a beaucoup de situations où nous devons remplacer INNER JOINavec CROSS APPLY.

1. Si nous voulons joindre 2 tables sur les TOP nrésultats avec des INNER JOINfonctionnalités

Considérez si nous devons sélectionner Idet Namede Masteret deux dernières dates pour chacune Idde 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

La requête ci-dessus génère le résultat suivant.

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

Voir, il a généré des résultats pour les deux dernières dates avec les deux dernières dates, Idpuis a joint ces enregistrements uniquement dans une requête externe Id, ce qui est faux. Pour ce faire, nous devons utiliser 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

et forme le résultat suivant.

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

Voici le travail. La requête à l'intérieur CROSS APPLYpeut référencer la table externe, où INNER JOINcela ne peut pas faire (génère une erreur de compilation). Lors de la recherche des deux dernières dates, la jonction se fait à l'intérieur, CROSS APPLYc'est- à- dire WHERE M.ID=D.ID.

2. Lorsque nous avons besoin de INNER JOINfonctionnalités utilisant des fonctions.

CROSS APPLYpeut être utilisé en remplacement INNER JOINlorsque nous devons obtenir le résultat de la Mastertable et a function.

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

Et voici la fonction

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

qui a généré le résultat suivant

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



                                                            APPLIQUER À L'EXTÉRIEUR

1. Si nous voulons joindre 2 tables sur les TOP nrésultats avec des LEFT JOINfonctionnalités

Considérez si nous devons sélectionner l'ID et le nom à partir de Masteret les deux dernières dates pour chaque ID de la Detailstable.

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

ce qui forme le résultat suivant

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

Cela entraînera des résultats erronés, c'est-à-dire qu'il n'apportera que les données des deux dernières dates de la Detailstable, Idmême si nous nous joignons Id. Donc, la bonne solution utilise 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

qui forme le résultat souhaité suivant

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. Lorsque nous avons besoin de LEFT JOINfonctionnalités en utilisant functions.

OUTER APPLYpeut être utilisé en remplacement LEFT JOINlorsque nous devons obtenir le résultat de la Mastertable et a function.

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

Et la fonction va ici.

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

qui a généré le résultat suivant

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



                             Caractéristique commune de CROSS APPLYetOUTER APPLY

CROSS APPLYou OUTER APPLYpeut être utilisé pour conserver les NULLvaleurs lors du non-pivotement, qui sont interchangeables.

Considérez que vous avez le tableau ci-dessous

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

Lorsque vous utilisez UNPIVOTpour amener FROMDATEAND TODATEà une colonne, il éliminera les NULLvaleurs par défaut.

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

qui génère le résultat ci-dessous. Notez que nous avons raté le record du Idnombre3

  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

Dans de tels cas, un CROSS APPLYou OUTER APPLYsera utile

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

qui forme le résultat suivant et conserve Idoù sa valeur est3

  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
Sarath Avanavu
la source
Au lieu d'afficher exactement la même réponse sur deux questions, pourquoi ne pas en signaler une comme duplicata?
Tab Alleman du
2
Je trouve que cette réponse est plus applicable pour répondre à la question initiale. Ses exemples montrent des scénarios «réels».
FrankO
Donc pour clarifier. Le scénario «top n»; cela pourrait-il être fait avec une jointure gauche / interne, mais en utilisant un "numéro_ligne sur la partition par id", puis en sélectionnant "WHERE M.RowNumber <3" ou quelque chose comme ça?
Chaitanya
1
Bonne réponse dans l'ensemble! Bien sûr, c'est une meilleure réponse que celle acceptée, car elle est: simple, avec des exemples visuels pratiques et des explications.
Arsen Khachaturyan le
9

Un exemple concret serait si vous aviez un planificateur et que vous vouliez voir quelle était l'entrée de journal la plus récente pour chaque tâche planifiée.

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
BJury
la source
dans nos tests, nous avons toujours trouvé que la fonction jointure avec fenêtre était la plus efficace pour les n premiers (je pensais que cela serait toujours vrai car apply et subquery sont tous deux cursifs / nécessitent des boucles imbriquées). bien que je pense que je l'ai peut-être maintenant craqué ... grâce au lien de Martin qui suggère que si vous ne renvoyez pas la table entière et qu'il n'y a pas d'index optimaux sur la table, le nombre de lectures serait beaucoup plus petit en utilisant cross apply (ou une sous-requête si top n où n = 1)
Lee Tickett
J'ai essentiellement cette requête ici et elle n'effectue certainement aucune sous-requête avec des boucles imbriquées. Étant donné que la table du journal a un PK de taskID et lastUpdateDate, c'est une opération très rapide. Comment reformeriez-vous cette requête pour utiliser une fonction de fenêtre?
BJury
2
select * from task t inner join (select taskid, logresult, lastupdatedate, rank () over (partition par taskid order by lastupdatedate desc) _rank) lg on lg.taskid = t.taskid and lg._rank = 1
Lee Tickett
5

Pour répondre au point ci-dessus, donnez un exemple:

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

Et maintenant, exécutez les deux requêtes avec un plan d'exécution.

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

Vous pouvez voir que la requête d'application externe est plus efficace. (Impossible de joindre le plan car je suis un nouvel utilisateur ... Doh.)

BJury
la source
le plan d'exécution m'intéresse - savez-vous pourquoi la solution rank () effectue une analyse d'index et un tri coûteux par opposition à une application externe qui effectue une recherche d'index et ne semble pas faire de tri (bien que cela doive parce que vous pouvez ' t faire un top sans tri?)
Lee Tickett
1
L'application externe n'a pas besoin d'effectuer un tri, car elle peut utiliser l'index sur la table sous-jacente. Vraisemblablement, la requête avec la fonction rank () doit traiter la table entière pour s'assurer que son classement est correct.
BJury
on ne peut pas faire un top sans tri. bien que votre point sur le traitement de la table entière POURRAIT être vrai, cela me surprendrait (je sais que l'optimiseur / compilateur SQL peut décevoir de temps en temps mais ce serait un comportement fou)
Lee Tickett
2
Vous pouvez terminer un sommet sans tri lorsque les données par lesquelles votre regroupement est par rapport à un index, car l'optimiseur sait qu'elles sont déjà triées, il suffit donc littéralement d'extraire la première (ou la dernière) entrée de l'index.
BJury