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

Почему порядок выполнения — не то же самое, что порядок написания

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

Вот как вы пишете запрос:

SELECT department, COUNT(*) AS cnt
FROM employees
WHERE is_active = TRUE
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY cnt DESC
LIMIT 10

А вот в каком порядке его выполняет база данных:

  1. FROM (+ JOIN)
  2. WHERE
  3. GROUP BY
  4. HAVING
  5. SELECT
  6. DISTINCT
  7. ORDER BY
  8. LIMIT / OFFSET

Если вы понимаете этот порядок, большинство «странных» ошибок в SQL перестают быть странными. Если не понимаете — будете гадать, почему алиас не работает в WHERE, а в ORDER BY работает. На собеседовании по SQL этот вопрос задают часто и ожидают чёткого ответа.

Каждый шаг по отдельности

1. FROM и JOIN — сборка исходных данных

Первый шаг — определить, откуда брать данные. Если в запросе есть JOIN, таблицы соединяются именно здесь. Результат — одна большая промежуточная таблица, с которой работают все последующие шаги.

FROM orders o
JOIN users u ON o.user_id = u.id

База строит декартово произведение, применяет условие ON и получает объединённый набор строк. Это первое, что происходит — ещё до любой фильтрации.

2. WHERE — фильтрация строк

WHERE работает со строками, которые пришли из FROM. Каждая строка проверяется на условие, и те, что не прошли, отбрасываются.

WHERE o.order_date >= '2025-01-01'
  AND u.country = 'RU'

Критически важно: на этом этапе ещё нет ни группировки, ни агрегатов, ни алиасов из SELECT. Поэтому вы не можете написать WHERE cnt > 5 — алиас cnt ещё не существует. И не можете написать WHERE COUNT(*) > 5 — агрегатная функция здесь недопустима, потому что строки ещё не сгруппированы.

3. GROUP BY — группировка

Оставшиеся после WHERE строки объединяются в группы по указанным столбцам. С этого момента база данных работает не с отдельными строками, а с группами.

GROUP BY u.country, DATE_TRUNC('month', o.order_date)

После GROUP BY в SELECT можно использовать только два типа выражений: столбцы из GROUP BY и агрегатные функции. Всё остальное — ошибка. Запрос SELECT country, city, COUNT(*) FROM orders GROUP BY country не выполнится, потому что city не входит в GROUP BY и не обёрнут в агрегат.

4. HAVING — фильтрация групп

HAVING работает аналогично WHERE, но применяется к группам, а не к строкам. Здесь можно использовать агрегатные функции, потому что группировка уже выполнена.

HAVING COUNT(*) > 5

HAVING отсеивает группы, не удовлетворяющие условию. В примере выше останутся только те комбинации «страна + месяц», где больше пяти заказов.

5. SELECT — вычисление выражений

Только на этом этапе вычисляются выражения и присваиваются алиасы. SELECT не фильтрует и не группирует — он формирует столбцы результата.

SELECT
    u.country,
    DATE_TRUNC('month', o.order_date) AS month,
    COUNT(*) AS order_count,
    SUM(o.amount) AS revenue

Здесь же вычисляются оконные функции. Оконные функции выполняются после GROUP BY и HAVING, но до ORDER BY. Это значит, что оконная функция видит уже сгруппированные строки и может работать с результатами агрегации.

6. DISTINCT — удаление дубликатов

Если указан DISTINCT, дубликаты удаляются после вычисления SELECT. Именно поэтому DISTINCT не влияет на агрегатные функции внутри SELECT — они уже вычислены.

7. ORDER BY — сортировка

ORDER BY выполняется почти последним. Именно поэтому здесь можно использовать алиасы из SELECT — к этому моменту они уже существуют.

ORDER BY revenue DESC

Вы также можете сортировать по столбцам, которых нет в SELECT, — по выражению или по номеру позиции. ORDER BY видит весь промежуточный результат.

8. LIMIT и OFFSET — обрезка результата

Последний шаг — ограничение количества строк. LIMIT без ORDER BY возвращает произвольные строки — порядок не гарантирован.

ORDER BY revenue DESC
LIMIT 10

Разбор запроса по шагам

Посмотрим, как база данных выполнит конкретный запрос. Задача: найти топ-5 категорий по выручке за 2025 год, у которых больше 100 заказов.

SELECT
    p.category,
    COUNT(*) AS order_count,
    SUM(o.amount) AS revenue,
    ROUND(AVG(o.amount), 2) AS avg_check
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE o.order_date >= '2025-01-01'
  AND o.order_date < '2026-01-01'
GROUP BY p.category
HAVING COUNT(*) > 100
ORDER BY revenue DESC
LIMIT 5

Что происходит по шагам:

FROM + JOIN. Таблица orders соединяется с products по product_id. Получается промежуточная таблица со всеми столбцами обеих таблиц.

WHERE. Из промежуточной таблицы убираются все строки, где order_date за пределами 2025 года. Остаются только заказы за нужный период.

GROUP BY. Оставшиеся строки группируются по p.category. Если в products 20 категорий и все они встречаются в заказах 2025 года — получается 20 групп.

HAVING. Отбрасываются категории, у которых 100 или меньше заказов. Допустим, осталось 12 категорий.

