Home Database SQL NULL Handling Explained — IS NULL, COALESCE and Common Traps

SQL NULL Handling Explained — IS NULL, COALESCE and Common Traps

In Plain English 🔥
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.
⚡ Quick Answer
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.sql · SQL
123456789101112131415161718192021222324252627282930313233343536
-- 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.

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.sql · SQL
1234567891011121314151617181920212223242526
-- 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.

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.sql · SQL
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
-- 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.
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

  • NULL means 'value unknown or absent' — it is NOT zero, NOT an empty string, and NOT false. It represents the total absence of information.
  • 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.
  • 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.
  • 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

  • Mistake 1: Using = NULL in a WHERE clause — Symptom: the query returns zero rows silently, even though NULLs clearly exist in the column — Fix: always replace = NULL with IS NULL and replace != NULL or <> NULL with IS NOT NULL. SQL evaluates = NULL as UNKNOWN, which WHERE treats as false.
  • Mistake 2: Forgetting that NULL arithmetic poisons the whole result — Symptom: a calculated column like base_salary + annual_bonus returns NULL for the entire row when annual_bonus is NULL, even though base_salary is a perfectly valid number — Fix: wrap nullable columns in COALESCE before doing arithmetic: base_salary + COALESCE(annual_bonus, 0).
  • Mistake 3: Assuming COUNT() and COUNT(column) are the same — Symptom: a report shows an unexpected row count because COUNT(column_name) silently skips NULL values while COUNT() counts every row — Fix: be deliberate about which you use. Use COUNT(*) to count all rows, COUNT(column_name) to count rows where that column has a value, and COUNT(DISTINCT column_name) to count unique non-NULL values.

Interview Questions on This Topic

  • QWhy does WHERE column_name = NULL return no rows in SQL, and how do you correctly filter for NULL values?
  • QWhat is the difference between COUNT(*) and COUNT(column_name)? Give an example where they return different results.
  • QYou have a query that calculates total compensation as base_salary + bonus. Some employees have no bonus yet. The results are showing NULL for total_compensation. How do you fix it, and can you explain why it happened?

Frequently Asked Questions

Is NULL the same as an empty string or zero in SQL?

No — NULL, empty string (''), and zero (0) are three completely different things in SQL. NULL means no value exists at all. An empty string is a value — it's just a string with no characters. Zero is a numeric value. A column can hold '' or 0 and those are not NULL. Use IS NULL to check for true absence of data.

Why does NULL = NULL return false (or UNKNOWN) in SQL?

Because NULL means 'unknown', and two unknowns are not necessarily the same unknown. SQL follows three-valued logic: TRUE, FALSE, and UNKNOWN. Comparing NULL to anything — including another NULL — produces UNKNOWN, not TRUE. This is by design in the SQL standard. To check if two columns are both NULL or both the same value, use IS NOT DISTINCT FROM in PostgreSQL, or a combination of IS NULL checks in other databases.

What happens when I use NULL in a JOIN condition?

NULLs in JOIN columns are silently dropped. Because NULL = NULL is UNKNOWN (not TRUE), any row where the join column contains NULL on either side will not match and won't appear in the result set. This is a common source of mysteriously missing rows in query results. If you need to match NULL-to-NULL in a join, use IS NOT DISTINCT FROM (PostgreSQL) or COALESCE the column to a sentinel value that can't appear in real data.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousSQL UNION and INTERSECTNext →SQL Indexes
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged