TGViewer
SQL Ready | Базы Данных SQL Ready | Базы Данных @sql_ready · 17.5K subscribers
Post #1909 1.85K
Почему после JOIN внезапно растут агрегаты!

Одна из самых неприятных ошибок в аналитическом SQL — когда запрос выглядит нормально, всё отрабатывает без ошибок, а цифры в итоге получаются больше, чем должны быть.

Классическая ситуация. Есть таблицы:
orders(id, customer_id, amount)
order_items(id, order_id, product_id, quantity)


Допустим, хотим посчитать сумму заказов. На первый взгляд кажется, что такой запрос окей:
SELECT SUM(o.amount) AS total_revenue
FROM orders o
JOIN order_items i
ON i.order_id = o.id;


Но тут и начинается подвох.

Если у одного заказа 3 позиции в order_items, то строка из orders после JOIN повторится 3 раза. И o.amount тоже попадёт в расчёт 3 раза. В итоге сумма завышается. Это как раз тот самый fan-out: одна строка размножается после JOIN.

Быстрая проверка, есть ли проблема:
SELECT
COUNT(*) AS rows_after_join,
COUNT(DISTINCT o.id) AS unique_orders
FROM orders o
JOIN order_items i
ON i.order_id = o.id;


Если строк после JOIN стало больше, чем уникальных заказов, значит у вас fan-out по уровню orders. Что делать правильно — зависит от задачи.

Если вам нужна просто сумма по заказам, то JOIN вообще не нужен:
SELECT SUM(amount)
FROM orders;


Если JOIN нужен только для фильтрации, например по конкретному товару, безопаснее использовать EXISTS:
SELECT SUM(o.amount)
FROM orders o
WHERE EXISTS (
SELECT 1
FROM order_items i
WHERE i.order_id = o.id
AND i.product_id = 10
);


Почему это хорошо: гранулярность orders не ломается. Один заказ остаётся одной строкой.

Ещё нормальный вариант — сначала убрать дубли на стороне order_items:
SELECT SUM(o.amount)
FROM orders o
JOIN (
SELECT DISTINCT order_id
FROM order_items
WHERE product_id = 10
) i ON i.order_id = o.id;


Тут мы заранее приводим данные к уровню одна строка = один заказ, и только потом джойним.

А если задача вообще на уровне позиций, например нужно посчитать общее количество товаров, тогда считать надо уже по order_items:
SELECT SUM(i.quantity)
FROM order_items i;


То есть важный момент очень простой: агрегировать нужно на том уровне, где реально живёт ваша метрика.

Отдельно про популярный костыль:
SELECT SUM(DISTINCT o.amount)
FROM orders o
JOIN order_items i
ON i.order_id = o.id;


С виду кажется, что DISTINCT сейчас всё починит, на деле — нет. Почему это плохая идея: если у двух разных заказов одинаковый amount, один из них просто схлопнется; запрос начинает давать вроде бы правдоподобный, но неверный результат.

Ещё неприятнее, когда JOIN не один, а цепочка: orders — order_items — products — categories

Практический способ быстро это поймать — смотреть количество строк после каждого шага:
SELECT COUNT(*) FROM orders;

SELECT COUNT(*)
FROM orders
JOIN order_items ON order_items.order_id = orders.id;

SELECT COUNT(*)
FROM orders
JOIN order_items ON order_items.order_id = orders.id
JOIN products ON products.id = order_items.product_id;


Так обычно сразу видно, на каком JOIN начинаются лишние строки.

🔥 Итог простой: перед тем как писать JOIN, всегда держите в голове гранулярность данных. Что у вас является одной строкой: заказ, позиция заказа, клиент? Сначала определяете уровень данных — потом джойните и агрегируете.

➡️ SQL Ready | #практика
  • ❤ 13
  • 🔥 11
  • 👍 8
  • 🤝 2
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 →