Дубликаты при джойнах

Почему после 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), затем соединяйте свёрнутое.
Проверьте себя
1. Почему после JOIN строк становится больше?
AJOIN всегда удваивает строки
BСоединение по неуникальному ключу даёт несколько пар на строку
CЭто баг СУБД
DИз-за ORDER BY
2. Что произойдёт с SUM(amount) при двойном JOIN с размножением строк?
AСумма не изменится
BСумма раздуется — значения посчитаются по несколько раз
CВозникнет ошибка
DВернётся NULL
3. Как корректно посчитать агрегаты по нескольким связанным таблицам?
AДобавить DISTINCT в SUM
BАгрегировать каждую таблицу отдельно (подзапрос/CTE), потом соединять
CИспользовать CROSS JOIN
DУбрать GROUP BY
Поддержать проект