Дубликаты при джойнах
Почему после JOIN строк становится больше, чем ожидалось, и как с этим не сесть в лужу.
Если ключ соединения не уникален в одной из таблиц, JOIN размножает строки — это частая причина «раздутых» сумм и счётчиков.
Откуда берутся лишние строки
JOIN соединяет каждую подходящую пару. Если у одного клиента несколько заказов, его строка из customers повторится столько раз, сколько у него заказов. Само по себе это нормально — но опасно при агрегации.
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers (id, name) VALUES (1,'Аня'), (2,'Борис');
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);
INSERT INTO orders (customer_id, amount) VALUES (1,500), (1,300), (2,900);
SELECT c.name, o.amount
FROM customers c
JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.amount;
Вывод:
Аня|300 Аня|500 Борис|900
Аня появилась дважды — по числу её заказов. Это ожидаемо. Проблема начинается, когда к такому джойну добавляют второй.
Двойной JOIN раздувает суммы
Соединим клиента и с заказами, и с обращениями в поддержку. У Ани 2 заказа и 2 обращения. JOIN перемножит их: 2×2 = 4 строки на Аню. Если теперь просуммировать amount, сумма заказов удвоится.
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers (id, name) VALUES (1,'Аня');
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);
INSERT INTO orders (customer_id, amount) VALUES (1,500), (1,300);
CREATE TABLE tickets (id INTEGER PRIMARY KEY, customer_id INTEGER);
INSERT INTO tickets (customer_id) VALUES (1), (1);
-- Сумма заказов Ани на самом деле 800. Но из-за двойного джойна...
SELECT c.name, SUM(o.amount) AS summa_zakazov
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN tickets t ON t.customer_id = c.id
GROUP BY c.name;
Вывод:
Аня|1600
Получилось 1600 вместо честных 800! Каждый заказ посчитан дважды, потому что джойн с tickets удвоил строки. Это очень частый баг в реальных отчётах.
Как чинить
Правильно — агрегировать каждую таблицу отдельно (подзапросом или CTE), а потом соединять уже свёрнутые результаты:
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers (id, name) VALUES (1,'Аня');
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);
INSERT INTO orders (customer_id, amount) VALUES (1,500), (1,300);
CREATE TABLE tickets (id INTEGER PRIMARY KEY, customer_id INTEGER);
INSERT INTO tickets (customer_id) VALUES (1), (1);
SELECT c.name,
(SELECT SUM(amount) FROM orders o WHERE o.customer_id = c.id) AS summa_zakazov,
(SELECT COUNT(*) FROM tickets t WHERE t.customer_id = c.id) AS chislo_obrasheniy
FROM customers c
GROUP BY c.name;
Вывод:
Аня|800|2
Теперь сумма честная — 800, и число обращений 2. Каждый агрегат считается по своей таблице независимо, перемножения строк нет.
Итог
- JOIN по неуникальному ключу размножает строки — это причина «раздутых»
SUM/COUNT. - Двойной JOIN перемножает количества строк связанных таблиц.
- Агрегируйте каждую таблицу отдельно (подзапрос/CTE), затем соединяйте свёрнутое.