SQL NULL Values
In SQL, NULL represents a missing, unknown, or inapplicable value — it is not the same as zero, an empty string, or false. A column holding NULL simply means "we don’t know" or "there is nothing here." Because NULL behaves differently from every other value in comparisons, arithmetic, and aggregation, it trips up beginners constantly. This lesson explains exactly how NULL works under the hood and how to handle it correctly in real queries.
Overview: How NULL Works
NULL is a special marker used by the database engine to represent the absence of a value. It can appear in almost any column unless that column has a NOT NULL constraint. The key thing that makes NULL different from other values is that SQL uses three-valued logic: every condition evaluates to TRUE, FALSE, or UNKNOWN (which behaves like NULL). Any comparison involving NULL — even NULL = NULL — evaluates to UNKNOWN, not TRUE. This is why WHERE column = NULL never matches anything, no matter what is actually stored in the column.
This same rule cascades through the engine: arithmetic with NULL produces NULL (5 + NULL is NULL), string concatenation with NULL usually produces NULL, and most aggregate functions (SUM, AVG, COUNT(column), MAX, MIN) silently skip NULL values rather than treating them as zero. Understanding this behavior is essential — it explains a huge share of "why did my query return the wrong rows" bugs.
Syntax
There is no operator that creates a literal NULL value beyond writing the keyword itself. The syntax you use is almost always about testing for or substituting NULL:
| Syntax | Purpose |
|---|---|
column IS NULL |
True only when the column’s value is missing |
column IS NOT NULL |
True only when the column has an actual value |
COALESCE(expr1, expr2, ...) |
Returns the first non-NULL expression in the list (standard SQL) |
IFNULL(expr, replacement) |
SQLite/MySQL shortcut for a two-argument COALESCE; SQL Server uses ISNULL() |
column_name TYPE NOT NULL |
Column constraint in CREATE TABLE that rejects NULL inserts |
ORDER BY column NULLS FIRST | NULLS LAST |
Explicitly controls where NULLs sort (supported in SQLite, PostgreSQL; MySQL requires a workaround) |
Never use = NULL or <> NULL to test for a missing value — always use IS NULL / IS NOT NULL.
Examples
All examples below use this sample table:
CREATE TABLE employees_null (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
manager_id INTEGER,
commission REAL
);
INSERT INTO employees_null (id, name, department, manager_id, commission) VALUES
(1, 'Alice', 'Sales', NULL, 500.0),
(2, 'Bob', 'Sales', 1, 250.0),
(3, 'Carol', 'Engineering', 1, NULL),
(4, 'Dave', 'Engineering', 1, NULL);
Alice is the top manager (no boss, so manager_id is NULL), and only the two Sales employees have a commission on file.
Example 1: Finding rows with IS NULL
SELECT name, commission
FROM employees_null
WHERE commission IS NULL;
| name | commission |
|---|---|
| Carol | NULL |
| Dave | NULL |
Only IS NULL correctly matches rows where commission was never set. Flipping it to IS NOT NULL would instead return Alice and Bob.
Example 2: NULL in arithmetic vs. COALESCE
SELECT name,
commission,
commission * 0.1 AS raw_bonus,
COALESCE(commission, 0) * 0.1 AS safe_bonus
FROM employees_null
ORDER BY id;
| name | commission | raw_bonus | safe_bonus |
|---|---|---|---|
| Alice | 500.0 | 50.0 | 50.0 |
| Bob | 250.0 | 25.0 | 25.0 |
| Carol | NULL | NULL | 0.0 |
| Dave | NULL | NULL | 0.0 |
Multiplying NULL by anything produces NULL — that’s why raw_bonus is empty for Carol and Dave. Wrapping the column in COALESCE(commission, 0) substitutes 0 before the multiplication, giving a usable number instead.
Example 3: NULL in aggregates and GROUP BY
SELECT department,
COUNT(*) AS total_employees,
COUNT(commission) AS employees_with_commission,
AVG(commission) AS avg_commission
FROM employees_null
GROUP BY department
ORDER BY department;
| department | total_employees | employees_with_commission | avg_commission |
|---|---|---|---|
| Engineering | 2 | 0 | NULL |
| Sales | 2 | 2 | 375.0 |
COUNT(*) counts rows regardless of content, while COUNT(commission) counts only the rows where that column is not NULL — that’s why Engineering shows 0 despite having two employees. AVG(commission) for Engineering is NULL because there are zero non-NULL values to average, not because the average is zero.
Example 4: NULL produced by a LEFT JOIN
SELECT e.name AS employee, m.name AS manager
FROM employees_null e
LEFT JOIN employees_null m ON e.manager_id = m.id
ORDER BY e.id;
| employee | manager |
|---|---|
| Alice | NULL |
| Bob | Alice |
| Carol | Alice |
| Dave | Alice |
This self-join looks up each employee’s manager by name. Alice’s manager_id is NULL, so the join condition e.manager_id = m.id can never match for her row — but because it’s a LEFT JOIN, her row is kept anyway with NULL filled in for the manager columns. This is one of the most common sources of NULL in query results: an outer join with no matching row on the other side.
How It Works Step by Step (Under the Hood)
The engine evaluates a query in logical order — FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY — and NULL affects several of these stages differently:
- WHERE: Any row where the condition evaluates to
UNKNOWNis dropped, exactly likeFALSE. This is whycolumn = NULLfilters out everything, including rows that actually containNULL. - JOIN: In a
LEFT/RIGHT/FULLjoin, unmatched rows on the outer side getNULLfilled in for every column from the non-matching table, as shown in Example 4. - GROUP BY: SQL treats all
NULLvalues as belonging to the same group, so rows with aNULLgrouping key are grouped together rather than each forming its own group. - Aggregate functions:
COUNT(*)counts every row; every other aggregate (COUNT(col),SUM,AVG,MIN,MAX) silently ignoresNULLinputs. - ORDER BY: By default in SQLite,
NULLis treated as smaller than any other value, so it sorts first in ascending order and last in descending order — unless you override this explicitly:
SELECT name, commission
FROM employees_null
ORDER BY commission ASC NULLS LAST, id ASC;
| name | commission |
|---|---|
| Bob | 250.0 |
| Alice | 500.0 |
| Carol | NULL |
| Dave | NULL |
The NULLS LAST clause forces the two NULL commissions to the bottom instead of the default top position. MySQL doesn’t support NULLS FIRST/LAST directly — the common workaround there is ORDER BY commission IS NULL, commission.
Common Mistakes
Mistake 1: Using = NULL instead of IS NULL
SELECT name FROM employees_null WHERE commission = NULL;
This looks reasonable but always returns zero rows, because commission = NULL evaluates to UNKNOWN for every row — even the ones where commission genuinely is NULL. Equality can never be used to test for "missing." The fix:
SELECT name FROM employees_null WHERE commission IS NULL;
This correctly returns Carol and Dave, because IS NULL is a dedicated null-test operator rather than a comparison.
Mistake 2: Forgetting that <> silently excludes NULL rows
SELECT name FROM employees_null WHERE manager_id <> 1;
The intent here is "show me everyone not managed by employee 1." Bob, Carol, and Dave all have manager_id = 1 so they’re correctly excluded — but Alice, whose manager_id is NULL, is also silently excluded, even though her manager clearly isn’t 1. NULL <> 1 evaluates to UNKNOWN, not TRUE, so this query actually returns zero rows. The fix is to explicitly include the NULL case:
SELECT name FROM employees_null WHERE manager_id <> 1 OR manager_id IS NULL;
This correctly returns Alice, the only employee not managed by employee 1.
Bonus: NOT NULL rejects the value at insert time
CREATE TABLE contacts (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
INSERT INTO contacts (id, email) VALUES (1, NULL);
Because email is declared NOT NULL, this INSERT is rejected by the engine with a constraint-violation error rather than silently storing an empty value. Declaring columns NOT NULL whenever a value is genuinely required is far safer than relying on application code to catch missing data later.
Best Practices
- Always use
IS NULL/IS NOT NULLto test for missing values — never=or<>. - When comparing a nullable column with
<>,NOT IN, or similar, explicitly decide whetherNULLrows should be included and addOR column IS NULLif so. - Use
COALESCE()(or your dialect’s shortcut likeIFNULL()/ISNULL()) to supply a default value before doing arithmetic, concatenation, or display formatting. - Declare columns
NOT NULLwhenever the business rule requires a value — it’s cheaper to reject bad data at insert time than to debug it later. - Remember
NOT IN (subquery)returns zero rows for the whole query if the subquery produces even oneNULL— preferNOT EXISTSwhen the subquery’s column can containNULL. - Use
COUNT(column)instead ofCOUNT(*)when you specifically want to count non-missing values. - Be explicit with
NULLS FIRST/NULLS LASTinORDER BYwhen NULL placement actually matters to your report, rather than relying on the engine’s default.
Practice Exercises
- Exercise 1: Using the
employees_nulltable from this lesson, write a query that lists every employee’s name along with their commission, replacing any missing commission with the text'No commission'instead of a number. (Hint:COALESCEonly works cleanly when both arguments are the same type — think about casting or usingCASE WHEN.) - Exercise 2: Write a query that counts how many employees have no manager assigned (
manager_id IS NULL) versus how many do. Expected result: 1 employee with no manager, 3 with a manager. - Exercise 3: Explain in one sentence why
SELECT * FROM employees_null WHERE manager_id NOT IN (SELECT manager_id FROM employees_null WHERE manager_id IS NOT NULL);behaves differently than you might expect if the subquery’s list happened to include aNULL, and rewrite it safely usingNOT EXISTS.
Summary
NULLmeans "unknown or missing," not zero or an empty string.- SQL uses three-valued logic — comparisons involving
NULLevaluate toUNKNOWN, which behaves likeFALSEinWHEREclauses. - Test for missing values with
IS NULL/IS NOT NULL— never=or<>. - Most aggregate functions ignore
NULLvalues;COUNT(*)is the one exception that counts every row regardless. - Outer joins (
LEFT/RIGHT/FULL) fill unmatched columns withNULL— this is expected behavior, not an error. COALESCE()substitutes a default value forNULL, making arithmetic and display logic safer.- A
NOT NULLconstraint enforces at the schema level that a column can never store a missing value.
