0Pricing
SQL Interview Prep · Урок

Размножение строк при JOIN

Узнайте, почему JOIN может вернуть больше строк, чем любая из таблиц, и как это проверяют на собеседовании.

«Размножение строк при JOIN» — бесплатный урок SQL Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения SQL Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс SQL 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) и разблокировать остальной курс SQL Interview Prep, подпишись на CoddyKit PRO. Курс SQL Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Размножение строк при JOIN»?

Узнайте, почему JOIN может вернуть больше строк, чем любая из таблиц, и как это проверяют на собеседовании. Ты практикуешь SQL Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать SQL Interview Prep?

Предыдущий опыт не требуется. SQL Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Размножение строк при JOIN»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке SQL Interview Prep?

Да. Каждый урок SQL Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Как INNER JOIN сопоставляет строки
  2. ON и WHERE в объединениях
  3. Размножение строк при JOIN
  4. Объединение трёх и более таблиц
← Назад к SQL Interview Prep