SQL ORDER BY
ORDER BY sorts query results before they’re returned. Without it, the database returns rows in an indeterminate order that can change between runs. If result order matters, ORDER BY is required.
Basic Syntax
SELECT columnsFROM table_nameORDER BY column ASC; -- ascending (A→Z, lowest to highest)ORDER BY column DESC; -- descending (Z→A, highest to lowest)ASC is the default — you can omit it:
ORDER BY name -- same as ORDER BY name ASCORDER BY price DESC -- descending: most expensive firstAscending vs Descending
-- Products alphabetically A → ZSELECT name, category FROM products ORDER BY name ASC;
-- Most expensive firstSELECT name, price FROM products ORDER BY price DESC;
-- Most recent orders firstSELECT order_id, amount, created_at FROM orders ORDER BY created_at DESC;
-- Oldest first (chronological)SELECT order_id, created_at FROM orders ORDER BY created_at ASC;Multi-Column Sorting
Specify multiple columns for primary → secondary → tertiary sort:
-- Primary: category alphabetically-- Secondary: within each category, most expensive firstSELECT name, category, priceFROM productsORDER BY category ASC, price DESC;Each column has its own direction independently:
-- Employees: by department (A→Z), then by hire date (oldest first), then salary (highest first)SELECT name, department, hire_date, salaryFROM employeesORDER BY department ASC, hire_date ASC, salary DESC;ORDER BY Column Position
Reference by position number in the SELECT list (1-indexed):
SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_priceFROM productsGROUP BY categoryORDER BY 2 DESC, 3 DESC; -- sort by cnt, then avg_pricePosition numbers are useful with long expressions but reduce readability — the number gives no hint of what’s being sorted.
ORDER BY Column Alias
Most databases allow sorting by an alias defined in SELECT:
SELECT customer_id, SUM(amount) AS total_spentFROM ordersGROUP BY customer_idORDER BY total_spent DESC; -- alias defined in SELECTORDER BY Expression
Sort on a computed value, not just a raw column:
-- Sort by computed revenueSELECT order_id, quantity, unit_priceFROM order_itemsORDER BY quantity * unit_price DESC;
-- Sort by string lengthSELECT name FROM products ORDER BY LENGTH(name);
-- Sort by age at signup (oldest first)SELECT name, birth_date FROM usersORDER BY EXTRACT(YEAR FROM AGE(NOW(), birth_date)) DESC;Handling NULLs in ORDER BY
Default NULL sorting varies by database. Control it explicitly:
-- PostgreSQL: NULLs at the end regardless of directionORDER BY last_login DESC NULLS LASTORDER BY last_login ASC NULLS LAST
-- NULLs firstORDER BY last_login DESC NULLS FIRST
-- MySQL / SQL Server workaround: push NULLs to endORDER BY CASE WHEN last_login IS NULL THEN 1 ELSE 0 END, last_login DESC;Custom Sort Order with CASE WHEN
When default alphabetical or numeric order isn’t what you need:
-- Business-logic sort: show 'urgent' status first, then 'pending', then othersSELECT ticket_id, subject, status, created_atFROM ticketsORDER BY CASE status WHEN 'urgent' THEN 1 WHEN 'pending' THEN 2 WHEN 'open' THEN 3 ELSE 4 END ASC, created_at ASC;ORDER BY with LIMIT (Top-N)
The most common pattern — get the top N rows by some criterion:
-- Top 10 products by revenueSELECT product_id, name, total_revenueFROM product_summaryORDER BY total_revenue DESCLIMIT 10;
-- 3 most recently updated recordsSELECT id, name, updated_at FROM itemsORDER BY updated_at DESCLIMIT 3;
-- 5 cheapest active productsSELECT name, price FROM productsWHERE is_active = TRUEORDER BY price ASCLIMIT 5;ORDER BY in Subqueries and CTEs
In most databases, ORDER BY in a subquery without LIMIT has no guaranteed effect on the outer query — the database may not preserve order between query layers. Sorting only matters in the final result:
-- Sorting in an outer query is meaningfulSELECT * FROM ( SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY 1) tORDER BY total DESC;
-- Sorting inside a CTE without LIMIT is ignored in the final outputWITH ranked AS ( SELECT *, ROW_NUMBER() OVER (ORDER BY score DESC) AS rn FROM scores)SELECT * FROM ranked WHERE rn <= 10 ORDER BY score DESC;Practical Examples
Sales report sorted by month and then category:
SELECT DATE_TRUNC('month', order_date) AS month, category, SUM(amount) AS revenueFROM ordersJOIN products USING (product_id)GROUP BY 1, 2ORDER BY 1 DESC, revenue DESC;Customer leaderboard:
SELECT customer_id, name, SUM(amount) AS lifetime_value, COUNT(*) AS order_countFROM customersJOIN orders USING (customer_id)WHERE orders.status = 'completed'GROUP BY customer_id, nameORDER BY lifetime_value DESCLIMIT 50;Recently active users:
SELECT user_id, email, last_active_atFROM usersWHERE last_active_at IS NOT NULLORDER BY last_active_at DESC NULLS LASTLIMIT 100;