open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 02 · 01

JOIN — это отфильтрованное произведение

Любой JOIN начинается как декартово произведение двух таблиц — каждая строка A в паре с каждой строкой B — а предикат ON это просто фильтр на этом произведении. Усвой это, и поведение JOIN перестанет быть магией.

SQL Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Отчётный запрос, прекрасно работавший в 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, применённым как фильтр: каждая левая строка спарена с каждой правой, затем несовпавшие пары отброшены.

Вспомните перед уходом
  1. 01
    Определи JOIN через декартово произведение и предикат.
  2. 02
    В users 3 строки, в orders 4. Сколько строк у CROSS JOIN и что вместо этого вернёт inner join по o.user_id = u.id?
  3. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.