skip to content

Aggregates and NULLs

Aggregates skip NULL inputs, which silently changes AVG denominators and makes SUM over an empty or all-NULL set return NULL rather than 0. Interviewers love this because it produces plausible-looking wrong numbers, and they expect me to defend the fix with COALESCE.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

5

Why can AVG(salary) differ from SUM(salary) / COUNT(*) on the same table?

level: juniorimportance: must knowfreq 82%

answer

  1. two averages, two divisors
  2. NULL rows never reach the denominator
  3. COUNT(*) counts rows; AVG counts values
  4. AVG(x) is SUM(x) / COUNT(x)

basics

~10 s

AVG divides by the number of non-NULL salaries, because aggregates discard NULL inputs first. SUM(salary) / COUNT(*) divides by every row, NULL ones included. The two agree only when the column has no NULLs.

solid answer

~40 s

Every aggregate except `COUNT(*)` throws away NULL inputs before it computes anything, so `AVG(salary)` is defined as `SUM(salary) / COUNT(salary)` — sum of the present salaries over the **count of present salaries**. `SUM(salary) / COUNT(*)` keeps the same numerator but divides by the full row count, so each NULL row pulls the result toward zero. With 10 employees, 6 salaries totalling 600000 and 4 NULLs, `AVG` gives 100000 and `SUM/COUNT(*)` gives 60000. Neither is wrong in itself: the question is what a NULL salary *means*. If it means "unknown", `AVG` is right and the NULL rows should not vote. If it means "paid nothing", you want the NULLs in the denominator — write `AVG(COALESCE(salary, 0))` rather than hand-rolling the division.

code

sql · 6 lines
sql
-- employees: 10 rows; 6 salaries totalling 600000, 4 salaries NULL
SELECT AVG(salary)                   AS avg_of_present,   -- 100000
       SUM(salary) * 1.0 / COUNT(*)  AS avg_over_rows,    -- 60000
       COUNT(*)                      AS rows_total,       -- 10
       COUNT(salary)                 AS rows_with_salary  -- 6
FROM employees;

go deeper

for a junior

Memorise the one-liner: aggregates ignore NULLs, and AVG divides by the count of values it actually saw. Be ready to compute both numbers on a small ten-row example at the whiteboard.

for a middle

Explain the mechanism — NULL elimination happens before the function computes, and AVG is defined as SUM over COUNT of the same column. Show the COALESCE-inside fix and mention integer division if you divide by hand.

for a senior

Treat it as a data-meaning decision: does NULL mean unknown or zero? Say how you would catch such a discrepancy in a report — reconciling row counts against value counts, and publishing the sample size beside every average.

for a principal

Own the convention: whether columns are allowed to be nullable at all, whether "missing" and "zero" get distinct encodings, and how metric definitions are documented so two dashboards computing the same average cannot disagree.

