The Silent Killer Understanding NULL's Impact on Database Performance
James Reed
Infrastructure Engineer · Leapcell

Introduction
In the world of databases, the concept of "NULL" is ubiquitous. It signifies the absence of a value, an unknown, or a non-applicable state. While seemingly innocuous, developers often fail to grasp the deeper implications of NULL. It's not just another data point; its unique characteristics can fundamentally alter how a database engine optimizes and executes queries. Specifically, NULL values can significantly impact the efficiency of critical database operations such as indexing, aggregate functions like COUNT(), and relational JOIN operations. Understanding these effects is crucial for writing performant and robust database applications. This article delves into how NULL silently influences these key areas, offering a clearer picture of its often-underestimated power.
Core Concepts
Before we dive into the specifics, let's briefly define some core concepts that are central to our discussion:
- NULL: In SQL, NULL represents the absence of any data value. It is not equivalent to zero, an empty string, or false. It's a special marker indicating that data is missing or unknown.
 - Index: A database index is a data structure, typically a B-tree or B+ tree, that improves the speed of data retrieval operations on a database table. It allows the database system to quickly locate the rows that match a query's criteria without scanning the entire table.
 - COUNT(): An aggregate function that returns the number of items in a group. 
COUNT(*)counts all rows, whileCOUNT(column_name)counts non-NULL values in the specified column. - JOIN: A SQL clause used to combine rows from two or more tables based on a related column between them. Common types include 
INNER JOIN,LEFT JOIN,RIGHT JOIN, andFULL OUTER JOIN. - Cardinality: The number of unique values in a column. High cardinality means many unique values, low cardinality means few unique values.
 - Selectivity: The ratio of unique values to the total number of rows. High selectivity means a column's values are good for filtering.
 
