Junior 3 min · March 05, 2026

SQL NULL Handling - 340 Employees Missing Bonuses

340 employees missed bonus payments because NULL arithmetic produced $0 compensation.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • NULL means unknown or absent — not zero, not empty string, not false
  • NULL comparisons always return UNKNOWN (not TRUE or FALSE) — = NULL, != NULL, < NULL all produce UNKNOWN
  • Use IS NULL and IS NOT NULL — these are the only correct ways to test for NULL
  • COALESCE(a, b, c) returns the first non-NULL value — the standard NULL-to-default replacement
  • NULLIF(a, b) returns NULL if a equals b — converts a specific value to NULL on purpose
  • Biggest mistake: NULL arithmetic — any expression involving NULL produces NULL (5 + NULL = NULL)
Plain-English First

Imagine you're filling out a form and you leave the 'middle name' box completely blank — not a dash, not a space, just nothing at all. That blank box is NULL in SQL. It doesn't mean zero, it doesn't mean empty string, it means 'we simply don't know or don't have this information.' Because NULL represents the absence of a value, normal math and comparisons don't work on it the same way — you can't ask 'is nothing equal to nothing?' and get a sensible yes or no.

Every real-world database has missing data. A customer who didn't provide a phone number. An order with no shipping date yet because it hasn't shipped. An employee whose bonus hasn't been decided. These gaps are everywhere, and SQL has a special way of representing them — NULL. If you don't understand NULL, your queries will silently return wrong answers without throwing a single error, which is one of the most dangerous bugs a database can have.

The tricky part is that NULL breaks the rules you're used to. In normal life, if you ask 'is blank equal to blank?' you'd say yes. SQL says 'I don't know' — and that 'I don't know' answer is what causes queries to behave unexpectedly. Filters exclude rows you expected to include, calculations return NULL when you expected a number, and JOINs quietly drop data. SQL gives you specific tools — IS NULL, IS NOT NULL, COALESCE, and NULLIF — to handle these gaps safely and deliberately.

By the end of this article you'll understand what NULL actually means (and what it doesn't), why you can't use = NULL in a WHERE clause, how to filter and replace NULLs with confidence, and how to avoid the silent bugs that trip up even experienced developers. You'll be writing NULL-safe SQL from scratch.

What NULL Actually Is — and Why = NULL Never Works

NULL isn't a value — it's the absence of a value. Think of it like a sealed envelope. You don't know what's inside. If someone hands you two sealed envelopes and asks 'are these the same?', the honest answer is 'I have no idea' — not yes, not no. SQL agrees. Comparing anything to NULL using = produces a third logical state called UNKNOWN, which is neither TRUE nor FALSE.

This is why WHERE salary = NULL will never return any rows, even if the salary column genuinely contains NULLs. SQL evaluates the condition, gets UNKNOWN for every row, and excludes them all. WHERE only keeps rows where the condition is TRUE.

SQL gives you a dedicated operator for NULL checks: IS NULL and IS NOT NULL. These are the only correct ways to test whether a value is absent. They return TRUE or FALSE — no UNKNOWN — which is exactly what WHERE needs to filter rows correctly.

This also means NULL = NULL is NOT true in SQL. Two unknown things are not automatically equal. This catches beginners off guard constantly, especially when writing JOIN conditions.

null_basics_check.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- Create a simple employees table to demonstrate NULL behavior
CREATE TABLE employees (
    employee_id   INT PRIMARY KEY,
    full_name     VARCHAR(100) NOT NULL,
    department    VARCHAR(50),
    phone_number  VARCHAR(20),   -- nullable: not every employee has provided one
    annual_bonus  DECIMAL(10,2)  -- nullable: bonus not yet decided for some staff
);

-- Insert sample rows — some have NULLs, some don't
INSERT INTO employees VALUES (1, 'Alice Mercer',   'Engineering', '555-0101', 4500.00);
INSERT INTO employees VALUES (2, 'Ben Okafor',     'Marketing',   NULL,       3200.00);
INSERT INTO employees VALUES (3, 'Clara Huang',    'Engineering', '555-0303', NULL);
INSERT INTO employees VALUES (4, 'David Nkosi',    'HR',          NULL,       NULL);
INSERT INTO employees VALUES (5, 'Eva Johansson',  'Marketing',   '555-0505', 2800.00);

