skip to content

Why does SUM(amount) return NULL instead of 0 when no rows match the WHERE clause?

level: middleimportance: must knowfreq 68%

answer

  1. nothing to add is not zero
  2. empty input, not a missing value
  3. COUNT is the exception that returns 0
  4. COALESCE goes outside the aggregate
  5. SUM(COALESCE(x,0)) does not help here

basics

~20 s

SUM over an empty input set is defined to return NULL, not 0 — there is nothing to add, and SQL reports "no value" rather than inventing a zero. Wrap it as COALESCE(SUM(amount), 0) when the caller needs a number.

solid answer

~50 s

An aggregate first discards NULL inputs; if nothing is left — because no rows matched, or because every value was NULL — `SUM` returns NULL. That is deliberate: zero is a real total, and SQL will not claim you summed to zero when you summed nothing at all. `COUNT` is the exception that proves the rule: it counts rows or values, so an empty input legitimately gives 0. The practical fix is `COALESCE(SUM(amount), 0)`, applied at the outermost level, because the NULL you are guarding comes from the aggregate, not from any row. This bites hardest in application code and in arithmetic: a NULL total flowing into `balance - SUM(amount)` makes the whole expression NULL, and a client that maps the column to a non-nullable numeric type throws. Decide once, per query, whether "no rows" should surface as 0 or stay distinguishable from a genuine zero total.

code

sql · 11 lines
sql
-- No matching rows: this returns one row containing NULL, not 0
SELECT SUM(amount) AS total
FROM payments
WHERE customer_id = 42
  AND paid_at >= DATE '2026-01-01';

-- The guard belongs outside the aggregate
SELECT COALESCE(SUM(amount), 0) AS total
FROM payments
WHERE customer_id = 42
  AND paid_at >= DATE '2026-01-01';

go deeper

for a junior

Know the fact and the fix: an empty SUM is NULL, and COALESCE(SUM(x), 0) makes it a number. Recognise the NullPointerException-shaped bug this causes in application code.

for a middle

Explain why the placement of COALESCE matters — inside guards row values, outside guards the aggregate — and why COUNT is the one aggregate that returns 0 on empty input.

for a senior

Show the judgment: coalescing hides the difference between "no rows" and "nets to zero". Be ready to describe returning COUNT(*) alongside the sum, and how a NULL total silently poisons downstream arithmetic in a scalar subquery.

for a principal

Own the contract between the query layer and its consumers: which measures are nullable, what a dashboard renders for absent data, and how metric definitions stop teams from papering over missing data with zeros.

