> SmartGate 41 752 9863

Порядок выполнения SQL-запроса

Почему SQL пишется в одном порядке, а PostgreSQL логически обрабатывает его в другом

Теория

Вот где у многих впервые ломается ощущение, что SQL читается сверху вниз как обычный текст. На экране мы пишем запрос, начиная со слова SELECT. Нам кажется, что СУБД идёт по строкам SQL запроса в том же порядке, в котором мы пишем. Но PostgreSQL устроен иначе (как и другие похожие СУБД). Он сначала собирает источник данных, потом фильтрует строки, потом группирует их, потом проверяет группы, и только после этого формирует итоговый набор столбцов.

Из-за этого часто появляются вопросы, которые сначала могут показаться странными. Почему псевдоним из SELECT нельзя использовать в WHERE, но можно использовать в ORDER BY? Почему агрегатная функция не работает в WHERE, а в HAVING уже работает? Почему LIMIT не берёт «первые попавшиеся» строки, если перед ним стоит сортировка?

Ответ везде один и тот же: у SQL есть логический порядок выполнения.

Синтаксический порядок написания операторов в запросе такой:
SELECT;
FROM;
WHERE;
GROUP BY;
HAVING;
ORDER BY;
LIMIT.

А если смотреть глазами PostgreSQL, такой:
• сначала FROM;
• потом WHERE;
• потом GROUP BY;
• потом HAVING;
• потом SELECT;
• потом ORDER BY;
• и только в конце LIMIT.

Давайте представим это как конвейер. В начале на ленту кладут целую таблицу. Потом с неё убирают лишние строки. Потом при необходимости собирают оставшиеся строки в группы. Затем отбрасывают лишние группы. Потом уже решают, какие именно столбцы показать в результате. И только в самом конце этот результат сортируют и, если нужно, обрезают по количеству строк.

Если разобраться как этот конвейер работает, половина SQL загадок перестанет быть таковыми. Псевдоним из SELECT не работает в WHERE просто потому, что до шага SELECT СУБД ещё даже не дошла. А в ORDER BY он уже виден, потому что итоговый набор столбцов к этому моменту существует.

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

Синтаксис

SELECT column_list
FROM table_name
WHERE condition
GROUP BY group_columns
HAVING group_condition
ORDER BY sort_columns
LIMIT row_count;

FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT
SQL

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

Часть запроса Что делает Когда реально срабатывает
FROM Определяет источник данных Самый ранний шаг
WHERE Фильтрует строки Сразу после FROM
GROUP BY Собирает строки в группы После фильтрации строк
HAVING Фильтрует уже готовые группы После GROUP BY
SELECT Формирует итоговые столбцы результата После строк и групп
ORDER BY Сортирует готовый результат После SELECT
LIMIT Оставляет нужное количество строк Самый последний шаг

1. Писать сверху вниз и исполнять сверху вниз — не одно и то же
Запрос на экране начинается с SELECT, потому что человеку так удобнее читать. Но PostgreSQL не может сначала выбрать столбцы, пока ещё не знает, из каких строк вообще будет строиться результат. Поэтому логический запуск начинается с FROM и дальше движется по конвейеру.


2. WHERE работает со строками, а HAVING — уже с группами
WHERE отбрасывает строки до группировки. HAVING включается позже, когда группы уже собраны. Поэтому агрегат вроде COUNT(*) в WHERE неуместен, а в HAVING — это как раз его место.


3. Псевдоним из SELECT не живёт в прошлом
Если столбец получил имя через AS внутри SELECT, это имя появляется только на шаге формирования результата. Именно поэтому такое имя уже может использовать ORDER BY, но WHERE его ещё не видит.


4. С GROUP BY есть тонкий нюанс
В PostgreSQL группировку иногда можно записать по псевдониму или по номеру столбца. Это удобство синтаксиса, а не повод думать, будто GROUP BY логически выполняется после SELECT.


5. LIMIT ограничивает готовый результат
Если перед LIMIT стоит сортировка, сначала PostgreSQL отсортирует строки, а потом возьмёт нужное количество строк сверху. Поэтому ORDER BY salary DESC LIMIT 3 и означает «три самые большие зарплаты», а не просто «какие-то три строки».

Примеры

1. Сначала фильтруем строки, потом уже показываем столбцы

Запрос написан с SELECT, но внутри база данных сначала берёт строки из workers, потом оставляет только зарплаты от 90000 и выше, потом формирует итоговые столбцы, затем сортирует их и в самом конце обрезает до пяти строк. Это хороший пример, чтобы увидеть конвейер без лишней нагрузки.

