Если вы пишете сложный 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)

db-fiddle с эксплейном

Почему не 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)


  1. Akina
    01.03.2026 13:25

    Также можно вспомнить, что в Postgres есть альтернативный синтаксис для CROSS JOIN, а именно: просто запятая.

    CROSS JOIN и операция "запятая" хоть и очень близки, но не полностью эквивалентны. Поэтому термин "альтернативный синтаксис" в корне неверен.

    К тому же операция "запятая" есть практически в любом диалекте SQL, Постгресс тут совершенно не оригинален.

    db-fiddle с эксплейном

    db-fiddle, как и большинство иных online fiddle, в нынешних реалиях весьма плохо проходят мимо ТСПУ и прочих фильтров. Какой смысл их использовать? тем более что у вас там всего кода только 2 запроса на 8 строк:

    CREATE TABLE orders (
      id bigint,
      item_name text,
      price decimal,
      quantity decimal  
    );
    insert into orders
    select generate_series(1, 1000000), 'test', 100, 100;

    Что же до латеральных запросов - их нужно использовать с крайней осторожностью. Нужно понимать, что любой чих может превратить групповые операции в итерационные, и тогда прощай всё - и скорость, и память, и т.д.


    1. varanio Автор
      01.03.2026 13:25

      Можно немного конкретики? Где именно разница между запятой и cross join? Я не смог найти.

      Про групповые и итерационные - пример был бы супер, так не оч понятно. Ну т.е я согласен, что бнздумно ничего делать нельзя, но если есть какой то кейс, который прям точно может всплыть, то хотелось бы увидеть


      1. Akina
        01.03.2026 13:25

        Где именно разница между запятой и cross join?

        Разница в приоритете этих операций.

        FROM A CROSS JOIN B INNER JOIN C ON a.x=c.y -- correct
        FROM A, B INNER JOIN C ON a.x=c.y           -- error

        Про групповые и итерационные - пример был бы супер, так не оч понятно.

        Если у вас между FROM orders o и CROSS JOIN LATERALбудет ещё несколько JOIN к orders или производным, в том числе как минимум один из них внешний, то скорее всего LATERAL будет выполняться после всего этого пакета, а не сразу после сканирования orders. А в случае операции "запятая" это будет почти что гарантированно. Вот получение JOIN-пирога и хреновой горы наJOINенных записей всё и скушает...


        1. varanio Автор
          01.03.2026 13:25

          а, понял, спасибо!

          Я бы вообще не стал смешивать синтаксис с запятой и JOIN в одном запросе, это превращает код в ребус


    1. fenixberg
      01.03.2026 13:25

      Нужно понимать, что любой чих может превратить групповые операции в итерационные

      Lateral join, это аналог аналитической функции, где каждая строка дочерней таблицы формируется из строки родительской. Те операция nested loops между двумя таблицами. На небольших объёмах данных незаметно, но начиная от 1млн строк будет падение производительности.

      Я бы использовал cte materialize.


  1. BareDreamer
    01.03.2026 13:25

    Postgres не знаю, но наверняка задачу можно решить вложенным запросом:

    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 мне больше нравится.


    1. LeVoN_CCCP
      01.03.2026 13:25

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


    1. erogov
      01.03.2026 13:25

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


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


    1. Akina
      01.03.2026 13:25

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


      1. BareDreamer
        01.03.2026 13:25

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


        1. Akina
          01.03.2026 13:25

          Но если убрать вызов функции, то будет ошибка.

          А операция скобка в Постгре не работает? т.е. просто (o.price * o.quantity) AS v1(subtotal), ?


          1. BareDreamer
            01.03.2026 13:25

            Обязательно должен быть вызов функции (я проверял на PostgreSQL 17.5)
            • если функция возвращает SETOF → даёт несколько строк (это очевидно)
            • если возвращает скаляр → даёт одну строку.
            Это работает точно так же как CROSS JOIN LATERAL.
            В СУБД Microsoft и Oralce вместо CROSS JOIN LATERAL надо писать CROSS APPLY. Краткого способа записи в этих СУБД нет.
            Информация из ответа ChatGPT.


            1. Akina
              01.03.2026 13:25

              Ну то есть операции "скобка" == "конвертировать в скалярное значение" в Постгрессе нет. Обидно, хотя и ожидаемо.


              1. BareDreamer
                01.03.2026 13:25

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


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