deшка
Урок 2.2

Логический порядок выполнения запроса

Запрос пишется в одном порядке, а обрабатывается в другом. Пока запросы простые, это незаметно. Как только появляются агрегаты, оконные функции и фильтры, половина «странных» ошибок и неверных цифр объясняется именно порядком обработки.

Порядок записи и порядок обработки

Пишем мы так: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Логически запрос обрабатывается иначе:

ШагЧто происходитЧто уже доступно
1. FROM и JOINсобирается набор строкколонки таблиц
2. WHEREотбрасываются строкиколонки таблиц
3. GROUP BYстроки сворачиваются в группыключи групп
4. HAVINGотбрасываются группыключи групп и агрегаты
5. оконные функциисчитаются по тому, что осталосьвсё выше
6. SELECTвычисляются выражения и алиасывсё выше
7. DISTINCTубираются повторырезультат SELECT
8. ORDER BYсортировкарезультат SELECT и алиасы
9. LIMIT и OFFSETобрезкаотсортированный результат

Каждый шаг видит только то, что подготовили предыдущие. Из этого следуют все правила ниже.

Алиас из SELECT не виден в WHERE

SELECT order_id, quantity * unit_price AS amount
FROM order_items
WHERE amount > 50000;
ERROR: column "amount" does not exist

WHERE выполняется на шаге 2, а алиас amount появляется на шаге 6. Для фильтра такой колонки ещё не существует. Вариантов три: повторить выражение, вынести вычисление в подзапрос или CTE, либо посчитать его в LATERAL.

-- повторить выражение
SELECT order_id, quantity * unit_price AS amount
FROM order_items
WHERE quantity * unit_price > 50000;
 
-- вычислить один раз и фильтровать уровнем выше
WITH priced AS (
  SELECT order_id, quantity * unit_price AS amount
  FROM order_items
)
SELECT * FROM priced WHERE amount > 50000;

Оба варианта дают 796 строк. Второй длиннее, но выражение записано один раз: когда формула суммы поменяется, её не придётся искать в трёх местах запроса.

ORDER BY выполняется после SELECT, поэтому алиасы в нём работают. PostgreSQL разрешает алиас и в GROUP BY, но это уступка для удобства, а не изменение порядка. В WHERE и HAVING алиасы не работают никогда.

WHERE фильтрует строки, HAVING фильтрует группы

Задача: клиенты, сделавшие 10 и больше заказов в 2026 году.

SELECT customer_id, count(*) AS orders
FROM orders
WHERE created_at >= '2026-01-01'      -- какие строки вообще считаем
GROUP BY customer_id
HAVING count(*) >= 10;                -- какие группы оставляем

Таких клиентов 25. Если убрать WHERE, получится 140: это клиенты с десятью заказами за всё время, совсем другой вопрос.

Правило простое. Условие на исходные строки пишется в WHERE, условие на результат агрегата в HAVING. Условие на обычную колонку технически можно написать и в HAVING, если она входит в GROUP BY, но тогда база сначала сгруппирует всё подряд, а потом выбросит лишнее. WHERE честнее описывает намерение и даёт оптимизатору отфильтровать строки до группировки.

Фильтр меняет то, что видят агрегаты и окна

Это самое важное следствие порядка. Задача: найти клиентов, чей самый первый заказ сделан в 2026 году. Напрашивается такой запрос:

SELECT customer_id, min(created_at) AS first_order_at
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY customer_id;

Он возвращает 1278 клиентов, и это неверный ответ. WHERE убрал заказы 2025 года до группировки, поэтому min нашёл первый заказ среди оставшихся. Получились клиенты, у которых в 2026 году был хоть один заказ.

Условие относится не к строкам, а к результату агрегата, значит его место в HAVING:

SELECT customer_id, min(created_at) AS first_order_at
FROM orders
GROUP BY customer_id
HAVING min(created_at) >= '2026-01-01';

Таких клиентов 154. Разница в восемь раз, и обе цифры выглядят правдоподобно. Именно поэтому такие ошибки доезжают до отчётов.

С оконными функциями то же самое. row_number() OVER (PARTITION BY customer_id ORDER BY created_at) нумерует только те заказы, которые пережили WHERE. Если отфильтровать год в том же запросе, номер 1 получит первый заказ года, а не первый заказ клиента.

Оконную функцию нельзя фильтровать в том же запросе

SELECT customer_id, order_id,
       row_number() OVER (PARTITION BY customer_id ORDER BY created_at) AS rn
FROM orders
WHERE rn = 2;
ERROR: column "rn" does not exist

Окна считаются на шаге 5, после WHERE и HAVING. Отфильтровать их результат можно только уровнем выше:

SELECT customer_id, order_id, created_at
FROM (
  SELECT customer_id, order_id, created_at,
         row_number() OVER (PARTITION BY customer_id ORDER BY created_at) AS rn
  FROM orders
) numbered
WHERE rn = 2;

Вторых заказов в базе 1340: столько клиентов сделали хотя бы два заказа.

LIMIT без ORDER BY не гарантирует ничего

LIMIT выполняется последним и обрезает то, что пришло. Без ORDER BY порядок строк не определён: сегодня база читает таблицу подряд, завтра после обновления строк или параллельного чтения порядок другой. Запрос «дай 1000 строк для проверки» вернёт разные строки на деве и на проде.

Для инженера данных это не теория. Выгрузка страницами через LIMIT и OFFSET без сортировки по уникальному ключу пропускает одни строки и дублирует другие. Если сортируете по неуникальной колонке, добавляйте ключ: ORDER BY created_at DESC, order_id DESC.

Практика

Во всех трёх задачах заготовка написана так, как её часто пишут с первого раза. Она либо падает, либо возвращает правдоподобный, но неверный результат.

postgresql · песочницарешено 0 / 3

Найдите позиции заказов, где стоимость без учёта скидки больше 150 000. Колонки: order_id, line_no, amount, где amount равен quantity * unit_price.

Заготовка падает с ошибкой. Исправьте её так, чтобы формула стоимости была записана в запросе один раз.

Ctrl+Enter
Войти, чтобы отмечатьдальше →
2.3 →NULL и трёхзначная логика