Skip to main content

LATERAL Joins

Learning Focus

Use this lesson to understand LATERAL joins — a DuckDB feature that lets subqueries reference columns from preceding tables in the FROM clause.

What is a LATERAL Join?

A LATERAL join allows a subquery (or function call) in the FROM clause to reference columns from tables that appear earlier in the same FROM clause. Think of it as a correlated subquery that returns a table instead of a scalar.

Basic Syntax

SELECT ...
FROM table_a,
LATERAL (SELECT ... FROM table_b WHERE table_b.col = table_a.col) sub;

Examples

Example 1: Top N per Group (Lateral Pattern)

-- Top 2 earners from each department (lateral alternative to row_number CTE)
SELECT e.department, top.name, top.salary
FROM (SELECT DISTINCT department FROM employees) e,
LATERAL (
SELECT name, salary
FROM employees
WHERE department = e.department
ORDER BY salary DESC
LIMIT 2
) top;

Example 2: Flatten a List Column

-- Unnest a list column into rows (DuckDB shorthand)
SELECT student_id, unnest(test_scores) AS score
FROM student_scores;

-- Equivalent LATERAL form
SELECT s.student_id, scores.score
FROM student_scores s,
LATERAL (SELECT unnest(s.test_scores) AS score) scores;

Example 3: Apply a Function Per Row

-- Compute stats for each department's salary array
SELECT e.department, stats.*
FROM (SELECT DISTINCT department FROM employees) e,
LATERAL (
SELECT
MIN(salary) AS min_sal,
MAX(salary) AS max_sal,
AVG(salary) AS avg_sal
FROM employees
WHERE department = e.department
) stats;

LATERAL vs Correlated Subquery

AspectCorrelated SubqueryLATERAL
ReturnsScalarTable (multiple rows/columns)
LocationWHERE / SELECTFROM clause
Multiple columnsNoYes

Common Pitfalls

PitfallConsequencePrevention
LATERAL without referencing outer queryIdentical to a plain subqueryEnsure the lateral references outer columns
Forgetting aliasesColumn name ambiguityAlways alias the LATERAL subquery
Heavy use on large tablesPer-row executionUse window functions or JOINs when possible

Quick Reference

-- Lateral with correlation
FROM outer_table, LATERAL (SELECT ... WHERE col = outer_table.col LIMIT n) sub

-- Unnest via lateral
FROM t, LATERAL (SELECT unnest(t.list_col) AS val) exploded

-- Cross apply equivalent
FROM t, LATERAL (SELECT ... FROM other WHERE other.id = t.id) derived

What's Next