🔄 Попробуйте изменить запрос:

  • • Уберите LIMIT 5 и посмотрите, что изменится
  • • Замените порог 90000 на 80000

2. Псевдоним работает в ORDER BY

Здесь псевдоним discounted_price используется в сортировке. Это работает потому, что ORDER BY идёт после шага SELECT, а значит итоговый набор столбцов к этому моменту уже понятен.

🔄 Попробуйте изменить запрос:

  • • Попробуйте отсортировать по discounted_price ASC

3. В WHERE выражение приходится повторять

А здесь видно, почему псевдоним не работает в WHERE. Чтобы отфильтровать будущую повышенную зарплату, выражение salary * 1.1 приходится писать прямо в фильтре. Псевдоним raised_salary появится позже, чем отработает WHERE.

🔄 Попробуйте изменить запрос:

  • • Попробуйте в уме представить, что будет, если написать в WHERE псевдоним raised_salary, а затем напишите и проверьте

4. WHERE работает до группировки, HAVING — после

Фильтрация в WHERE сначала отбрасывает товары, дешевле 10000. Потом PostgreSQL собирает оставшиеся строки в группы по категории. И только после этого HAVING решает, какие группы вообще достойны остаться в результате. Поэтому COUNT(*) и пишется именно в HAVING.

🔄 Попробуйте изменить запрос:

  • • Замените порог в WHERE с 10000 на 50000
  • • Поменяйте условие HAVING COUNT(*) >= 2 на псевдоним products_count >= 2

5. LIMIT ограничивает отсортированный результат

Последовательность здесь принципиальна. Сначала остаются только доставленные заказы. Потом они сортируются от самых поздних к самым ранним. После этого LIMIT 3 оставляет верхние три строки. В результате оказываются три самых свежих доставленных заказа, а не случайные три записи.

🔄 Попробуйте изменить запрос:

  • • Замените LIMIT 3 на LIMIT 5
  • • Попробуйте убрать сортировку и посмотрите как отличается результат

Типичные ошибки

Читают SQL как будто он исполняется сверху вниз
На экране запрос действительно начинается с SELECT, но логически PostgreSQL сначала работает с FROM и WHERE. Если об этом забыть, сразу появляются странные ожидания от псевдонимов, агрегатов и сортировки.

Пишут псевдоним из SELECT в WHERE
Это одна из самых частых ловушек. До шага WHERE псевдоним ещё не появился, поэтому фильтр его не видит. Если нужно фильтровать по выражению, это выражение приходится повторять.

Путают WHERE и HAVING
WHERE фильтрует строки до группировки. HAVING фильтрует уже готовые группы. Можно прямо так просто это и запомнить.

Думают, что LIMIT выбирает строки раньше сортировки
Нет. Если перед LIMIT есть ORDER BY, сначала результат сортируется, а уже потом ограничивается.

Делают выводы из удобства синтаксиса, а не из логики выполнения
Например, PostgreSQL позволяет использовать псевдоним в GROUP BY. Это не значит, что группировка выполняется после SELECT. Это всего лишь удобство записи.

Практика

Проверь себя

Ответьте на вопросы, чтобы закрепить материал:

1
Какая часть запроса логически срабатывает раньше остальных в обычном SELECT-запросе?
2
Почему псевдоним из SELECT обычно не работает в WHERE, но работает в ORDER BY?
3
Где уместно писать условие с агрегатом вроде COUNT(*) >= 2?
4
Что логически произойдёт раньше в запросе с ORDER BY salary DESC LIMIT 3?
5
Какое утверждение про GROUP BY и псевдонимы из SELECT ближе всего к истине для PostgreSQL?
6
Напишите запрос к таблице workers, который покажет department_id и столбец workers_count. Сначала оставьте только работников с зарплатой не меньше 70000 и с указанной должностью. Потом сгруппируйте строки по department_id. В результат должны попасть только те отделы, где после такой фильтрации осталось минимум 2 сотрудника. Отсортируйте результат по workers_count от большего к меньшему, а затем по department_id.
7
Напишите запрос к таблице orders, который выведет order_id, user_id, order_date и столбец next_day. В next_day нужно показать order_date + INTERVAL '1 day'. Оставьте только заказы, у которых этот следующий день не раньше '2024-05-01 00:00:00'::timestamp, и одновременно исключите заказы со статусом 'отменён'. Отсортируйте результат по next_day от более поздней даты к более ранней, а затем по order_id.