-- ❌ WRONG: This returns 0 rows even though NULLs exist in phone_number
-- WHERE evaluates phone_number = NULL as UNKNOWN for every row
SELECT full_name
FROM   employees
WHERE  phone_number = NULL;
-- Output: (0 rows) — silent bug!

-- ✅ CORRECT: Use IS NULL to find employees without a phone number
SELECT full_name
FROM   employees
WHERE  phone_number IS NULL;
-- Output: Ben Okafor, David Nkosi

-- ✅ CORRECT: Use IS NOT NULL to find employees who DO have a phone number
SELECT full_name, phone_number
FROM   employees
WHERE  phone_number IS NOT NULL;
-- Output: Alice Mercer | 555-0101
--         Clara Huang  | 555-0303
--         Eva Johansson| 555-0505
Output
-- phone_number = NULL query:
(0 rows returned)
-- IS NULL query:
full_name
----------
Ben Okafor
David Nkosi
-- IS NOT NULL query:
full_name | phone_number
----------------|-------------
Alice Mercer | 555-0101
Clara Huang | 555-0303
Eva Johansson | 555-0505
Watch Out:
Never use = NULL or != NULL in a WHERE clause. They will silently return zero rows without any error message. SQL won't warn you — it'll just quietly drop your data. Always use IS NULL or IS NOT NULL.
Production Insight
The three-valued logic of SQL (TRUE, FALSE, UNKNOWN) is the source of countless production bugs.
NULL in a WHERE condition produces UNKNOWN, which is treated as FALSE — the row is excluded.
CONCAT with NULL produces NULL in some databases (PostgreSQL) and skips NULL in others (MySQL) — use COALESCE before CONCAT.
Key Takeaway
NULL = NULL is UNKNOWN, not TRUE — never use = or != to test for NULL.
Any expression involving NULL produces NULL — the result is unknown, not zero or false.
IS NULL and IS NOT NULL are the only correct operators for NULL tests.

COALESCE — Replacing NULL With a Sensible Default

Now that you can detect NULLs, the next question is: what do you do with them? Often you want to replace a NULL with a default value so your reports and application logic get something useful instead of a blank.

That's exactly what COALESCE does. You give it a list of values and it hands back the first one that isn't NULL. Think of it like a backup plan — 'give me the first option, but if that's blank, try the second, and if that's blank too, try the third.'

Coalesce is standard SQL and works in MySQL, PostgreSQL, SQL Server, SQLite, and Oracle. It's one of the most-used NULL-handling functions in production databases.

A key use case is calculated columns. If annual_bonus is NULL and you add it to a base salary, the entire result becomes NULL — SQL says 'I can't add a known number to an unknown number and give you a real answer.' COALESCE lets you substitute zero (or any default) so your arithmetic stays intact.

COALESCE can take more than two arguments. COALESCE(a, b, c, d) returns the first non-NULL across all four — really useful when you have primary, secondary, and fallback data sources.

coalesce_bonus_report.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- COALESCE(value_to_check, fallback_if_null)
-- Returns the first argument that is NOT NULL

-- Use case 1: Display a friendly label instead of NULL in a report
SELECT
    full_name,
    COALESCE(phone_number, 'No phone on file') AS contact_number
FROM employees;

-- Use case 2: Safe arithmetic — treat a missing bonus as 0
-- Without COALESCE, NULL bonus would make the entire total NULL
SELECT
    full_name,
    80000                             AS base_salary,
    COALESCE(annual_bonus, 0)         AS bonus,           -- swap NULL for 0
    80000 + COALESCE(annual_bonus, 0) AS total_compensation
FROM employees;

-- Use case 3: Multiple fallbacks — try preferred_email, then work_email, then a default
-- (Imaginary columns to show the pattern)
-- COALESCE(preferred_email, work_email, 'no-email@company.com')

