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

Ментальная модель SELECT: логический порядок обработки

Ты пишешь SELECT … FROM … WHERE …, но Postgres вычисляет FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Этот зазор объясняет, почему алиас работает в ORDER BY, но не в WHERE.

SQL Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Коллега пишет SELECT price * qty AS revenue FROM orders WHERE revenue > 100; и получает ERROR: column "revenue" does not exist. Он клянётся, что алиас вот же он, на той же строке. Потом переносит тот же revenue в ORDER BY revenue — и всё работает. Одно слово, один запрос, два противоположных исхода. Это не причуда Postgres — это самое важное, что нужно усвоить о том, как SELECT на самом деле вычисляется.

Порядок, в котором пишешь — не порядок, в котором выполняется

Оператор SELECT пишется в одном порядке, а вычисляется в совершенно другом. Письменный порядок задан грамматикой — SELECT всегда идёт первым. Но логический порядок обработки (порядок, в котором по стандарту SQL клаузы концептуально вычисляются) ставит SELECT ближе к концу:

  1. FROM (и JOIN) — построить рабочий набор строк из исходных таблиц.
  2. WHERE — оставить только строки, где предикат TRUE (это selection, отбор).
  3. GROUP BY — схлопнуть выжившие строки в группы.
  4. HAVING — оставить только группы, где предикат TRUE.
  5. SELECT — вычислить выходные выражения и присвоить алиасы (это projection, проекция).
  6. DISTINCT — убрать дублирующиеся выходные строки.
  7. ORDER BY — отсортировать результат.
  8. LIMIT / OFFSET — отрезать кусок отсортированного результата.

Восемь шагов складываются в конвейер: шаги 1–4 решают, какие строки и группы выживут, шаг 5 решает, как выглядит каждая выходная строка, а шаги 6–8 формируют финальную презентацию. Без этого порядка в голове ошибки видимости алиасов и сюрпризы производительности выглядят как причуды Postgres — но это не так.

Это логическая модель: исполнитель не материализует буквально полный набор после каждого шага (он стримит, а планировщик переупорядочивает ради скорости — это конвейер из прошлого урока и глава execution-plans трека databases). Но правила разрешения имён, которые ты можешь наблюдать, следуют этому порядку точно, и именно это людей и кусает.

Проекция против отбора: две разные работы

Два слова из реляционной алгебры снимают бо́льшую часть путаницы:

  • Selection (отбор) = выбор строк. Это WHEREHAVING для групп). Отвечает на «какие строки выживут?».
  • Projection (проекция) = выбор и вычисление колонок. Это SELECT. Отвечает на «как выглядит каждая выходная строка?».

Это отдельные стадии. WHERE бежит на шаге 2 на сырых колонках таблицы; SELECT бежит на шаге 5 и является единственным местом, где рождаются алиасы выходных колонок.

-- orders(id, user_id, price, qty, status, created_at)
SELECT
  user_id,
  price * qty AS revenue          -- 'revenue' рождается ЗДЕСЬ, на шаге 5
FROM orders                       -- шаг 1
WHERE status = 'paid'             -- шаг 2: 'revenue' ещё не существует
ORDER BY revenue DESC             -- шаг 7: 'revenue' уже существует
LIMIT 20;                         -- шаг 8

Почему алиас работает в ORDER BY, но не в WHERE

Теперь противоречие из вступления растворяется. WHERE — это шаг 2; алиас revenue из SELECT не создаётся до шага 5. Значит, ссылка на него в WHERE — это ошибка путешествия во времени: ты именуешь то, чего ещё нет. Postgres отвергает запрос с column "revenue" does not exist.

ORDER BY — это шаг 7, после SELECT. К этому моменту алиас уже существует, поэтому ORDER BY revenue разрешается чисто. ORDER BY — единственная клауза, которая видит имена выхода SELECT, именно потому что бежит после проекции.

Фикс для случая с WHERE — повторить выражение (под хорошим планом движок всё равно вычислит его один раз) или обернуть запрос в подзапрос / CTE, чтобы алиас материализовался на внутреннем шаге:

-- Вариант A: повторить выражение в WHERE (оно ссылается на базовые колонки, всегда легально)
SELECT user_id, price * qty AS revenue
FROM orders
WHERE status = 'paid' AND price * qty > 100;

