LATERAL JOIN in PostgreSQL erklärt
Samstag, 18. April 2026
Es gibt SQL-Probleme, die sich komisch anfühlen. Man weiß irgendwie, was man haben will – aber jeder Lösungsversuch endet in einer verschachtelten Subquery-Hölle oder einem Window-Function-Gewimmel, das sich keiner mehr traut anzufassen.
Einen Klassiker kennt wahrscheinlich jeder: Die letzten 3 Bestellungen pro Kunde. Oder: Der teuerste Artikel pro Kategorie. Klingt simpel. Wird aber schnell unhandlich.
Genau hier kommt LATERAL JOIN ins Spiel. Und wenn du es einmal verstanden hast, fragst du dich, wie du je ohne es ausgekommen bist.
Das Problem mit normalen Joins
Nehmen wir ein konkretes Beispiel. Du hast zwei Tabellen:
-- Kunden
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT
);
-- Bestellungen
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT,
amount NUMERIC,
created_at TIMESTAMP
);
Jetzt willst du für jeden Kunden die neuesten 3 Bestellungen. Mit einem normalen JOIN kommst du da nicht weit – du kannst nicht innerhalb des Joins auf die Zeile aus der äußeren Tabelle referenzieren, um z. B. per WHERE customer_id = customers.id zu filtern.
Klar, du könntest eine Subquery schreiben. Oder ROW_NUMBER() benutzen. Beides funktioniert, aber beides hat seinen Preis: entweder schlechte Lesbarkeit oder Performance-Probleme bei großen Tabellen.
LATERAL: Die Subquery, die nach außen schaut
Ein LATERAL-Join erlaubt es einer Subquery im FROM-Clause, auf Spalten der vorherigen Tabellen in derselben Query zuzugreifen. Das ist der entscheidende Unterschied zu einem normalen Join, bei dem jede Subquery komplett isoliert ausgeführt wird.
Schauen wir uns das direkt an:
SELECT
c.name,
recent.amount,
recent.created_at
FROM customers c
CROSS JOIN LATERAL (
SELECT amount, created_at
FROM orders o
WHERE o.customer_id = c.id
ORDER BY created_at DESC
LIMIT 3
) AS recent;
Siehst du, was hier passiert? Die Subquery referenziert c.id – also eine Spalte aus der äußeren Tabelle. Das ist mit einem normalen Join nicht möglich. Aber mit LATERAL kein Problem.
PostgreSQL führt die Subquery für jede Zeile aus customers einmal aus. Das klingt teuer, aber in der Praxis ist es oft deutlich effizienter als alternative Ansätze – besonders wenn gute Indizes vorhanden sind.
LEFT JOIN LATERAL – wenn nicht jede Zeile ein Ergebnis hat
Das obige Beispiel mit CROSS JOIN LATERAL hat einen Haken: Kunden ohne Bestellungen verschwinden aus dem Ergebnis. Das Verhalten kennst du von normalen INNER JOINs.
Wenn du auch Kunden ohne Bestellungen sehen willst, benutzt du LEFT JOIN LATERAL ... ON TRUE:
SELECT
c.name,
recent.amount,
recent.created_at
FROM customers c
LEFT JOIN LATERAL (
SELECT amount, created_at
FROM orders o
WHERE o.customer_id = c.id
ORDER BY created_at DESC
LIMIT 3
) AS recent ON TRUE;
Das ON TRUE sieht komisch aus, ist aber korrekt – wir wollen alle Zeilen aus der linken Seite behalten, unabhängig davon, was die Subquery zurückgibt. Für Kunden ohne Bestellungen stehen dann NULL-Werte in den Spalten von recent.
Ein anderes Beispiel: berechnete Werte pro Zeile
LATERAL ist nicht nur für Top-N nützlich. Stell dir vor, du willst für jeden Kunden den Durchschnittswert seiner Bestellungen berechnen – und das Ergebnis direkt in einer anderen Berechnung weiterverwenden, ohne die Subquery zu wiederholen.
SELECT
c.name,
stats.avg_amount,
stats.order_count,
stats.avg_amount * 1.1 AS mit_aufschlag
FROM customers c
CROSS JOIN LATERAL (
SELECT
AVG(amount) AS avg_amount,
COUNT(*) AS order_count
FROM orders o
WHERE o.customer_id = c.id
) AS stats;
Hier ist LATERAL praktisch, weil du stats.avg_amount direkt in der äußeren SELECT-Liste weiternutzen kannst – ohne dieselbe Subquery zweimal zu schreiben.
Performance: Wann macht LATERAL Sinn?
Ehrlich gesagt: LATERAL JOIN ist kein Allheilmittel. Bei kleinen Tabellen oder wenn kein passender Index vorhanden ist, kann eine Window-Function oder eine CTE schneller sein.
Aber LATERAL glänzt besonders, wenn:
- du einen guten Index auf der inneren Tabelle hast (z. B.
(customer_id, created_at DESC)) - du mit
LIMITarbeitest, um nur wenige Zeilen pro äußerer Zeile zu holen - die Alternative eine vollständige Tabellen-Scan-Subquery wäre
Mit dem richtigen Index kann PostgreSQL für jede Kunden-Zeile direkt in den Index springen und die ersten 3 Einträge holen – ohne die gesamte orders-Tabelle zu scannen.
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, created_at DESC);
Mit diesem Index ist das Top-3-Query oben blitzschnell, auch bei Millionen von Bestellungen.
Vergleich: LATERAL vs. Window Functions
Window Functions wie ROW_NUMBER() OVER (PARTITION BY ...) lösen das Top-N-Problem auch – und sind manchmal der natürlichere Ansatz:
SELECT name, amount, created_at
FROM (
SELECT
c.name,
o.amount,
o.created_at,
ROW_NUMBER() OVER (
PARTITION BY c.id
ORDER BY o.created_at DESC
) AS rn
FROM customers c
JOIN orders o ON o.customer_id = c.id
) sub
WHERE rn <= 3;
Das funktioniert gut, hat aber einen Nachteil: PostgreSQL muss erst alle Bestellungen joinen und dann nach rn <= 3 filtern. Mit einem LATERAL-Join mit LIMIT 3 kann der Optimizer das früher abschneiden.
Faustregel: Wenn du viele Zeilen pro Gruppe hast und nur wenige brauchst → LATERAL. Wenn die Gruppen sowieso klein sind → Window Function ist oft lesbarer.
Kurz zusammengefasst
LATERAL JOIN in PostgreSQL erlaubt Subqueries im FROM-Clause, die auf Spalten der äußeren Tabelle zugreifen können. Das macht es ideal für:
- Top-N pro Gruppe (z. B. letzte 3 Bestellungen pro Kunde)
- Berechnete Werte, die du mehrfach in der äußeren Query nutzen willst
- Korrelierte Subqueries, die du lesbarer im
FROM-Block schreiben willst
Kombiniert mit dem richtigen Index ist LATERAL JOIN oft die schnellste und übersichtlichste Lösung für diese Klasse von Problemen.