NULL Values: The Unknown in SQL

Concept. NULL means "unknown." Comparing anything to NULL, including NULL itself, returns NULL, not TRUE or FALSE.

Intuition. When Mickey's listen_time is missing, the database stores NULL, "unknown," not zero or empty. Any comparison against NULL returns NULL, and WHERE treats NULL as false, so the row drops out. Use IS NULL to test for it.

Example. WHERE rating = NULL returns zero rows, even for the Listens row with a NULL rating. Use WHERE rating IS NULL to actually find it.

NULL: SQL's Third Truth Value Binary World Traditional TRUE/FALSE only 4.5 > 4.0 = TRUE 3.9 > 4.0 = FALSE T F Clean & Simple Every comparison is TRUE or FALSE NULL World Introduces NULL + special operators rating = NULL when comparing with NULL = ? NULL rating IS NULL when rating is NULL = rating IS NULL when rating = 4.5 = rating IS NOT NULL when rating = 4.5 = IS NULL Never Returns NULL Special operators always return TRUE/FALSE

Bi-Value Logic Refresher

In standard boolean logic, every condition is TRUE or FALSE. Four operators:

  • AND: TRUE AND FALSE → FALSE. Both must be TRUE.

  • OR: TRUE OR FALSE → TRUE. At least one must be TRUE.

  • NOT: NOT TRUE → FALSE. Flips the value.

  • = (equality): 4 = 4 → TRUE. Matches values.


SQL Adds NULL: A Third Truth Value

When a value is missing, SQL stores NULL for "unknown." That introduces a third truth value beyond TRUE and FALSE: UNKNOWN.

(SQL standard: NULL is the value, UNKNOWN is the truth value. Some databases like SQLite also use None/NaN.)


What Changes When NULL Enters

Read each example as a question. 4.5 > NULL → NULL asks "is 4.5 greater than NULL?" The answer is NULL because we don't know NULL's value.

#OperatorPatternExamples
1ANDFALSE winsFALSE AND NULL → FALSE
TRUE AND NULL → NULL
2ORTRUE winsTRUE OR NULL → TRUE
FALSE OR NULL → NULL
3NOTNULL stays NULLNOT NULL → NULL
4Comparisons
=, !=, <, >, <=, >=
NULL winsNULL = NULL → NULL
5IS NULL
IS NOT NULL
The test for NULLNULL IS NULL → TRUE
4 IS NOT NULL → TRUE

NULL also has its own quirks in aggregates and DISTINCT, covered in their own sections below. GROUP BY and JOINs handle NULL with similar quirks; those live on the GROUP BY and JOINs pages.


NULL in Comparisons

-- Test how NULL behaves with different comparison operators
SELECT 
    listen_id,
    rating,
    rating = 4.5,
    rating > 4.0,
    rating IS NULL
FROM Listens
WHERE listen_id IN (1, 3, 9)
listen_id rating rating = 4.5 rating > 4.0 rating IS NULL
1 4.5
3 3.9
9 NULL ? ?

Legend: ✓ = TRUE, ✗ = FALSE, ? = NULL

Row 9 demonstrates rule 4: every comparison with NULL evaluates to NULL. To find that row you need IS NULL (rule 5).


NULL with AND/OR Logic

-- Demonstrate three-valued logic with AND/OR operations
SELECT 
    listen_id,
    user_id,
    rating,
    (user_id = 3 AND rating > 4.0) AS and_result,
    (user_id = 3 OR rating > 4.0) AS or_result
FROM Listens
WHERE listen_id IN (1, 2, 9)
listen_iduser_idratingand_resultor_result
114.5
214.2
93NULL?

Logic Demonstration

  • Rows 1 & 2: user_id = 3 is FALSE, rating > 4 is TRUE → AND returns FALSE, OR returns TRUE

  • Row 9: TRUE AND NULL → NULL

  • Row 9: TRUE OR NULL → TRUE (TRUE dominates in OR)


NULL in WHERE Clause

Query 1: Find the NULL ratings

SELECT listen_id, rating 
FROM Listens 
WHERE rating IS NULL
listen_idrating
9NULL