NULL and Indexes
The interaction between NULL values and indexes is one of the most critical areas where performance can be affected. Most database systems handle NULLs specifically when building and traversing indexes.
How Indexes Handle NULLs:
Generally, B-tree indexes, which are the most common type, do not explicitly store NULL values in their leaf nodes for columns that allow NULLs. This is because NULL has no inherent order relative to other values, and including them would complicate index structure and traversal logic. However, the exact behavior can vary:
- Most Databases (e.g., MySQL, PostgreSQL, SQL Server): By default, single-column B-tree indexes generally do not include rows where the indexed column is NULL. This means that a query like 
SELECT * FROM my_table WHERE my_column IS NULLwill typically result in a full table scan, even ifmy_columnhas an index. The index is simply not used because it doesn't contain entries for NULLs. - Composite Indexes: In a composite (multi-column) index, if any of the columns in the index are NULL for a given row, that row might not be included in the index, or its entry might be stored in a special way. For example, in MySQL, a row is indexed if all columns in the composite index are NOT NULL. If even one is NULL, the row might not appear in the entire index.
 - Specialized NULL Handling: Some databases offer specific ways to index NULLs. For example, PostgreSQL allows "partial indexes" or using expressions in indexes (
CREATE INDEX ON my_table ((my_column IS NULL))), which can force NULLs to be indexed. SQL Server's filtered indexes can also achieve this by including aWHEREclause (CREATE INDEX ix ON MyTable (Col1) WHERE Col1 IS NOT NULL). 
Example: Impact on Index Usage
Consider a table orders with an index on delivery_date.
CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_id INT, order_date DATE NOT NULL, delivery_date DATE -- Can be NULL if not yet delivered ); CREATE INDEX idx_delivery_date ON orders (delivery_date); -- Query 1: Efficient, uses index EXPLAIN ANALYZE SELECT * FROM orders WHERE delivery_date = '2023-10-26'; -- Query 2: Might result in a full table scan, depending on the DB -- because NULLs are not typically indexed EXPLAIN ANALYZE SELECT * FROM orders WHERE delivery_date IS NULL; -- Query 3: Using a composite index example CREATE INDEX idx_customer_delivery ON orders (customer_id, delivery_date); -- This query might use the composite index for customer_id but then resort to filtering -- for NULL delivery_date if the DB doesn't index NULLs in composite indexes. EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 AND delivery_date IS NULL;
Practical Implications:
- Inefficient 
IS NULLqueries: Queries filtering forIS NULLwill often bypass standard B-tree indexes, leading to full table scans and poor performance, especially on large tables. - Index size and overhead: If a column has many NULLs but is part of an index, the index might still be larger than necessary or less effective if many critical queries involve 
IS NULLpredicates. - Solutions:
- Partial/Filtered Indexes: If your database supports them, create an index specifically for NULL values (
CREATE INDEX idx_null_delivery ON orders ((delivery_date IS NULL))) or for non-NULL values (CREATE INDEX idx_not_null_delivery ON orders (delivery_date) WHERE delivery_date IS NOT NULL). - Default Values: If possible and semantically correct, assign a default non-NULL value (e.g., a specific date 
9999-12-31for not delivered, or0for an integer field). However, this can complicate application logic. - Separate Tables: For columns with very high NULL percentages and distinct query patterns, consider splitting the table or using a sparse column approach, though this adds complexity.
 
 - Partial/Filtered Indexes: If your database supports them, create an index specifically for NULL values (
 
NULL and COUNT() Performance
The COUNT() aggregate function behaves differently based on whether it's COUNT(*) or COUNT(column_name), and this distinction is directly related to NULL values.
COUNT(*): This counts all rows in the result set, regardless of whether any column contains NULLs. It's generally the most efficient form ofCOUNT()because many database systems can optimize it by using primary key indexes (which are always NOT NULL) or by simply fetching row counts from metadata.COUNT(column_name): This form counts only the rows wherecolumn_nameis NOT NULL. If the specified column allows NULLs and has many NULL values,COUNT(column_name)will perform an additional check for NULLity for each row, potentially making it slower thanCOUNT(*). If the column is indexed and NOT NULL,COUNT(column_name)can also be very fast.
Example: COUNT() Behavior
SELECT COUNT(*) FROM orders; -- Counts all rows SELECT COUNT(delivery_date) FROM orders; -- Counts rows where delivery_date IS NOT NULL -- Let's say we have 100,000 orders, with 50,000 having NULL delivery_date. -- COUNT(*) will return 100,000 quickly. -- COUNT(delivery_date) will return 50,000, and might be slower -- because it needs to examine the delivery_date column for each row -- or utilize an index that specifically excludes NULLs.
Performance Implications:
COUNT(*)vs.COUNT(column_name): If you need the total number of rows, always preferCOUNT(*)for performance reasons. If you specifically need to count non-NULL values in a column,COUNT(column_name)is appropriate, but be aware of its potential performance characteristics compared toCOUNT(*).- Index impact on 
COUNT(column_name): Ifcolumn_nameis indexed and NOT NULL,COUNT(column_name)can be very fast as the optimizer can use the index to count entries. However, ifcolumn_nameallows NULLs, the index might not be fully leveraged, or the database might still need to scan the actual rows to check for NULLs for every count. 
Practical Implications:
- Choose 
COUNT(*)strategically: UseCOUNT(*)when you want total rows, not when you specifically care about non-NULL values in a column. - Consider NOT NULL constraints: If a column should never be NULL, enforce a 
NOT NULLconstraint. This not only improves data integrity but also allows the database to make better assumptions for aggregate counts and index usage. 
NULL and JOIN Performance
NULL values have a peculiar effect on JOIN operations due to SQL's three-valued logic (TRUE, FALSE, UNKNOWN). When comparing a value with NULL, the result is always UNKNOWN.
- 
INNER JOIN: Rows are returned only when there's a match in both tables. If the joining column in either table contains a NULL, it will never match another NULL or any non-NULL value, nor will it join with another NULL. Therefore, rows with NULL in the joining column will be excluded from theINNER JOINresult. This is generally performant because fewer rows need to be matched.SELECT o.order_id, c.customer_name FROM orders o INNER JOIN customers c ON o.customer_id = c.customer_id; -- If o.customer_id is NULL for an order, that order will not appear here. - 
LEFT JOIN(andRIGHT JOIN): ALEFT JOINreturns all rows from the left table, and matching rows from the right table. If there's no match for a row in the left table, the columns from the right table will contain NULLs. If the joining column on the right table is NULL, it will not match anything, but the row from the left table will still be included. If the joining column on the left table is NULL, it effectively behaves like anINNER JOINfor that specific value, meaning it won't find a match in the right table's non-NULL values, resulting in NULLs for the right table's columns.SELECT o.order_id, c.customer_name FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id; -- If o.customer_id is NULL for an order, it will still appear. -- c.customer_name will be NULL for those orders. -- If c.customer_id is NULL for a customer, that customer would never join with an order. - 
FULL OUTER JOIN: Returns all rows when there is a match in one of the tables. If some rows don't have a match, NULLs occur on the side of the table that has no match. Again, NULLs in the joining columns themselves will not generate a match. 
Performance Implications:
- No Equality with NULL: The fundamental rule 
NULL = NULLis FALSE (or UNKNOWN to be precise in SQL's three-valued logic). This means that you cannot join on NULL values directly using standard equality operators. If you have many NULLs in your join columns and you intend to join on them (e.g., "join when both are unknown"), you need special handling. - Join Predicate Complexity: If your 
JOINconditions involveORclauses to handleIS NULLscenarios (e.g.,ON a.col = b.col OR (a.col IS NULL AND b.col IS NULL)), these predicates are generally much harder for the optimizer to use indexes efficiently. This can lead to less optimal join plans, such as hash joins or nested loop joins that scan larger parts of the tables. - Index Disuse: If a join predicate like 
a.col = b.colinvolves columns that frequently contain NULLs, and the chosen index doesn't properly handle NULLs (as discussed earlier), the optimizer might choose not to use the index because it knows it won't contain all relevant keys. 
Example: Joining with NULLs
Let's assume we want to join employees and departments tables.
CREATE TABLE employees ( emp_id INT PRIMARY KEY, emp_name VARCHAR(50), dept_id INT -- Can be NULL for unassigned employees ); CREATE TABLE departments ( dept_id INT PRIMARY KEY, dept_name VARCHAR(50) ); -- Imagine employee 'John Doe' has emp.dept_id = NULL. -- Imagine department 'R&D' has dept_id = 1. -- Imagine another employee 'Jane Smith' has emp.dept_id = 1. -- INNER JOIN will exclude John Doe: SELECT e.emp_name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id; -- Results: Jane Smith, R&D -- LEFT JOIN will include John Doe, with NULL department: SELECT e.emp_name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id; -- Results: Jane Smith, R&D -- John Doe, NULL -- Attempt to join NULLs (will not work as expected via equality): -- This will NOT match John Doe's NULL dept_id if another table had a NULL to join on. -- For example, if there was a "unassigned_dept_id" column in 'departments' that was NULL. -- The following would NEVER match when both sides are NULL: SELECT e.emp_name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id OR (e.dept_id IS NULL AND d.dept_id IS NULL); -- This `OR` condition often forces a full scan/expensive join.
Practical Implications:
- Understand NULL behavior in JOINs: Be explicitly aware that 
NULL = NULLis not true. This often catches developers unaware. - Avoid complex 
JOINpredicates: If possible, structure your data or pre-process it to avoidJOINconditions that involveIS NULLlogic, asORconditions with NULLs can be very detrimental to join performance and index utilization. - Sanitize data: If NULL in a join column genuinely represents an unknown or non-applicable state, it might be better to filter those rows before joining or handle them explicitly with 
LEFT JOIN. If NULL implies something specific (e.g., "default department"), consider replacing it with a well-defined non-NULL value to facilitate joins. 
Conclusion
NULLs in a database are more than just empty placeholders; they are a fundamental aspect of SQL's data model with profound performance implications. For indexes, NULLs often lead to their exclusion, forcing full table scans for IS NULL queries. With COUNT(), COUNT(*) reigns supreme in efficiency over COUNT(column_name) due to NULL exclusion. In JOIN operations, NULLs pose a unique challenge, as they defy standard equality comparisons, potentially complicating join predicates and hindering index usage. By understanding these behaviors, developers can design more efficient schema, write smarter queries, and ultimately build more performant database applications. Treating NULLs as the silent performance killers they can be is key to database optimization.