## The rule underneath everything Standard SQL gives one rule that explains this whole family of surprises: **with the single exception of `COUNT(*)`, an aggregate function first removes the NULL values from its input, then computes over what remains.** `SUM` adds only the non-NULL values, `MIN`/`MAX` compare only non-NULL values, `AVG` averages only non-NULL values. NULLs are not treated as zero, and they do not poison the result the way they poison a row-level expression — they are simply *not there* as far as the aggregate is concerned. That is different from ordinary arithmetic, where `100 + NULL` is NULL. Inside an aggregate the NULL row is dropped, not propagated. Candidates who expect propagation predict that `SUM(salary)` is NULL as soon as one salary is missing; it is not. ## What AVG actually computes The standard defines `AVG(x)` as `SUM(x) / COUNT(x)`. Both halves are NULL-skipping, so the denominator is the count of **non-NULL** `x` values, not the number of rows. That single fact is the whole question. ```sql -- employees: 10 rows, 6 salaries totalling 600000, 4 NULL salaries SELECT AVG(salary) AS avg_of_present, -- 100000 SUM(salary) * 1.0 / COUNT(*) AS avg_over_rows, -- 60000 COUNT(*) AS rows_total, -- 10 COUNT(salary) AS rows_with_salary -- 6 FROM employees; ``` The numerator is identical in both columns — 600000. Only the divisor moves: 6 versus 10. In general, when the values are non-negative, `SUM(x)/COUNT(*)` is the smaller number, and the gap widens with the share of NULLs. ## Which one do you want? This is a *modelling* question, not a syntax question, and a good answer says so. Ask what a NULL in the column means: - **Unknown / not yet recorded.** A missing salary is not a salary of zero; averaging it in would invent data. `AVG(salary)` over the rows that have a value is the honest number, and you should report the sample size next to it (`COUNT(salary)`) so the reader knows how many rows it rests on. - **Known absence, encoded as NULL.** A commission column where NULL means "earned no commission" really is zero for arithmetic. Then the missing rows belong in the denominator: write `AVG(COALESCE(commission, 0))`, which normalises the input before the aggregate sees it, rather than dividing by `COUNT(*)` yourself. The second form is preferable to the hand-rolled division for a practical reason too: `SUM(x) / COUNT(*)` invites integer division, since both operands may be integers and some engines then truncate. Multiplying by `1.0`, or casting, avoids that — but pushing the fix inside the aggregate avoids the whole issue. ## Edge cases worth knowing - **Every value NULL.** `AVG(x)` returns NULL, not 0 and not an error: there are zero non-NULL inputs, so no division happens at all. `SUM(x)/COUNT(*)` returns NULL as well, because the numerator is NULL. - **No rows at all.** Same result — NULL — and the query still returns exactly one row when there is no `GROUP BY`. - **Adding `WHERE salary IS NOT NULL`.** This does not change `AVG(salary)` by a single digit, because those rows were never in the average. It *does* change everything else in the same `SELECT` list, notably `COUNT(*)`, so it is a blunt instrument in a multi-measure query. - **Silent by design.** The standard defines a completion warning for the case where NULLs were eliminated by a set function, and some engines surface it (SQL Server, for example, reports "Null value is eliminated by an aggregate or other SET operation" when ANSI warnings are on). Most tools never show it, which is exactly why the wrong number ships. ## Saying it in an interview "Aggregates skip NULLs, so `AVG` divides by the count of non-NULL values while `SUM/COUNT(*)` divides by all rows. They differ by exactly the NULL rows. Which one is correct depends on whether a NULL means unknown or means zero; if it means zero, I put `COALESCE` inside the aggregate instead of changing the divisor." That answer shows you know the mechanism *and* that you treat it as a data-meaning decision.

  • What does AVG(rating) return when every row's rating is NULL?
    NULL. After NULL-skipping there are no inputs left, so there is nothing to sum and nothing to divide by — the engine returns NULL rather than 0 or a division-by-zero error. If the caller needs a number, wrap it: `COALESCE(AVG(rating), 0)`, and be sure that zero is a defensible stand-in for "no data".
  • If I want NULL salaries counted as zero, where should COALESCE go?
    Inside the aggregate: `AVG(COALESCE(salary, 0))`. That replaces the value before NULL-skipping can drop the row, so the row lands in both the sum and the divisor. `COALESCE(AVG(salary), 0)` is a different thing entirely — it leaves the average of the present salaries untouched and only guards the case where there is no data at all.
  • Does adding WHERE salary IS NOT NULL change AVG(salary)?
    No — those rows contribute nothing to `AVG` either way, so the average is identical. It does change any `COUNT(*)`, `SUM` over other columns, or ratio in the same select list, because you have removed whole rows rather than one column's NULLs. Filter for that reason, never to "fix" an average.

Averaging exam scores where some students were absent: AVG grades only the papers handed in, while SUM/COUNT(*) divides by the whole class roster, effectively scoring every absentee zero.

saying these in an interview costs you the question

  • Says one NULL makes the whole SUM or AVG NULL
  • Believes aggregates treat NULL as zero
  • Thinks AVG divides by COUNT(*)
  • Adds WHERE col IS NOT NULL expecting AVG to change
  • Cannot say which denominator the business actually wants

context

open as a page

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

level: middleimportance: must knowfreq 68%

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.

open as a page

What does SELECT COUNT(*), SUM(total) FROM orders return when orders is empty?

level: middleimportance: should knowfreq 51%

basics

~20 s

Exactly one row, holding 0 and NULL. An aggregate query with no GROUP BY always produces one row; COUNT of an empty input is 0, while SUM of an empty input is NULL. Adding GROUP BY would return zero rows instead.

open as a page

How does SUM(base + bonus) differ from SUM(base) + SUM(bonus) when bonus is NULL?

level: middleimportance: should knowfreq 44%

basics

~20 s

A NULL bonus makes base + bonus NULL for that row, so the aggregate skips the row entirely and loses its base too. SUM(base) + SUM(bonus) drops only the missing bonus. Fix with SUM(base + COALESCE(bonus, 0)).

open as a page

Should COALESCE go inside AVG(rating) or around it, and what changes?

level: seniorimportance: should knowfreq 38%

basics

~20 s

They answer different questions. AVG(COALESCE(rating, 0)) counts unrated rows as zero and lowers the average; COALESCE(AVG(rating), 0) leaves the average of rated rows untouched and only substitutes a value when there is no data at all.

open as a page