Мы прошли четыре операции реляционной алгебры — σ, π, ⨯/⋈, ∪/∩/\\ — и увидели, как SQL прячет их за WHERE, SELECT, JOIN, UNION. Время сложить всё вместе.
В этом уроке возьмём один реалистичный аналитический запрос и разложим его на цепочку алгебраических операций. Эта привычка — «читать SQL как алгебру» — главное умение, которое отличает уверенного middle от человека, который пишет запросы методом «копи-паст-подбора».
Задача
Найди уникальные пары (email, country) клиентов из России или Германии, у которых был хотя бы один заказ в статусе paid.
Прежде чем смотреть SQL — попробуй сформулировать словами, какие операции нам понадобятся. Пауза. Подумай.
…
Вот моё разложение:
- σ на customers: отобрать только тех, у кого
country IN ('RU', 'DE'). - σ на orders: отобрать только заказы со статусом
paid. - ⋈ (equi-join) этих двух выборок по
customers.id = orders.customer_id. - π на результате: оставить только
emailиcountry. - DISTINCT — превратить bag-проекцию в множество.
В виде формулы:
В виде SQL:
Тот же самый запрос. Глянь результат и сверь со своим ответом:
Дерево алгебры
Читается снизу вверх: листья — таблицы, корень — результат.
Заметь, что обе селекции в моём разложении применяются ДО соединения. Это и есть push-down — каждый из фильтров не зависит от данных другой таблицы, значит, его можно «протолкнуть» как можно ниже. После такого разложения в JOIN идут уже меньшие выборки (~9 customers и ~3-4 orders вместо 12 и 20), и комбинаций становится в разы меньше.
Если бы я записал разложение как σ[...AND...] (customers ⋈ orders) — это тоже корректно алгебраически, но менее эффективно: пришлось бы джойнить полные таблицы и фильтровать только потом. Оптимизатор PostgreSQL в любом случае сам сделает push-down — но привычка «думать пушдауном» помогает писать запросы, в которых оптимизатору ничего не нужно править.
Тренировка
Перепиши тот же запрос через UNION двух выборок (одна для RU, одна для DE), без IN:
Тот же результат! Алгебраически: σ[country='RU'](...) ∪ σ[country='DE'](...) ≡ σ[country IN ('RU','DE')](...). Это — частный случай алгебраического тождества: дизъюнкция предикатов разворачивается в объединение селекций.
Оптимизатор PostgreSQL знает эти тождества. Если ты напишешь через UNION, он может (но не обязан) переписать в IN — и наоборот. Оба плана работают одинаково на маленьких данных; на больших — обычно IN чуть лучше, потому что не требует дедупликации UNION’ом.
Что нужно унести из модуля 2
После шести уроков ты:
- Знаешь, что реляция — это множество кортежей, и понимаешь следствия: NULL-семантика, отсутствие гарантированного порядка, разница между set и bag.
- Узнаёшь в SQL четыре основные операции — σ (WHERE), π (SELECT), ⋈ (JOIN ON), ∪/∩/\\ (UNION/INTERSECT/EXCEPT).
- Понимаешь, что SQL декларативный, а СУБД переводит твой запрос в дерево алгебры и оптимизирует через перестановки операций (push-down, reorder JOIN).
Это база, к которой мы будем возвращаться каждый раз, когда увидим новый SQL-конструкт. JOIN’ы из модуля 5, агрегации из модуля 6, оконные функции из модуля 8 — всё это надстройка над теми же операциями. Понимая фундамент, ты не будешь учить SQL «по рецептам» — ты будешь видеть, что любой запрос — это просто комбинация шести базовых кирпичей.
В следующем модуле углубимся в одну специфическую тему, которая красной нитью идёт через весь модуль 2 — типы данных и NULL-семантика. Разберём, почему PostgreSQL хранит числа в одной из шести разных представлений, почему '1' = 1 иногда работает, а иногда нет, и почему IS DISTINCT FROM — твой друг при работе с NULL.