CROSS APPLY & OUTER APPLY in SQL Server
Viele SQL-Entwickler kennen JOIN, LEFT JOIN oder INNER JOIN.
Aber CROSS APPLY und OUTER APPLY bleiben oft ungenutzt – obwohl sie in vielen Situationen elegante und leistungsstarke Lösungen bieten.
Dieser Artikel erklärt Ihnen, was hinter den APPLY-Operatoren steckt, wie sie sich von Joins unterscheiden und wo sie ihre Stärken ausspielen. Anhand praxisnaher Beispiele werden Sie sehen, wie Sie CROSS APPLY und OUTER APPLY effektiv einsetzen können.
1. Was ist APPLY überhaupt?
Die APPLY-Operatoren (eingeführt mit SQL Server 2005) rufen für jede Zeile der linken Tabelle eine tabellenwertige Funktion (Table-Valued Function, TVF) oder eine Unterabfrage auf, die eine Tabelle zurückgibt. Man sagt auch: „Die rechte Seite wird pro linker Zeile ausgewertet“.
Es gibt zwei Varianten:
- CROSS APPLY – liefert nur Zeilen, bei denen die rechte Seite (Funktion/Subquery) mindestens eine Zeile zurückgibt. (Ähnlich wie INNER JOIN, nur dynamischer)
- OUTER APPLY – liefert alle Zeilen der linken Tabelle, auch wenn die rechte Seite keine Zeile liefert. In diesem Fall werden die Spalten der rechten Seite mit
NULLaufgefüllt. (Ähnlich wie LEFT JOIN)
APPLY können Sie Parameter aus der aktuellen Zeile an eine Funktion übergeben – das geht mit einem einfachen JOIN nicht.
2. Der entscheidende Unterschied zu JOIN
Ein JOIN verknüpft zwei unabhängige Mengen (Tabellen oder Unterabfragen) über eine Gleichheitsbedingung. Die rechte Seite wird nicht für jede Zeile der linken Seite neu berechnet – sie ist statisch.
APPLY hingegen erlaubt es, dass die rechte Seite auf die Werte der aktuellen linken Zeile referenziert. Dies ist besonders mächtig, wenn Sie mit Tabellenwertfunktionen arbeiten, die Parameter erwarten, oder wenn Sie eine Unterabfrage schreiben, die auf die äußere Zeile zugreift (korrelierte Unterabfrage, aber als Tabellenausgabe).
3. Grundlegende Syntax
SELECT
left.Spalte,
right.Spalte
FROM
LeftTable left
CROSS APPLY (SELECT ... FROM tvf(left.ID)) right; -- CROSS APPLY
-- Oder OUTER APPLY:
FROM
LeftTable left
OUTER APPLY dbo.GetDetails(left.ID) right;
4. Praxisbeispiele – Schritt für Schritt
Beispiel 1: Top-N pro Gruppe (klassische Aufgabe)
Angenommen, Sie haben eine Tabelle Auftraege (Auftragskopf) und eine Tabelle Auftragspositionen. Sie möchten für jeden Auftrag die drei teuersten Positionen ermitteln – in einer einzigen Abfrage. Mit CROSS APPLY wird das ganz einfach.
| Tabelle: Auftraege | Tabelle: Positionen |
|---|---|
| AuftragID | Kunde 1 | 'ABC GmbH' 2 | 'XYZ AG' |
PosID | AuftragID | Artikel | Preis 101 | 1 | Laptop | 1200 102 | 1 | Maus | 25 103 | 1 | Tastatur | 45 104 | 2 | Monitor | 350 105 | 2 | Dock | 180 |
SELECT
a.AuftragID,
a.Kunde,
p.Artikel,
p.Preis
FROM
Auftraege a
CROSS APPLY (
SELECT TOP 3 Artikel, Preis
FROM Positionen p2
WHERE p2.AuftragID = a.AuftragID
ORDER BY p2.Preis DESC
) p
ORDER BY a.AuftragID, p.Preis DESC;
Ergebnis: Pro Auftrag die drei teuersten Positionen. Falls ein Auftrag weniger als drei Positionen hat, wird er bei CROSS APPLY nicht angezeigt. Möchten Sie auch Aufträge ohne Positionen behalten, verwenden Sie OUTER APPLY – dann erscheinen NULLs.
Beispiel 2: Dynamische XML/JSON-Auswertung mit Tabellenwertfunktionen
SQL Server bietet Funktionen wie OPENJSON oder XML.nodes(), die eine Tabelle zurückgeben. Mit APPLY können Sie diese pro Zeile auswerten.
-- Angenommen, eine Tabelle "Logs" mit einer Spalte "Payload" (JSON)
SELECT
l.LogID,
j.Wert,
j.Ebene
FROM
Logs l
CROSS APPLY OPENJSON(l.Payload, '$.details')
WITH (Wert NVARCHAR(100) '$.value', Ebene INT '$.level') AS j
WHERE l.LogDate > '2024-01-01';
Für jede Zeile in Logs wird das JSON-Feld details geöffnet und als relationale Tabelle bereitgestellt.
Beispiel 3: Erweiterte Berechnungen mit einer eigenen Inline-Funktion
Angenommen, Sie haben eine benutzerdefinierte Inline-Tabellenwertfunktion, die zu einem ProduktID die neuesten Bewertungen liefert.
CREATE FUNCTION dbo.GetTopBewertungen (@ProduktID INT)
RETURNS TABLE
AS
RETURN
SELECT TOP 2 BewertungText, Sterne, Erstelldatum
FROM Bewertungen
WHERE ProduktID = @ProduktID
ORDER BY Erstelldatum DESC;
GO
-- Anwendung mit CROSS APPLY:
SELECT
p.ProduktName,
p.Preis,
b.BewertungText,
b.Sterne
FROM
Produkte p
CROSS APPLY dbo.GetTopBewertungen(p.ID) b
WHERE p.Kategorie = 'Elektronik';
5. CROSS APPLY vs. OUTER APPLY – Unterschied im Ergebnis
Wir nehmen noch einmal das Top-3-Beispiel, aber fügen einen Auftrag ohne Positionen hinzu:
-- Zusätzlicher Auftrag 3 ohne Positionen
INSERT INTO Auftraege VALUES (3, 'OhnePos GmbH');
-- CROSS APPLY zeigt Auftrag 3 nicht.
-- OUTER APPLY zeigt Auftrag 3 mit NULL in Artikel/Preis.
| Mit CROSS APPLY | Mit OUTER APPLY |
|---|---|
| Auftrag 1 + max. 3 Zeilen | Auftrag 1 + max. 3 Zeilen |
| Auftrag 2 + max. 3 Zeilen | Auftrag 2 + max. 3 Zeilen |
| Auftrag 3 fehlt | Auftrag 3 | NULL | NULL |
Wählen Sie OUTER APPLY immer dann, wenn Sie die linke Tabelle vollständig behalten wollen, auch ohne Treffer in der Funktion/Unterabfrage.
6. Zwei überraschende Einsatzgebiete
6.1 Kombination mit VALUES (generierte Zeilen pro Datensatz)
-- Aus einem einzelnen Datum mehrere abgeleitete Zeilen erzeugen
SELECT
o.OrderID,
SplitValues.Value
FROM
Orders o
CROSS APPLY (VALUES ('FlagA'), ('FlagB'), ('FlagC')) AS SplitValues(Value)
WHERE o.OrderStatus = 'Active';
Dies verhältet sich wie ein dynamisches UNPIVOT auf Zeilenebene – sehr nützlich für Berichte oder Parameteraufbereitungen.
6.2 Zugriff auf berechnete Spalten einer korrelierten Unterabfrage
SELECT
p.Name,
agg.TotalSales,
agg.AveragePrice
FROM
Products p
OUTER APPLY (
SELECT
SUM(s.Amount) AS TotalSales,
AVG(s.Price) AS AveragePrice
FROM Sales s
WHERE s.ProductID = p.ID
AND s.SaleDate > '2023-01-01'
) agg
WHERE agg.TotalSales > 1000 OR agg.TotalSales IS NULL;
Hier verwenden Sie OUTER APPLY wie ein LEFT JOIN mit einer Aggregationsunterabfrage – sehr übersichtlich und ohne GROUP BY im Hauptquery.
7. Performance-Tipps & Fallstricke
- Optimierung: Genau wie bei korrelierten Unterabfragen sollte die rechte Seite gut indiziert sein (besonders die JOIN-/Filterbedingungen auf der TVF).
- Vorsicht vor vielen Zeilen: Da
APPLYdie rechte Seite pro Zeile ausführt, kann bei großen linken Tabellen und aufwendigen Funktionen die Performance leiden. Testen Sie Alternativen mitROW_NUMBER()oder Fensterfunktionen. - Inline-TVFs sind schneller: Bevorzugen Sie Inline-Tabellenwertfunktionen (RETURNS TABLE) anstelle von Multistatement-TVFs – der Optimizer kann Letztere oft nicht gut verarbeiten.
CROSS APPLYkann auch alsJOINmit einer Unterabfrage umgeschrieben werden, wenn die Unterabfrage unkorreliert ist – aber dann geht der Vorteil der Dynamik verloren.
APPLY ist automatisch besser als ein JOIN. Testen Sie beide Varianten bei großen Datenmengen mit dem tatsächlichen Ausführungsplan.
8. Zusammenfassung
CROSS APPLY und OUTER APPLY sind extrem hilfreich, wenn Sie:
- ✔ für jede Zeile eine tabellenwertige Funktion mit Parametern aufrufen möchten.
- ✔ Top-N pro Gruppe effizient abbilden wollen (ohne Fensterfunktionen oder temporäre Tabellen).
- ✔ XML/JSON/SPLIT-Funktionen pro Zeile anwenden.
- ✔ korrelierte Unterabfragen sauber als „virtuelle Tabelle“ modellieren.
Sobald Sie das Konzept verstanden haben, werden Sie APPLY in vielen komplexen Abfragen schätzen lernen – es macht den Code lesbarer und oft auch performanter als umständliche Workarounds.
9. Weiterführende Ressourcen
- Microsoft Docs: FROM + APPLY (offizielle Dokumentation)
- T-SQL Tutorial: Tabellenwertfunktionen