SQL IS NULL and IS NOT NULL
NULL is one of the most misunderstood concepts in SQL. It doesn’t mean zero, it doesn’t mean empty string — it means “unknown value.” Understanding how NULL behaves is essential for writing correct queries.
What is NULL?
NULL represents the absence of a value — information that’s unknown, missing, or not applicable:
- A customer with no phone number:
phone = NULL - An order that hasn’t shipped:
shipped_at = NULL - A product with no description:
description = NULL
NULL is not a value you can compare with =. Comparing NULL to anything — even another NULL — returns NULL (unknown), not TRUE:
NULL = NULL -- NULL (not TRUE!)NULL != NULL -- NULL (not FALSE!)NULL = 5 -- NULLNULL > 0 -- NULL1 = NULL -- NULLBecause of this, WHERE column = NULL never returns any rows.
IS NULL
Use IS NULL to find rows where a column has no value:
-- Find customers without a phone numberSELECT name, emailFROM customersWHERE phone IS NULL;
-- Find orders that haven't been shippedSELECT order_id, customer_id, amountFROM ordersWHERE shipped_at IS NULL AND status != 'cancelled';
-- Find products with no category assignedSELECT product_id, nameFROM productsWHERE category_id IS NULL;IS NOT NULL
Use IS NOT NULL to find rows where a column has a value:
-- Find customers who have provided a phone numberSELECT name, phone FROM customers WHERE phone IS NOT NULL;
-- Find orders that have been shippedSELECT order_id, shipped_at FROM orders WHERE shipped_at IS NOT NULL;
-- Require all contact fields to be presentSELECT name FROM customersWHERE email IS NOT NULL AND phone IS NOT NULL AND address IS NOT NULL;Common Mistake: Using = NULL
This is a very common error that silently returns no rows:
-- WRONG: always returns 0 rowsSELECT * FROM customers WHERE phone = NULL;SELECT * FROM orders WHERE shipped_at = NULL;
-- CORRECTSELECT * FROM customers WHERE phone IS NULL;SELECT * FROM orders WHERE shipped_at IS NULL;NULL in Aggregate Functions
All standard aggregate functions ignore NULL values:
SELECT COUNT(*) AS total_rows, -- counts all rows including NULLs COUNT(phone) AS with_phone, -- only counts rows where phone is NOT NULL COUNT(DISTINCT city) AS unique_cities, -- unique non-null cities SUM(amount) AS total_amount, -- ignores NULL amounts AVG(amount) AS avg_amount -- ignores NULL amounts (denominator is non-null count)FROM customers;AVG divides by the count of non-null values, not total rows. If 2 out of 10 amounts are NULL, AVG(amount) divides by 8, not 10.
NULL in JOIN Conditions
NULL values in join columns never match — they’re excluded from INNER JOINs:
-- If product_id is NULL in order_items, that row won't match any productSELECT p.name, oi.quantityFROM products pJOIN order_items oi ON p.product_id = oi.product_id;-- Rows where oi.product_id IS NULL are excludedUse LEFT JOIN to keep rows even when the join column is NULL:
SELECT oi.order_id, p.name, oi.quantityFROM order_items oiLEFT JOIN products p ON oi.product_id = p.product_id;-- Rows where oi.product_id is NULL appear with p.name = NULLCOALESCE: Replace NULL with a Default
COALESCE returns the first non-NULL argument:
-- Display "No phone" when phone is NULLSELECT name, COALESCE(phone, 'No phone on file') AS phone_displayFROM customers;
-- Use 0 when amount is NULLSELECT order_id, COALESCE(discount_amount, 0) AS discountFROM orders;
-- Try multiple fallbacksSELECT COALESCE(mobile_phone, home_phone, work_phone, 'No number') AS best_phoneFROM contacts;NULLIF: Convert a Value to NULL
NULLIF(a, b) returns NULL if a = b, otherwise returns a. Useful to avoid division by zero:
-- Avoid division by zeroSELECT revenue / NULLIF(visits, 0) AS revenue_per_visitFROM campaign_stats;-- When visits = 0, returns NULL instead of error
-- Treat empty strings as NULLSELECT NULLIF(TRIM(notes), '') AS notes_cleanedFROM records;NULL in ORDER BY
NULLs sort differently by default in different databases:
- PostgreSQL: NULLs last in ASC, first in DESC
- MySQL: NULLs first in both ASC and DESC
- SQL Server: NULLs last in ASC, first in DESC
Control NULL sort order explicitly (PostgreSQL):
ORDER BY last_login DESC NULLS LAST -- put NULLs at the endORDER BY last_login ASC NULLS FIRST -- put NULLs at the startNULL vs Empty String
NULL and empty string ('') are different in SQL:
-- NULL: no value was ever providedWHERE notes IS NULL
-- Empty string: value was provided, but it's blankWHERE notes = ''
-- Check for either (no useful content)WHERE notes IS NULL OR notes = ''WHERE COALESCE(NULLIF(TRIM(notes), ''), NULL) IS NULL -- null or blankIn PostgreSQL, '' and NULL are completely different. In Oracle, they’re treated the same (empty string becomes NULL).
Practical Examples
Report completeness: find records with missing required fields:
SELECT customer_id, name, CASE WHEN email IS NULL THEN 'Missing' ELSE 'OK' END AS email_status, CASE WHEN phone IS NULL THEN 'Missing' ELSE 'OK' END AS phone_status, CASE WHEN address IS NULL THEN 'Missing' ELSE 'OK' END AS address_statusFROM customersWHERE email IS NULL OR phone IS NULL OR address IS NULLORDER BY name;Safe average that excludes outliers:
SELECT AVG(NULLIF(response_time_ms, 0)) AS avg_response_timeFROM api_logsWHERE endpoint = '/api/search';-- Zeros (likely errors or cached) treated as NULL and excluded from average