NULL Values: The Unknown in SQL

Core Concept

NULL is SQL's way of saying "I don't know." Not zero, not empty - genuinely unknown.

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 UNKNOWN + special operators rating = NULL when comparing with NULL = ? UNKNOWN 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 UNKNOWN Special operators always return TRUE/FALSE

Three-Valued Logic Foundation

Truth Tables for Three-Valued Logic

AND Logic - FALSE dominates

AND TRUE FALSE UNKNOWN
TRUE TRUE FALSE UNKNOWN
FALSE FALSE FALSE FALSE
UNKNOWN UNKNOWN FALSE UNKNOWN

Rule: FALSE AND anything = FALSE

OR Logic - TRUE dominates

OR TRUE FALSE UNKNOWN
TRUE TRUE TRUE TRUE
FALSE TRUE FALSE UNKNOWN
UNKNOWN TRUE UNKNOWN UNKNOWN

Rule: TRUE OR anything = TRUE

NOT Logic - UNKNOWN stays

Value NOT Result
TRUE FALSE
FALSE TRUE
UNKNOWN UNKNOWN

Rule: You can't negate the unknown

Key Rules

  1. NULL Propagation: Any operation with NULL returns NULL (except IS NULL/IS NOT NULL)

  2. WHERE Filtering: Only TRUE conditions pass; UNKNOWN filtered out like FALSE ⚠️

  3. Aggregates: Most functions ignore NULL (except COUNT(*) because it's UNKNOWN) ⚠️

  4. DISTINCT/GROUP BY: NULL values are considered equal to each other

  5. JOIN Behavior: NULL values don't match in joins (even NULL ≠ NULL)


NULL in Comparisons

-- Test how NULL behaves with different comparison operators
SELECT 
    listen_id,
    rating,
    rating = 4.5 AS equals_check,
    rating > 4.0 AS greater_check,
    rating IS NULL AS is_null_check
FROM Listens
ORDER BY listen_id
listen_idratingequals_checkgreater_checkis_null_checkNotes
14.5Normal comparisons work
24.2Normal comparisons work
33.9Normal comparisons work
44.7Normal comparisons work
54.6Normal comparisons work
63.9Normal comparisons work
72.9Normal comparisons work
84.9Normal comparisons work
9NULL??NULL comparisons = UNKNOWN

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

bugfix: sept24. column headers fixed to match AS aliases


NULL with AND/OR Logic

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

Logic Demonstration


NULL in WHERE Clause

Query 1: High ratings

SELECT listen_id, rating 
FROM Listens 
WHERE rating > 4.0
listen_idrating
14.5
24.2
44.7
54.6
84.9

Result: 5 rows

Query 2: Low ratings

SELECT listen_id, rating 
FROM Listens 
WHERE rating <= 4.0
listen_idrating
33.9
63.9
72.9

Result: 3 rows

Query 3: NULL ratings

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

Result: 1 row

Addn: sept24. added output tables to show each query separately

Debug: How WHERE Evaluates Each Row to Produce Above Tables

listen_id rating rating > 4.0 rating <= 4.0 rating IS NULL
1 4.5 ✓ TRUE ✗ FALSE ✗ FALSE
2 4.2 ✓ TRUE ✗ FALSE ✗ FALSE
3 3.9 ✗ FALSE ✓ TRUE ✗ FALSE
4 4.7 ✓ TRUE ✗ FALSE ✗ FALSE
5 4.6 ✓ TRUE ✗ FALSE ✗ FALSE
6 3.9 ✗ FALSE ✓ TRUE ✗ FALSE
7 2.9 ✗ FALSE ✓ TRUE ✗ FALSE
8 4.9 ✓ TRUE ✗ FALSE ✗ FALSE
9 NULL ? UNKNOWN ? UNKNOWN ✓ TRUE

Missing row alert: Row 9 (NULL) excluded from both Query 1 and Query 2!

Key Points


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:

Addn: sept24. added table to show aggregate results as columns

Key Points


NULL in DISTINCT and GROUP BY

-- Get unique rating values (NULLs treated as one group)
SELECT DISTINCT rating
FROM Listens
ratingAnnotations
NULLNULL forms one unique group
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
-- Count occurrences of each rating (NULLs form one group)
SELECT rating, COUNT(*) AS COUNT
FROM Listens
GROUP BY rating
ratingcountAnnotations
NULL1NULLs grouped together (listen_id 9)
2.91Single occurrence (listen_id 7)
3.92Two occurrences (listen_ids 3, 6)
4.21Single occurrence (listen_id 2)
4.51Single occurrence (listen_id 1)
4.61Single occurrence (listen_id 5)
4.71Single occurrence (listen_id 4)
4.91Single occurrence (listen_id 8)

Addn: sept24. Simplified table to show only relevant columns

Key Points


Common Patterns


Common Mistakes

The = NULL Trap

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

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

Problem: NULL = NULL returns UNKNOWN, not TRUE

The NOT NULL Gotcha

-- 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

Problem: NOT UNKNOWN = UNKNOWN, still filtered out

COUNT(*) vs COUNT(column)

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

Problem: COUNT(column) skips NULL values