Дайджесты новостей
Логический конвейер выполнения SQL-запроса от фильтрации FROM и WHERE до финального SELECT

Порядок выполнения SQL: почему синтаксис обманчив и как это использовать для оптимизации

В большинстве языков программирования код исполняется ровно так, как написан: сверху вниз, строка за строкой. Однако в SQL все иначе. Это декларативный язык: разработчик формулирует, что он хочет получить, а движок СУБД решает, как это извлечь. Из-за этого порядок написания команд кардинально расходится с внутренним логическим конвейером обработки данных (Logical Query Processing).

Хотя любой запрос на чтение начинается со слова SELECT, движок базы данных (PostgreSQL, MySQL, Oracle) начинает вычисления с другого конца:

  1. FROM и JOIN: сбор целевых таблиц, вычисление декартова произведения и применение условий ON.
  2. WHERE: построчная фильтрация строк до любых группировок.
  3. GROUP BY: разделение строк на группы по указанным ключам.
  4. HAVING: фильтрация сформированных групп по агрегатным условиям (COUNT, SUM).
  5. SELECT: расчет скалярных выражений и присвоение псевдонимов (алиасов).
  6. DISTINCT: удаление дублирующихся строк из проекции.
  7. ORDER BY: финальная сортировка (здесь алиасы из SELECT уже доступны).
  8. LIMIT и OFFSET: отсечение заданного количества строк.

Почему алиасы не работают в фильтрах и где прячется цена GROUP BY

Осознание этого конвейера объясняет типичные ошибки. Классический пример: попытка написать WHERE total_amount > 1000, где total_amount — псевдоним из блока SELECT. СУБД выдает ошибку «колонка не существует», ведь на этапе WHERE (шаг 2) блок SELECT (шаг 5) еще не вычислялся.

Та же логика определяет разницу между WHERE и HAVING. Фильтрация в WHERE отсекает записи в самом начале, сокращая объем данных в памяти. Если же простые условия перенести в HAVING, СУБД потратит процессорное время на группировку миллионов строк, чтобы затем их отбросить.

Избавляемся от повторных вычислений с помощью CTE

Когда в фильтрации нужно вычисляемое поле, надежный подход — вынести логику в обобщенное табличное выражение (CTE) с ранним отсечением:

-- ОШИБКА: алиас total_price недоступен в WHERE на шаге 2
-- SELECT id, quantity * price AS total_price FROM orders WHERE total_price > 1000;

-- ЭФФЕКТИВНО: раннее отсечение статуса и декомпозиция через CTE
WITH filtered_orders AS (
    SELECT
        id,
        user_id,
        quantity * price AS total_price,
        created_at
    FROM orders
    WHERE status = 'completed' -- Шаг 2: отбрасываем лишнее до группировки
)
SELECT
    user_id,
    SUM(total_price) AS user_spending
FROM filtered_orders
WHERE total_price > 1000
GROUP BY user_id
HAVING COUNT(id) >= 2
ORDER BY user_spending DESC
LIMIT 10;

Ловушка функций в индексах и маскировка ошибок через DISTINCT

Вторая частая беда — нарушение принципа SARGability (Search Argument Able). Когда столбец оборачивают в функцию (например, WHERE EXTRACT(YEAR FROM created_at) = 2026), база не может использовать B-Tree индекс и запускает полный перебор таблицы (Sequential Scan). Предикат нужно формулировать как диапазон дат для индексного сканирования.

Не менее опасен бездумный DISTINCT. Часто разработчик замечает дубликаты после некорректного JOIN и гасит их с помощью DISTINCT. Но это маскирует ошибку связывания и заставляет базу делать тяжелую сортировку в памяти. Если нужно лишь проверить наличие связанных записей, полусоединение через EXISTS работает намного быстрее:

-- НЕЭФФЕКТИВНО: функция ломает индекс, а DISTINCT маскирует дубликаты
SELECT DISTINCT u.id, u.email
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE EXTRACT(YEAR FROM o.created_at) = 2026;

-- ОПТИМИЗИРОВАНО: полусоединение EXISTS без сортировки и диапазон дат для B-Tree
SELECT u.id, u.email
FROM users u
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.user_id = u.id
      AND o.created_at >= '2026-01-01 00:00:00'
      AND o.created_at < '2027-01-01 00:00:00'
);

Главное правило: доверяй оптимизатору, но проверяй EXPLAIN

Логический конвейер описывает стандарт языка SQL. На практике оптимизатор стоимости (Cost-Based Optimizer) в PostgreSQL может менять физические операции местами — например, проталкивать предикаты внутрь сканирования индексов, если результат гарантированно совпадает.

Поэтому сверяйте гипотезы с реальностью через команду EXPLAIN (ANALYZE, BUFFERS). Понимание логического порядка помогает отсекать узкие места еще при написании запроса, сохраняя ресурсы базы данных под высокой нагрузкой.