Loading learning content...
We've learned that NULL represents missing information and that three-valued logic governs how NULL propagates through expressions. But in practical applications, we often need to replace NULL with a concrete value—a default, a placeholder, or a fallback.
This is where NULL-handling functions come in:
These functions transform NULL uncertainty into definite values, making calculations possible, displays cleaner, and business logic predictable.
By the end of this page, you will master COALESCE and NVL in depth—understanding their syntax, subtle behavioral differences, performance implications, advanced patterns for chaining fallbacks, and best practices for choosing between them.
COALESCE is an ANSI SQL standard function that returns the first non-NULL value from a list of expressions. It's universally supported across PostgreSQL, MySQL, SQL Server, Oracle, SQLite, and all other major databases.
Syntax:
COALESCE(expression1, expression2, ..., expressionN)
Behavior:
123456789101112131415161718192021222324252627282930313233343536
-- Basic COALESCE usage -- Provide a default value when column is NULLSELECT customer_id, first_name, COALESCE(middle_name, '') AS middle_name, -- Empty string if NULL last_name, COALESCE(phone, 'No phone on file') AS phone, COALESCE(email, 'No email on file') AS emailFROM customers; -- Numeric default valuesSELECT product_id, product_name, COALESCE(discount, 0) AS discount, -- 0 if no discount COALESCE(stock_quantity, 0) AS stock, -- 0 if unknown stock price * (1 - COALESCE(discount, 0) / 100) AS final_priceFROM products; -- Date default valuesSELECT order_id, order_date, COALESCE(ship_date, CURRENT_DATE) AS ship_date_or_today, COALESCE(delivery_date, ship_date, order_date) AS best_known_dateFROM orders; -- Boolean-like default valuesSELECT user_id, username, COALESCE(is_active, TRUE) AS is_active, COALESCE(is_admin, FALSE) AS is_adminFROM users;COALESCE is essential for calculations involving nullable columns. Since NULL propagates through arithmetic (5 + NULL = NULL), wrapping nullable operands with COALESCE ensures you get a numeric result: price * COALESCE(quantity, 0) returns 0 when quantity is NULL, not NULL.
One of COALESCE's most powerful features is the ability to specify multiple fallback expressions. SQL evaluates them in order until one returns non-NULL.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
-- Multiple fallback values for contact informationSELECT customer_id, -- Try work phone, then mobile, then home, then 'No phone' COALESCE(work_phone, mobile_phone, home_phone, 'No phone') AS best_phone, -- Try personal email, then work email, then 'No email' COALESCE(personal_email, work_email, 'No email') AS best_emailFROM customer_contacts; -- Hierarchical address fallbackSELECT customer_id, COALESCE( shipping_address, -- First choice: explicit shipping address billing_address, -- Second choice: billing address primary_address, -- Third choice: primary address 'Address Required' -- Final fallback: placeholder ) AS delivery_addressFROM customers; -- Cascading name displaySELECT user_id, COALESCE( display_name, -- User's chosen display name username, -- Username if no display name email, -- Email if no username 'Anonymous User' -- Last resort ) AS shown_nameFROM users; -- Fallback calculation sourcesSELECT product_id, product_name, COALESCE( promotional_price, -- Use promo price if running sale_price, -- Use sale price if on sale regular_price, -- Use regular price 0 -- Emergency fallback (shouldn't happen) ) AS effective_priceFROM products; -- Fallback from related tables via subqueriesSELECT e.employee_id, e.name, COALESCE( e.direct_manager_id, e.department_default_manager_id, ( SELECT id FROM employees WHERE role = 'CEO' LIMIT 1 ) ) AS effective_manager_idFROM employees e;COALESCE stops at the first non-NULL value. Order your expressions from most preferred to least preferred. Each expression is only evaluated if all previous expressions were NULL, which can impact performance if expressions are expensive (like subqueries).
NVL is Oracle's proprietary function for handling NULL values. It predates the ANSI COALESCE and remains widely used in Oracle environments.
Syntax:
NVL(expression, replacement_value)
Behavior:
expression if it's not NULLreplacement_value if expression is NULL1234567891011121314151617181920212223242526272829303132
-- Basic NVL usage (Oracle) -- Replace NULL with default valueSELECT employee_id, first_name, last_name, NVL(commission_pct, 0) AS commission_pct, salary + (salary * NVL(commission_pct, 0)) AS total_compensationFROM employees; -- String defaultSELECT customer_id, NVL(phone_number, 'Not Available') AS phone, NVL(email, 'Not Available') AS emailFROM customers; -- Date defaultSELECT order_id, order_date, NVL(ship_date, SYSDATE) AS ship_or_todayFROM orders; -- Nested NVL for multiple fallbacks (less elegant than COALESCE)SELECT customer_id, NVL(work_phone, NVL(mobile_phone, NVL(home_phone, 'No phone'))) AS best_phoneFROM customer_contacts;-- Compare to COALESCE:-- COALESCE(work_phone, mobile_phone, home_phone, 'No phone') -- Much cleaner!While NVL and COALESCE seem similar, they have important differences: (1) NVL takes exactly 2 arguments; COALESCE takes any number. (2) In Oracle, NVL always evaluates both arguments; COALESCE short-circuits. (3) NVL's data type rules differ slightly from COALESCE. (4) COALESCE is ANSI standard; NVL is Oracle-specific.
NVL2: Oracle's Extended NULL Check
Oracle also provides NVL2, which takes three arguments and returns different values based on whether the first is NULL or non-NULL:
12345678910111213141516171819202122232425262728
-- NVL2 syntax: NVL2(expression, value_if_not_null, value_if_null) -- Display different text based on NULL statusSELECT employee_id, first_name, commission_pct, NVL2(commission_pct, 'Has Commission', 'No Commission') AS commission_status, NVL2(manager_id, 'Has Manager', 'Top Level') AS manager_statusFROM employees; -- Calculate differently based on NULLSELECT product_id, product_name, -- If discount exists, apply it; otherwise use original price NVL2(discount_pct, price * (1 - discount_pct/100), price) AS final_priceFROM products; -- Equivalent using CASE (works in all databases)SELECT product_id, product_name, CASE WHEN discount_pct IS NOT NULL THEN price * (1 - discount_pct/100) ELSE price END AS final_priceFROM products;Different database systems provide their own NULL-handling functions. Understanding these helps when working with specific platforms or porting queries between systems.
| Database | Function | Equivalent To |
|---|---|---|
| All (ANSI) | COALESCE(a, b, c, ...) | First non-NULL value |
| Oracle | NVL(a, b) | COALESCE(a, b) (but always evaluates both) |
| Oracle | NVL2(a, b, c) | CASE WHEN a IS NOT NULL THEN b ELSE c END |
| MySQL | IFNULL(a, b) | COALESCE(a, b) |
| SQL Server | ISNULL(a, b) | COALESCE(a, b) (with type differences) |
| PostgreSQL | COALESCE(a, b, ...) | Standard COALESCE |
| SQLite | IFNULL(a, b) or COALESCE | Both supported |
123456789101112131415161718192021222324252627
-- MySQL: IFNULLSELECT customer_id, IFNULL(phone, 'No phone') AS phone, IFNULL(email, 'No email') AS emailFROM customers; -- SQL Server: ISNULL (note: different from IS NULL predicate!)SELECT customer_id, ISNULL(phone, 'No phone') AS phone, ISNULL(email, 'No email') AS emailFROM customers; -- SQL Server: ISNULL has type quirks-- ISNULL determines return type from first argument onlySELECT ISNULL(NULL, 'Hello World'); -- Returns NULL because first arg is NULL and has no type context! -- COALESCE determines type from all argumentsSELECT COALESCE(NULL, 'Hello World'); -- Returns 'Hello World' correctly -- Best Practice: Use COALESCE everywhere for portability-- If you must use ISNULL in SQL Server, be careful with types:DECLARE @value VARCHAR(100) = NULL;SELECT ISNULL(@value, 'Default'); -- Works because @value has defined typeUnless you're writing Oracle-only code and need NVL's specific behavior, use COALESCE. It's ANSI standard, supported everywhere, accepts multiple arguments, and has predictable short-circuit evaluation. This makes your SQL portable and your intent clear.
COALESCE and similar functions must determine the return type when given arguments of different types. Understanding these rules prevents unexpected errors or truncation.
1234567891011121314151617181920212223242526272829303132333435
-- COALESCE determines return type from all arguments -- Mixing numeric types: result uses the highest precisionSELECT COALESCE(integer_col, decimal_col, 0.00); -- Returns DECIMAL to accommodate all possible values -- Mixing string lengths: result accommodates longestSELECT COALESCE(varchar_50_col, varchar_100_col, 'default'); -- Returns VARCHAR that fits all values -- Type mismatch errors-- This fails because types are incompatible:SELECT COALESCE(date_column, 'no date'); -- ERROR in strict databases-- Fix: Cast to string explicitlySELECT COALESCE(CAST(date_column AS VARCHAR), 'no date'); -- Or provide typed NULL as first argument to set return typeSELECT COALESCE( date_column, CAST(NULL AS DATE), -- Ensures DATE type even if first is NULL CURRENT_DATE); -- Oracle NVL has stricter type requirements-- If first argument is NULL literal, must cast:SELECT NVL(NULL, 'default') FROM dual; -- May error or behave unexpectedlySELECT NVL(CAST(NULL AS VARCHAR2(100)), 'default') FROM dual; -- Safe -- Practical example: Format nullable numbers safelySELECT product_id, product_name, '$' || CAST(COALESCE(price, 0) AS VARCHAR(20)) AS display_price, COALESCE(CAST(quantity AS VARCHAR(10)), 'N/A') AS display_quantityFROM products;In some databases (especially SQL Server with ISNULL), the return type is determined by the first argument. If the first argument is VARCHAR(10) and the fallback is longer, the result may be truncated. COALESCE considers all arguments when determining the result type, making it safer for strings.
COALESCE and NVL have performance implications that should influence how you use them, particularly with complex expressions or high-volume queries.
WHERE COALESCE(col, 0) > 5 likely won't use an index on col.1234567891011121314151617181920212223242526272829303132333435363738394041424344
-- Anti-pattern: COALESCE in WHERE prevents index usageSELECT * FROM ordersWHERE COALESCE(customer_id, 0) = 123;-- The index on customer_id cannot be used! -- Better: Use explicit NULL handlingSELECT * FROM ordersWHERE customer_id = 123; -- Uses index-- Handle NULL case separately if needed:SELECT * FROM ordersWHERE customer_id = 123 OR (customer_id IS NULL AND 0 = 123); -- Anti-pattern: Expensive operation evaluated even when not needed (NVL)-- Oracle NVL evaluates both arguments:SELECT NVL(cached_value, expensive_calculation()) FROM table;-- expensive_calculation() runs even when cached_value is not NULL! -- Better: Use COALESCE (short-circuits) or CASESELECT COALESCE(cached_value, expensive_calculation()) FROM table;-- expensive_calculation() only runs when cached_value IS NULL -- Or explicitly with CASE:SELECT CASE WHEN cached_value IS NOT NULL THEN cached_value ELSE expensive_calculation() ENDFROM table; -- Order expressions by likelihood and cost-- Most common case first, expensive operation last:SELECT COALESCE( frequently_populated_column, -- Check this first sometimes_populated_column, -- Then this (SELECT value FROM config_table) -- Subquery last ) AS resultFROM main_table; -- Computed column to avoid repeated COALESCE-- If you frequently query COALESCE(nullable_col, default):ALTER TABLE products ADD effective_price AS COALESCE(sale_price, regular_price) PERSISTED;-- Now queries can use the computed column directly with index supportIn Oracle, NVL always evaluates both arguments. This is a documented behavior difference from COALESCE. If your second argument is expensive (function call, subquery), prefer COALESCE or use CASE with explicit IS NULL check for guaranteed short-circuit behavior.
Beyond simple default values, COALESCE enables sophisticated patterns for data transformation, reporting, and business logic.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
-- PATTERN 1: Dynamic column selection-- Display whichever value is availableSELECT order_id, COALESCE( special_instructions, shipping_notes, customer_notes, 'No special instructions' ) AS instructionsFROM orders; -- PATTERN 2: Aggregate NULL handling-- Replace NULL aggregate results with meaningful defaultsSELECT department_id, COALESCE(SUM(salary), 0) AS total_salary, COALESCE(AVG(salary), 0) AS avg_salary, COALESCE(MAX(hire_date), '1900-01-01') AS earliest_hireFROM employeesWHERE department_id = 999 -- Non-existent departmentGROUP BY department_id;-- Without COALESCE, empty group returns NULLs -- PATTERN 3: Pivoting with COALESCESELECT product_id, COALESCE(MAX(CASE WHEN region = 'North' THEN sales END), 0) AS north_sales, COALESCE(MAX(CASE WHEN region = 'South' THEN sales END), 0) AS south_sales, COALESCE(MAX(CASE WHEN region = 'East' THEN sales END), 0) AS east_sales, COALESCE(MAX(CASE WHEN region = 'West' THEN sales END), 0) AS west_salesFROM regional_salesGROUP BY product_id; -- PATTERN 4: Concatenation with NULL safetySELECT customer_id, COALESCE(first_name, '') || ' ' || COALESCE(middle_name || ' ', '') || COALESCE(last_name, '') AS full_nameFROM customers;-- Handles any combination of NULL name parts -- PATTERN 5: Conditional calculationsSELECT order_id, subtotal, COALESCE(discount_amount, 0) AS discount, COALESCE(tax_amount, subtotal * 0.08) AS tax, -- Default tax rate if not specified COALESCE(shipping_cost, CASE WHEN subtotal > 100 THEN 0 ELSE 9.99 END -- Free shipping over $100 ) AS shipping, subtotal - COALESCE(discount_amount, 0) + COALESCE(tax_amount, subtotal * 0.08) + COALESCE(shipping_cost, CASE WHEN subtotal > 100 THEN 0 ELSE 9.99 END) AS totalFROM orders; -- PATTERN 6: Reporting friendly NULLsSELECT d.department_name, COALESCE(CAST(COUNT(e.employee_id) AS VARCHAR), '0') AS employee_count, COALESCE(CAST(AVG(e.salary) AS VARCHAR(20)), 'N/A') AS avg_salary, COALESCE(m.name, 'No Manager Assigned') AS manager_nameFROM departments dLEFT JOIN employees e ON d.id = e.department_idLEFT JOIN employees m ON d.manager_id = m.idGROUP BY d.department_name, m.name; -- PATTERN 7: UPSERT-style merge logicMERGE INTO target_table tUSING source_table sON t.id = s.idWHEN MATCHED THEN UPDATE SET t.name = COALESCE(s.name, t.name), -- Keep old if new is NULL t.email = COALESCE(s.email, t.email), t.updated_at = CURRENT_TIMESTAMPWHEN NOT MATCHED THEN INSERT (id, name, email, created_at) VALUES (s.id, s.name, s.email, CURRENT_TIMESTAMP);The MERGE pattern COALESCE(new_value, old_value) is powerful for partial updates. If the new value is NULL, keep the existing value. This is useful for API-driven updates where clients only send changed fields.
We've thoroughly explored COALESCE, NVL, and related NULL-handling functions. Let's consolidate the key insights:
Module Complete:
Congratulations! You've now mastered NULL handling in SQL—from the philosophical meaning of NULL through three-valued logic to practical handling with COALESCE and NVL. You understand why NULL requires special treatment and have the tools to handle it correctly in any situation.
What you can now do:
You've completed the NULL Handling module. You now have a comprehensive, production-ready understanding of how NULL works in SQL databases—from theory to practice. This knowledge will help you write correct, efficient, and maintainable SQL code throughout your career.