open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 02 · 05

LATERAL join

LATERAL позволяет подзапросу в FROM ссылаться на более ранние элементы FROM, поэтому он выполняется по разу на внешнюю строку. Это чистый способ сделать top-N на группу и развернуть set-returning функции — обычно за один проход, обгоняя коррелированные скалярные подзапросы.

SQL Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

«Показать 3 последних заказа каждого пользователя» звучит тривиально, пока не попробуешь. GROUP BY не может вернуть целые строки, только агрегаты. Оконная функция работает, но читает каждый заказ, чтобы их ранжировать. Первая версия команды стреляла одним запросом на пользователя из кода приложения — паттерн N+1 — и эндпоинт занимал 4 секунды на 800 пользователей. Переписывание в один запрос, доведшее его до 90 мс, использовало фичу, к которой большинство инженеров никогда не тянется: LATERAL. Это чистейший инструмент для «top-N на группу», и обретя его, ты перестаёшь писать построчные циклы.

Что LATERAL меняет во FROM

Прежде чем тянуться к N+1 циклу или нагромождать три коррелированных подзапроса, спроси себя: зависит ли каждая строка результата от текущего значения внешней строки? Если да — LATERAL сделает из этого один запрос вместо многих. Обычно табличные выражения в клаузе FROM независимы: FROM users u, orders o не даёт o знать ничего об u — условие join связывает их потом. Подзапрос во FROM такой же; он не видит другие таблицы. LATERAL убирает эту стену. LATERAL-подзапрос (или функция) во FROM может ссылаться на колонки элементов FROM, идущих перед ним, и вычисляется по разу на строку этих более ранних элементов.

В этом вся идея: коррелированный подзапрос, но во FROM, а не в SELECT или WHERE, поэтому он может вернуть несколько колонок и несколько строк на внешнюю строку, а не единственный скаляр.

-- Для каждого пользователя его 3 последних заказа — целые строки, не агрегаты
SELECT u.id, u.name, recent.id AS order_id, recent.created_at
FROM users u
CROSS JOIN LATERAL (
  SELECT o.id, o.created_at
  FROM orders o
  WHERE o.user_id = u.id        -- ссылается на u — законно лишь благодаря LATERAL
  ORDER BY o.created_at DESC
  LIMIT 3
) AS recent;

Внутренний подзапрос ссылается на u.id, что разрешено лишь из-за LATERAL. Для каждой строки пользователя Postgres выполняет этот маленький запрос «топ-3 заказа для этого пользователя» и приджойнивает (до 3) результаты обратно. Это каноническое решение top-N на группу, и нет краткого нелатерального эквивалента, возвращающего целые строки.

CROSS против LEFT JOIN LATERAL и set-returning функции

CROSS JOIN LATERAL (...) выбрасывает внешнюю строку, если её подзапрос ничего не вернул — пользователь без заказов исчезает. Чтобы оставить каждую внешнюю строку (NULL-расширенной при пустом подзапросе), используй LEFT JOIN LATERAL (...) ON true. ON true — обязательный синтаксис: латеральному left join всё равно нужен ON, а корреляция уже живёт внутри подзапроса, так что условие join — просто «всегда».

LATERAL также сияет с set-returning функциями — функциями, возвращающими много строк, как jsonb_array_elements или generate_series. Чтобы развернуть JSONB-массив-колонку в строки, по одной на элемент на родительскую строку:

-- По строке на каждый тег в JSONB-массиве tags каждого товара
SELECT p.id, tag.value AS tag
FROM products p
CROSS JOIN LATERAL jsonb_array_elements_text(p.tags) AS tag(value);

Вызов функции ссылается на p.tags, что неявно латерально, когда функция во FROM ссылается на более раннюю таблицу — Postgres даже позволяет опустить ключевое слово для функций, но писать его яснее. Этот паттерн «развернуть на строку» повсюду, как только ты хранишь массивы или JSON (раздел 06 глубоко копает JSONB).

