Если вы пишете сложный SELECT, в котором одно и тоже вычисляется несколько раз, например, для массовой обработки данных пачками, то наверняка вам хотелось завести локальную переменную
вот пример:
SELECT price * quantity AS total_price, (price * quantity) * 0.15 AS tax, ((price * quantity) + ((price * quantity) * 0.15)) AS grand_total FROM orders;
Здесь price * quantity вычисляется несколько раз, и tax вычисляется дважды. Но это выглядит еще ок, в реальности это зачастую всякие монструозные свитч-кейсы с NULLIF, COALESCE и прочими ребятами.
Но не суть, вам хочется один раз вычислить значение, без дублирования кода.
Long story short, это делается так:
SELECT o.item_name, o.price, o.quantity, v1.subtotal, v2.tax, v1.subtotal + v2.tax as grandtotal FROM orders o -- "Объявляем" subtotal CROSS JOIN LATERAL ( SELECT o.price * o.quantity AS subtotal ) AS v1 -- "Объявляем" tax, используя subtotal CROSS JOIN LATERAL ( SELECT v1.subtotal * 0.15 AS tax ) AS v2
Т.е. мы выделяем расчеты в cross join lateral, давая получившимся полям человеческие имена. И хотя в целом строк больше, но меньше дублирования кода, в котором можно ошибиться, и читается это зачастую проще.
Также можно вспомнить, что в Postgres есть альтернативный синтаксис для CROSS JOIN, а именно: просто запятая. Тогда код будет выглядить еще компактнее (хотя лично я предпочитаю явный ситаксис):
SELECT o.item_name, o.price, o.quantity, v1.subtotal, -- используем переменные v2.tax, v1.subtotal + v2.tax as grandtotal FROM orders o, -- вот тут запятая в конце LATERAL ( SELECT o.price * o.quantity AS subtotal ) AS v1, -- и тут LATERAL ( SELECT v1.subtotal * 0.15 AS tax ) AS v2
Вот db-fiddle, чтобы поиграться.
Что там по плану запроса?
Ясное дело, люди боятся лишних джойнов, и это понятно. Но в данном случае, когда в этих cross join lateral всего одна строка, вычислимая без побочных эффектов, то оптимизатор скорее всего это всё заинлайнит, и в Query Plan вы вообще не увидите ничего. В данном случае остался только Seq Scan по orders
QUERY PLAN |
|---|
Seq Scan on orders o (cost=0.00..17055.68 rows=388570 width=192) |
Почему не CTE?
Иногда можно для целей читабельности использовать и CTE, почему нет. Но вот в данном конкретном случае, пришлось бы делать два подзапроса с перечислением вообще всех нужных полей, а не только subtotal и tax.
WITH base AS ( SELECT o.item_name, o.price, o.quantity, o.price * o.quantity AS subtotal FROM orders o ), tax_calculated AS ( SELECT item_name, price, quantity, subtotal, subtotal * 0.15 AS tax FROM base ) SELECT item_name, price, quantity, subtotal, tax, subtotal + tax AS grandtotal FROM tax_calculated;
Традиционно приглашаю вас подписаться на мой tg-канал Cross Join
Комментарии (16)

BareDreamer
01.03.2026 13:25Postgres не знаю, но наверняка задачу можно решить вложенным запросом:
SELECT c.*, total_price + tax AS grand_total FROM ( SELECT b.*, total_price * 0.15 AS tax FROM ( SELECT a.*, price * quantity AS total_price FROM orders a ) b ) cОднако, ваш вариант с CROSS JOIN LATERAL мне больше нравится.

LeVoN_CCCP
01.03.2026 13:25Я так нашёл, что в принципе они идентичны (вложенный запрос и латерал - апплай на мс). Почему латерал использовать удобней - при множественных джойнах и зависимостях вложенность может возрасти нелинейно И даже преобразоваться в дубликат запросов. А потому латерал в конец на преобразование результата запроса. То есть чтобы он обработку производил после выгрузки и фильтрации а не до. Иначе мы будем тратить цпу на все строки, а потом какую-то часть отфильтруем.

erogov
01.03.2026 13:25Справедливости ради, и в CTE можно написать
base.*. Тогда и вложенности не будет, и догадываться «что хотел сказать автор» не придется.

