Why a JOIN duplicates rows, and how to fix cardinality
A JOIN matches rows from both sides. When one customer has many orders, a plain JOIN returns one row per order, so the customer appears several times and any per-customer total is wrong.
The quick answer
Decide the grain you want. For a per-customer total, aggregate the many side with SUM and GROUP BY the customer, instead of hiding the duplicates with DISTINCT.
A minimal example
SELECT a.id, a.name, b.title
FROM authors a
JOIN books b ON b.author_id = a.id;SELECT a.id, a.name, COUNT(b.id) AS book_count
FROM authors a
JOIN books b ON b.author_id = a.id
GROUP BY a.id, a.name;The bugged query returns a row for every order, so a customer with two orders appears twice. Grouping by the customer and summing the order total returns one correct row per customer.
How to debug it
- State the grain you expect: one row per customer, per order, or per day.
- Count rows on each side of the JOIN for a known key.
- If the result has more rows than the intended grain, the JOIN is fanning out.
- Aggregate the many side to the intended grain rather than masking rows with DISTINCT.
Common wrong fixes
- Adding DISTINCT, which hides duplicates but still sums nothing and can drop legitimately equal rows.
- Using LIMIT to make the row count look right without fixing the grain.
- Joining on an incomplete key so some rows match by accident.
Limits of this guide
This covers one-to-many fan-out for a per-customer total. Cardinality bugs also appear with many-to-many joins and missing predicates. The challenge focuses on the fan-out case shown here.
Authoritative references
Ready to try it?
Fix this bug in a free challengeRelated pages
The missing await bug in JavaScript async functions
A missing await makes JavaScript read a Promise as if it were the resolved value. See the failure, the one-line fix, and the wrong fixes to avoid.
Why a mutable default argument leaks state in Python
A list used as a Python default argument is created once and shared across calls. See why state leaks between calls and the None-guard fix that stops it.
Guides
Short debugging guides that each explain one common bug, show a minimal fix, and open a matching free challenge. Browse guides by language.