JOIN — это отфильтрованное произведение
Любой JOIN начинается как декартово произведение двух таблиц — каждая строка A в паре с каждой строкой B — а предикат ON это просто фильтр на этом произведении. Усвой это, и поведение JOIN перестанет быть магией.
Отчётный запрос, прекрасно работавший в staging, вернул в проде 4,2 миллиарда строк и держал CPU на пределе девять минут, пока его не убили. Причина — одно пропущенное слово в ON. Автор думал о JOIN как о «сопоставлении строк». Движок думает о нём куда грубее: спарь всё со всем, а потом выбрось то, что не совпало. Стоит увидеть JOIN глазами движка — и тот девятиминутный инцидент становится очевиден заранее. Через десять минут ты будешь знать, что именно движок делает до запуска — и как заметить взрыв строк до того, как он произойдёт.
Декартово произведение — это фундамент
На минуту забудь про «сопоставление». Базовейшая комбинация двух таблиц — это декартово произведение (CROSS JOIN): взять каждую строку users и спарить её с каждой строкой orders. Если в users 3 строки, а в orders 4, то в произведении ровно 3 × 4 = 12 строк. Никакого условия, никакого сопоставления — просто все возможные пары.
-- Сырое произведение: каждый пользователь с каждым заказом
SELECT u.id AS user_id, o.id AS order_id
FROM users u
CROSS JOIN orders o;
-- 3 пользователя × 4 заказа = 12 строк, неважно кто что заказалЧисло строк произведения — |A| × |B|, оно умножается. Этот единственный факт и есть причина, по которой JOIN может взорваться. У двух таблиц по 100k строк произведение — 10 миллиардов строк. Движок никогда не материализует их все, если может избежать, но логический смысл твоего запроса всё равно — «начать с этого произведения». Когда видишь неожиданно большой результат, первый вопрос: где именно счётчик строк умножился?
INNER JOIN — это то же произведение, отфильтрованное
INNER JOIN ... ON <предикат> по определению — это декартово произведение с предикатом, применённым как фильтр. Эти два запроса логически тождественны:
-- В виде JOIN (так ты будешь писать всегда)
SELECT u.id, o.id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;
-- В виде произведение + фильтр (вот что это ЗНАЧИТ)
SELECT u.id, o.id, o.total
FROM users u
CROSS JOIN orders o
WHERE o.user_id = u.id;Оба спрашивают: из всех 12 возможных пар оставить те, где o.user_id = u.id. Клауза ON — не особый синтаксис, а фильтр на произведении. Планировщик никогда не строит буквально сетку из 12 строк (он применяет hash, merge или nested-loop стратегии, чтобы дёшево прийти к тому же ответу — это урок про join-algorithms трека databases), но результат гарантированно равен «произведение, затем фильтр».
Эта переформулировка окупается сразу. Число строк на выходе INNER JOIN — это не |A| и не |B|. Это «сколько пар пережили фильтр», и зависит оно целиком от отношения. Если у каждого заказа ровно один совпадающий пользователь, в результате будет |orders| строк. Если ключ join неуникален с обеих сторон, можно получить больше строк, чем в любой из таблиц — это fan-out, тема последнего урока этого раздела.
Почему поведение CROSS JOIN — это фича, а не только грабли
Инцидент из хука был случайным произведением: кто-то написал JOIN, забыл предикат ON (или написал ON true) и получил |A| × |B|. SQL тебя не останавливает, потому что произведение иногда — ровно то, что нужно: календарная сетка, матрица комбинаций «каждый товар × каждый размер», заполнение пропусков. Четвёртый урок разбирает осознанные cross join. Дисциплина: клауза ON, которая на самом деле не ограничивает отношение, — это молча произведение, а произведение двух больших таблиц — это авария.
▸Почему это работает
Зачем думать «произведение, затем фильтр», если планировщик произведение не строит? Потому что это делает кардинальность предсказуемой, а кардинальность управляет стоимостью. Когда ты можешь ответить «сколько строк выдаст этот JOIN?» до запуска, ты замечаешь fan-out, случайные cross join и пропущенные предикаты на глаз. Вся работа планировщика — избежать материализации произведения, возвращая ответ «произведение-затем-фильтр», но сам ответ определён произведением, поэтому и твоя модель должна начинаться оттуда. Глава execution-plans трека databases показывает, как nested-loop, hash и merge join приходят к этому ответу с разными профилями стоимости.
В users 5 строк, в orders 8 строк. Ты запускаешь SELECT * FROM users CROSS JOIN orders. Сколько строк вернётся?
Что логически эквивалентно: FROM users u JOIN orders o ON o.user_id = u.id ?
Заполни пропуск: INNER JOIN — это декартово _______ двух таблиц с предикатом ON, применённым как фильтр: каждая левая строка спарена с каждой правой, затем несовпавшие пары отброшены.
- 01Определи JOIN через декартово произведение и предикат.
- 02В users 3 строки, в orders 4. Сколько строк у CROSS JOIN и что вместо этого вернёт inner join по o.user_id = u.id?
- 03Почему «произведение, затем фильтр» — правильная модель, хотя Postgres не материализует полное произведение?
Фундамент любого JOIN — декартово произведение (Cartesian product): спарь каждую строку A с каждой строкой B, давая |A| × |B| строк — число, которое умножается, отчего JOIN и может взорваться. CROSS JOIN — это сырое произведение; INNER JOIN ... ON p — это ровно произведение с p как фильтром, тождественно CROSS JOIN ... WHERE p. Из-за этого размер вывода JOIN — это «сколько пар пережили предикат», никогда не просто размер одной из таблиц — поэтому пропущенная или слишком слабая клауза ON молча вырождается в полное произведение и аварию. Планировщик не строит буквальную сетку; он применяет hash, merge или nested-loop стратегии (урок join-algorithms трека databases), чтобы дёшево прийти к тому же ответу «произведение-затем-фильтр». Держи эту модель — и весь остаток раздела (outer join, semi/anti join, LATERAL и ловушки fan-out) сводится к «какие пары оставляем и что делаем со строками без партнёра». Теперь, когда видишь запрос с неожиданно большим результатом, первое, что проверяешь — клаузу ON: пропущенный или слабый предикат молча означает полное произведение.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.