SQL

БЛОК 9. Логика выполнения — 23. Порядок выполнения SQL-запроса

📚 24 вопросовПройти тест →
Лекция

БЛОК 9. Логика выполнения — 23. Порядок выполнения SQL-запроса

SQL

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

🧭 Введение: почему «читать SQL сверху вниз» недостаточно

Новички обычно читают SQL в том порядке, как он написан: SELECT -> FROM -> WHERE ....
Но БД исполняет запрос в другом логическом порядке, и именно из-за этого появляются ошибки в фильтрах, alias и агрегатах.
В этой теме разберём базовый execution-order:
  • FROMJOIN);
  • WHERE;
  • GROUP BY;
  • HAVING;
  • SELECT;
  • ORDER BY;
  • и отдельно поймём, почему alias из SELECT нельзя использовать в WHERE.
⚖️ Важно: логический порядок и физический план — не одно и то же: Логический порядок показывает, как SQL «задуман» (FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY).
Физический план показывает, как оптимизатор реально выполнит запрос, и это видно в EXPLAIN.
💡 Совет: Если запрос «ведёт себя странно», почти всегда проблема в том, что вы мысленно перепутали порядок выполнения.
Вывод: Логический порядок SQL важнее визуального порядка строк в запросе.

⚠️ Проблема -> решение

Типичная проблема:
  1. фильтр написан корректно «по синтаксису», но возвращает не тот набор строк;
  2. запрос падает ошибкой «column does not exist» при обращении к alias в WHERE;
  3. агрегаты считают не то, потому что WHERE и HAVING перепутаны.
Решение:
  1. мысленно раскладывать запрос по этапам исполнения;
  2. писать условия на правильном этапе (WHERE для строк, HAVING для групп);
  3. использовать подзапрос/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

Визуально 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. FROMJOIN) — старт выборки

Любой 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;
  • WHERE alias не видит;
  • 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

КритерийWHEREHAVING
Когда выполняетсяДо 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 BY alias обычно видит, а GROUP BY alias зависит от СУБД (в 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 из «магии» в управляемый инженерный инструмент.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 9. Логика выполнения — 23. Порядок выполнения SQL-запроса»

Пройти тест →