Чому SQL JOIN завищує суму замовлень

Практичні поради команди AriZone

У навчальному звіті магазин має два замовлення, але після додавання таблиці товарних позицій загальна сума несподівано зростає. Дані ніхто не змінював, платежів не побільшало, а запит виконується без помилки. Причина може бути в тому, що SQL JOIN повторює рядок замовлення для кожної відповідної позиції. Якщо потім підсумувати суму замовлення, вона врахується кілька разів.

Це не клієнтський кейс і не фінансовий звіт: усі числа нижче умовні. Приклад потрібен, щоб навчитися знаходити помилку рівня деталізації до використання результату в адміністративній панелі. Він не оновлює таблиці, не видаляє записи й не звертається до продуктивної бази. Працюйте в локальному тестовому середовищі PostgreSQL через уже налаштований SQL-клієнт; додаткові пакети для наведених запитів не потрібні.

Домовтеся, що означає один рядок

Почніть не з виправлення SUM, а з визначення одиниці обліку. У таблиці замовлень один рядок зазвичай представляє замовлення, а в таблиці позицій — окрему позицію. Зв’язок одного замовлення з кількома позиціями нормальний. Ненормально очікувати, що після такого з’єднання кожне замовлення й надалі представлене рівно одним рядком. Запишіть очікувану деталізацію результату поруч із запитом.

Сума замовлення й сума товарної позиції — різні поля з різними правилами. Загальна сума може включати доставку, знижку або інші компоненти, тому її не можна без домовленості замінювати сумою позицій. Спочатку уточніть призначення показника: це вартість замовлень, вартість товарів чи фактично отримані платежі. Коректна арифметика не виправляє неправильне визначення показника.

Для перевірки візьмемо два замовлення по 100 умовних одиниць. Перше має дві позиції, друге — одну. Очікувана сума замовлень дорівнює 200. Після прямого з’єднання утворяться три рядки, і механічний SUM дасть 300. Рівність сум двох замовлень обрана навмисно: вона допоможе показати, чому популярне виправлення через DISTINCT теж може бути неправильним.

Відтворіть помилку без справжніх таблиць

Вставте наступний запит у нову вкладку SQL-клієнта, підключеного до тестового PostgreSQL. Робочий каталог не має значення: запит виконується в редакторі SQL, а не в системному терміналі. Імена orders та items тут належать тимчасовим виразам WITH і не читають однойменні фізичні таблиці. VALUES задає лише навчальні рядки всередині поточного запиту; права створення таблиць не потрібні.

WITH orders(id, total) AS (
  VALUES (1, 100), (2, 100)
), items(order_id) AS (
  VALUES (1), (1), (2)
)
SELECT
  (SELECT SUM(o.total)
   FROM orders o JOIN items i ON i.order_id = o.id) AS joined_total,
  (SELECT SUM(total) FROM orders) AS actual_total,
  (SELECT SUM(DISTINCT o.total)
   FROM orders o JOIN items i ON i.order_id = o.id) AS distinct_total;

Очікувані результати: joined_total дорівнює 300, actual_total — 200, distinct_total — 100. Це розрахунок для заданого прикладу, а не вимірювання вашого магазину. Перша колонка повторює суму першого замовлення, а третя залишає лише одне унікальне числове значення. DISTINCT відрізняє значення, а не бізнес-об’єкти: два різні замовлення з однаковою сумою не повинні перетворюватися на одне.

Якщо результати відрізняються, перевірте, чи виконано весь запит і чи не змінено навчальні рядки. Не додавайте обмеження за статусом або датою, поки не відтворили базову помилку. У реальному звіті такі обмеження необхідні, але під час вправи вони можуть приховати неправильне з’єднання. Поведінку JOIN та виразів WITH можна звірити з офіційною документацією PostgreSQL.

Поверніть деталізацію перед з’єднанням

Коли потрібні і сума замовлення, і кількість його позицій, спершу згрупуйте позиції за order_id. Так права сторона з’єднання матиме не більше одного рядка на замовлення. Потім використайте LEFT JOIN, щоб зберегти також замовлення без позицій. У цьому навчальному запиті count означає кількість рядків позицій, а не кількість одиниць товару: поле кількості тут не задано.

WITH orders(id, total) AS (
  VALUES (1, 100), (2, 100)
), items(order_id) AS (
  VALUES (1), (1), (2)
), item_counts AS (
  SELECT order_id, COUNT(*) AS item_rows
  FROM items
  GROUP BY order_id
)
SELECT o.id, o.total, COALESCE(i.item_rows, 0) AS item_rows
FROM orders o
LEFT JOIN item_counts i ON i.order_id = o.id
ORDER BY o.id;

Результат має містити два рядки: для першого замовлення item_rows дорівнює 2, для другого — 1; total залишається 100 у кожному. Підсумок total на цьому рівні вже не множиться на кількість позицій. COALESCE потрібний для випадку без відповідної групи: тоді кількість показується як нуль. Не переносіть це правило автоматично на гроші, де NULL може означати невідоме значення.

Якщо позиції потрібні лише як умова відбору, розгляньте EXISTS замість з’єднання з усіма рядками. Якщо дані позицій узагалі не використовуються, зайвий JOIN краще прибрати після перевірки вимог. Не вважайте будь-яке з’єднання небезпечним: проблема виникає, коли кількість відповідностей не узгоджена з показником. Окремо перевіряйте кожен наступний зв’язок, зокрема платежі, доставку або журнал статусів.

Закріпіть перевірку перед зміною звіту

Додайте до тестового набору замовлення без позицій, замовлення з багатьма позиціями та два замовлення з однаковими сумами. Перевірте кількість замовлень і підсумок до та після кожного JOIN. Не порівнюйте лише загальний підсумок: випадкове завищення й пропущене замовлення іноді компенсують одне одного. Для кількох конкретних ідентифікаторів перегляньте також рядки, що утворилися після з’єднання.

Перед перенесенням у робочий звіт збережіть попередній запит у Git і перевірте зміни. Навіть запит лише читання може бути дорогим на великій базі, тому спочатку використайте тестову копію та обмежений період. Якщо змінюєте визначення показника, явно позначте це користувачам звіту. Для відкату поверніть попередню версію запиту, не змінюючи й не очищаючи історичні замовлення.

Висновок: правильний SQL-запит повинен зберігати зміст показника, а не просто повертати правдоподібне число. Визначена деталізація, контроль кількості відповідностей і невеликий набір навчальних тестів допомагають знайти множення сум. Якщо адміністративний звіт показує суперечливі результати, AriZone може допомогти перевірити логіку запиту й підготувати безпечне виправлення на тестовій копії.

Обговорення

Залиште коментар

Будьте першими, хто прокоментує

Ваш email не буде опубліковано. Обов’язкові поля позначені *.

Захист від спаму Cloudflare Turnstile