Working with NULL
NULL is not zero. It is not an empty string. It is not false. It means
unknown — the database has no value here — and treating it as anything else
is the origin of more quietly wrong SQL than any other single cause.
Three-valued logic
Most languages have true and false. SQL has true, false, and unknown.
SQLSELECT NULL = NULL AS equals, NULL <> NULL AS not_equals, NULL IS NULL AS is_null;
The first two come back NULL — not true, not false, unknown. Only the third is
1. This is not a quirk, it is the definition: if two salaries are both unknown,
is one equal to the other? The honest answer is that nobody knows.
The consequence is a rule with no exceptions: WHERE keeps rows where the
condition is true. Unknown is not true, so the row is dropped — and no error
is raised anywhere along the way.
Truth tables
AND — false wins, because one false makes the whole thing false regardless:
| AND | true | false | unknown |
|---|---|---|---|
| true | true | false | unknown |
| false | false | false | false |
| unknown | unknown | false | unknown |
OR — true wins, for the mirror reason:
| OR | true | false | unknown |
|---|---|---|---|
| true | true | true | true |
| false | true | false | unknown |
| unknown | true | unknown | unknown |
NOT unknown is unknown. You cannot negate your way out of it.
Testing for NULL
IS NULL and IS NOT NULL are the only tests that work:
SQLSELECT first_name, salary FROM employees WHERE salary IS NULL;
WHERE salary = NULL returns nothing — not an error, just an empty result, which
looks exactly like "nobody matched".
NULL propagates through arithmetic
SQLSELECT 1 + NULL AS arithmetic, 'a' || NULL AS concatenation;
Both NULL. Any expression with an unknown input has an unknown result. So a
bonus calculation of salary * 1.1 is NULL for the employee with no salary,
and a report built from first_name || ' — ' || email silently loses the row
with no email address. (SQL Server's + and MySQL's CONCAT behave the same
way; MySQL's CONCAT_WS is the exception and skips NULLs.)
Aggregates skip NULLs — except COUNT(*)
This is where NULL does real financial damage.
SQLSELECT COUNT(*) AS employees, COUNT(salary) AS with_salary, SUM(salary) AS total, AVG(salary) AS average FROM employees;
Ten employees, nine salaries. SUM and AVG ignore the tenth row entirely, so
the average is the total divided by nine, not ten. Whether that is right
depends on what the NULL means: an employee whose salary is not recorded should
probably be excluded, but an unpaid intern whose salary is genuinely zero should
be counted — and stored as 0, not NULL.
COUNT(*) counts rows. COUNT(column) counts non-NULL values in that column.
The gap between those two numbers is the fastest data-quality check there is.
COUNT(DISTINCT dept_id) returns 3, not 4 — DISTINCT inside an aggregate
discards NULL as well.
Replacing NULL: COALESCE
COALESCE returns the first argument that is not NULL:
SQLSELECT first_name, COALESCE(email, 'no email on file') AS contact, COALESCE(salary, 0) AS salary_for_report FROM employees ORDER BY emp_id;
COALESCE is standard and takes any number of arguments, so it works everywhere.
The per-database two-argument shorthands — IFNULL (MySQL, SQLite),
ISNULL (SQL Server), NVL (Oracle) — all do the same job less portably.
Be careful what you substitute: COALESCE(salary, 0) inside an AVG changes the
answer, because the row is no longer skipped. Substituting zero is a display
decision, not a maths one.
The inverse, NULLIF(a, b), returns NULL when the two are equal — most often
used as NULLIF(divisor, 0) to turn a division-by-zero error into a NULL result.
Sorting NULLs
ORDER BY has to put unknowns somewhere, and databases disagree about where:
PostgreSQL and Oracle sort them last ascending, MySQL and SQL Server sort them
first. NULLS FIRST / NULLS LAST makes it explicit on PostgreSQL and Oracle;
elsewhere, sort by CASE WHEN col IS NULL THEN 1 ELSE 0 END first.
Where NULLs come from
- A column that was never filled in.
- A
LEFT JOINwith no match on the right — every column from the right side is NULL. - An aggregate over zero rows:
SUMof nothing is NULL, thoughCOUNTof nothing is 0. - An outer-joined or filtered-away subquery result.
The second one is why the joins module comes back to this lesson.
Common mistakes
= NULLinstead ofIS NULL— matches nothing, warns nobody.- Expecting
<>orNOT INto include NULLs — they never do. - Reading
AVGas "total ÷ row count" — it is total ÷ non-NULL count. COALESCE-ing before aggregating without meaning to — it changes the answer.- Storing
''or0or-1for "unknown" — now every query needs to know your private convention, and the database's own NULL handling cannot help you.
Interview question
What is the difference between
COUNT(*)andCOUNT(column)?
COUNT(*) counts rows; COUNT(column) counts rows where that column is not
NULL. A strong answer continues: the difference tells you how many values are
missing, and SUM/AVG skip those rows too.
Check yourself
- Why does
WHERE salary <> 90000omit employees with no salary? - What is
NULL OR true? What isNULL AND false? - When would
COALESCE(salary, 0)give a misleading average?
Module complete
You can now filter precisely: comparisons, compound conditions with the right brackets, ranges, patterns, and unknowns. Next: grouping rows and computing aggregates over them — where the NULL rules you just learned start affecting totals people rely on.