В 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 не интерпретирует запрос построчно. Внутри происходит четыре этапа:
SQL → дерево алгебры → оптимизированное дерево → физический план → результат
Этап 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';
Логически это:
Но это можно записать иначе — сначала JOIN всех клиентов с заказами, потом фильтр:
Эти две версии алгебраически эквивалентны — результат одинаков. Но производительно они радикально разные.
В первом варианте сначала мы из 12 клиентов оставляем 6 российских (σ), потом джойним 6 строк с 20 заказами — получаем максимум 20 строк. Это push-down селекции: фильтр «протолкнули» вниз, ближе к источнику.
Во втором — джойним 12×20 = 240 потенциальных пар (после σ join-предиката — 20), потом фильтруем по country. Лишняя работа на этапе соединения.
Оптимизатор PostgreSQL сам выбирает первый план: он понимает, что предикат c.country = 'RU' не зависит от orders, значит, его можно применить к customers до соединения. Это и есть push-down — фундаментальная техника query optimization.
Самое важное: ты в SQL не пишешь, какой план использовать. Ты говоришь что нужно, СУБД решает как. И именно эта свобода даёт ей возможность выбрать миллион оптимизаций, которые тебе вручную пришлось бы кодировать с нуля каждый раз.
А что, если оптимизатор ошибётся?
Иногда (редко) оптимизатор выбирает плохой план — потому что статистика устарела, или потому что запрос написан с подвохом. Тогда ты можешь:
- Посмотреть, какой план он выбрал — командой
EXPLAIN. Мы детально разберём её в модуле 13. - Переписать запрос так, чтобы намекнуть оптимизатору правильный путь.
- В крайнем случае — обновить статистику (
ANALYZE) или подсказать через настройки.
Но 99% запросов в реальных проектах никогда не доходят до этого этапа: оптимизатор работает хорошо. Ключ — писать читаемый, простой SQL и доверять движку.
Посмотри на план запроса — на этой данной СУБД выберет либо seq scan, либо hash join. EXPLAIN покажет, что именно:
Не пугайся, если первое впечатление от вывода — «нечитаемо». В модуле 13 мы научимся читать план как обычный текст. Сейчас обрати внимание только на одно: где-то в выводе есть Filter: (country = 'RU'). PostgreSQL действительно применил селекцию до соединения — push-down сработал.
Аналогия: рецепт vs готовка
Декларативный SQL похож на заказ в ресторане:
«Принесите мне борщ по-украински, без сметаны».
Императивный код — это пошаговый рецепт:
«Возьмите свеклу, помойте, очистите, нарежьте соломкой, обжарьте 5 минут на среднем огне с томатной пастой…»
В первом случае повар сам решает, нарезать соломкой или кубиками, на масле или на сале — он профессионал, у него знаний больше, чем у тебя. Во втором — ты диктуешь шаги, и если на кухне есть лучший способ — он не будет использован.
SQL — это заказ. СУБД — это повар с двадцатилетним опытом и доступом к свежей статистике. Не пиши императивно, пиши декларативно.
Чек-лист
- SQL — декларативный язык. Ты пишешь «что», СУБД решает «как».
- Запрос проходит четыре этапа: парсинг → переписывание в алгебру → оптимизация → исполнение.
- Алгебраическая эквивалентность позволяет оптимизатору переставлять операции (push-down σ, reorder JOIN) без изменения результата.
EXPLAINпоказывает выбранный план. Подробно — в модуле 13.- Пиши простой SQL. Не пытайся «помочь» оптимизатору, пока он сам не справляется.