SQLLab
Все статьи

LATERAL JOIN в PostgreSQL: когда подзапрос становится циклом

LATERAL JOIN в PostgreSQL: синтаксис, применение для топ-N в каждой группе, вызов функций для каждой строки, сравнение с обычным подзапросом.

19 марта 2026 г.·4 мин чтения·

Обычный подзапрос в FROM не видит другие таблицы того же запроса — он изолирован. LATERAL снимает это ограничение: подзапрос справа от LATERAL может ссылаться на строки слева, как будто выполняется для каждой строки отдельно.

Синтаксис

SELECT *
FROM таблица_a
JOIN LATERAL (
    SELECT ...
    FROM таблица_b
    WHERE таблица_b.id = таблица_a.id  -- ссылка на внешнюю строку
) sub ON true;

Ключевое отличие: WHERE таблица_b.id = таблица_a.id — здесь таблица_a.id берётся из текущей строки внешнего запроса. Без LATERAL это вызвало бы ошибку.

ON true используется когда условие уже внутри подзапроса. Для LEFT JOIN LATERAL это позволяет сохранить строку даже если подзапрос вернул 0 результатов.


Зачем нужен LATERAL: реальная задача

Топ-N в каждой группе

Классическая задача: вывести 3 последних заказа для каждого клиента. Без LATERAL это решается через оконные функции с подзапросом. С LATERAL — чище и нагляднее.

-- Таблица клиентов и таблица заказов
SELECT
    c.id,
    c.name,
    recent.order_id,
    recent.total,
    recent.created_at
FROM customers c
JOIN LATERAL (
    SELECT o.id AS order_id, o.total, o.created_at
    FROM orders o
    WHERE o.customer_id = c.id
    ORDER BY o.created_at DESC
    LIMIT 3
) recent ON true
ORDER BY c.id, recent.created_at DESC;

PostgreSQL выполняет подзапрос для каждой строки customers. Итог: ровно 3 строки на каждого клиента, без оконных функций и лишних CTE.

Сравни с вариантом на ROW_NUMBER():

-- То же самое через оконную функцию
SELECT id, name, order_id, total, created_at
FROM (
    SELECT
        c.id, c.name,
        o.id AS order_id, o.total, o.created_at,
        ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY o.created_at DESC) AS rn
    FROM customers c
    JOIN orders o ON o.customer_id = c.id
) t
WHERE rn <= 3;

Оба варианта корректны. LATERAL выигрывает когда нужен LIMIT внутри подзапроса — оконные функции не умеют ограничивать результат сами по себе.


Вызов функций для каждой строки

LATERAL незаменим при работе с функциями, возвращающими набор строк (set-returning functions).

unnest с параллельными массивами

SELECT
    p.name,
    attrs.key,
    attrs.value
FROM products p
JOIN LATERAL unnest(p.attribute_keys, p.attribute_values)
    AS attrs(key, value) ON true;

jsonb_array_elements

-- Каждый элемент JSON-массива как отдельная строка
SELECT
    o.id AS order_id,
    item->>'product_id' AS product_id,
    (item->>'qty')::int AS qty
FROM orders o
JOIN LATERAL jsonb_array_elements(o.items) AS item ON true;

Без LATERAL jsonb_array_elements тоже работает в SELECT, но в FROM с привязкой к конкретной строке — только через LATERAL.


LATERAL vs обычный подзапрос

Что нужноОбычный подзапросLATERAL
Независимый подзапрос
Ссылка на внешнюю строку
LIMIT внутри подзапроса
Вызов SRF для каждой строки
Сохранить строку при пустом результатеLEFT JOIN LATERAL ... ON true
-- Этот запрос НЕ сработает (обычный подзапрос не видит c.id)
SELECT c.name, (
    SELECT o.total
    FROM orders o
    WHERE o.customer_id = c.id  -- в скалярном подзапросе можно, но не в FROM
    ORDER BY o.created_at DESC
    LIMIT 1
) AS last_order
FROM customers c;

-- А вот так — нельзя без LATERAL:
SELECT c.name, last_order.total
FROM customers c,
LATERAL (
    SELECT o.total
    FROM orders o
    WHERE o.customer_id = c.id
    ORDER BY o.created_at DESC
    LIMIT 1
) last_order;

Запятая между таблицами в FROM эквивалентна CROSS JOIN LATERAL — PostgreSQL поддерживает оба синтаксиса.


Производительность

LATERAL выполняет подзапрос для каждой строки внешней таблицы — это похоже на Nested Loop. Поэтому:

  • Индекс на колонке связи во внутренней таблице критически важен
  • При больших внешних таблицах без индекса — это N полных сканирований
-- Без этого индекса LATERAL будет медленным
CREATE INDEX idx_orders_customer_id ON orders (customer_id, created_at DESC);

Проверь план:

EXPLAIN ANALYZE
SELECT c.name, top3.*
FROM customers c
JOIN LATERAL (
    SELECT total, created_at FROM orders
    WHERE customer_id = c.id
    ORDER BY created_at DESC LIMIT 3
) top3 ON true;

В плане увидишь Nested Loop с Index Scan на внутренней таблице — это оптимальный вариант.


Итог

LATERAL JOIN решает задачи, где обычный JOIN не справляется:

  • Топ-N в каждой группе — самый частый кейс
  • Раскрытие массивов/JSON с привязкой к строке
  • Вызов функций для каждой строки внешней таблицы
  • Вычисление зависимых агрегатов без дополнительных CTE

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

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →