How to Debug a SQL Query COUNT and GROUP BY That Double-Counts

A revenue report shows Electronics at ~$600 when the raw orders add up to ~$300 - a classic SQL query COUNT GROUP BY double-counting bug. The cause is a one-to-many JOIN to product_tags that fans each order row out to one row per tag before the SUM runs. Drop the join and the totals snap back to the real numbers.

Data Engineersqlpostgresqlaggregation

Why the totals are doubled

The finance team's daily revenue report is reading roughly double what the raw order data says. Electronics shows about $600, but spot-checking the orders puts it near $300. The numbers are not random - they are inflated by a consistent multiple, which is the fingerprint of a JOIN fan-out.

The report groups order line items by product category and sums the revenue:

SELECT
    p.category,
    COUNT(oi.id) AS total_items,
    SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN product_tags pt ON pt.product_id = p.id
GROUP BY p.category
ORDER BY total_revenue DESC;

The trap is the last JOIN. product_tags holds many rows per product - a Wireless Mouse is tagged both wireless and peripheral, a USB-C Hub has three tags. Because the JOIN matches on product_id alone, every order_items row is duplicated once per tag before SUM and COUNT ever run. A product with two tags counts its revenue twice; a product with three tags counts it three times.

Confirm the row explosion

Before changing anything, prove where the extra rows come from. Count tags per product:

psql -U postgres -d app -c "SELECT product_id, COUNT(*) AS tag_count FROM product_tags GROUP BY product_id ORDER BY product_id;"

Then compare the row count per category with the tags JOIN versus without it. The correct count comes straight from order_items:

psql -U postgres -d app -c "SELECT p.category, COUNT(*) FROM order_items oi JOIN products p ON p.id=oi.product_id GROUP BY p.category;"

Add the JOIN product_tags pt ON pt.product_id = p.id and the count balloons - that gap is exactly the duplication the SUM is charging you for.

The fix - drop the join the report never needed

The report groups by category and sums quantity * unit_price. Nothing in the SELECT, GROUP BY, or SUM references a tag. The product_tags JOIN adds no column and no value - it only multiplies rows. Remove it:

SELECT
    p.category,
    COUNT(oi.id) AS total_items,
    SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY p.category
ORDER BY total_revenue DESC;

Rerun the script and the totals match the real orders:

python3 report.py

The grand total lands at $609.88 instead of the inflated figure, and each category now reflects the actual line-item revenue.

When you actually need the tagged table

Sometimes you genuinely need a one-to-many table in the query - say, to filter by tag. Do not let it fan out your aggregate. Two safe patterns:

The general rule: a JOIN to a table with multiple rows per key multiplies your grain. Always check the row count at each stage of a query - if a JOIN doubles the rows, it will double every SUM and COUNT that follows.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why does my SQL COUNT and GROUP BY return double the expected total?

A one-to-many JOIN earlier in the query is duplicating rows before the aggregate runs. If you join to a table with several rows per key - like tags per product - each base row is repeated once per match, so COUNT and SUM count it multiple times. Remove or pre-aggregate that join.

How do I stop a JOIN from inflating a SUM in SQL?

Keep the many-to-one grain intact. Either drop a join the query does not actually use, aggregate the base table in a subquery and join the extra table to that result, or collapse the many side with EXISTS or a grouped subquery so each key contributes one row before the SUM.

How can I tell which JOIN is duplicating rows in my query?

Run the GROUP BY with COUNT(*) both with and without the suspect JOIN and compare. If adding a JOIN increases the row count per group, that table has multiple rows per join key and is fanning out your data. COUNT(DISTINCT id) versus COUNT(id) also exposes the duplication.

Does adding a JOIN change the result of GROUP BY and SUM?

Yes. GROUP BY and SUM operate on the row set produced after all JOINs. If a JOIN to a one-to-many table multiplies the rows, the SUM adds the same values several times, so totals inflate even though the JOIN adds no columns to the output.

Keep learning

Write a Top-5 Revenue SQL ReportData projectSpeed Up a Slow SQL Report QueryData projectRank Rows Per Group With SQL Window FunctionsData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →