Loading content...
NULL presents a fundamental challenge to ordering operations. By definition, NULL represents unknown or missing data—it's not a value, but the absence of a value. How do you compare unknown quantities? Where should rows with missing values appear in a sorted list?
These questions don't have mathematically "correct" answers—they require convention. Unfortunately, SQL implementations have historically chosen different conventions, creating a cross-platform compatibility challenge.
This page comprehensively explores:
NULL ordering defaults differ significantly between databases. Code that works correctly in PostgreSQL may produce unexpected ordering in MySQL or SQL Server. Always specify NULL behavior explicitly if it matters to your application.
By the end of this page, you will understand why NULL complicates ordering, know the default behavior of every major database, master NULLS FIRST/LAST syntax, implement workarounds for unsupported systems, and write portable NULL-aware ORDER BY clauses.
SQL uses three-valued logic (3VL) rather than simple TRUE/FALSE. Any comparison involving NULL yields UNKNOWN, not TRUE or FALSE:
| Expression | Result |
|---|---|
| 5 > 3 | TRUE |
| 5 < 3 | FALSE |
| 5 > NULL | UNKNOWN |
| NULL > 3 | UNKNOWN |
| NULL = NULL | UNKNOWN |
| NULL <> NULL | UNKNOWN |
This creates a fundamental problem for sorting: if we can't determine whether NULL is greater than, less than, or equal to any value, where should NULL rows appear?
123456789101112131415161718
-- Demonstrate NULL comparison behaviorSELECT CASE WHEN NULL > 1 THEN 'TRUE' WHEN NOT (NULL > 1) THEN 'FALSE' ELSE 'UNKNOWN' END AS result;-- Result: 'UNKNOWN' SELECT CASE WHEN NULL = NULL THEN 'TRUE' WHEN NOT (NULL = NULL) THEN 'FALSE' ELSE 'UNKNOWN' END AS result;-- Result: 'UNKNOWN' (NULL is not equal to itself!) -- This is why you must use IS NULL, not = NULLSELECT * FROM employees WHERE manager_id IS NULL; -- CorrectSELECT * FROM employees WHERE manager_id = NULL; -- Always returns 0 rows!Since NULL comparisons yield UNKNOWN, databases must make a design decision about NULL placement in sorted results. Two approaches exist:
Neither is more "correct"—they represent different design philosophies:
The SQL standard doesn't mandate which default to use. It only requires that databases provide a consistent convention and offer NULLS FIRST/LAST for explicit control. Unfortunately, not all databases implement NULLS FIRST/LAST.
Understanding each database's default is essential for portable code and debugging unexpected ordering.
| Database | NULL Position in ASC | NULL Position in DESC | NULLS FIRST/LAST Support |
|---|---|---|---|
| PostgreSQL | Last (NULL largest) | First (NULL largest) | ✓ Full support |
| Oracle | Last (NULL largest) | First (NULL largest) | ✓ Full support |
| SQL Server | First (NULL smallest) | Last (NULL smallest) | ✗ Not supported |
| MySQL | First (NULL smallest) | Last (NULL smallest) | ✗ Not supported (pre-8.0.21) |
| MySQL 8.0.21+ | First (NULL smallest) | Last (NULL smallest) | ✓ Supported via parser |
| SQLite | First (NULL smallest) | Last (NULL smallest) | ✓ Full support (3.30.0+) |
| DB2 | Last (NULL largest) | First (NULL largest) | ✓ Full support |
1234567891011121314151617181920
-- Sample data-- products: (1, 'Widget', 29.99), (2, 'Gadget', NULL), (3, 'Thing', 19.99) -- PostgreSQL / Oracle: NULL is largestSELECT name, price FROM products ORDER BY price ASC;-- Result: Thing (19.99), Widget (29.99), Gadget (NULL)-- NULL appears LAST in ascending order SELECT name, price FROM products ORDER BY price DESC;-- Result: Gadget (NULL), Widget (29.99), Thing (19.99)-- NULL appears FIRST in descending order -- MySQL / SQL Server: NULL is smallestSELECT name, price FROM products ORDER BY price ASC;-- Result: Gadget (NULL), Thing (19.99), Widget (29.99)-- NULL appears FIRST in ascending order SELECT name, price FROM products ORDER BY price DESC;-- Result: Widget (29.99), Thing (19.99), Gadget (NULL)-- NULL appears LAST in descending orderIf your application moves from PostgreSQL to MySQL (or vice versa), NULL ordering will flip! Any UI relying on specific NULL placement will break. Always test sorting with NULL values when changing database platforms.
The SQL standard provides NULLS FIRST and NULLS LAST keywords to explicitly specify where NULL values should appear in sorted results, overriding any default behavior.
ORDER BY column [ASC|DESC] [NULLS {FIRST | LAST}]
Each column in ORDER BY can independently specify its NULL behavior.
12345678910111213141516171819202122232425
-- Force NULLs to end of ascending order (PostgreSQL default)SELECT name, price FROM products ORDER BY price ASC NULLS LAST; -- Force NULLs to beginning of ascending order (MySQL/SQL Server default)SELECT name, price FROM products ORDER BY price ASC NULLS FIRST; -- Common pattern: Show products with prices first, NULL prices lastSELECT product_id, name, priceFROM productsORDER BY price ASC NULLS LAST; -- Force NULLs to end of descending order (opposite of PostgreSQL default)SELECT name, hire_date FROM employees ORDER BY hire_date DESC NULLS LAST; -- Multi-column with independent NULL handlingSELECT employee_id, department_id, manager_id, salaryFROM employeesORDER BY department_id ASC NULLS FIRST, -- No department at start manager_id ASC NULLS LAST, -- No manager at end within dept salary DESC;| ORDER BY Clause | Result Order |
|---|---|
ORDER BY col ASC NULLS FIRST | NULL, 1, 2, 3, ... (NULLs at start) |
ORDER BY col ASC NULLS LAST | 1, 2, 3, ..., NULL (NULLs at end) |
ORDER BY col DESC NULLS FIRST | NULL, ..., 3, 2, 1 (NULLs at start) |
ORDER BY col DESC NULLS LAST | ..., 3, 2, 1, NULL (NULLs at end) |
For most user interfaces, NULLS LAST is preferred. Users typically want to see items with known values first, with 'missing data' items appearing at the bottom of the list. Make NULLS LAST your default choice unless there's a specific reason to show NULLs first.
SQL Server and older MySQL versions don't support NULLS FIRST/LAST syntax. However, you can achieve equivalent behavior using expressions in ORDER BY.
123456789101112131415161718192021222324252627
-- Emulate NULLS LAST in ascending order (for MySQL/SQL Server)SELECT name, price FROM products ORDER BY CASE WHEN price IS NULL THEN 1 ELSE 0 END, -- NULLs sort last price ASC; -- Emulate NULLS FIRST in ascending order (for MySQL/SQL Server)SELECT name, price FROM products ORDER BY CASE WHEN price IS NULL THEN 0 ELSE 1 END, -- NULLs sort first price ASC; -- Emulate NULLS LAST in descending orderSELECT name, price FROM products ORDER BY CASE WHEN price IS NULL THEN 1 ELSE 0 END, -- NULLs sort last price DESC; -- Emulate NULLS FIRST in descending orderSELECT name, price FROM products ORDER BY CASE WHEN price IS NULL THEN 0 ELSE 1 END, -- NULLs sort first price DESC;For numeric columns, you can use COALESCE with extreme values to push NULLs to desired positions:
1234567891011121314151617181920212223242526
-- NULLS LAST in ascending order (replace NULL with huge value)SELECT name, price FROM products ORDER BY COALESCE(price, 999999999) ASC;-- NULLs treated as very large value, sort to end -- NULLS FIRST in ascending order (replace NULL with tiny value)SELECT name, price FROM products ORDER BY COALESCE(price, -999999999) ASC;-- NULLs treated as very small value, sort to start -- NULLS LAST in descending order (replace NULL with tiny value)SELECT name, price FROM products ORDER BY COALESCE(price, -999999999) DESC; -- NULLS FIRST in descending order (replace NULL with huge value)SELECT name, price FROM products ORDER BY COALESCE(price, 999999999) DESC; -- SQL Server specific: Use ISNULL instead of COALESCESELECT name, price FROM products ORDER BY ISNULL(price, 999999999) ASC;The COALESCE approach has risks: (1) If your data can contain the sentinel value (999999999), ordering breaks; (2) Type mismatches can occur; (3) Performance may suffer if the expression prevents index use. The CASE approach is generally safer.
| Aspect | CASE Workaround | COALESCE Workaround |
|---|---|---|
| Type safety | ✓ Works with any type | ⚠ Requires compatible sentinel value |
| Data collision risk | ✓ None | ⚠ Sentinel might exist in data |
| Readability | ✓ Clear intent | ⚠ Less obvious purpose |
| Performance | Similar unless indexed | Similar unless indexed |
| Works for strings | ✓ Yes | ⚠ Tricky (need lexicographically extreme values) |
| Works for dates | ✓ Yes | ✓ Use extreme dates ('1900-01-01', '9999-12-31') |
NULL handling in ORDER BY affects index usability. Understanding these dynamics is crucial for performance.
Most B-tree indexes store NULL values in a predictable position:
This physical placement affects whether ORDER BY can use the index without additional sorting.
123456789101112131415161718
-- PostgreSQL: Create index with explicit NULL ordering to match queryCREATE INDEX idx_price_nulls_last ON products (price ASC NULLS LAST); -- Now this query can use the index without sorting:SELECT * FROM products ORDER BY price ASC NULLS LAST; -- But this query may require a sort:SELECT * FROM products ORDER BY price ASC NULLS FIRST; -- Create both indexes if both patterns are common:CREATE INDEX idx_price_nulls_first ON products (price ASC NULLS FIRST); -- Oracle: Include NULL in index using function-based index-- (Standard Oracle doesn't index NULL in single-column indexes)CREATE INDEX idx_price ON products (price, 0);-- The constant 0 ensures NULL rows are included in index1234567891011121314151617181920212223
-- PROBLEM: Workaround prevents index use-- This expression prevents the optimizer from using idx_price:SELECT * FROM products ORDER BY CASE WHEN price IS NULL THEN 1 ELSE 0 END, price ASC; -- SOLUTION: Expression index (PostgreSQL)CREATE INDEX idx_price_null_sort ON products ( (CASE WHEN price IS NULL THEN 1 ELSE 0 END), price ASC); -- SOLUTION: Computed column + index (SQL Server)ALTER TABLE products ADD price_null_flag AS (CASE WHEN price IS NULL THEN 1 ELSE 0 END) PERSISTED; CREATE INDEX idx_price_null ON products (price_null_flag, price); -- Now the workaround query can use the index:SELECT * FROM products ORDER BY price_null_flag, price ASC;Always verify NULL ordering performance with EXPLAIN. Check whether the database is using an index scan (good) or performing an explicit sort (potentially bad). The presence of a 'Sort' operation in the plan indicates the index couldn't fully satisfy the ORDER BY.
Let's examine practical scenarios where NULL ordering matters and how to handle each appropriately.
123456789101112
-- Users with optional "company" field-- Business rule: Show users with companies first, solo users after SELECT user_id, name, company, created_atFROM usersORDER BY company ASC NULLS LAST, created_at DESC; -- Result:-- (Alice, Acme Corp, ...)-- (Bob, Beta Inc, ...)-- (Carol, NULL, ...) -- Users without company appear last-- (Dan, NULL, ...)12345678910111213
-- Data quality review: prioritize records with missing data-- Business rule: Rows with NULLs need attention, show first SELECT customer_id, email, phone, last_contact_dateFROM customersORDER BY -- Prioritize rows with more NULL fields (CASE WHEN email IS NULL THEN 1 ELSE 0 END + CASE WHEN phone IS NULL THEN 1 ELSE 0 END + CASE WHEN last_contact_date IS NULL THEN 1 ELSE 0 END) DESC, customer_id ASC; -- Result: Records with most missing fields appear first for cleanup1234567891011121314151617
-- E-commerce: NULL price means "Contact for quote"-- Business rule: Show priced items sorted by price, then inquire-only items SELECT product_id, name, price, CASE WHEN price IS NULL THEN 'Contact for Quote' ELSE CAST(price AS VARCHAR) END AS display_priceFROM productsWHERE category = 'Enterprise'ORDER BY CASE WHEN price IS NULL THEN 1 ELSE 0 END, -- NULL prices last price ASC; -- Result:-- (Server X, 4999.00)-- (Server Y, 7999.00)-- (Custom Solution, NULL, "Contact for Quote")12345678910111213141516171819
-- Tasks with optional due dates-- Business rule: Show tasks by urgency—dated first (soonest first), undated last SELECT task_id, title, priority, due_dateFROM tasksWHERE status = 'open'ORDER BY CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, -- Dated tasks first due_date ASC, -- Soonest due first priority ASC; -- Then by priority -- For the reverse (undated = "ongoing" = highest priority):SELECT task_id, title, priority, due_dateFROM tasksWHERE status = 'open'ORDER BY CASE WHEN due_date IS NULL THEN 0 ELSE 1 END, -- Undated first priority ASC, due_date ASC;There's no universally 'correct' NULL position. The appropriate choice depends on business context. A missing due date might mean 'not urgent' or 'always active'—the business domain defines which interpretation applies.
NULL ordering is a subtle but important aspect of SQL that creates cross-platform compatibility challenges. Let's consolidate the essential knowledge:
123456789101112131415
-- PORTABLE PATTERN: Always explicit, works everywhere-- Use CASE workaround even when NULLS LAST is supported-- for maximum cross-platform compatibility -- "Safe" NULLS LAST for ascending order:SELECT * FROM productsORDER BY CASE WHEN price IS NULL THEN 1 ELSE 0 END, price ASC; -- "Safe" NULLS FIRST for descending order:SELECT * FROM productsORDER BY CASE WHEN price IS NULL THEN 0 ELSE 1 END, price DESC;You now have complete mastery of NULL ordering: its theoretical basis, database-specific defaults, explicit control syntax, portable workarounds, and performance optimization. Next, we'll explore expression ordering—using computed values, functions, and CASE statements to create dynamic sort behaviors.