CROSS JOIN and LATERAL
CROSS JOIN pairs every row with every row; LATERAL lets a subquery refer to the row on its left.
Syntax
FROM a CROSS JOIN b -- |a| × |b| rows
FROM a, b -- same thing (old style)
FROM a CROSS JOIN LATERAL (SELECT ... WHERE x = a.x ... LIMIT n) s
FROM a LEFT JOIN LATERAL (...) s ON true- A cross join returns the Cartesian product: every combination. 10 × 10 rows = 100 rows. Use it on purpose to build grids (every product × every month) and fill gaps with
LEFT JOIN. - An accidental Cartesian product (missing
ON, orFROM a, bwithout aWHERElink) is a classic bug that explodes row counts. - Cross joining a one-row result (e.g. a total) attaches that value to every row.
- LATERAL: a subquery in
FROMnormally cannot see otherFROMitems. WithLATERALit is evaluated per left row and may reference its columns — the neat way to get top N per group. UseLEFT JOIN LATERAL ... ON trueto keep left rows with no result. Set-returning function calls inFROMare implicitly lateral.
Examples
SELECT c.category, s.status
FROM (SELECT DISTINCT category FROM products) c
CROSS JOIN (VALUES ('processing'), ('shipped'), ('delivered'), ('cancelled')) AS s(status)
ORDER BY c.category, s.status;Every category paired with every status — a scaffold to left join counts onto.
SELECT p.category, sum(o.quantity) AS units,
round(100.0 * sum(o.quantity) / t.total_units, 1) AS pct_units
FROM orders o
JOIN products p USING (product_id)
CROSS JOIN (SELECT sum(quantity) AS total_units FROM orders) t
GROUP BY p.category, t.total_units
ORDER BY units DESC;The single total row is attached to every row before grouping.
SELECT c.category_name, top.product_name, top.unit_price
FROM categories c
CROSS JOIN LATERAL (
SELECT p.product_name, p.unit_price
FROM products p
WHERE p.category_id = c.category_id
ORDER BY p.unit_price DESC, p.product_id
LIMIT 2
) AS top
ORDER BY c.category_name, top.unit_price DESC;The subquery runs once per category; categories without products disappear (use LEFT JOIN LATERAL ... ON true to keep them).
Pitfalls
An accidental cross join from a missing join condition.
Cross joining two large tables (row count multiplies).
Using CROSS JOIN LATERAL when left rows with no match must stay (use LEFT JOIN LATERAL ... ON true).
Other databases
For orientation only: SQL Quest executes PostgreSQL.
- SQL Server
LATERAL is written CROSS APPLY / OUTER APPLY.
- Oracle
Supports LATERAL and CROSS/OUTER APPLY (12c+).
- MySQL
LATERAL derived tables since 8.0.14.