Почему LATERAL часто бьёт коррелированный скалярный подзапрос

Иногда можно подделать построчные результаты коррелированными скалярными подзапросами в SELECT — но каждый возвращает единственное значение, и нужен отдельный подзапрос на колонку, каждый пересканирует те же заказы. Хочешь id последнего заказа и дату и сумму? Это три коррелированных подзапроса, три скана. Один LATERAL возвращает все три колонки из одного вычисления на строку. Меньше сканов, меньше round trip’ов, чем у N+1 цикла на стороне приложения, и планировщик может выбрать индексный nested loop, где внутренний LIMIT 3 читает лишь несколько индексных записей на пользователя — превращая 4-секундный N+1 эндпоинт в запрос менее 100 мс.

Почему это работает

Когда LATERAL — неправильный инструмент? Когда тебе действительно нужно ранжирование или скользящее вычисление по всем строкам — оконная функция (раздел 04) часто чище и может быть одним последовательным проходом. Грубое правило: LATERAL хорош для маленького top-N на группу (LIMIT k на внешнюю строку), потому что индексный nested loop читает лишь k записей на группу; оконная функция хороша, когда нужно значение, выведенное из всей партиции (rank, скользящий итог), и иначе пришлось бы пересканировать. Для топ-3-последних на индексированном (user_id, created_at) LATERAL с LIMIT 3 обычно дешевле; для «ранжировать каждый заказ внутри его пользователя» — оконная функция.

Викторина

Что именно включает LATERAL, чего обычный подзапрос во FROM не может?

Викторина

Ты используешь CROSS JOIN LATERAL, чтобы получить топ-3 заказа каждого пользователя, но пользователи без заказов исчезают из результата. Как их оставить?

Закончи аналогию

Заполни пропуск: LATERAL-подзапрос вычисляется по разу на _______ строку более ранних элементов FROM и может ссылаться на колонки этой строки — поэтому он и может вернуть топ-N заказов каждого пользователя.

Вспомните перед уходом
  1. 01
    Одним предложением: что такое LATERAL join и как он вычисляется?
  2. 02
    Напиши форму запроса топ-3-заказа-на-пользователя через LATERAL и скажи, как оставить пользователей без заказов.
  3. 03
    Почему один LATERAL часто обгоняет несколько коррелированных скалярных подзапросов или N+1 цикл приложения?
Итог

LATERAL поднимает стену между элементами FROM: LATERAL-подзапрос или функция может ссылаться на колонки идущих перед ним элементов и вычисляется по разу на внешнюю строку, делая его коррелированным подзапросом во FROM, способным вернуть много строк и колонок на строку. Его главное применение — top-N на группуCROSS JOIN LATERAL (SELECT ... WHERE o.user_id = u.id ORDER BY created_at DESC LIMIT 3) — у которого нет краткого целостроичного эквивалента без него. Используй CROSS JOIN LATERAL, чтобы выбросить внешние строки с пустым подзапросом, или LEFT JOIN LATERAL (...) ON true, чтобы оставить их NULL-расширенными. LATERAL также разворачивает set-returning функции вроде jsonb_array_elements и generate_series, по строке-элементу на родителя. Он выигрывает у коррелированных скалярных подзапросов (одно вычисление возвращает все колонки, а не скан на поле) и у N+1 цикла на стороне приложения (единый запрос, который планировщик исполнит как индексный nested loop, читающий лишь k записей на группу). Граница с оконными функциями: LATERAL для маленького top-N на группу по индексу; оконная функция (раздел 04), когда нужно значение, вычисленное по всей партиции. Теперь, когда нужны топ-N строк на группу в одном запросе, потянешься к CROSS JOIN LATERAL (... ORDER BY ... LIMIT N) — а не к циклу с одним запросом на группу из кода приложения.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки
опирается на

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.