NULL as Unknown/Missing, Not an Ordinary Value
corebeginnerNULL is not zero, not an empty string, and not false — it is the absence of a known value. A numeric column that is NULL is not "0," a text column that is NULL is not "''," and a boolean column that is NULL is not "false." Each of those is a real, specific value; NULL is the claim that no value is recorded at all.
Think of it as
Every other value in a column is a specific fact: 0 means "the count is exactly zero," '' means "the string is exactly empty." NULL makes no factual claim about the data at all — it says the fact is not present in this row, for whatever reason the schema allows (not yet known, not applicable, deliberately withheld). Treating NULL as "the smallest/default value of its type" is exactly the mistake this distinction guards against.
What we're doing: Show that AVG and COUNT(col) skip NULL rows entirely, rather than treating NULL as zero.
- 2
- Grace's bonus is genuinely 0 (a fact); Linus's bonus is NULL (no fact recorded).
- 4–6
- count(*) counts all 3 rows; count(bonus) counts only the 2 rows with a real value, skipping Linus.
all_employees | employees_with_a_recorded_bonus | avg_bonus
---------------+----------------------------------+-----------
3 | 2 | 500.0000000000000000Why this works: avg_bonus is 500 (the average of 1000 and 0), not 333.33 (the average of 1000, 0 and 0-for-Linus) — PostgreSQL never substitutes 0 for a NULL bonus, it simply excludes Linus's row from the computation entirely, dividing by 2 known values, not 3. This is the concrete consequence of NULL meaning "no value recorded" rather than "the value is zero": treating them the same would silently understate the true average whenever some rows genuinely have no data.
Assuming AVG divides by every row, including NULLs, as if they were zero
Wrong
Better
What you see: A computed average, sum, or other aggregate looks too high compared to what a reader expected if they assumed every row (including NULL ones) contributed to the divisor.
Why: PostgreSQL's aggregate functions are specified to ignore NULL input values entirely — this is the correct behavior when NULL genuinely means "this employee's bonus is not applicable or not yet known," since including an unknown value as though it were zero would be asserting a fact nobody actually recorded. When the intended semantics really is "treat missing as zero," COALESCE(bonus, 0) makes that substitution explicit and visible in the query, rather than relying on an aggregate function's default behavior to happen to match the intent.
- bonus = 0
- a fact: this employee's bonus is exactly zero
- participates normally in SUM/AVG
- counted by COUNT(bonus)
- bonus = NULL
- no fact recorded — bonus amount unknown/not applicable
- skipped entirely by SUM/AVG, not treated as 0
- NOT counted by COUNT(bonus)
NULL vs the nearest "empty-looking" real value, per type
Together
Remember: NULL means no value was recorded — not zero, not empty string, not false. Aggregate functions like SUM/AVG/COUNT(col) skip NULL rows entirely rather than treating them as zero.
See also: null semantics · coalesce and nullif

