Раздел 42 · Аналитика данных с нуля

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

junior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Мира · маркетолог магазина настолок «Кубик»

Я сама собрала отчёт «выручка по службам доставки», JOIN-ы как ты учил. Вышло 23,2 млн. А финансисты говорят, что выручка без отмен 21,9 млн. Откуда у меня лишние деньги?

В прошлом уроке мы заметили, что клиент с пятью заказами превращается после JOIN в пять строк. Пока мы считали заказы, это было нормально. Сегодня разбираемся, что происходит, когда после такого соединения что-то суммируют.

Ксерокс в бухгалтерии

Представь, что бухгалтер складывает чеки за месяц. К каждому чеку степлером прикреплены квитанции курьера. Обычно одна, но если курьер не застал клиента и приезжал дважды, квитанций две. Бухгалтер решил, что так неудобно, и сделал ксерокопию каждого чека для каждой квитанции. Теперь чек с двумя доставками лежит в стопке дважды, и сумма месяца выросла, хотя денег не прибавилось.

Это размножение строк, по-английски fan-out. JOIN делает строку на каждую пару, и если у заказа две доставки, все его позиции повторятся дважды.

Ловим на одном заказе

Начнём с подозреваемого. Найдём заказы, у которых в deliveries больше одной строки:

SELECT order_id, count(*) AS attempts
FROM deliveries
GROUP BY order_id
HAVING count(*) > 1
ORDER BY order_id
LIMIT 3;

Таких заказов 418, у каждого две попытки: первая failed, вторая delivered (у четырёх она ещё in_transit). Возьмём первый из них, заказ 38, и посмотрим, во что он превращается после соединения:

SELECT o.id, oi.product_id, oi.price, d.attempt, d.status
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN deliveries d   ON d.order_id  = o.id
WHERE o.id = 38
ORDER BY oi.product_id, d.attempt;
 id | product_id | price   | attempt | status
----+------------+---------+---------+-----------
 38 |          2 | 2440.00 |       1 | failed
 38 |          2 | 2440.00 |       2 | delivered
 38 |          5 | 1270.00 |       1 | failed
 38 |          5 | 1270.00 |       2 | delivered

В заказе две игры на 3710 ₽. После соединения с доставками каждая игра встретилась с каждой попыткой: 2 × 2 = 4 строки. Сумма price по ним 7420 ₽, ровно вдвое больше. Вот и вся загадка.

Сколько лишнего и где

У 418 заказов с повторной доставкой 678 позиций на 1 403 509 ₽. Столько отчёт Миры и посчитал дважды. А ещё 74 283 ₽ потерялись: у 12 новых заказов доставки пока нет, и INNER JOIN их выбросил. Две ошибки разного знака, и итог всё равно не сходится с финансистами.

Общее правило: если таблица в JOIN связана с твоей строкой как один-ко-многим, строки умножаются. Безопасны соединения «многие-к-одному»: заказ с клиентом, позиция с игрой. У заказа один клиент, лишних строк не появится. Опасны соединения в сторону «многих»: заказ с позициями, с доставками, с возвратами.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.