Guide contents

Lesson 4 of 30

NULL and Three-Valued Logic

NULL means "unknown or missing". It is not zero and not an empty string. Any comparison with an unknown value is unknown, so SQL has three truth values: TRUE, FALSE, and UNKNOWN. WHERE keeps only rows where the condition is TRUE.

Example 1: comparing with NULL

Tara has no department, so dept_id is NULL.

SQL
SELECT name FROM employees WHERE dept_id = NULL;
Output
 name 
------
(0 rows)

No rows. dept_id = NULL is UNKNOWN, never TRUE. Use IS NULL.

SQL
SELECT name FROM employees WHERE dept_id IS NULL;
Output
 name 
------
 Tara
(1 row)

Example 2: NULL = NULL and NULL <> NULL

psql prints NULL as an empty cell, so a blank result below means NULL.

SQL
SELECT NULL = NULL AS eq, NULL <> NULL AS ne, NULL IS NULL AS is_null;
Output
 eq | ne | is_null 
----+----+---------
    |    | t
(1 row)

Example 3: a filter silently drops NULL rows

SQL
SELECT count(*) FROM employees WHERE dept_id <> 1;
Output
 count 
-------
     6
(1 row)

There are 12 employees, 5 in department 1, and 6 elsewhere. Tara is in neither list because NULL <> 1 is UNKNOWN. To include her:

SQL
SELECT count(*) FROM employees WHERE dept_id IS DISTINCT FROM 1;
Output
 count 
-------
     7
(1 row)

IS DISTINCT FROM treats NULL as an ordinary value. It is standard SQL.

Example 4: three-valued AND and OR

a b a AND b a OR b
TRUE UNKNOWN UNKNOWN TRUE
FALSE UNKNOWN FALSE UNKNOWN
SQL
SELECT (TRUE OR NULL) AS t_or_null, (FALSE AND NULL) AS f_and_null, (TRUE AND NULL) AS t_and_null;
Output
 t_or_null | f_and_null | t_and_null 
-----------+------------+------------
 t         | f          | 
(1 row)

Example 5: COALESCE gives a default

COALESCE returns its first non-NULL argument.

SQL
SELECT name, COALESCE(dept_id::text, 'none') AS dept
FROM employees
WHERE id IN (11, 12);
Output
 name  | dept 
-------+------
 Rohan | 4
 Tara  | none
(2 rows)

Example 6: NULLIF avoids divide by zero

NULLIF(a, b) returns NULL when a = b, otherwise a.

SQL
SELECT 100 / NULLIF(0, 0) AS safe;
Output
 safe 
------
     
(1 row)

Example 7: aggregates skip NULL

count(*) counts rows. count(column) counts non-NULL values.

SQL
SELECT count(*) AS all_rows,
       count(dept_id) AS with_dept,
       count(manager_id) AS with_manager
FROM employees;
Output
 all_rows | with_dept | with_manager 
----------+-----------+--------------
       12 |        11 |           11
(1 row)

avg, sum, min, and max also ignore NULL. sum over zero rows, or only NULLs, is NULL, not 0.

SQL
SELECT sum(salary) AS total FROM employees WHERE dept_id = 5;
Output
 total 
-------
      
(1 row)
SQL
SELECT COALESCE(sum(salary), 0) AS total FROM employees WHERE dept_id = 5;
Output
 total 
-------
     0
(1 row)

Example 8: NOT IN with NULL returns nothing

This is a classic trap. Find departments that have no employees:

SQL
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees);
Output
 name 
------
(0 rows)

No rows, although Legal has no employees. The subquery returns a NULL (Tara). id NOT IN (1, 2, 3, 4, NULL) becomes id <> 1 AND ... AND id <> NULL, which is never TRUE.

Two safe fixes:

SQL
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS NOT NULL);
Output
 name  
-------
 Legal
(1 row)
SQL
SELECT d.name FROM departments d
WHERE NOT EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);
Output
 name  
-------
 Legal
(1 row)

Prefer NOT EXISTS.

Example 9: sorting NULLs

PostgreSQL sorts NULL as larger than any value: last in ASC, first in DESC. Control it explicitly with NULLS FIRST or NULLS LAST.

SQL
SELECT name, dept_id FROM employees ORDER BY dept_id NULLS FIRST, id LIMIT 3;
Output
 name  | dept_id 
-------+---------
 Tara  |        
 Meera |       1
 Ravi  |       1
(3 rows)

Example 10: NULLs form one group

DISTINCT and GROUP BY treat all NULLs as one group, even though NULL = NULL is not true. (A UNIQUE constraint in PostgreSQL, by contrast, allows several NULLs.)

SQL
SELECT dept_id, count(*) FROM employees GROUP BY dept_id ORDER BY dept_id;
Output
 dept_id | count 
---------+-------
       1 |     5
       2 |     3
       3 |     2
       4 |     1
         |     1
(5 rows)

Summary

  • Test with IS NULL and IS NOT NULL, never = NULL.
  • WHERE keeps only TRUE. UNKNOWN rows are dropped.
  • NOT IN plus a NULL in the list returns no rows. Use NOT EXISTS.
  • COALESCE supplies defaults, NULLIF produces NULL on purpose.