-- Вариант B: назвать один раз во внутреннем запросе, фильтровать во внешнем (алиас теперь виден)
SELECT * FROM (
  SELECT user_id, price * qty AS revenue
  FROM orders WHERE status = 'paid'
) s
WHERE s.revenue > 100;
Граничные случаи

GROUP BY — промежуточный случай. Стандарт SQL говорит, что GROUP BY (шаг 3) бежит до SELECT (шаг 5), так что по стандарту алиас SELECT там не должен быть виден. Postgres ослабляет это ради удобства: GROUP BY может видеть алиас SELECT (GROUP BY revenue), и HAVING тоже. Но WHERE — никогда, он строго до группировки. Не полагайся на это расширение GROUP BY-алиаса в переносимом SQL; другие движки его отвергают.

Колонка против выражения на выходе, и одна продакшен-ловушка

Каждый элемент списка SELECT — это либо голая колонка (user_id), либо вычисляемое выражение (price * qty, lower(email), count(*)). Голая колонка без алиаса сохраняет своё имя; выражение без алиаса получает уродливое авто-имя вроде ?column?. Всегда давай алиас выражениям — нижестоящий код, BI-инструменты и ORDER BY ключуются по этому имени.

Продакшен-ловушка, прямо вытекающая из порядка обработки: перенос тяжёлой логики в SELECT не уменьшает число сканируемых строк. WHERE уже отработал. Если твой SELECT зовёт дорогую функцию на строку, а WHERE неселективен, ты всё равно платишь за эту функцию на каждой выжившей строке. Фильтрация — рычаг для скольких строк; проекция лишь формирует что показывает каждая строка. Однажды команда перенесла медленный regex в SELECT, думая, что он «будет работать меньше» — он отработал на всех 4 миллионах выживших строк ровно как раньше, потому что WHERE не изменился. Селективность живёт в WHERE, а не в SELECT.

Расставь шаги по порядку

Расставь клаузы по их ЛОГИЧЕСКОМУ порядку вычисления (а не по порядку, в котором ты их печатаешь):

  1. 1 FROM — построить набор строк из исходных таблиц
  2. 2 WHERE — оставить строки, где предикат TRUE
  3. 3 GROUP BY — схлопнуть строки в группы
  4. 4 HAVING — оставить группы, где предикат TRUE
  5. 5 SELECT — вычислить выходные колонки и присвоить алиасы
  6. 6 ORDER BY — отсортировать спроецированный результат
  7. 7 LIMIT — отрезать кусок отсортированного результата
Викторина

Почему SELECT price*qty AS revenue ... WHERE revenue > 100 падает, а ORDER BY revenue в том же запросе работает?

Викторина

Запрос медленный. Перенос дорогого вызова функции на строку из WHERE в список SELECT…

Вспомните перед уходом
  1. 01
    Сформулируй логический порядок обработки SELECT и противопоставь его письменному порядку.
  2. 02
    Почему алиас SELECT можно использовать в ORDER BY, но не в WHERE?
  3. 03
    Определи проекцию против отбора и почему это важно для производительности.
Итог

Оператор SELECT пишется как SELECT … FROM … WHERE …, но логически вычисляется в другом порядке: FROM строит строки, WHERE фильтрует их (отбор), GROUP BY и HAVING агрегируют и фильтруют группы, SELECT вычисляет колонки и присваивает алиасы (проекция), затем DISTINCT, ORDER BY и LIMIT завершают. Поскольку алиас создаётся на шаге SELECT, он невидим для более раннего WHERE (там используй сырое выражение или подзапрос), но видим для более позднего ORDER BY. Держи две работы раздельно: отбор (WHERE) решает, сколько строк, проекция (SELECT) решает, что показывает каждая строка — поэтому селективность всегда живёт в WHERE. Физический план может переупорядочить эти шаги ради скорости, но правила видимости не меняются никогда. Теперь, когда встретишь ошибку «column does not exist» на алиасе, который ты только что определил, — сразу смотри, какая клауза жалуется: если это WHERE, алиас просто ещё не существует, и правильный ход — повторить выражение или обернуть в подзапрос.

Практика

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

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.