Join Fan-Out: How a Correct Query Returns Wrong Revenue
Joining two child tables of the same parent multiplies rows. Learn to state the grain of every table and aggregate before you join.
The bug
An order has one line worth ₹300 and two payment attempts (one failed, one succeeded).
SELECT SUM(l.line_revenue) AS revenue
FROM order_lines l
JOIN payments p ON p.order_id = l.order_id;
-- returns ₹600 — the single line appears once per payment attemptNothing errors. The query is syntactically and relationally "correct". The answer is wrong.
| O101 | line 1 | ₹300 |
| O101 | P101 | FAILED |
| O101 | P102 | SUCCESS |
| line 1 | P101 | ₹300 |
| line 1 | P102 | ₹300 |
Wrong: the single ₹300 line is counted once per payment attempt.
Fix: aggregate payments to one row per order first
line 1 · paid ✓ · ₹300 → SUM = ₹300
Why it happens
order_lines and payments are both children of orders. Joining two one-to-many children on the parent key produces a many-to-many result: every line is paired with every payment of the same order.
Rendering entity relationship diagram
The fix: state the grain, then aggregate to it
WITH paid_orders AS (
SELECT order_id
FROM payments
GROUP BY order_id
HAVING MAX(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) = 1 -- one row per order
)
SELECT SUM(l.line_revenue) AS revenue
FROM order_lines l
JOIN paid_orders po ON po.order_id = l.order_id;Now the right side has exactly one row per order, so lines are not duplicated.
A habit that prevents it
Before writing a join, write one sentence per table: "one row per order line", "one row per payment attempt". If the result of your join has no single-sentence grain, you are about to fan out.
Finished this note?
