SQL LATERAL JOIN: Correlated Subqueries in FROM Explained

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 stage.

What LATERAL Does

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.

Prepare for the interview
01 / Open invite
02min.

Know Joins the way the interviewer who asks it knows it.

a Joins query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1SELECT user_id,
2 COUNT(*) AS sessions
3FROM events
4WHERE ts >= NOW() - INTERVAL '7 day'
5
Execute your solution0.4s avg.
Capital OneInterview question
Solve a Joins problem
1:N
Row fan-out per call
17%
Rounds use LEFT JOIN
L6
Where LATERAL shows up
172
Staff-level rounds

PostgreSQL LATERAL Syntax

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.

Without LATERAL vs With LATERAL

-- 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.

Use Case: Top-N Per Group

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.

Prepare for the interview
03 / From the bank03 of many
03hand-picked.

Top 2 Ad Campaigns by Spend

Medium18 min

Two campaigns. Most of the budget.

PostgreSQL LATERAL Syntax

-- 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;

Use Case: Unnesting Arrays and JSON

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.

SQL Server Equivalent: CROSS APPLY / 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;

Window Function vs LATERAL for Top-N

-- 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;

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.

Unnesting Arrays and JSON

-- 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);

-- 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);

LATERAL vs Correlated Subquery vs Window Function

FeatureLATERALCorrelated SubqueryWindow Function
PositionFROM clauseSELECT or WHERESELECT
ReturnsMultiple rows + columnsSingle valueSingle value per row
LIMIT per groupYes (native)AwkwardVia CTE + filter
Set-returning functionsYesNoNo
Engine supportPG, Oracle, MySQL 8.0.14+, SS (as APPLY)All enginesAll modern engines

3 LATERAL JOIN Interview Questions

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.

Q1

For each department, find the 3 most recent hires. Use LATERAL.

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.

Q2

Explain the difference between LATERAL JOIN and CROSS APPLY. When would you use each?

CROSS APPLY is SQL Server syntax for the same concept. CROSS APPLY = CROSS JOIN LATERAL. OUTER APPLY = LEFT JOIN LATERAL ... ON true. Oracle supports both syntaxes.

Q3

You have a table with a JSONB column storing an array of skill objects. Write a query that returns one row per employee-skill pair.

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.

LATERAL JOIN FAQ

What does LATERAL mean in SQL?+
LATERAL is a keyword placed before a subquery in the FROM clause. It allows that subquery to reference columns from preceding tables in the same FROM clause. Without LATERAL, subqueries in FROM are independent and cannot see columns from other tables listed before them. LATERAL turns a FROM-clause subquery into a correlated subquery: for each row from the left side, the LATERAL subquery runs with access to that row's columns. PostgreSQL, Oracle, MySQL 8.0.14+, and SQL Server (using CROSS APPLY / OUTER APPLY) all support this concept.
What is the difference between LATERAL JOIN and CROSS APPLY?+
They do the same thing with different syntax. PostgreSQL uses LATERAL JOIN (either CROSS JOIN LATERAL or LEFT JOIN LATERAL). SQL Server uses CROSS APPLY (equivalent to CROSS JOIN LATERAL) and OUTER APPLY (equivalent to LEFT JOIN LATERAL). Oracle supports both LATERAL and CROSS APPLY. The optimizer behavior is identical: for each row on the left, evaluate the right-side subquery with access to left-side columns.
When should I use a LATERAL JOIN instead of a window function?+
Use LATERAL when you need to return a different number of columns or rows per group, when you want to run a function that returns a set (like unnest or generate_series), or when the correlated subquery involves complex logic that does not map cleanly to a window frame. Window functions are better when you need a single computed value per row (rank, running total) and the result stays in the same row structure. For top-N per group, both work, but LATERAL can be more readable and sometimes faster because the engine can use an index scan limited to N rows per group.
02 / Why practice

Make LATERAL a default, not an escape hatch

  1. 01

    Active recall beats re-reading by 50%

    Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    Five problem shapes cover 80% of data engineer loops

    Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition

Related Guides