1 row: Daffy's listen. IS NULL (rule 5) returns a definite TRUE for row 9, so WHERE keeps it.

Now look at the other two filters. Where does row 9 go?

Query 2: WHERE rating > 4.0

listen_idrating
14.5
24.2
44.7
54.6
84.9

5 rows. Row 9 missing.

Query 3: WHERE rating <= 4.0

listen_idrating
33.9
63.9
72.9

3 rows. Row 9 missing again.

5 + 3 + 1 = 9 rows total. Daffy disappears from Queries 2 and 3. Why?

How WHERE handles row 9 across all three filters:

filterrow 9 evaluates toWHERE action
rating IS NULLTRUE✓ keeps
rating > 4.0NULL✗ drops
rating <= 4.0NULL✗ drops

By rule 4, every comparison with NULL is NULL. WHERE keeps only TRUE rows, so NULL is silently dropped. Only IS NULL (rule 5) returns a definite TRUE and finds Daffy.


NULL in Aggregates

-- Compare how aggregates handle NULL values
SELECT 
    COUNT(*) AS total_rows,
    COUNT(rating) AS rated_rows,
    AVG(rating) AS avg_rating,
    SUM(rating) AS sum_rating,
    MIN(rating) AS min_rating,
    MAX(rating) AS max_rating
FROM Listens
total_rowsrated_rowsavg_ratingsum_ratingmin_ratingmax_ratingNotes
984.233.62.94.9COUNT(*) counts all; others skip NULL

Calculation Details:

  • COUNT(*): All 9 rows in table

  • COUNT(rating): Only 8 non-NULL values (row 9 has NULL)

  • AVG(rating): (4.5+4.2+3.9+4.7+4.6+3.9+2.9+4.9)/8 = 4.2

  • SUM(rating): Sum of 8 non-NULL values = 33.6

  • MIN(rating): Minimum non-NULL value = 2.9

  • MAX(rating): Maximum non-NULL value = 4.9

Key Points

  • COUNT(*) counts every row

  • COUNT(column) skips NULLs

  • AVG/SUM/MIN/MAX ignore NULLs

  • AVG divides by non-NULL count


NULL in DISTINCT

-- Get unique rating values (NULLs treated as one group)
SELECT DISTINCT rating
FROM Listens
ratingAnnotations
NULLNULL forms one unique value
2.9From listen_id 7
3.9Keeping only one (from listen_ids 3, 6)
4.2From listen_id 2
4.5From listen_id 1
4.6From listen_id 5
4.7From listen_id 4
4.9From listen_id 8

DISTINCT treats all NULLs as one unique value, even though NULL = NULL is NULL by rule 4. (GROUP BY behaves the same way; covered on the GROUP BY page.)


Common Mistakes

The = NULL Trap

Rule 4 violation (NULL wins). rating = NULL returns NULL on every row, and WHERE drops NULL.

-- WRONG: rating = NULL always returns NULL (not TRUE)
SELECT * FROM Listens WHERE rating = NULL

-- CORRECT: Use IS NULL to properly test for NULL
SELECT * FROM Listens WHERE rating IS NULL

The NOT NULL Gotcha

Rules 3 + 4 violation (NOT NULL stays NULL; comparisons with NULL produce NULL). When rating is NULL, rating > 4.0 is NULL, and NOT NULL is still NULL, so WHERE silently drops the row.

-- WRONG: NOT (rating > 4.0) still excludes NULLs
SELECT * FROM Listens WHERE NOT (rating > 4.0)

-- CORRECT: Explicitly include NULLs if you want them
SELECT * FROM Listens 
WHERE rating <= 4.0 OR rating IS NULL

COUNT(*) vs COUNT(column)

Aggregate behavior (separate from the logic rules above). SUM, AVG, MIN, MAX, and COUNT(column) all skip NULLs. Only COUNT(*) counts every row.

-- COUNT(*) vs COUNT(column) behave differently with NULLs
SELECT COUNT(*) AS all_rows,           -- Returns 9
       COUNT(rating) AS rated_rows      -- Returns 8
FROM Listens