Skip to content
When it goes wrong

Common SQL gotchas

The SQL mistakes that catch almost everyone: queries that return no rows, GROUP BY errors, missing dates, inflated counts, NULL surprises, and JOIN filters that silently drop data.

SQL mistakes are often quiet: the query runs, but the answer is empty, inflated, truncated, or missing rows you expected to keep. Sometimes the database rejects the query with a clause-order error; other times it accepts the query but the result still needs investigation.

Use these pages as a debugging checklist. If a filter returns no rows, check NULL handling and AND/OR logic. If a count looks too high, inspect the join shape and whether COUNT(column) skipped NULL values. If a metric was averaged twice, recompute it from totals. If a LEFT JOIN loses rows, look for right-table filters in WHERE. If GROUP BY fails, compare each selected column with the grouped or aggregated columns.

The fixes are intentionally small and testable: isolate the table, inspect a few rows, add one condition at a time, then compare the row count before and after each change. That habit catches more SQL bugs than memorizing error text.

Quick SQL gotcha fixes

Why = NULL never matches

Comparisons to NULL return unknown, so the row is dropped. Use IS NULL.

COUNT or SUM is too high after a JOIN

A one-to-many join repeats rows, inflating totals. Count what you mean.

Why a JOIN without ON multiplies rows

A missing join condition pairs every row with every row. Add the key match.

Why a many-to-many JOIN inflates totals

When both sides have repeats, the join multiplies combinations. Pre-aggregate first.

Why joining on names instead of ids is risky

Names and labels can repeat or change. Join on stable ids whenever the schema gives you one.

Why my query returns no rows

Usually a filter that does not match the real data: case, ranges, or AND/OR.

Why my percentage comes out 0

Integer division truncates. Multiply by 100.0 and round.

Why a window running total jumps on ties

The default RANGE frame can group tied ORDER BY values. Use ROWS for row-by-row totals.

Why NOT IN with a NULL returns nothing

A single NULL in the list makes every NOT IN test unknown. Use NOT EXISTS.

LIMIT without ORDER BY gives unpredictable rows

With no ORDER BY, the database can return any rows in any order. Sort first.

Why a date range drops the last day

Comparing a timestamp to a bare date cuts off that day. Use a half-open range.

Why a LEFT JOIN turns into an INNER JOIN

A WHERE filter on the right table removes NULL matches. Move it into ON.

Why INNER JOIN filters belong in the right place

Relationship predicates belong in ON; row filters belong in WHERE. Mixing them hides bugs.

Why COUNT(column) skips NULL values

COUNT(*) counts rows, but COUNT(column) counts only non-NULL values.

Why averaging averages gives the wrong result

An average of group averages ignores group size. Recompute from totals.

Why OR without parentheses changes your filter

AND runs before OR. Add parentheses so the filter matches your intent.

Why UNION ALL keeps duplicate rows

UNION ALL stacks results exactly as written. Use UNION only when duplicates should collapse.

Want to practice spotting these? Work through Chapter 9: Query Debugging, then keep the SQL error message decoder nearby.