SELECT. Для каждой из 12 оставшихся групп вычисляются COUNT(*), SUM(amount), AVG(amount). Присваиваются алиасы order_count, revenue, avg_check.

ORDER BY. 12 строк сортируются по revenue от большего к меньшему.

LIMIT. Берутся первые 5 строк — это и есть финальный результат.

Практические следствия, которые проверяют на собеседованиях

Алиас из SELECT нельзя использовать в WHERE

-- Ошибка:
SELECT amount * 0.2 AS tax
FROM orders
WHERE tax > 1000

-- Правильно:
SELECT amount * 0.2 AS tax
FROM orders
WHERE amount * 0.2 > 1000

WHERE выполняется до SELECT, поэтому алиас tax ещё не существует. Приходится дублировать выражение.

Алиас из SELECT можно использовать в ORDER BY

SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
ORDER BY cnt DESC

ORDER BY выполняется после SELECT — алиас cnt уже определён. Это работает в любой СУБД.

Оконные функции нельзя использовать в WHERE и HAVING

-- Ошибка:
SELECT user_id, ROW_NUMBER() OVER (ORDER BY created_at) AS rn
FROM users
WHERE rn = 1

-- Правильно — через подзапрос или CTE:
WITH numbered AS (
    SELECT user_id, ROW_NUMBER() OVER (ORDER BY created_at) AS rn
    FROM users
)
SELECT user_id
FROM numbered
WHERE rn = 1

Оконные функции вычисляются на этапе SELECT, а WHERE выполняется раньше. Чтобы отфильтровать по результату оконной функции, нужен подзапрос.

Агрегат в WHERE — ошибка, в HAVING — нет

-- Ошибка:
SELECT user_id, COUNT(*)
FROM orders
WHERE COUNT(*) > 3
GROUP BY user_id

-- Правильно:
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 3

Группировка ещё не произошла на этапе WHERE, поэтому COUNT(*) вызвать нельзя. HAVING работает после GROUP BY — там агрегатная функция допустима.

Что спрашивают на собеседованиях

  1. В каком порядке SQL выполняет запрос? — FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. Не путайте с порядком написания.

  2. Почему нельзя использовать алиас из SELECT в WHERE? — Потому что WHERE выполняется до SELECT. Алиас ещё не определён.

  3. Почему в ORDER BY можно использовать алиас, а в WHERE нельзя? — ORDER BY выполняется после SELECT, а WHERE — до.

  4. На каком этапе выполняются оконные функции? — На этапе SELECT, после GROUP BY и HAVING, но до ORDER BY. Именно поэтому оконные функции видят сгруппированные строки, но для фильтрации по их результату нужен подзапрос.

  5. Чем отличается WHERE от HAVING? — WHERE фильтрует строки до группировки. HAVING фильтрует группы после группировки. WHERE не может содержать агрегатные функции, HAVING — может.

  6. Запрос возвращает ошибку: column "name" must appear in the GROUP BY clause. Почему? — После GROUP BY в SELECT можно использовать только столбцы из GROUP BY и агрегатные функции. Столбец name не входит в GROUP BY — нужно либо добавить его туда, либо обернуть в агрегат (например, MAX(name)).

Как запомнить

Мнемоника: Фрукты Вырастают — Группами Храним, Сортируем Ограниченно. Первые буквы: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. Или просто запомните, что база сначала собирает данные (FROM), потом фильтрует (WHERE), потом группирует (GROUP BY, HAVING), потом вычисляет (SELECT), потом оформляет результат (ORDER BY, LIMIT). Логика идёт от сырых данных к готовому отчёту.

Как тренироваться

Порядок выполнения запроса — это не теория ради теории. Он объясняет, почему одни конструкции работают, а другие нет. Если вы его усвоили, GROUP BY и HAVING, оконные функции и подзапросы становятся логичными, а не набором правил для заучивания.

Тренажёр Карьерник содержит вопросы на порядок выполнения, алиасы, агрегатные функции и оконные функции — с разборами. Можно тренироваться по 15 минут в день в Telegram, и за неделю эти темы перестанут вызывать затруднения.

Больше вопросов по SQL — в разделе подготовки к собеседованию. Если готовитесь комплексно, посмотрите примеры вопросов по всем темам.

FAQ

В каком порядке SQL выполняет запрос?

FROM (и JOIN), WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT. Это отличается от порядка написания, где первым идёт SELECT. Понимание этого порядка объясняет, почему одни конструкции работают, а другие вызывают ошибку.

Почему алиас из SELECT нельзя использовать в WHERE?

Потому что WHERE выполняется до SELECT — на момент фильтрации алиас ещё не определён. Приходится дублировать выражение. А в ORDER BY алиас работает, потому что ORDER BY выполняется после SELECT.

На каком этапе выполняются оконные функции в SQL?

Оконные функции вычисляются на этапе SELECT, после GROUP BY и HAVING, но до ORDER BY. Поэтому для фильтрации по результату оконной функции нужен подзапрос или CTE — в WHERE и HAVING их использовать нельзя.

Чем отличается WHERE от HAVING в SQL?

WHERE фильтрует строки до группировки и не может содержать агрегатные функции. HAVING фильтрует группы после GROUP BY и может использовать COUNT, SUM и другие агрегаты. Если нужно отфильтровать по результату агрегации — это HAVING.