TGViewer
SQL Ready | Базы Данных SQL Ready | Базы Данных @sql_ready · 17.5K subscribers
Post #1933 2.6K
Дубликаты после JOIN — откуда берутся и как контролировать!

Одна из частых проблем — внезапное размножение строк после JOIN. Это базовое поведение, если не учтена кардинальность связей.

Таблицы:
orders(id, customer_id, amount)
payments(id, order_id, status)


Задача: получить заказы с информацией об оплате.
SELECT 
o.id,
o.amount,
p.status
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;


Если у одного заказа несколько платежей — в результат попадёт несколько строк. Фактически вы получаете по одной строке на каждое совпадение orders — payments. То есть один заказ повторится столько раз, сколько у него записей в payments.

Для связи 1:N это абсолютно ожидаемо. Где начинаются проблемы — агрегация:
SELECT 
COUNT(*) AS total_orders
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;


Здесь COUNT(*) считает строки уже после JOIN, а не заказы. Если у заказа 3 платежа — он попадёт в счёт 3 раза. Это одна из самых частых причин кривых метрик.

Корректный вариант:
SELECT 
COUNT(DISTINCT o.id) AS total_orders
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;


Так считаются уникальные заказы, независимо от числа платежей. Но важно помнить, что DISTINCT — это дополнительная операция, и на больших объёмах она может стоить дорого.

И отдельный момент, если вам нужно просто количество заказов без условий по payments, JOIN здесь вообще лишний. Лучше контролировать кардинальность до JOIN

Если задача — проверить наличие успешной оплаты, проще и дешевле использовать EXISTS:
SELECT 
o.id,
o.amount
FROM orders o
WHERE EXISTS (
SELECT 1
FROM payments p
WHERE p.order_id = o.id
AND p.status = 'success'
);


EXISTS работает как semi-join: он проверяет факт наличия строки, но не тянет её в результат. За счёт этого одна строка заказа остаётся одной строкой.

Если JOIN всё-таки нужен — агрегируем заранее:
SELECT 
o.id,
o.amount,
COALESCE(p.has_success, 0) AS has_success
FROM orders o
LEFT JOIN (
SELECT
order_id,
MAX(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS has_success
FROM payments
GROUP BY order_id
) p ON p.order_id = o.id;


Здесь мы сначала приводим payments к одной строке на order_id, и только потом делаем JOIN. После этого результат становится понятным: одна строка на заказ, без раздувания.

Типичная ошибка:
GROUP BY o.id, o.amount, p.status


Это не решит проблему. Такой GROUP BY просто фиксирует текущую детализацию. Если у заказа было несколько статусов — строки никуда не денутся.

🔥 JOIN не создаёт дубликаты сам по себе. Он возвращает строки в соответствии с числом совпадений по условию ON. Если после JOIN строк стало больше — значит реальная связь между таблицами не 1:1, а 1:N или даже N:M.

➡️ SQL Ready | #практика
  • 🔥 17
  • 👍 11
  • 🤝 7
  • ❤ 3
More from @sql_ready
  1. Oct 6, 2026Проверяйте связи с учётом периода! В PostgreSQL 18 связь может учитывать ещё и период его…
  2. Oct 6, 2026⚡️5 фундаментальных курсов по ИБ по цене одного Это предложение для тех, кто готов войти в…
  3. Oct 6, 2026👍 Устройство PostgreSQL — подробная документация на русском языке! Материалы посвящены не…
  4. Oct 5, 2026COUNT(*) в PostgreSQL: MVCC, visibility map и стоимость выполнения! В PostgreSQL точный CO…
  5. Oct 5, 2026Как оплачивать зарубежные сервисы в 2026 году? Можно бегать между посредниками и бояться б…
  6. Oct 5, 2026Проверяйте JSON без преобразования! Если JSON приходит как text, необязательно делать ::js…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →