LATERAL is a primitive, not a convenience. It belongs in the same architectural tier as window functions and recursive CTEs: the pieces you reach for when the normal join model stops describing the shape of the data. Think of it as the pipeline stage that turns a driving table into a row generator and lets a parameterized subquery run once per emitted tuple, with full visibility into the prior row's columns.
Where it fits in the pipeline: any time your fact query needs a per-row lookup into a function, an array, or a top-N slice of a related table, LATERAL is the right place to put the coupling. Window functions compute per partition. LATERAL computes per row and can return many rows back. That asymmetry is the whole reason it exists.
Row fan-out per call
Rounds use LEFT JOIN
Where LATERAL shows up
Staff-level rounds
Source: DataDriven analysis of 1,042 verified data engineering interview rounds.
In a normal FROM clause, each table or subquery is independent. A subquery in FROM cannot see columns from other items in the same FROM list. LATERAL removes that restriction. It tells the engine: “Evaluate this subquery once per row from the left side, and give it access to that row's columns.”
Architecturally, this is the same shape as a correlated subquery, relocated into the FROM pipeline so it can contribute rows instead of scalars. A SELECT-clause correlated subquery collapses to one value per row. LATERAL decouples that limit: the parameterized subquery becomes a set-returning function of its input row, and the resulting fan-out feeds directly into downstream joins and aggregates.
-- WITHOUT LATERAL: this fails
-- The subquery cannot reference d.id
SELECT d.name, recent.total
FROM departments d
JOIN (
SELECT department_id, SUM(amount) AS total
FROM orders
WHERE department_id = d.id -- ERROR: d is not visible
GROUP BY department_id
) recent ON true;
-- WITH LATERAL: this works
SELECT d.name, recent.total
FROM departments d
CROSS JOIN LATERAL (
SELECT SUM(amount) AS total
FROM orders
WHERE department_id = d.id
AND order_date >= CURRENT_DATE - INTERVAL '30 days'
) recent;Interview note: If an interviewer asks what LATERAL means, start with this: “It lets a subquery in FROM see columns from the tables listed before it. It runs once per row from the left side.” That is the complete mental model in two sentences.
PostgreSQL uses the LATERAL keyword directly in the FROM clause. You can combine it with CROSS JOIN, LEFT JOIN, or INNER JOIN. The subquery gets an alias, and the join condition (if needed) goes in the ON clause.
-- CROSS JOIN LATERAL: exclude rows where subquery returns nothing
SELECT u.name, latest.order_id, latest.amount
FROM users u
CROSS JOIN LATERAL (
SELECT order_id, amount
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
LIMIT 3
) latest;
-- LEFT JOIN LATERAL: keep all users, even those with no orders
SELECT u.name, latest.order_id, latest.amount
FROM users u
LEFT JOIN LATERAL (
SELECT order_id, amount
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
LIMIT 3
) latest ON true;CROSS JOIN LATERAL: For each row on the left, run the subquery. If the subquery returns zero rows, the left row is excluded entirely. Equivalent to SQL Server's CROSS APPLY.
LEFT JOIN LATERAL ... ON true: Same as above, but if the subquery returns zero rows, the left row is kept with NULLs for the subquery columns. Equivalent to SQL Server's OUTER APPLY.
-- SQL Server: CROSS APPLY = CROSS JOIN LATERAL
SELECT u.name, latest.order_id, latest.amount
FROM users u
CROSS APPLY (
SELECT TOP 3 order_id, amount
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
) latest;
-- SQL Server: OUTER APPLY = LEFT JOIN LATERAL
SELECT u.name, latest.order_id, latest.amount
FROM users u
OUTER APPLY (
SELECT TOP 3 order_id, amount
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
) latest;The top-N per group problem appears in interviews constantly. “Find the 3 most recent orders per customer.” “Get the 5 highest-paid employees per department.” Window functions (ROW_NUMBER + filter) solve this, but LATERAL can be more efficient when an index exists on the sort column.
-- Window function: scans all orders, then filters
WITH ranked AS (
SELECT
user_id,
order_id,
amount,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY order_date DESC
) AS rn
FROM orders
)
SELECT user_id, order_id, amount
FROM ranked
WHERE rn <= 3;-- LATERAL: uses index to fetch only 3 rows per user
SELECT u.id, latest.order_id, latest.amount
FROM users u
CROSS JOIN LATERAL (
SELECT order_id, amount
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
LIMIT 3
) latest;The LATERAL version can be faster because the engine runs a targeted index scan per user, fetching only 3 rows each time. The window function version scans the entire orders table first, assigns row numbers, and then filters. For large tables with a covering index on (user_id, order_date DESC), LATERAL wins significantly on execution time.
Interview note: If you solve top-N with ROW_NUMBER, mention LATERAL as an alternative and explain the performance tradeoff. This shows depth. If you only know one approach, go with ROW_NUMBER since it works on all engines.
When a column stores an array or JSON array, LATERAL JOIN with a set-returning function (like unnest or json_array_elements) expands each element into its own row. This pattern is essential for normalizing semi-structured data stored in PostgreSQL arrays or JSONB columns.
-- Unnest tags array: one row per product-tag combination
SELECT p.id, p.name, t.tag
FROM products p
CROSS JOIN LATERAL unnest(p.tags) AS t(tag);
-- Unnest JSONB array
SELECT
e.id,
e.name,
skill.value ->> 'name' AS skill_name,
(skill.value ->> 'years')::INT AS years
FROM employees e
CROSS JOIN LATERAL jsonb_array_elements(e.skills) AS skill(value);In PostgreSQL, unnest() and jsonb_array_elements() implicitly use LATERAL when called in the FROM clause. But writing LATERAL explicitly makes the intent clear to readers and to interviewers.
LATERAL also enables calling table-valued functions with parameters from the left side. This includes generate_series() with dynamic bounds, custom functions that return sets, and foreign data wrapper queries parameterized per row.
-- Generate a row per month for each subscription's active period
SELECT
s.id,
s.plan,
month.dt AS active_month
FROM subscriptions s
CROSS JOIN LATERAL generate_series(
s.start_date,
COALESCE(s.end_date, CURRENT_DATE),
INTERVAL '1 month'
) AS month(dt);| Feature | LATERAL | Correlated Subquery | Window Function |
|---|---|---|---|
| Position | FROM clause | SELECT or WHERE | SELECT |
| Returns | Multiple rows + columns | Single value | Single value per row |
| LIMIT per group | Yes (native) | Awkward | Via CTE + filter |
| Set-returning functions | Yes | No | No |
| Engine support | PG, Oracle, MySQL 8.0.14+, SS (as APPLY) | All engines | All modern engines |
LATERAL shows up in senior-level interviews and in any interview that asks top-N per group. Knowing the pattern gives you an alternative when the window function solution feels clunky.
What they test:
Basic LATERAL syntax and the top-N per group pattern. They want CROSS JOIN LATERAL with ORDER BY and LIMIT inside the subquery.
Approach:
FROM departments d CROSS JOIN LATERAL (SELECT * FROM employees WHERE department_id = d.id ORDER BY hire_date DESC LIMIT 3) recent. Mention that an index on (department_id, hire_date DESC) makes this efficient.
What they test:
Cross-engine knowledge. They want to hear that CROSS APPLY is SQL Server syntax for the same concept, and that LEFT JOIN LATERAL maps to OUTER APPLY.
Approach:
State the equivalences. CROSS APPLY = CROSS JOIN LATERAL. OUTER APPLY = LEFT JOIN LATERAL ... ON true. Mention that Oracle supports both syntaxes.
What they test:
LATERAL with set-returning functions. They want to see jsonb_array_elements or unnest used inside a LATERAL subquery.
Approach:
CROSS JOIN LATERAL jsonb_array_elements(e.skills) AS skill(value). Access fields with value ->> 'name'. Mention that PostgreSQL treats set-returning functions in FROM as implicitly LATERAL.
Senior candidates treat LATERAL as a first-class operator in their query plans. Practice enough problems and you'll reach for it before you reach for a nested window rank.