Размножение строк при JOIN
Узнайте, почему JOIN может вернуть больше строк, чем любая из таблиц, и как это проверяют на собеседовании.
«Размножение строк при JOIN» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Когда соединение возвращает слишком много строк
Один из самых показательных вопросов на собеседовании звучит безобидно: «может ли соединение вернуть больше строк, чем таблица с большим количеством строк?» Ответ — да; это явление называют размножением строк.
Кандидаты, которые говорят «соединение просто объединяет таблицы», упускают суть. Кандидаты, способные предсказать точное количество строк, получают работу. Этот урок развивает такой навык прогнозирования.
Причина: соответствия «один ко многим»
Размножение строк происходит, когда одной строке слева соответствуют многие строки справа. Каждое совпадение создаёт отдельную выходную строку.
В таблицах клиентов и заказов у Ады (одного клиента) два заказа. Соединение выдаёт по одной строке на заказ, поэтому Ада появляется дважды. Поля клиента повторяются, а отличаются только поля заказа.
SELECT c.name, o.amount
FROM customers c
JOIN orders o ON o.customer_id = c.id;
-- Ada appears twice (she has 2 orders)
-- name | amount
-- Ada | 50
-- Ada | 20
-- Bob | 99Подсчёт выходных строк
Количество выходных строк равно сумме совпадений для каждой строки левой таблицы, а не количеству клиентов.
- Ада -> 2 заказа -> 2 строки
- Боб -> 1 заказ -> 1 строка
- Клео -> 0 заказов -> 0 строк (отбрасывается INNER JOIN)
Итого = 3 строки, хотя в таблице клиентов тоже 3 строки. Если у Ады будет 10 заказов, результат вырастет до 11 строк.
Связь «многие ко многим» приводит к взрывному росту
Размножение строк усиливается, когда обе стороны имеют несколько совпадений для одного ключа. Если ключ K встречается 3 раза слева и 4 раза справа, соединение создаёт для этого ключа 3 x 4 = 12 строк.
Так небольшое на вид соединение разрастается до миллионов строк. Интервьюеры любят давать повторяющиеся ключи с обеих сторон, чтобы проверить, заметите ли Вы умножение.
-- left has 3 rows with tag 'A', right has 4 rows with tag 'A'
SELECT l.id, r.id
FROM left_t l
JOIN right_t r ON r.tag = l.tag;
-- tag 'A' alone yields 3 * 4 = 12 output rowsЛовушка агрегации
Вот ошибку, которую интервьюеры подстраивают чаще всего. Вы соединяете заказы с позициями заказа, чтобы получить сведения о позициях, а затем применяете SUM к сумме заказа. Поскольку каждый заказ размножается в несколько строк позиций, сумма заказа учитывается по одному разу для каждой позиции.
Теперь результат SUM сильно завышен. Запрос выглядит правильным и даже выполняется, что и делает эту ошибку опасной.
-- BUG: order.amount duplicated across items
SELECT SUM(o.amount) AS total
FROM orders o
JOIN order_items i ON i.order_id = o.id;
-- a 3-item order counts o.amount 3 timesКак увидеть завышение
Предположим, один заказ имеет сумму 100 и три позиции. Соединение создаёт три строки, в каждой из которых указана сумма 100. SUM(o.amount) возвращает 300, а не 100.
Исправление — выполнять агрегацию на правильном уровне детализации: суммировать позиции или отдельно суммировать заказы без повторений. Никогда не применяйте SUM к значению родительской записи по дочернему соединению с размножением строк.
o.id | o.amount | i.id
7 | 100 | 71
7 | 100 | 72
7 | 100 | 73
-- SUM(o.amount) = 300 (WRONG, should be 100)Исправление 1: сначала агрегируйте дочернюю таблицу
Самое чистое исправление — предварительно агрегировать сторону «многие» в подзапросе или CTE, чтобы для каждого родителя находилась ровно одна итоговая строка. Нет размножения строк — нет завышения результатов.
Здесь мы сворачиваем позиции до одной строки на заказ перед соединением, поэтому сумма родительской записи никогда не дублируется.
SELECT o.id, o.amount, i.item_count
FROM orders o
JOIN (
SELECT order_id, COUNT(*) AS item_count
FROM order_items
GROUP BY order_id
) i ON i.order_id = o.id;Исправление 2: COUNT(DISTINCT) и условные суммы
Если после соединения с размножением строк необходимо выполнить агрегацию, подсчитывайте или суммируйте данные на правильном уровне детализации. Используйте COUNT(DISTINCT o.id), чтобы считать заказы, а не строки позиций.
Замечание: SUM(DISTINCT o.amount) — это НЕ безопасное исправление, поскольку у двух разных заказов вполне может быть одинаковая сумма, и они будут объединены. Предварительная агрегация надёжнее.
SELECT COUNT(DISTINCT o.id) AS num_orders,
COUNT(i.id) AS num_items
FROM orders o
JOIN order_items i ON i.order_id = o.id;Как обнаружить размножение строк до того, как оно создаст проблемы
Быстрая диагностическая проверка, которую любят интервьюеры: убедитесь, что ключ соединения уникален на стороне, которую Вы считаете стороной «один». Если количество уникальных ключей меньше количества строк, на этой стороне есть дубликаты, и она вызовет размножение строк.
-- if this returns rows, order_id is NOT unique in order_items
SELECT order_id, COUNT(*) AS n
FROM order_items
GROUP BY order_id
HAVING COUNT(*) > 1;Проверка уровня детализации с помощью подсчёта
Перед тем как доверять любой агрегатной функции над результатом соединения, проверьте количество строк. Быстрый приём — сравнить количество строк после соединения с количеством строк таблицы, которая, как Вы ожидаете, задаёт уровень детализации.
Если COUNT(*) для соединения больше, чем COUNT(*) для заказов, соединение вызвало размножение строк, и любая агрегация по заказам находится под угрозой. Эта однострочная проверка спасла немало ответов на собеседованиях.
-- joined rows should equal order count if no fan-out
SELECT COUNT(*) AS joined_rows
FROM orders o
JOIN order_items i ON i.order_id = o.id;
SELECT COUNT(*) AS order_rows FROM orders;
-- joined_rows > order_rows => fan-out presentРазмножение строк не всегда является ошибкой
Иногда Вы хотите получить по одной строке на дочернюю запись. Вывод каждой позиции вместе с заголовком её заказа — это правильное размножение строк. Важно понимать целевой уровень детализации: сколько строк должна порождать одна сущность?
Сначала определите уровень детализации, а уже потом пишите запрос. Формулировки «мне нужна одна строка на позицию заказа» и «мне нужна одна строка на заказ» определяют, является ли размножение строк особенностью или ошибкой.
Быстрая проверка
Предскажите результат соединения «один ко многим».
Повторение: размножение строк
Что нужно помнить:
- Соединение выдаёт по одной строке на каждую совпадающую пару, поэтому соответствия «один ко многим» дублируют сторону «один».
- При связи «многие ко многим» ключи перемножаются: для этого ключа 3 x 4 = 12 строк.
- Агрегация значения родительской записи по соединению с размножением строк завышает суммы и количества.
- Исправляйте это, предварительно агрегируя дочернюю таблицу или подсчитывая и суммируя данные на правильном уровне детализации, например с помощью
COUNT(DISTINCT). - Всегда сначала определяйте нужный уровень детализации; размножение строк является ошибкой только тогда, когда нарушает его.
Часто задаваемые вопросы
Урок «Размножение строк при JOIN» бесплатный?
Да — полный текст урока «Размножение строк при JOIN» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Размножение строк при JOIN»?
Узнайте, почему JOIN может вернуть больше строк, чем любая из таблиц, и как это проверяют на собеседовании. Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Размножение строк при JOIN»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Как INNER JOIN сопоставляет строки
- ON и WHERE в объединениях
- Размножение строк при JOIN
- Объединение трёх и более таблиц