Unit 03.01: Three-valued logic: TRUE, FALSE, and UNKNOWN
Unit ID: SQL-M03-U02 - Estimated active time: 14-18 minutes Objective: evaluate conditions involving NULL correctly and predict which rows survive a filter.
NULL means unknown, so comparisons return unknown
SELECT NULL = NULL; -- NULL
SELECT NULL <> 'IN'; -- NULL
SELECT NULL > 0; -- NULL
Is one unknown value equal to another unknown value? There is no way to know, so SQL answers "unknown" rather than guessing. That is the whole rule; everything else follows from it.
WHERE keeps only rows where the condition is TRUE. UNKNOWN is not TRUE, so those rows are dropped.
Seeing it in the data
300 of our 4,812 customers have no recorded country:
SELECT COUNT(*) FROM customers WHERE country IS NULL; -- 300
SELECT COUNT(*) FROM customers WHERE country <> 'IN'; -- 1699
SELECT COUNT(*) FROM customers WHERE country = 'IN'; -- 2813
2,813 + 1,699 = 4,512, not 4,812. The two filters together miss 300 rows. Neither = 'IN' nor <> 'IN' is true for a NULL, so those customers belong to neither group.
This is the single most common way a category split silently loses people.
Writing the condition you meant
-- fragment: WHERE clause shown on its own
-- Customers we know are outside India
WHERE country <> 'IN'
-- Customers not recorded as being in India
WHERE country <> 'IN' OR country IS NULL
-- Everyone, split into three honest buckets
SELECT
CASE WHEN country IS NULL THEN 'unknown'
WHEN country = 'IN' THEN 'India'
ELSE 'international' END AS segment,
COUNT(*) AS customers
FROM customers
GROUP BY segment
ORDER BY customers DESC;
-- India | 2813
-- international | 1699
-- unknown | 300
The third version is usually the right answer for a report, because it adds up to the total and shows the reader the uncertainty instead of hiding it.
Non-example
IS NULL is not a comparison and behaves normally:
SELECT COUNT(*) FROM customers WHERE country IS NULL; -- 300, always reliable
Use IS NULL / IS NOT NULL. Never = NULL - that is always UNKNOWN and matches nothing.
Practice
Predict each result, then run them:
SELECT COUNT(*) FROM feedback WHERE rating < 3;SELECT COUNT(*) FROM feedback WHERE rating >= 3;- Do 1 and 2 add up to 500? If not, where did the rest go?
Check your answer
- 180
- 260
- No - they total 440. The other 60 rows have
rating IS NULL, so neither condition is TRUE for them.
To account for everyone:
SELECT
CASE WHEN rating IS NULL THEN 'no rating'
WHEN rating < 3 THEN 'low'
ELSE 'high' END AS band,
COUNT(*)
FROM feedback
GROUP BY band;
-- high | 260
-- low | 180
-- no rating | 60
Takeaway
= and <> both reject NULL. Any two-way split built from them will quietly lose the unknown rows - check that your buckets sum to the total.
---
