Beispiel aus der Praxis, wann OUTER / CROSS APPLY in SQL verwendet werden soll


124

Ich habe CROSS / OUTER APPLYmit einem Kollegen nachgesehen und wir bemühen uns, Beispiele aus der Praxis zu finden, wo wir sie verwenden können.

Ich habe ziemlich viel Zeit damit verbracht, zu prüfen, wann ich Cross Apply über Inner Join verwenden soll. und googeln, aber das Hauptbeispiel (nur) scheint ziemlich bizarr zu sein (mithilfe der Zeilenanzahl aus einer Tabelle zu bestimmen, wie viele Zeilen aus einer anderen Tabelle ausgewählt werden sollen).

Ich dachte, dieses Szenario könnte profitieren von OUTER APPLY:

Kontakttabelle (enthält 1 Datensatz für jeden Kontakt) Tabelle der Kommunikationseinträge (kann n Telefon, Fax, E-Mail für jeden Kontakt enthalten)

Bei Verwendung von Unterabfragen scheinen allgemeine Tabellenausdrücke OUTER JOINmit RANK()und OUTER APPLYalle gleich zu funktionieren . Ich vermute, dies bedeutet, dass das Szenario nicht anwendbar ist APPLY.

Bitte teilen Sie einige Beispiele aus dem wirklichen Leben mit und erklären Sie die Funktion!


5
"top n per group" oder das Parsen von XML ist üblich. Sehen Sie einige meiner Antworten stackoverflow.com/…
gbn




Antworten:


174

Einige Anwendungen für APPLY sind ...

1) Top N pro Gruppenabfragen (kann für einige Kardinalitäten effizienter sein)

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) Aufrufen einer Tabellenwertfunktion für jede Zeile in der äußeren Abfrage

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

3) Wiederverwenden eines Spaltenalias

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) Nicht mehr als eine Gruppe von Spalten schwenken

Nimmt an, dass 1NF die Tabellenstruktur verletzt ....

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

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

Beispiel mit der VALUESSyntax 2008+ .

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

Im Jahr 2005 UNION ALLkann stattdessen verwendet werden.

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
Eine schöne Liste der Verwendungszwecke, aber der Schlüssel sind die Beispiele aus dem wirklichen Leben - ich würde gerne eines für jedes sehen.
Lee Tickett

Für # 1 kann dies gleichermaßen mit Rang, Unterabfragen oder allgemeinen Tabellenausdrücken erreicht werden? Können Sie ein Beispiel nennen, wenn dies nicht der Fall ist?
Lee Tickett

@ LeeTickett - Bitte lesen Sie den Link. Es gibt eine 4-seitige Diskussion darüber, wann Sie eine der anderen vorziehen würden.
Martin Smith

1
Stellen Sie sicher, dass Sie den in Beispiel 1 enthaltenen Link besuchen. Ich habe beide Ansätze (ROW OVER und CROSS APPLY) verwendet, wobei beide in verschiedenen Szenarien gut abschneiden, aber ich habe nie verstanden, warum sie unterschiedlich funktionieren. Dieser Artikel wurde vom Himmel geschickt !! Der Fokus auf die richtige Indizierung, die der Reihenfolge nach Anweisungen entspricht, half in hohem Maße bei Abfragen, die eine "richtige" Struktur haben, aber bei Abfragen Leistungsprobleme aufweisen. Vielen Dank für die Aufnahme!
Chris Porter

1
@mr_eclair sieht aus wie es jetzt bei itprotoday.com/software-development/… ist
Martin Smith

87

Es gibt verschiedene Situationen , in denen man nicht vermeiden kann CROSS APPLYoder OUTER APPLY.

Angenommen, Sie haben zwei Tabellen.

MASTER TABLE

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

DETAILS TABELLE

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

Es gibt viele Situation , wo wir ersetzen müssen INNER JOINmitCROSS APPLY .

1. Wenn wir 2 Tabellen zu TOP nErgebnissen mit verbinden möchtenINNER JOIN Funktionalität

Überlegen Sie, ob wir müssen wählen Idund Nameaus Masterund die letzten beiden Tage , für jeden Idaus 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

Die obige Abfrage generiert das folgende Ergebnis.

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

Es wurden Ergebnisse für die letzten beiden Daten mit den letzten beiden Daten generiert Idund diese Datensätze dann nur in der äußeren Abfrage am verknüpft Id, was falsch ist. Um dies zu erreichen, müssen wir verwenden 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

und bildet er folgendes Ergebnis.

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

Hier ist die Arbeit. Die Abfrage im Inneren CROSS APPLYkann auf die äußere Tabelle verweisen, wo INNER JOINdies nicht möglich ist (löst einen Kompilierungsfehler aus). Wenn Sie die letzten beiden Daten gefunden haben, erfolgt der Beitritt innerhalb von, CROSS APPLYdhWHERE M.ID=D.ID .

2. Wenn wir INNER JOINFunktionalität mit Funktionen benötigen .

CROSS APPLYkann als Ersatz verwendet werden, INNER JOINwenn wir Ergebnisse aus MasterTabelle und a erhalten müssen function.

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

Und hier ist die Funktion

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

welches das folgende Ergebnis erzeugte

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



                                                            AUSSEN ANWENDEN

1. Wenn wir 2 Tabellen zu TOP nErgebnissen mit verbinden möchtenLEFT JOIN Funktionalität

Überlegen Sie, ob wir ID und Name aus Masterund die letzten beiden Daten für jede ID aus der DetailsTabelle auswählen müssen .

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