-- Use case 4: COALESCE in a WHERE clause to filter on computed values
SELECT full_name
FROM   employees
WHERE  COALESCE(annual_bonus, 0) < 3000;  -- treats NULL bonus as 0 for comparison
Output
-- Contact number report:
full_name | contact_number
----------------|------------------
Alice Mercer | 555-0101
Ben Okafor | No phone on file
Clara Huang | 555-0303
David Nkosi | No phone on file
Eva Johansson | 555-0505
-- Total compensation report:
full_name | base_salary | bonus | total_compensation
----------------|-------------|---------|-------------------
Alice Mercer | 80000 | 4500.00 | 84500.00
Ben Okafor | 80000 | 3200.00 | 83200.00
Clara Huang | 80000 | 0.00 | 80000.00
David Nkosi | 80000 | 0.00 | 80000.00
Eva Johansson | 80000 | 2800.00 | 82800.00
-- Employees with bonus under 3000 (NULLs treated as 0):
full_name
----------
Clara Huang
David Nkosi
Eva Johansson
Pro Tip:
COALESCE is the standard SQL way to handle NULLs. MySQL also has IFNULL(value, fallback) which does the same job but only takes two arguments. COALESCE is more portable — use it when you care about your SQL running across different database engines.
Production Insight
COALESCE is evaluated lazily — it returns the first non-NULL from left to right and stops evaluating further arguments.
COALESCE(a, expensive_function()) is safe — the function only runs if a is NULL.
For default display values in reports, COALESCE(col, 'N/A') is cleaner than CASE WHEN col IS NULL THEN 'N/A' ELSE col END.
Key Takeaway
COALESCE is the standard NULL-to-default function — use it in any column that might be NULL in reports.
COALESCE(a, b, c) returns the first non-NULL — multiple fallback values are supported.
Always COALESCE numeric columns before arithmetic when NULL should mean zero.

NULLIF and Aggregate Functions — NULLs You Create on Purpose

COALESCE turns NULLs into real values. NULLIF does the opposite — it turns a specific real value into NULL. Why on earth would you want that?

The classic example is preventing division by zero. If you divide by zero in SQL, you get an error. But if you use NULLIF(denominator, 0), SQL turns any zero denominator into NULL — and dividing by NULL returns NULL instead of crashing. You can then wrap that in COALESCE to display something readable.

Aggregate functions like COUNT, SUM, AVG, MIN, and MAX have a specific and important relationship with NULL: they all silently skip NULL values. This is usually what you want — averaging a salary column where some salaries aren't set yet should average the known salaries, not treat unknowns as zero. But it means COUNT(*) and COUNT(column_name) give different answers, which surprises many beginners.

COUNT(*) counts every row. COUNT(phone_number) counts only rows where phone_number is not NULL. This distinction matters enormously in reports.

nullif_and_aggregates.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
-- NULLIF(expression, unwanted_value)
-- Returns NULL if expression equals unwanted_value, otherwise returns expression

-- Use case: safe division — avoid divide-by-zero error
-- Imagine a sales table with targets and actual sales
CREATE TABLE sales_performance (
    rep_name    VARCHAR(100),
    target      INT,
    actual_sold INT
);

INSERT INTO sales_performance VALUES ('Nia Patel',    100, 85);
INSERT INTO sales_performance VALUES ('Omar Silva',   0,   0);  -- new rep, no target set
INSERT INTO sales_performance VALUES ('Priya Chen',   200, 210);

-- ❌ WRONG: This crashes when target = 0
-- SELECT actual_sold / target FROM sales_performance;

-- ✅ CORRECT: NULLIF converts 0 target to NULL, avoiding the crash
-- COALESCE then turns the NULL result into a readable message
SELECT
    rep_name,
    target,
    actual_sold,
    COALESCE(
        CAST(actual_sold AS DECIMAL) / NULLIF(target, 0) * 100,
        0
    ) AS achievement_pct   -- 0 when target was NULL/zero
FROM sales_performance;

-- ─────────────────────────────────────────
-- Aggregate functions and NULL — key demo
-- ─────────────────────────────────────────

-- COUNT(*) counts ALL rows including those with NULLs
SELECT COUNT(*) AS total_employees
FROM   employees;
-- Returns: 5

