SQL NULL Handling - 340 Employees Missing Bonuses
340 employees missed bonus payments because NULL arithmetic produced $0 compensation.
20+ years shipping high-throughput database systems. Written from production experience, not tutorials.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- 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)
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.
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.
expensive_function()) is safe — the function only runs if a is NULL.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.
The Nullability Tax — Why Your Indexes Lie to You
You've been burned by this before: a query that should fly runs a full table scan. You check the execution plan, and there it is — a NULL-friendly column wrecking your index selectivity. NULLs are not values, but SQL Server still stores them. In a B-tree index, every NULL goes into the same linked list. That means a seek becomes a scan of all NULL rows before you find anything useful.
This isn't a bug. It's a design decision that punishes lazy schema design. If you filter WHERE column IS NULL, the engine can't do a simple equality check. It has to chase pointers. The fix? Filtered indexes. CREATE INDEX idx_active_orders ON orders (status) WHERE status IS NOT NULL. That index only lives where the data exists. Your query suddenly becomes logarithmic instead of linear.
The WHY is simple: NULL breaks the binary assumption of value comparison. Indexes rely on ordered value sets. NULL has no order. Don't let your database pretend otherwise.
Three-Valued Logic — The Boolean Trap That Eats Joins
Here's where juniors burn hours debugging: you write a LEFT JOIN with a WHERE clause, and rows vanish. The culprit is three-valued logic. SQL doesn't do true/false like your programming language. It has TRUE, FALSE, and UNKNOWN. Any comparison with NULL produces UNKNOWN. And WHERE clauses only return rows where the condition is TRUE. UNKNOWN is treated as false.
Example: SELECT * FROM customers c LEFT JOIN orders o ON c.id = o.customer_id WHERE o.total > 100. This kills all customers with no orders — because NULL > 100 is UNKNOWN, not FALSE. The join returns them, but the WHERE filter discards them. You just turned a LEFT JOIN into an INNER JOIN without knowing it.
The fix is explicit: move the condition into the JOIN clause itself. LEFT JOIN orders o ON c.id = o.customer_id AND o.total > 100. Or use COALESCE to provide a default. But understand the logic first. Every time you write a WHERE clause on a nullable column from an outer join, you're playing Russian roulette with your result set.
A Real-World Scenario
Why bother with NULL handling? Because NULLs silently break business logic in production. Consider an employee bonus system: your query calculates annual bonus as base_salary * performance_multiplier. If performance_multiplier is NULL (manager didn't submit review), the result is NULL, not zero. Payroll runs, employees with missing reviews get zero bonus — without any error or warning. The real cost hits when accounting runs reconciliation: the total bonus payout doesn't match the query output by thousands of dollars. You need to catch these NULL-drift paths before they corrupt downstream reports.
Wrapping Up
Three lessons to carry forward. First, NULL is not a value — it's a marker for the absence of a value, and SQL's three-valued logic (TRUE, FALSE, UNKNOWN) means your WHERE clauses can silently exclude rows you expect to include. Second, use COALESCE to replace NULLs with defaults at query output, but never inside JOIN conditions — that masks missing relationships and corrupts referential integrity. Third, NULLIF and NULL-handling in aggregates let you intentionally exclude data (e.g., divide-by-zero prevention). Every NULL in your schema imposes a cognitive tax: each query writer must remember where NULLs live and how they propagate. Normalize your data to avoid optional columns where possible, and document every nullable column's business meaning in your schema comments.
NULL vs DBNULL vs DEFAULT: Database-Specific Behavior
Different databases treat NULL, DBNULL, and DEFAULT values in distinct ways, which can lead to subtle bugs when porting SQL code. In SQL Server, DBNull is a .NET concept representing a database null, but in T-SQL, NULL is the only representation. MySQL treats NULL and DEFAULT differently: a column defined with a DEFAULT value will use that value when no explicit value is provided, but inserting NULL explicitly stores NULL, not the default. PostgreSQL follows the SQL standard strictly: NULL means unknown, and DEFAULT is used only when the column is omitted in an INSERT. For example, in PostgreSQL:
```sql CREATE TABLE employees ( id SERIAL PRIMARY KEY, name TEXT, bonus NUMERIC DEFAULT 0 );
INSERT INTO employees (name) VALUES ('Alice'); -- bonus = 0 (DEFAULT) INSERT INTO employees (name, bonus) VALUES ('Bob', NULL); -- bonus = NULL ```
In MySQL, the same behavior applies, but with a nuance: if the column is defined as NOT NULL DEFAULT 0, inserting NULL will cause an error or be converted to the default depending on the SQL mode. SQL Server behaves similarly to PostgreSQL but with I as a T-SQL extension. Understanding these differences is crucial when writing cross-database applications or migrating data.SNULL()
NULL in Indexes: PostgreSQL vs MySQL vs SQL Server
How databases index NULL values varies significantly and affects query performance. PostgreSQL treats NULLs as distinct values and includes them in B-tree indexes by default. In PostgreSQL, NULLs are considered larger than all non-null values (unless using NULLS FIRST/LAST). MySQL's InnoDB also indexes NULLs, but they are stored at the beginning of the index (like NULLS FIRST). SQL Server indexes NULLs as well, but they are treated as equal to each other and are stored at the end of the index (like NULLS LAST). This difference impacts queries with IS NULL or IS NOT NULL conditions. For example, in PostgreSQL, an index on a nullable column can efficiently support WHERE col IS NULL because NULLs are indexed. In SQL Server, the same index also supports IS NULL efficiently. However, in MySQL, IS NULL can use the index but may require scanning a range of NULL entries. A critical nuance: in PostgreSQL, a unique index allows multiple NULL values (since NULL != NULL), while in SQL Server and MySQL, a unique constraint also allows multiple NULLs. But in older MySQL versions (before 5.7), a unique index allowed only one NULL. Always check your database version. For partial indexes (PostgreSQL), you can create an index that only includes rows where the column is NULL, improving performance for sparse null checks.
COALESCE, NULLIF, ISNULL: Function Comparison Across Dialects
SQL provides several functions to handle NULLs, but their syntax and behavior vary across databases. COALESCE is standard SQL and returns the first non-null argument. It is supported in PostgreSQL, MySQL, SQL Server, and others. NULLIF returns NULL if two expressions are equal, otherwise returns the first expression. ISNULL is a T-SQL function in SQL Server that replaces NULL with a specified value, but it is not standard. MySQL has IFNULL which does the same. PostgreSQL does not have ISNULL; use COALESCE instead. For example:
```sql -- COALESCE: standard, works everywhere SELECT COALESCE(bonus, 0) AS effective_bonus FROM employees;
-- NULLIF: returns NULL if equal SELECT NULLIF(commission, 0) AS non_zero_commission FROM sales;
-- ISNULL (SQL Server only) SELECT ISNULL(bonus, 0) FROM employees;
-- IFNULL (MySQL only) SELECT IFNULL(bonus, 0) FROM employees; ```
A common pitfall: ISNULL in SQL Server takes only two arguments, while COALESCE can take multiple. Also, COALESCE evaluates arguments lazily in some databases, but not all. In PostgreSQL, COALESCE evaluates all arguments before checking for NULL, which can cause side effects if using functions. NULLIF is often used to avoid division by zero: NULLIF(denominator, 0). In MySQL, IFNULL is limited to two arguments, so use COALESCE for more. For cross-database compatibility, prefer COALESCE and NULLIF over vendor-specific functions.
Bonus Payments Not Sent to 340 Employees Due to NULL Arithmetic
- 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
| File | Command / Code | Purpose |
|---|---|---|
| null_basics_check.sql | CREATE TABLE employees ( | What NULL Actually Is |
| coalesce_bonus_report.sql | SELECT | COALESCE |
| nullif_and_aggregates.sql | CREATE TABLE sales_performance ( | NULLIF and Aggregate Functions |
| FilteredIndexFix.sql | CREATE TABLE orders ( | The Nullability Tax |
| JoinLogicMeltdown.sql | CREATE TABLE customers ( | Three-Valued Logic |
| BonusNullDrift.sql | SELECT | A Real-World Scenario |
| NullGuardPattern.sql | SELECT | Wrapping Up |
| null-vs-default.sql | CREATE TABLE test ( | NULL vs DBNULL vs DEFAULT |
| null-indexes.sql | CREATE INDEX idx_bonus ON employees(bonus); | NULL in Indexes |
| null-functions.sql | SELECT COALESCE(bonus, commission, 0) AS payment FROM employees; | COALESCE, NULLIF, ISNULL |
Key takeaways
Interview Questions on This Topic
Why does WHERE column_name = NULL return no rows in SQL?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Written from production experience, not tutorials.
That's SQL Basics. Mark it forged?
7 min read · try the examples if you haven't