## Empty input, not missing data SQL's aggregates operate on a multiset of input values. Before computing, every aggregate except `COUNT(*)` removes the NULLs from that multiset. Two situations then leave the aggregate with **nothing to work on**: 1. No row satisfied the `WHERE` clause (or the table is empty), so the input was empty to begin with. 2. Rows matched, but the aggregated column was NULL in every one of them, so NULL elimination emptied it. The standard specifies the same outcome for both: `SUM`, `AVG`, `MIN` and `MAX` over an empty input return **NULL**. `COUNT` is the deliberate exception — counting an empty set is 0, an exact and meaningful answer. The design reasoning is worth being able to state. `SUM` returning 0 would assert "the total is zero", which is a claim about data that does not exist. NULL asserts "there is no total to report". Those are genuinely different facts: a customer with no payments at all is not the same as a customer whose payments net to zero, and a report that conflates them is lying quietly. ## The failure it causes ```sql SELECT SUM(amount) AS total -- NULL when the customer has no 2026 payments FROM payments WHERE customer_id = 42 AND paid_at >= DATE '2026-01-01'; ``` That NULL then travels. In SQL, NULL propagates through arithmetic, so `credit_limit - SUM(amount)` is NULL rather than `credit_limit`. In application code, a driver hands back a null reference where the caller expected a number — a `NullPointerException` in Java, a `None` in Python arithmetic, a non-nullable-decimal mapping error. The bug typically appears only for the new customer, the empty date range, or the first day of the month, which is why it survives testing. ## The fix, and where it goes ```sql SELECT COALESCE(SUM(amount), 0) AS total FROM payments WHERE customer_id = 42 AND paid_at >= DATE '2026-01-01'; ``` `COALESCE` returns its first non-NULL argument, so this yields the real total when rows exist and 0 when they do not. The placement matters and interviewers probe it: **`SUM(COALESCE(amount, 0))` does not fix this case.** That form protects against NULLs *inside* rows — it converts a NULL `amount` on an existing row into 0 — but when there are no rows at all there is nothing for the inner `COALESCE` to act on, and `SUM` still returns NULL. The guard has to sit outside the aggregate because the NULL is produced by the aggregate. The same guard applies to `AVG`, `MIN` and `MAX`, though for `AVG` a 0 default is usually the wrong choice: "average of nothing" is rarely "zero average", and passing the NULL through so the presentation layer can render an em dash is often more honest. ## When you should *not* coalesce Suppressing the NULL destroys information. If downstream logic must distinguish "no activity" from "activity that nets to zero", keep the NULL, or return both measures: ```sql SELECT COUNT(*) AS payment_count, -- 0 tells you there were no rows COALESCE(SUM(amount), 0) AS total FROM payments WHERE customer_id = 42; ``` `COUNT(*)` is the disambiguator, since it returns 0 rather than NULL on empty input. This pairing — a count next to a coalesced sum — is the idiom experienced authors reach for in reporting queries. ## The related shape that catches people A scalar subquery used inside a larger expression has the same hazard: ```sql SELECT c.id, c.credit_limit - COALESCE((SELECT SUM(p.amount) FROM payments p WHERE p.customer_id = c.id), 0) AS remaining FROM customers c; ``` Without the `COALESCE`, every customer with no payments gets a NULL `remaining` instead of their full limit — the arithmetic, not the aggregate, is what finally surfaces the problem. ## What a strong answer sounds like "`SUM` over an empty set is NULL by definition — zero is a total, and there was no total. `COUNT` is the exception because counting nothing really is zero. I wrap it in `COALESCE(SUM(x), 0)` at the outer level, not `SUM(COALESCE(x, 0))`, since that inner form only helps when rows exist. And I only coalesce when zero is a truthful default; otherwise I return `COUNT(*)` alongside so the caller can tell empty from zero."

  • Why doesn't SUM(COALESCE(amount, 0)) solve the empty-result problem?
    Because the inner `COALESCE` only runs on rows that exist. It rewrites a NULL `amount` on a real row to 0, which changes nothing for `SUM` anyway since that row was already skipped. When no rows match, the aggregate's input is empty regardless, and `SUM` still returns NULL. The guard must wrap the aggregate: `COALESCE(SUM(amount), 0)`.
  • Do COUNT, MIN and MAX behave the same way on an empty input?
    `MIN` and `MAX` behave exactly like `SUM` — empty input gives NULL, because there is no smallest or largest value to name. `COUNT` is the exception: `COUNT(*)` and `COUNT(col)` both return 0 over an empty input, since counting nothing is a well-defined zero. That is why a `COUNT(*)` beside a coalesced `SUM` lets a caller tell "no rows" from "sums to zero".
  • When would you deliberately let the NULL through instead of coalescing it?
    Whenever zero would be a false claim. A dashboard tile showing "0 revenue" for a region with no reporting yet is worse than one showing "no data"; an average of nothing is not an average of zero. Let the NULL reach the presentation layer and render it as a dash, or return `COUNT(*)` alongside so downstream logic can branch explicitly.

Adding up an empty stack of receipts: you cannot honestly announce "the total is zero" — there was no total. SQL says NULL; you decide whether the report prints 0 or a dash.

saying these in an interview costs you the question

  • Claims SUM returns 0 when nothing matches
  • Fixes it with SUM(COALESCE(x, 0))
  • Says COUNT also returns NULL on an empty table
  • Coalesces every aggregate to 0 without asking if zero is truthful
  • Thinks the query returns no rows rather than one NULL row

context