RinNas
01.03.2026 13:25Можно ещё проще. Хитрость в том, чтобы вместо подзапроса выполнять вызов функции.
Например,
LATERAL ( SELECT o.price * o.quantity AS subtotal ) AS v1,можно упростить до
COALESCE(o.price * o.quantity) AS v1(subtotal),Пример конвейера (цепочек) вычислений в одном SELECT запросе без CTE: https://github.com/rin-nas/postgresql-patterns-library/tree/master/experiments/compression/README.md

Akina
01.03.2026 13:25А зачем тут COALESCE(), если все исходные данные в одной базовой таблице?

BareDreamer
01.03.2026 13:25Так можно вызывать любую функцию, в том числе возвращающую скаляр. Это особенность PostgreSQL. Но если убрать вызов функции, то будет ошибка. Тут функция COALESCE используется поскольку просто возвращает свой аргумент.

Akina
01.03.2026 13:25Но если убрать вызов функции, то будет ошибка.
А операция скобка в Постгре не работает? т.е. просто
(o.price * o.quantity) AS v1(subtotal),?
BareDreamer
01.03.2026 13:25Обязательно должен быть вызов функции (я проверял на PostgreSQL 17.5)
• если функция возвращаетSETOF→ даёт несколько строк (это очевидно)
• если возвращает скаляр → даёт одну строку.
Это работает точно так же какCROSS JOIN LATERAL.
В СУБД Microsoft и Oralce вместоCROSS JOIN LATERALнадо писатьCROSS APPLY. Краткого способа записи в этих СУБД нет.
Информация из ответа ChatGPT.
Akina
01.03.2026 13:25Ну то есть операции "скобка" == "конвертировать в скалярное значение" в Постгрессе нет. Обидно, хотя и ожидаемо.

BareDreamer
01.03.2026 13:25Значение скалярное как со скобками, так и без скобок. Надо конвертировать в табличное значение. Магия PostgreSQL: вызов скалярной функции в
FROMконвертируется в таблицу.

cross_join
01.03.2026 13:25Простой вариант в стиле вьюшки, но без её создания. Можно и в CTE, но нет большого смысла здесь.
SELECT amount, tax, amount + tax AS amount_with_tax FROM ( SELECT amount, amount * 0.15 AS tax FROM ( SELECT price * quantity AS amount FROM orders ) o1 ) o2 ;
Akina
CROSS JOIN и операция "запятая" хоть и очень близки, но не полностью эквивалентны. Поэтому термин "альтернативный синтаксис" в корне неверен.
К тому же операция "запятая" есть практически в любом диалекте SQL, Постгресс тут совершенно не оригинален.
db-fiddle, как и большинство иных online fiddle, в нынешних реалиях весьма плохо проходят мимо ТСПУ и прочих фильтров. Какой смысл их использовать? тем более что у вас там всего кода только 2 запроса на 8 строк:
Что же до латеральных запросов - их нужно использовать с крайней осторожностью. Нужно понимать, что любой чих может превратить групповые операции в итерационные, и тогда прощай всё - и скорость, и память, и т.д.
varanio Автор
Можно немного конкретики? Где именно разница между запятой и cross join? Я не смог найти.
Про групповые и итерационные - пример был бы супер, так не оч понятно. Ну т.е я согласен, что бнздумно ничего делать нельзя, но если есть какой то кейс, который прям точно может всплыть, то хотелось бы увидеть
Akina
Разница в приоритете этих операций.
Если у вас между
FROM orders oиCROSS JOIN LATERALбудет ещё несколькоJOINкordersили производным, в том числе как минимум один из них внешний, то скорее всегоLATERALбудет выполняться после всего этого пакета, а не сразу после сканированияorders. А в случае операции "запятая" это будет почти что гарантированно. Вот получение JOIN-пирога и хреновой горы наJOINенных записей всё и скушает...varanio Автор
а, понял, спасибо!
Я бы вообще не стал смешивать синтаксис с запятой и JOIN в одном запросе, это превращает код в ребус
fenixberg
Lateral join, это аналог аналитической функции, где каждая строка дочерней таблицы формируется из строки родительской. Те операция nested loops между двумя таблицами. На небольших объёмах данных незаметно, но начиная от 1млн строк будет падение производительности.
Я бы использовал cte materialize.