Hook: NULL is SQL's way of saying it is unknown, and interviewers love this question because one wrong = can quietly erase rows from a report.
Question: How do you handle NULL values in SQL when selecting and filtering rows?
Answer: NULL means missing or unknown, not zero or an empty string. To filter it, I use IS NULL or IS NOT NULL, because normal comparisons like = do not work with NULL the way people expect. For display and calculations, I often use COALESCE to replace NULL with a safe default, and I remember that COUNT(column) skips NULLs while COUNT(*) counts rows.
Interview-Ready Answer: I treat NULL as unknown, not as zero or an empty value. In filters, I always use IS NULL or IS NOT NULL because = NULL evaluates to unknown and does not match rows. For output and calculations, I use COALESCE when I want a default, and I remember the big count rule: COUNT(*) counts rows, but COUNT(col) ignores NULLs.
Detailed Explanation:
In SQL, NULL is not a value like 0 or ''. It means missing, unknown, or not applicable. The key idea is three-valued logic (logic with TRUE, FALSE, and UNKNOWN): comparisons against NULL usually become UNKNOWN, and a WHERE clause keeps only rows where the predicate is TRUE.
salary > 50000 or email = NULL.UNKNOWN, not TRUE and not FALSE.WHERE only returns rows where the predicate is TRUE. Rows that evaluate to FALSE or UNKNOWN are filtered out.col = NULL matches nothing, while col IS NULL checks the internal null marker directly.COALESCE is a safe display helper: it returns the first non-NULL value, so you can show a default without changing stored data.NULLIF is the opposite-style helper: it turns a chosen value into NULL, which is useful when a sentinel value like 0 should be treated as missing.Think in terms of intent: are you testing for missing data, showing a fallback, or counting real values?
| Task | Bad choice | Better choice | Why |
|---|---|---|---|
| Test missing | col = NULL | col IS NULL | = gives UNKNOWN |
| Test present | col <> NULL | col IS NOT NULL | same NULL rule |
| Show default | n/a | COALESCE(col, 0) | safe output value |
| Count rows | COUNT(col) | COUNT(*) | skips NULLs |
A plain IS NULL predicate is usually sargable (index-friendly; the optimizer can use an index instead of scanning every row). If you wrap the column in a function inside WHERE, such as COALESCE(col, 0) = 0, you may make the predicate harder to index and force a scan.
O(n) because every row is checked.O(log n + k), where k is the number of matched rows.COUNT(col), SUM(col), and AVG(col) ignore NULL values; if all inputs are NULL, the result can still be NULL.NULLS FIRST or NULLS LAST.The mental model to remember is simple: NULL is not a number, not a blank string, and not false; it is unknown. Once you think that way, the correct SQL usually becomes obvious.
Real-World Story: Imagine a checkout service for an online store. Orders have a nullable shipped_at column, and a dashboard uses it to count pending shipments. A developer writes WHERE shipped_at = NULL instead of IS NULL, so the report shows zero pending orders even though the warehouse is backed up.
What goes wrong next is painful: customer support says, “Why are 400 orders late?” while the operations dashboard looks healthy. Logs show no SQL error, because the query is valid SQL; it just evaluates to UNKNOWN and filters everything out. The bug slips through because the result set is empty, not obviously broken, so the team trusts a bad metric for hours.
The fix is not just changing one operator. The real lesson is to treat nullable fields as first-class data, use IS NULL in filters, and be careful with defaults in reporting queries so the business sees reality, not a misleading zero.
-- NULL handling demo: filtering, defaults, counts, and a common failure path.
-- SQLite-compatible SQL.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer TEXT NOT NULL,
shipped_at TEXT, -- NULL means not shipped yet
discount_pct REAL -- NULL means no discount recorded
);
INSERT INTO orders (order_id, customer, shipped_at, discount_pct) VALUES
(1, 'Ava', NULL, 10.0),
(2, 'Ben', '2026-07-10', NULL),
(3, 'Chloe', NULL, NULL),
(4, 'Diego', '2026-07-11', 5.0);
-- Correct: find rows where the value is missing.
-- This works because IS NULL checks the null marker directly.
SELECT order_id, customer, shipped_at
FROM orders
WHERE shipped_at IS NULL
ORDER BY order_id;
-- Wrong: this returns no rows, because NULL is not equal to anything,
-- and the comparison becomes UNKNOWN instead of TRUE.
SELECT order_id, customer
FROM orders
WHERE shipped_at = NULL;
-- Use COALESCE when you want a display default, not when you want to change stored data.
SELECT
order_id,
customer,
COALESCE(discount_pct, 0) AS discount_pct_for_display,
CASE
WHEN shipped_at IS NULL THEN 'pending'
ELSE 'shipped'
END AS shipping_status
FROM orders
ORDER BY order_id;
-- COUNT(*) counts rows.
-- COUNT(column) counts only non-NULL values.
SELECT
COUNT(*) AS total_orders,
COUNT(shipped_at) AS shipped_orders,
COUNT(*) - COUNT(shipped_at) AS pending_orders
FROM orders;
-- Edge case: if every value in the group is NULL, AVG returns NULL, not 0.
-- COALESCE is useful when the business wants a fallback value.
SELECT
AVG(discount_pct) AS avg_discount_raw,
COALESCE(AVG(discount_pct), 0) AS avg_discount_with_default
FROM orders
WHERE customer IN ('Ben', 'Chloe');
-- NULLIF can turn a sentinel value into NULL.
-- Here, 0 becomes NULL so it can be treated as missing data.
SELECT
order_id,
customer,
NULLIF(discount_pct, 0) AS discount_pct_without_zero_sentinel
FROM orders
ORDER BY order_id;Follow-up & Tricky Questions:
col = NULL return nothing? Because the comparison is not TRUE; it becomes UNKNOWN, and WHERE only keeps TRUE rows.COUNT(*) and COUNT(col)? COUNT(*) counts every row, while COUNT(col) ignores NULLs and counts only rows where the column has a real value.COALESCE instead of CASE? Use COALESCE for a simple default, like replacing NULL with 0 or 'N/A'. Use CASE when the logic has more branches than just “first non-NULL”.GROUP BY? NULLs are grouped together, so all rows with NULL in the grouped column end up in the same group.IS NULL filters? Yes, often it can. A simple IS NULL predicate is usually index-friendly, but wrapping the column in a function in the WHERE clause can block index use.SUM(col) treat NULL as zero? No. Aggregate functions usually ignore NULLs, and if every input is NULL the result can still be NULL.NULL = NULL ever return TRUE? In standard comparisons, no. It is UNKNOWN, which is why you must use NULL-aware syntax or special operators supported by some databases.Common Mistakes:
= NULL or <> NULL — correction: use IS NULL or IS NOT NULL.COUNT(col) counts every row — correction: it skips NULLs; use COUNT(*) for total rows.WHERE — correction: prefer simple predicates like col IS NULL so the optimizer can use indexes more easily.Memory Hook: NULL is a question mark, not a value. You do not compare a question mark with =; you ask whether the box is empty with IS NULL.
Cheat Sheet:
IS NULL / IS NOT NULL for filtering.WHERE keeps only TRUE; FALSE and UNKNOWN are dropped.COALESCE gives a default display value.COUNT(*) counts rows; COUNT(col) skips NULLs.Practice Tasks:
phone_number is NULL.COALESCE(phone_number, 'N/A') in the SELECT list.COUNT(*), COUNT(phone_number), and COUNT(*) - COUNT(phone_number).