Что такое LATERAL JOIN в PostgreSQL?
LATERAL JOIN в PostgreSQL
Bez LATERAL подзапрос в секции FROM вычисляется один раз и изолирован от соседних таблиц. Ключевое слово LATERAL снимает это ограничение: подзапрос превращается в коррелированный и выполняется отдельно для каждой строки левой таблицы, при этом может возвращать произвольное количество строк и столбцов.
Синтаксис
SELECT t1.id, sub.value
FROM table1 t1
JOIN LATERAL (
SELECT value
FROM table2 t2
WHERE t2.foreign_id = t1.id -- ссылка на строку из t1
ORDER BY t2.created_at DESC
LIMIT 3
) sub ON true;
ON true обязателен синтаксически при использовании JOIN LATERAL — условие соединения уже задано внутри подзапроса.
Типичные сценарии
TOP-N на группу — классическая задача, где LATERAL значительно проще ROW_NUMBER() + CTE:
-- Последние 3 заказа для каждого пользователя
SELECT u.name, o.id AS order_id, o.total
FROM users u
JOIN LATERAL (
SELECT id, total
FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 3
) o ON true;
Раскрытие массивов — функции в FROM неявно LATERAL:
-- Короткий синтаксис: запятая вместо JOIN LATERAL
SELECT u.name, tag
FROM users u,
unnest(u.tags) AS tag;
Вычисление агрегата с контекстом строки:
SELECT p.title, stats.avg_rating
FROM products p
JOIN LATERAL (
SELECT avg(rating) AS avg_rating
FROM reviews r
WHERE r.product_id = p.id
AND r.created_at > p.release_date -- release_date из внешней строки
) stats ON true;
LEFT JOIN LATERAL
Если внутренний подзапрос не возвращает строк, обычный JOIN LATERAL отфильтрует строку внешней таблицы. LEFT JOIN LATERAL ... ON true сохраняет все строки:
SELECT u.name, COALESCE(last_order.total, 0) AS last_total
FROM users u
LEFT JOIN LATERAL (
SELECT total FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 1
) last_order ON true;
LATERAL vs коррелированный подзапрос в SELECT
| Аспект | Подзапрос в SELECT | LATERAL в FROM |
|---|---|---|
| Место | SELECT-список | FROM-список |
| Возвращает | одно скалярное значение | множество строк и столбцов |
| LEFT JOIN | невозможен | поддерживается |
| LIMIT внутри | ограничен | свободно |
Производительность
Планировщик выбирает Nested Loop: для каждой строки внешней таблицы выполняется отдельный seek во внутренней. Это эффективно только при наличии индекса по join-колонке. Без индекса LATERAL деградирует до N полных сканирований. Проверяйте план через EXPLAIN (ANALYZE, BUFFERS).
Что хочет услышать интервьюер
Кандидат объясняет главное: LATERAL разрешает подзапросу ссылаться на столбцы внешней таблицы и возвращать несколько строк — в отличие от обычного подзапроса в FROM
Называет практические сценарии: TOP-N per group, раскрытие массивов через unnest(), вычисления с контекстом конкретной строки
Понимает разницу между JOIN LATERAL и LEFT JOIN LATERAL, знает зачем ON true
Осознаёт, что функции в FROM (unnest, generate_series) неявно LATERAL
Говорит о производительности: N выполнений подзапроса, критичность индекса на join-колонке, Nested Loop в плане
Пример: TOP-3 заказа на каждого пользователя
SELECT u.id, u.name, o.id AS order_id, o.total, o.created_at
FROM users u
JOIN LATERAL (
SELECT id, total, created_at
FROM orders
WHERE user_id = u.id -- ссылка на текущего пользователя
ORDER BY created_at DESC
LIMIT 3
) o ON true;
-- Индекс: CREATE INDEX ON orders(user_id, created_at DESC);
Пример: LEFT JOIN LATERAL — пользователи без заказов тоже попадут в результат
SELECT u.name,
COALESCE(last_order.total, 0) AS last_total
FROM users u
LEFT JOIN LATERAL (
SELECT total
FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 1
) last_order ON true;
-- Без LEFT: строки без заказов отфильтруются как при INNER JOIN
Пример: Раскрытие массива тегов через unnest (неявный LATERAL)
-- Запятая в FROM — это сокращение JOIN LATERAL
SELECT u.name, tag
FROM users u,
unnest(u.tags) AS tag;
-- Эквивалентная явная форма:
SELECT u.name, tag
FROM users u
JOIN LATERAL unnest(u.tags) AS tag ON true;
Типичные ошибки
Путают LATERAL с коррелированным подзапросом в SELECT: оба работают построчно, но LATERAL в FROM и возвращает множество строк/столбцов
Забывают ON true после LATERAL-подзапроса — без него синтаксис некорректен
Применяют LATERAL там, где достаточно обычного JOIN + GROUP BY, усложняя запрос без необходимости
Не создают индекс на join-колонке внутренней таблицы и получают N полных сканирований
Не знают, что синтаксис через запятую (FROM t1, unnest(...)) — это неявный LATERAL без ключевого слова


