Learning Platform
Урок 03.05 · 13 мин
Начальный
Declarative programmingQuery optimizerAlgebraic equivalenceEXPLAINPush-down

В Python ты пишешь:

result = []
for c in customers:
    if c.country == 'DE':
        result.append(c.email)

Это императивный стиль: ты диктуешь шаги — открой цикл, проверь условие, добавь в результат. Если завтра у тебя миллиард клиентов, и Python вдруг сможет распараллелить эту работу — он не сможет, потому что ты уже зафиксировал, что обход идёт «по одному». Шаги — твоя ответственность.

В SQL та же задача звучит так:

SELECT email FROM customers WHERE country = 'DE';

Это декларативный стиль: ты говоришь «дай email-ы немецких клиентов», и не говоришь как. Это не лень — это контракт. СУБД получает право решать, как именно достать данные: последовательное сканирование, индекс по country, параллельный обход на 16 ядрах, перенос вычисления на другой узел кластера. Твой запрос не изменится — и это его сила.

Что СУБД делает с твоим SQL

Когда ты нажимаешь «Запустить», PostgreSQL не интерпретирует запрос построчно. Внутри происходит четыре этапа:

Жизненный цикл запроса в PostgreSQL

SQL → дерево алгебры → оптимизированное дерево → физический план → результат

1. ПарсингSQL → AST
2. RewriteAST → дерево алгебры
3. Planpush-down σ, reorder JOIN, выбор индексов
4. Executeпо физическому плану

Этап 2 — самый интересный для нашей темы. Парсер превращает твой SELECT ... FROM ... WHERE ... JOIN ... в дерево операций реляционной алгебры: где-то стоит σ, где-то π, где-то ⨯, где-то ∪. Это уже не текст, это формальная структура.

А этап 3 пользуется тем, что многие алгебраические операции коммутативны и ассоциативны — их можно переставлять без изменения результата.

Эквивалентность планов

Возьмём пример. Запрос: «email-ы российских клиентов, у которых есть хотя бы один заказ».

SELECT DISTINCT c.email
FROM customers c
JOIN orders o ON c.id = o.customer_id
WHERE c.country = 'RU';

Логически это:

πemail(σcountry=RU(customers)orders)\pi_{email}(\sigma_{country='RU'}(\text{customers}) \bowtie \text{orders})

Но это можно записать иначе — сначала JOIN всех клиентов с заказами, потом фильтр:

πemail(σcountry=RU(customersorders))\pi_{email}(\sigma_{country='RU'}(\text{customers} \bowtie \text{orders}))

Эти две версии алгебраически эквивалентны — результат одинаков. Но производительно они радикально разные.

В первом варианте сначала мы из 12 клиентов оставляем 6 российских (σ), потом джойним 6 строк с 20 заказами — получаем максимум 20 строк. Это push-down селекции: фильтр «протолкнули» вниз, ближе к источнику.

Во втором — джойним 12×20 = 240 потенциальных пар (после σ join-предиката — 20), потом фильтруем по country. Лишняя работа на этапе соединения.

Оптимизатор PostgreSQL сам выбирает первый план: он понимает, что предикат c.country = 'RU' не зависит от orders, значит, его можно применить к customers до соединения. Это и есть push-down — фундаментальная техника query optimization.

Самое важное: ты в SQL не пишешь, какой план использовать. Ты говоришь что нужно, СУБД решает как. И именно эта свобода даёт ей возможность выбрать миллион оптимизаций, которые тебе вручную пришлось бы кодировать с нуля каждый раз.

А что, если оптимизатор ошибётся?

Иногда (редко) оптимизатор выбирает плохой план — потому что статистика устарела, или потому что запрос написан с подвохом. Тогда ты можешь:

  1. Посмотреть, какой план он выбрал — командой EXPLAIN. Мы детально разберём её в модуле 13.
  2. Переписать запрос так, чтобы намекнуть оптимизатору правильный путь.
  3. В крайнем случае — обновить статистику (ANALYZE) или подсказать через настройки.

Но 99% запросов в реальных проектах никогда не доходят до этого этапа: оптимизатор работает хорошо. Ключ — писать читаемый, простой SQL и доверять движку.

Посмотри на план запроса — на этой данной СУБД выберет либо seq scan, либо hash join. EXPLAIN покажет, что именно:

PostgreSQL

Не пугайся, если первое впечатление от вывода — «нечитаемо». В модуле 13 мы научимся читать план как обычный текст. Сейчас обрати внимание только на одно: где-то в выводе есть Filter: (country = 'RU'). PostgreSQL действительно применил селекцию до соединения — push-down сработал.

Аналогия: рецепт vs готовка

Декларативный SQL похож на заказ в ресторане:

«Принесите мне борщ по-украински, без сметаны».

Императивный код — это пошаговый рецепт:

«Возьмите свеклу, помойте, очистите, нарежьте соломкой, обжарьте 5 минут на среднем огне с томатной пастой…»

В первом случае повар сам решает, нарезать соломкой или кубиками, на масле или на сале — он профессионал, у него знаний больше, чем у тебя. Во втором — ты диктуешь шаги, и если на кухне есть лучший способ — он не будет использован.

SQL — это заказ. СУБД — это повар с двадцатилетним опытом и доступом к свежей статистике. Не пиши императивно, пиши декларативно.

Проверка знанийKnowledge check
Два запроса возвращают один и тот же результат, но используют разный SQL — JOIN ON в одном, IN-подзапрос в другом. Оптимизатор выбирает разный план для них?
ОтветAnswer
Часто — нет. Современный PostgreSQL умеет переписывать многие IN/EXISTS-подзапросы в семантически эквивалентные JOIN'ы (subquery flattening). На этапе оптимизации он работает с деревом алгебры, а не с исходным SQL. Поэтому два разных текста могут дать идентичный план. Но не всегда — есть кейсы, где переписывание не срабатывает (например, коррелированный подзапрос с агрегатом). В сомнительных случаях запусти EXPLAIN на обоих вариантах и сравни.
Pipeline запроса: parser, rewriter, planner, executor Логический vs физический план запроса Logical planning в Trino: Plan IR и PlanNode

Чек-лист

  • SQL — декларативный язык. Ты пишешь «что», СУБД решает «как».
  • Запрос проходит четыре этапа: парсинг → переписывание в алгебру → оптимизация → исполнение.
  • Алгебраическая эквивалентность позволяет оптимизатору переставлять операции (push-down σ, reorder JOIN) без изменения результата.
  • EXPLAIN показывает выбранный план. Подробно — в модуле 13.
  • Пиши простой SQL. Не пытайся «помочь» оптимизатору, пока он сам не справляется.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 3. Что значит «SQL — декларативный язык»?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6