-- COUNT(column) counts only NON-NULL values in that column
SELECT COUNT(annual_bonus) AS employees_with_bonus
FROM   employees;
-- Returns: 3  (Clara and David have NULL bonus — they are skipped)

-- AVG also skips NULLs — it averages only the 3 known bonuses
SELECT AVG(annual_bonus) AS average_known_bonus
FROM   employees;
-- Returns: 3500.00  (4500 + 3200 + 2800) / 3

-- If you want NULL bonuses treated as 0 in the average:
SELECT AVG(COALESCE(annual_bonus, 0)) AS average_bonus_including_none
FROM   employees;
-- Returns: 2100.00  (4500 + 3200 + 0 + 0 + 2800) / 5
Output
-- Safe division result:
rep_name | target | actual_sold | achievement_pct
------------|--------|-------------|----------------
Nia Patel | 100 | 85 | 85.00
Omar Silva | 0 | 0 | 0.00
Priya Chen | 200 | 210 | 105.00
-- COUNT(*): 5
-- COUNT(annual_bonus): 3
-- AVG(annual_bonus): 3500.00
-- AVG with COALESCE: 2100.00
Interview Gold:
Interviewers love asking 'what's the difference between COUNT() and COUNT(column_name)?' The answer is that COUNT() includes NULL rows in its count; COUNT(column_name) does not. Knowing this cold will impress any SQL interviewer.
Production Insight
NULLIF is the least-used NULL function but solves division-by-zero cleanly: revenue / NULLIF(clicks, 0) returns NULL instead of dividing by zero.
DIVIDE BY NULL produces NULL (not an error) in SQL — the NULLIF pattern exploits this to avoid exceptions.
NULLIF is also used to turn sentinel values like 0, -1, or 'N/A' into NULL for proper NULL handling downstream.
Key Takeaway
NULLIF(a, b) = NULL when a equals b — use it to turn zero into NULL before division to avoid divide-by-zero.
NULLIF is the clean alternative to CASE WHEN divisor = 0 THEN NULL ELSE numerator / divisor END.
Aggregate functions skip NULLs — converting unwanted values to NULL with NULLIF makes aggregates exclude them.
● Production incidentPOST-MORTEMseverity: high

Bonus Payments Not Sent to 340 Employees Due to NULL Arithmetic

Symptom
340 employees didn't receive their quarterly payment. Payroll ran successfully without errors. The employees' base salary existed; their bonus_amount was NULL (not yet approved for that quarter).
Assumption
The calculation base_salary + bonus_amount would treat NULL bonus as zero, paying just the base salary component.
Root cause
NULL + any number = NULL. The payment calculation produced NULL for employees without an approved bonus, and the downstream system treated NULL total_compensation as 'no payment required'.
Fix
Changed to base_salary + COALESCE(bonus_amount, 0). Added a pre-run data quality check: SELECT COUNT(*) FROM employees WHERE total_compensation IS NULL before any payment batch.
Key lesson
  • NULL arithmetic is NULL — any calculation involving NULL produces NULL, not zero
  • Always COALESCE numeric columns that can be NULL before performing arithmetic
  • Add data quality checks before any financial batch run: assert zero NULL values in critical payment columns
Production debug guideDiagnosing invisible rows, wrong aggregates, and arithmetic surprises3 entries
Symptom · 01
WHERE filter returns zero rows but data definitely exists
Fix
You are using = NULL. Run SELECT COUNT(*) FROM table WHERE col IS NULL to confirm NULL rows exist. Then use IS NULL for the filter. WHERE col = NULL always returns zero rows because NULL = NULL evaluates to UNKNOWN.
Symptom · 02
SUM or AVG is lower than expected
Fix
NULL values are excluded from SUM and AVG. Run SELECT COUNT(), COUNT(col), SUM(col) together — the difference between COUNT() and COUNT(col) is your NULL row count. Use COALESCE(col, 0) inside the aggregate if zero is the correct treatment.
Symptom · 03
Calculated column shows NULL for some rows unexpectedly
Fix
One of the input columns is NULL. NULL arithmetic propagates: price NULL = NULL, price + NULL = NULL. Find which column is NULL with: SELECT id, price, discount, price (1 - discount) FROM products WHERE price * (1 - discount) IS NULL.
AspectIS NULL / IS NOT NULLCOALESCENULLIF
PurposeTest whether a value is absentReplace NULL with a fallback valueTurn a specific value into NULL
ReturnsTRUE or FALSEFirst non-NULL value from the listNULL or the original value
Use in WHERE clauseYes — primary use caseYes — to filter on substituted valuesRarely — mainly in SELECT or arithmetic
Number of arguments1 (the column to test)2 or more (value then fallbacks)Exactly 2
Prevents errorsNo — detection onlyPrevents NULL propagation in mathPrevents divide-by-zero crashes
SQL standardYes — all major databasesYes — all major databasesYes — all major databases
MySQL-specific alternativeNone neededIFNULL(value, fallback)No direct alternative