welches das folgende Ergebnis bildet

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

Dies führt zu falschen Ergebnissen, dh es werden nur die Daten der letzten beiden Daten aus der DetailsTabelle angezeigt, unabhängig davon Id, ob wir uns anschließen oder nicht Id. Die richtige Lösung ist also 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

welches das folgende gewünschte Ergebnis bildet

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. Wenn wir LEFT JOINFunktionalität mit benötigen functions.

OUTER APPLYkann als Ersatz verwendet werden, LEFT JOINwenn wir Ergebnisse aus MasterTabelle und a erhalten müssen function.

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

Und die Funktion geht hier.

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

welches das folgende Ergebnis erzeugte

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



                             Gemeinsames Merkmal von CROSS APPLYundOUTER APPLY

CROSS APPLYoder OUTER APPLYkann verwendet werden, um NULLbeim Schwenken Werte beizubehalten, die austauschbar sind.

Angenommen, Sie haben die folgende Tabelle

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

Wenn Sie UNPIVOTbringen FROMDATEund TODATEzu einer Spalte, wird es beseitigen NULLWerte standardmäßig.

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

Dies erzeugt das folgende Ergebnis. Beachten Sie, dass wir den Datensatz der IdNummer verpasst haben3

  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

In solchen Fällen ist ein CROSS APPLYoder OUTER APPLYnützlich

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

Dies bildet das folgende Ergebnis und behält bei, Idwo sein Wert ist3

  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

Warum nicht eine Antwort als Duplikat markieren, anstatt genau dieselbe Antwort auf zwei Fragen zu veröffentlichen?
Tab Alleman

2
Ich finde diese Antwort besser geeignet, um die ursprüngliche Frage zu beantworten. Die Beispiele zeigen reale Szenarien.
FrankO

Also zur Klarstellung. Das "Top n" -Szenario; könnte das mit left / inner join gemacht werden, aber mit einer "row_number over partition by id" und dann mit "WHERE M.RowNumber <3" oder so?
Chaitanya

1
Tolle Antwort insgesamt! Dies ist sicherlich eine bessere Antwort als die akzeptierte, denn es ist: einfach, mit praktischen visuellen Beispielen und Erklärungen.
Arsen Khachaturyan

8

Ein Beispiel aus dem wirklichen Leben wäre, wenn Sie einen Planer hätten und sehen möchten, was der letzte Protokolleintrag für jede geplante Aufgabe ist.

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

In unseren Tests haben wir immer festgestellt, dass die Verknüpfung mit der Fensterfunktion für top n am effizientesten ist (ich dachte, dies würde immer zutreffen, da apply und subquery beide kursiv sind / verschachtelte Schleifen erfordern). Obwohl ich denke, dass ich es jetzt vielleicht geknackt habe ... dank Martins Link, der darauf hinweist, dass wenn Sie nicht die gesamte Tabelle zurückgeben und es keine optimalen Indizes für die Tabelle gibt, die Anzahl der Lesevorgänge mit cross apply (oder) viel geringer wäre eine Unterabfrage, wenn top n mit n = 1)
Lee Tickett

Ich habe im Wesentlichen diese Abfrage hier und es wird sicherlich keine Unterabfrage mit verschachtelten Schleifen ausgeführt. Da die Protokolltabelle eine PK von taskID und lastUpdateDate hat, ist dies eine sehr schnelle Operation. Wie würden Sie diese Abfrage reformieren, um eine Fensterfunktion zu verwenden?
BJury

2
select * from Task t INNER JOIN (select taskid, logresult, LASTUPDATEDATE, Rang () über (Partition von taskid um durch LASTUPDATEDATE ab) _rank) lg auf lg.taskid = t.taskid und lg._rank = 1
Lee Tickett

5

Um den obigen Punkt zu beantworten, schlagen Sie ein Beispiel vor:

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

Führen Sie nun die beiden Abfragen mit einem Ausführungsplan aus.

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

Sie können sehen, dass die äußere Anwendungsabfrage effizienter ist. (Der Plan konnte nicht angehängt werden, da ich ein neuer Benutzer bin ... Doh.)


Der Ausführungsplan interessiert mich. Wissen Sie, warum die rank () -Lösung einen Index-Scan und eine teure Sortierung durchführt, im Gegensatz zu Outer Apply, bei der ein Index gesucht wird und anscheinend keine Sortierung durchgeführt wird (obwohl dies erforderlich ist, weil Sie es können?) t ein Top ohne eine Art zu tun?)
Lee Tickett

1
Die äußere Anwendung muss keine Sortierung durchführen, da sie den Index für die zugrunde liegende Tabelle verwenden kann. Vermutlich muss die Abfrage mit der Funktion rank () die gesamte Tabelle verarbeiten, um sicherzustellen, dass die Rangfolge korrekt ist.
BJury

Ohne eine Sorte kann man kein Top machen. Obwohl Ihr Standpunkt zur Verarbeitung der gesamten Tabelle wahr sein KÖNNTE, würde es mich überraschen (ich weiß, dass der SQL-Optimierer / Compiler von Zeit zu Zeit enttäuschen kann, aber dies wäre ein verrücktes Verhalten)
Lee Tickett

2
Sie können ein Top ohne Sortierung anführen, wenn die Daten, nach denen Sie gruppieren, gegen einen Index gerichtet sind, da der Optimierer weiß, dass sie bereits sortiert sind, sodass nur der erste (oder letzte) Eintrag aus dem Index entfernt werden muss.
BJury
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.