23. Порядок выполнения SQL-запроса
🧭 Введение: почему «читать SQL сверху вниз» недостаточно
Новички обычно читают SQL в том порядке, как он написан:
Но БД исполняет запрос в другом логическом порядке, и именно из-за этого появляются ошибки в фильтрах, alias и агрегатах.
SELECT -> FROM -> WHERE ....Но БД исполняет запрос в другом логическом порядке, и именно из-за этого появляются ошибки в фильтрах, alias и агрегатах.
В этой теме разберём базовый execution-order:
FROM(иJOIN);WHERE;GROUP BY;HAVING;SELECT;ORDER BY;- и отдельно поймём, почему alias из
SELECTнельзя использовать вWHERE.
⚖️ Важно: логический порядок и физический план — не одно и то же:
Логический порядок показывает, как SQL «задуман» (
Физический план показывает, как оптимизатор реально выполнит запрос, и это видно в
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY).Физический план показывает, как оптимизатор реально выполнит запрос, и это видно в
EXPLAIN.💡 Совет:
Если запрос «ведёт себя странно», почти всегда проблема в том, что вы мысленно перепутали порядок выполнения.
✅ Вывод:
Логический порядок SQL важнее визуального порядка строк в запросе.
⚠️ Проблема -> решение
Типичная проблема:
- фильтр написан корректно «по синтаксису», но возвращает не тот набор строк;
- запрос падает ошибкой «column does not exist» при обращении к alias в
WHERE; - агрегаты считают не то, потому что
WHEREиHAVINGперепутаны.
Решение:
- мысленно раскладывать запрос по этапам исполнения;
- писать условия на правильном этапе (
WHEREдля строк,HAVINGдля групп); - использовать подзапрос/CTE, если нужно фильтровать по alias.
🟢 Если совсем просто:
Проблема не в SQL-синтаксисе, а в неправильной модели «когда что выполняется».
🎯 Как понять, что этап прошёл успешно:
Вы можете объяснить любой
SELECT как конвейер из шести этапов.🛠️ Чем помогает и как работает
Порядок выполнения SQL — это базовая модель мышления для фильтрации, группировки и сортировки.
Если она поставлена правильно, большинство ошибок исчезает ещё до запуска запроса.
Если она поставлена правильно, большинство ошибок исчезает ещё до запуска запроса.
🟢 Если совсем просто:
SQL работает как pipeline: каждый следующий этап получает результат предыдущего.
🎯 Как понять, что этап прошёл успешно:
Вы заранее понимаете, на каком этапе доступен нужный столбец или alias.
Чем помогает:
- исключает типичные ошибки с alias в
WHERE; - правильно разделяет условия до/после группировки;
- упрощает чтение сложных отчётных запросов;
- ускоряет отладку в code review.
Как это работает:
- Шаг 1:
FROMформирует исходный набор строк; - Шаг 2:
WHEREотбрасывает строки до группировки; - Шаг 3:
GROUP BYсобирает оставшиеся строки в группы; - Шаг 4:
HAVINGфильтрует уже группы; - Шаг 5:
SELECTформирует итоговые колонки и alias; - Шаг 6:
ORDER BYсортирует финальный набор.
✅ Вывод:
Правильное понимание execution-order делает SQL предсказуемым.
📚 Ключевые термины (простыми словами)
Перед практикой синхронизируем словарь.
🟢 Если совсем просто:
Без терминов сложно обсуждать «почему здесь не работает alias».
🎯 Как понять, что этап прошёл успешно:
Вы уверенно различаете row-level и group-level фильтрацию.
- Execution order (логический порядок выполнения) — порядок, в котором БД обрабатывает части запроса.
- Row-level filter — фильтр по строкам до группировки (
WHERE). - Group-level filter — фильтр по группам после агрегации (
HAVING). - Alias — временное имя колонки в
SELECT. - Aggregate — агрегатная функция (
COUNT,SUM,AVG,MIN,MAX). - Derived table / subquery — подзапрос, который становится источником строк для внешнего запроса.
- CTE — именованный подзапрос (
WITH ...) для читаемого многошагового SQL.
✅ Вывод:
Эти термины покрывают 90% обсуждений по логике выполнения
SELECT.🧩 1. Логический порядок выполнения SQL
Визуально
Это ключевой факт, который объясняет поведение alias, агрегатов и сортировок.
SELECT написан первым, но логически он почти в конце.Это ключевой факт, который объясняет поведение alias, агрегатов и сортировок.
🟢 Если совсем просто:
Запрос «появляется сверху», но «считается снизу».
🎯 Как понять, что этап прошёл успешно:
Вы называете порядок без подсказки:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY.Назначение:
Построить правильную mental model для чтения и отладки SQL.
Простыми словами:
Сначала БД ищет данные, потом фильтрует, потом группирует, и только потом строит финальные колонки.
Для новичка:
Если путаете порядок, пишите рядом с запросом мини-шпаргалку этапов.
Аналогия:
Это как производственная линия: нельзя покрасить деталь (
SELECT alias), которой ещё нет (FROM/WHERE не пройдены).Пример:
SELECT o.user_id, COUNT(*) AS orders_countFROM orders oWHERE o.status = 'paid'GROUP BY o.user_idHAVING COUNT(*) >= 3ORDER BY orders_count DESC;🔎 Как это происходит на практике:
- Контекст: отчёт по активным покупателям.
- Действия: читаем запрос по execution-order, а не по порядку строк.
- Результат: сразу видно, где должен стоять каждый фильтр.
Характеристики:
- логический порядок не зависит от визуального расположения
SELECT; WHEREвсегда раньшеGROUP BY;ORDER BYработает уже с финальным набором.
Когда использовать:
Во всех запросах с фильтрацией, агрегатами и сортировкой.
✅ Вывод:
Execution-order — база, без которой сложно писать устойчивый SQL.
🔎 2. FROM (и JOIN) — старт выборки
Любой
Именно после
SELECT начинается с формирования исходного набора строк.Именно после
FROM/JOIN становится понятно, какие колонки вообще доступны на следующих шагах.🟢 Если совсем просто:
Сначала решаем, «из каких таблиц берём строки».
🎯 Как понять, что этап прошёл успешно:
Вы можете перечислить, какие поля пришли после
FROM/JOIN.Назначение:
Собрать первичный dataset для дальнейших условий.
Простыми словами:
Без правильного
FROM остальная логика запроса не имеет смысла.Для новичка:
Сначала проверяйте кардинальность после
JOIN, потом пишите агрегаты.Пример:
SELECT u.id, o.id AS order_idFROM users uLEFT JOIN orders o ON o.user_id = u.id;Мини-демо ловушки «JOIN размножает строки»:
SELECT o.id, i.id AS item_idFROM orders oJOIN order_items i ON i.order_id = o.id;💡 Совет:
Если считаете количество заказов после такого
JOIN, обычно нужен COUNT(DISTINCT o.id) или отдельная агрегация order_items до соединения.🔎 Как это происходит на практике:
- Контекст: нужно получить пользователей и их заказы.
- Действия: строим набор строк через
FROM + JOIN. - Результат: дальше можно фильтровать строки в
WHERE.
Характеристики:
JOINможет размножать строки;- тип
JOINвлияет на набор доWHERE; - при подсчётах это часто требует
COUNT(DISTINCT ...)или предварительной агрегации; - ошибки на этом этапе влияют на все следующие шаги.
Когда использовать:
В любом запросе, где участвуют две и более таблицы.
✅ Вывод:
Качество
FROM/JOIN определяет корректность всего запроса.🧪 3. WHERE — фильтр строк до группировки
WHERE работает по отдельным строкам и срабатывает до GROUP BY.Здесь нельзя использовать агрегаты и alias из
SELECT.🟢 Если совсем просто:
WHERE убирает ненужные строки до подсчётов.🎯 Как понять, что этап прошёл успешно:
Вы отделяете условия «по строке» от условий «по группе».
Назначение:
Сократить набор данных до группировки и агрегации.
Простыми словами:
WHERE — ранний фильтр, который снижает объём работы следующих этапов.Для новичка:
Чем точнее
WHERE, тем дешевле GROUP BY и ORDER BY.Пример:
SELECT o.user_id, o.total_amountFROM orders oWHERE o.status = 'paid' AND o.created_at >= DATE '2026-03-01';🔎 Как это происходит на практике:
- Контекст: отчёт только по оплаченных заказам за период.
- Действия: ставим условия в
WHERE, чтобы не тащить лишние строки. - Результат: дальше группируется уже очищенный набор.
Характеристики:
WHEREне видит alias изSELECT;WHEREне принимает агрегаты (COUNT/SUM/...);WHEREобычно улучшает производительность за счёт раннего отсечения.
Когда использовать:
Для любых фильтров по «сырым» строкам.
✅ Вывод:
WHERE — это фильтр до агрегации, а не после.🧱 4. GROUP BY — формирование групп
После
На этом этапе данные перестают быть «отдельными строками» и становятся «группами».
WHERE БД группирует строки по указанным полям.На этом этапе данные перестают быть «отдельными строками» и становятся «группами».
🟢 Если совсем просто:
GROUP BY объединяет строки с одинаковыми ключами в группы.🎯 Как понять, что этап прошёл успешно:
Вы понимаете, почему нельзя выбрать неагрегированную колонку вне
GROUP BY.Назначение:
Подготовить данные к агрегации по бизнес-сущности.
Простыми словами:
Если группируете по
user_id, итог будет «одна строка на пользователя».Для новичка:
Любая колонка в
SELECT должна быть либо в GROUP BY, либо под агрегатом.Пример:
SELECT o.user_id, COUNT(*) AS orders_countFROM orders oWHERE o.status = 'paid'GROUP BY o.user_id;🔎 Как это происходит на практике:
- Контекст: нужно посчитать количество заказов на пользователя.
- Действия: группируем после row-level фильтра.
- Результат: получаем компактный агрегированный набор.
Характеристики:
- группы формируются после
WHERE; - агрегаты считаются внутри групп;
- ошибка с выбором колонок вне
GROUP BYловится сразу.
Когда использовать:
В отчётах и аналитических срезах по категориям/пользователям/датам.
✅ Вывод:
GROUP BY меняет «зерно» результата: из строк в группы.🚦 5. HAVING — фильтр групп после агрегации
HAVING выполняется после GROUP BY и работает с агрегированными значениями.Это место для условий вроде «только группы с
COUNT(*) >= 3».🟢 Если совсем просто:
WHERE фильтрует строки, HAVING фильтрует группы.🎯 Как понять, что этап прошёл успешно:
Вы не путаете
HAVING с WHERE в агрегатных условиях.Назначение:
Отфильтровать итоговые группы по агрегатным критериям.
Простыми словами:
Сначала считаем группы, потом убираем лишние группы.
Для новичка:
Если в условии есть агрегат, почти всегда это
HAVING.Пример:
SELECT o.user_id, COUNT(*) AS paid_ordersFROM orders oWHERE o.status = 'paid'GROUP BY o.user_idHAVING COUNT(*) >= 3;🔎 Как это происходит на практике:
- Контекст: сегмент «пользователи с 3+ покупками».
- Действия: фильтруем строки в
WHERE, затем группы вHAVING. - Результат: корректный сегмент без ручной пост-обработки.
Характеристики:
- работает только после группировки;
- может использовать агрегаты;
- часто идёт в паре с
GROUP BY.
Когда использовать:
Когда нужно условие на агрегированную метрику.
✅ Вывод:
HAVING — пост-агрегатный фильтр, а не замена WHERE.🧠 6. SELECT, alias и почему alias нельзя в WHERE
Alias появляется на этапе
Поэтому выражение
SELECT, а WHERE уже отработал раньше.Поэтому выражение
WHERE alias > ... не сработает: alias физически ещё не существует на этапе WHERE.🟢 Если совсем просто:
WHERE не видит то, что создаётся позже в SELECT.🎯 Как понять, что этап прошёл успешно:
Вы сразу выбираете корректный обходной путь: повтор выражения или подзапрос/CTE.
Назначение:
Избежать одной из самых частых ошибок новичка с alias.
Простыми словами:
Alias — это «ярлык результата», а не исходная колонка для ранних этапов.
Для новичка:
Если хочется фильтровать по alias, вынесите
SELECT в подзапрос и фильтруйте снаружи.⚙️ Нюанс по СУБД:
ORDER BYчасто видит alias изSELECT;WHEREalias не видит;GROUP BYв PostgreSQL alias изSELECTобычно не видит, поэтому для вычислений лучше выражение, позиция илиCTE.
Пример (ошибка):
SELECT o.total_amount * 0.2 AS vat_amountFROM orders oWHERE vat_amount > 100;Пример (правильно через повтор выражения):
SELECT o.total_amount * 0.2 AS vat_amountFROM orders oWHERE o.total_amount * 0.2 > 100;Пример (правильно через подзапрос):
SELECT x.vat_amountFROM ( SELECT o.total_amount * 0.2 AS vat_amount FROM orders o) xWHERE x.vat_amount > 100;Пример (если нужно GROUP BY по вычислению — через CTE):
WITH base AS ( SELECT o.total_amount * 0.2 AS vat_amount FROM orders o)SELECT b.vat_amount, COUNT(*) AS cntFROM base bGROUP BY b.vat_amount;🔎 Как это происходит на практике:
- Контекст: разработчик добавил вычисляемое поле и пытается фильтровать по alias.
- Действия: переносим фильтрацию на правильный уровень.
- Результат: запрос работает предсказуемо и читаемо.
Характеристики:
- alias надёжно доступен в
ORDER BY(в большинстве СУБД); - alias недоступен в
WHEREпо логике порядка выполнения; - подзапрос/CTE делает фильтрацию по alias корректной.
Когда использовать:
При вычисляемых полях и необходимости фильтра по их результату.
✅ Вывод:
Ошибка с alias в
WHERE — прямое следствие execution-order.📈 7. ORDER BY — финальная сортировка
ORDER BY работает по финальному результату после SELECT.На этом этапе уже доступны alias и агрегированные колонки.
🟢 Если совсем просто:
Сортировка — это последний шаг перед выдачей результата.
🎯 Как понять, что этап прошёл успешно:
Вы понимаете, почему
ORDER BY alias работает, а WHERE alias — нет.Назначение:
Упорядочить готовый результирующий набор.
Простыми словами:
Сначала «что выбрать», потом «как отсортировать».
Для новичка:
Всегда задавайте явный порядок в API-запросах, чтобы избежать «прыгающих» результатов.
Пример:
SELECT o.user_id, COUNT(*) AS orders_countFROM orders oWHERE o.status = 'paid'GROUP BY o.user_idORDER BY orders_count DESC;🔎 Как это происходит на практике:
- Контекст: рейтинг клиентов по числу заказов.
- Действия: считаем агрегаты и сортируем их в конце.
- Результат: предсказуемый и читаемый output для отчёта.
Характеристики:
ORDER BYвидит alias изSELECT;- сортировка выполняется на финальном наборе;
- без
ORDER BYпорядок строк недетерминирован.
Когда использовать:
Почти всегда в пользовательских списках и отчётах.
✅ Вывод:
ORDER BY — финальный слой представления результата.🧪 8. Полная цепочка FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY
Сложные запросы проще отлаживать, если читать их именно в этом порядке.
Ниже цельный пример, где задействованы все ключевые этапы.
Ниже цельный пример, где задействованы все ключевые этапы.
🟢 Если совсем просто:
Одна цепочка закрывает почти все типовые SQL-задачи.
🎯 Как понять, что этап прошёл успешно:
Вы можете объяснить каждый блок примера как отдельный шаг конвейера.
Назначение:
Закрепить порядок выполнения на цельном production-паттерне.
Простыми словами:
Это «эталонный» пример, где всё стоит на своём месте.
Для новичка:
При отладке временно комментируйте части запроса и проверяйте результат этапами.
Пример:
SELECT o.user_id, COUNT(*) AS paid_orders, SUM(o.total_amount) AS revenueFROM orders oWHERE o.status = 'paid' AND o.created_at >= DATE '2026-03-01'GROUP BY o.user_idHAVING SUM(o.total_amount) >= 1000ORDER BY revenue DESC;🔎 Как это происходит на практике:
- Контекст: отчёт по ценным покупателям за период.
- Действия: проходим все этапы execution-order.
- Результат: корректный агрегированный рейтинг пользователей.
Характеристики:
WHEREуменьшает объём до агрегации;HAVINGфильтрует уже посчитанные группы;- alias агрегатов удобно использовать в
ORDER BY.
Когда использовать:
В продуктовой аналитике и финансовых срезах.
✅ Вывод:
Полная цепочка этапов делает сложный SQL прозрачным.
🆚 Сравнение: WHERE vs HAVING
| Критерий | WHERE | HAVING |
|---|---|---|
| Когда выполняется | До GROUP BY | После GROUP BY |
| Что фильтрует | Отдельные строки | Группы |
| Можно ли агрегаты | Обычно нет | Да |
| Типичный пример | status='paid' | COUNT(*) >= 3 |
| Ошибка новичка | Пытаться писать COUNT(*) в WHERE | Использовать вместо WHERE без нужды |
✅ Вывод:
WHERE и HAVING решают разные задачи и не заменяют друг друга.🧠 Must-Know (запомнить)
- Логический порядок выполнения:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY. - Логический порядок и физический план (
EXPLAIN) — разные уровни анализа. - SQL читают глазами сверху вниз, но исполняют в другом порядке.
WHEREфильтрует строки до агрегации.HAVINGфильтрует группы после агрегации.- Alias создаётся на этапе
SELECT. - Alias из
SELECTнельзя использовать вWHERE. ORDER BYalias обычно видит, аGROUP BYalias зависит от СУБД (в PostgreSQL обычно нет).- Для фильтра по alias используйте повтор выражения или подзапрос/CTE.
ORDER BYработает по финальному набору и обычно видит alias.JOINможет размножать строки доWHERE/GROUP BY, что влияет наCOUNT/SUM.- Перепутанный execution-order — частая причина «странных» результатов запроса.
✅ Вывод:
Понимание порядка выполнения SQL снимает большую часть логических ошибок.
❌ Частые мифы
❌ Миф: SQL выполняется в порядке написания строк.
✅ Как правильно: Логический порядок другой:
FROM раньше SELECT.
📎 Почему это важно: Иначе появляются ошибки в alias и агрегатах.❌ Миф: Alias из
SELECT можно всегда использовать в WHERE.
✅ Как правильно: WHERE выполняется раньше, поэтому alias там недоступен.
📎 Почему это важно: Это одна из самых частых runtime-ошибок новичков.❌ Миф:
HAVING можно использовать вместо WHERE везде.
✅ Как правильно: HAVING нужен для групп и агрегатов, WHERE — для строк.
📎 Почему это важно: Неверный этап фильтрации портит и корректность, и производительность.❌ Миф: Если запрос вернул «примерно то же», порядок этапов не важен.
✅ Как правильно: При росте данных и усложнении логики ошибка порядка обязательно проявится.
📎 Почему это важно: Execution-order критичен для устойчивого production SQL.
🎤 Часто спрашивают на собеседованиях
❓ Вопрос: Какой логический порядок выполнения SQL-запроса?
✅ Ответ:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY.❓ Вопрос: Чем логический порядок отличается от физического плана?
✅ Ответ: Логический порядок описывает смысл SQL, а физический план показывает реальный путь выполнения оптимизатора в
EXPLAIN.❓ Вопрос: Почему alias из
SELECT нельзя использовать в WHERE?
✅ Ответ: Потому что WHERE исполняется раньше, чем SELECT создаёт alias.❓ Вопрос: В чём ключевая разница
WHERE и HAVING?
✅ Ответ: WHERE фильтрует строки до группировки, HAVING фильтрует группы после агрегации.❓ Вопрос: Можно ли писать агрегат в
WHERE?
✅ Ответ: Обычно нет, агрегатные условия пишут в HAVING.❓ Вопрос: Почему
ORDER BY alias обычно работает?
✅ Ответ: Потому что сортировка выполняется после этапа SELECT, где alias уже создан.❓ Вопрос: Можно ли использовать alias из SELECT в GROUP BY?
✅ Ответ: Зависит от СУБД; в PostgreSQL alias из
SELECT обычно недоступен в GROUP BY, поэтому лучше выражение/позиция/CTE.❓ Вопрос: Как фильтровать по вычисляемому alias корректно?
✅ Ответ: Либо повторить выражение в
WHERE, либо вынести вычисление в подзапрос/CTE и фильтровать снаружи.❓ Вопрос: Что чаще всего ломает логику агрегатного запроса?
✅ Ответ: Неправильное размещение условий между
WHERE и HAVING.❓ Вопрос: Что делать, если запрос «логически верный», но результат странный?
✅ Ответ: Разобрать запрос по execution-order и проверить каждый этап отдельно.
🚨 Типичные ошибки
- Читать SQL только «по порядку строк», игнорируя execution-order.
- Писать
WHERE alias > ...и получать ошибку недоступного столбца. - Ставить агрегатные условия в
WHEREвместоHAVING. - Использовать
HAVINGтам, где нужен обычныйWHERE. - Не учитывать, что
JOINразмножает строки до группировки. - Считать
COUNT(*)послеJOINбез проверки, не нужно лиCOUNT(DISTINCT ...). - Сортировать без явного
ORDER BYи ожидать стабильного порядка.
✅ Вывод:
Большинство ошибок в теме — это ошибки этапа выполнения, а не синтаксиса.
✅ Best Practices
- Всегда держите рядом шпаргалку execution-order для сложных запросов.
- Разделяйте row-level (
WHERE) и group-level (HAVING) условия. - При ошибке alias в
WHEREпереходите на подзапрос/CTE. - Для сложных отчётов отлаживайте запрос по шагам:
FROM->WHERE->GROUP BY. - В code review проверяйте, что каждое условие стоит на правильном этапе.
- Пишите явный
ORDER BYв API-списках и отчётах. - Документируйте нестандартные части запроса короткими комментариями.
✅ Вывод:
Execution-order должен быть частью ежедневной SQL-дисциплины команды.
🏁 Заключение
Порядок выполнения SQL-запроса — это фундамент, который связывает фильтры, агрегации, alias и сортировку в единую логику.
Если вы уверенно держите цепочку
Если вы уверенно держите цепочку
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY и понимаете ограничение alias в WHERE, вы пишете SQL значительно стабильнее и предсказуемее.✅ Вывод:
Правильная модель execution-order превращает SQL из «магии» в управляемый инженерный инструмент.