Одна из частых проблем — внезапное размножение строк после 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 | #практика