Key takeaways

1
NULL means 'value unknown or absent'
it is NOT zero, NOT an empty string, and NOT false. It represents the total absence of information.
2
You can NEVER test for NULL with = or !=. Use IS NULL and IS NOT NULL exclusively
using = NULL silently returns zero rows with no error.
3
COALESCE(column, fallback) is your go-to tool for replacing NULL with a safe default
use it in SELECT to display fallbacks, in arithmetic to prevent NULL propagation, and in WHERE to filter on computed defaults.
4
Aggregate functions (SUM, AVG, MIN, MAX, COUNT(column)) all skip NULL rows automatically
COUNT(*) is the only one that counts every row regardless of NULLs. This distinction changes your totals and averages in ways that can silently break reports.

Common mistakes to avoid

3 patterns
×

Using = NULL in a WHERE clause

Symptom
The query returns zero rows silently — even when NULL rows definitely exist in the column. No error is thrown, making this especially hard to debug.
Fix
Use IS NULL for NULL equality: WHERE phone IS NULL. Use IS NOT NULL to exclude NULLs. The = operator produces UNKNOWN (not TRUE) when either operand is NULL, and UNKNOWN rows are excluded from WHERE results.
×

NULL arithmetic returning NULL instead of the expected value

Symptom
Calculated columns (base_salary + bonus) return NULL for rows where one input is NULL — payments skipped, totals wrong, no error raised
Fix
COALESCE every potentially-NULL numeric column before arithmetic: base_salary + COALESCE(bonus, 0). Add a data quality check before any batch calculation: SELECT COUNT(*) FROM table WHERE critical_column IS NULL should return zero.
×

Assuming COUNT(*) and COUNT(column) return the same value

Symptom
A report shows an unexpectedly low count — the discrepancy equals the number of NULL rows in the counted column, which COUNT(column) silently excludes
Fix
COUNT() counts all rows; COUNT(column) counts only non-NULL rows. Run both together: SELECT COUNT() AS all_rows, COUNT(email) AS with_email FROM users to expose the difference. Use COUNT(*) for row counts and COUNT(column) only when you explicitly want to exclude NULLs.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why does WHERE column_name = NULL return no rows in SQL?
Q02JUNIOR
What does COALESCE do and when would you use it?
Q03JUNIOR
You have a query that calculates total compensation as base_salary + bon...
Q01 of 03JUNIOR

Why does WHERE column_name = NULL return no rows in SQL?

ANSWER
In SQL, NULL represents an unknown value. The comparison expression NULL = NULL evaluates to UNKNOWN (SQL uses three-valued logic: TRUE, FALSE, UNKNOWN) — not TRUE. The WHERE clause only includes rows where the condition evaluates to TRUE, so rows where the comparison evaluates to UNKNOWN are excluded. This is why = NULL never matches any row. The correct operators are IS NULL (returns TRUE when the value is NULL) and IS NOT NULL (returns TRUE when the value is not NULL). These are special operators specifically designed to test for the absence of a value, where = cannot be used.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Is NULL the same as an empty string or zero in SQL?
02
Why does NULL = NULL return false (or UNKNOWN) in SQL?
03
What happens when I use NULL in a JOIN condition?
🔥

That's SQL Basics. Mark it forged?

3 min read · try the examples if you haven't

Previous
SQL UNION and INTERSECT
12 / 16 · SQL Basics
Next
SQL CASE Statement