SQL Reference

SQL LATERAL JOIN 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 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.

1:N

Row fan-out per call

17%

Rounds use LEFT JOIN

L6

Where LATERAL shows up

172

Staff-level rounds

Source: DataDriven analysis of 1,042 verified data engineering interview rounds.

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.

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.

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.

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

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.

Window Function Approach

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

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

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.

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

Parameterized Table Functions

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

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.

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.

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

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.

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.

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.

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.

Make LATERAL a default, not an escape hatch

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.