MySQLORDER BY

ORDER BY in MySQL

The ORDER BY clause sorts a query result set. Without it, MySQL makes no guarantee about row order — the physical storage order, join order, or optimizer choices can all change between executions. Always add ORDER BY when order matters.

Basic Syntax

SQL
-- Ascending order (default)
SELECT first_name, last_name, salary
FROM employees
ORDER BY last_name ASC;

-- Descending order
SELECT product_name, price
FROM products
ORDER BY price DESC;

-- ASC is the default, so these are equivalent:
-- ORDER BY last_name
-- ORDER BY last_name ASC
Multi-Level Sort Strategies

You can sort by several columns. The second column breaks ties in the first, the third breaks ties in the second, and so on. Each column can have its own direction.

SQL
-- Sort by department ASC, then salary DESC within each department
SELECT first_name, last_name, department_id, salary
FROM employees
ORDER BY department_id ASC, salary DESC;

-- Three-level sort: region, then country, then city
SELECT city, country, region, population
FROM cities
ORDER BY region ASC, country ASC, city ASC;

-- Mix directions freely
SELECT product_name, category, price, stock
FROM products
ORDER BY category ASC, price DESC, stock ASC;
ORDER BY Column Position (Legacy Syntax)

You can reference SELECT columns by their ordinal position (1-based). This is valid SQL but fragile — if someone reorders the SELECT list, the sort changes silently.

SQL
-- 1 = first_name, 2 = last_name, 3 = salary
SELECT first_name, last_name, salary
FROM employees
ORDER BY 3 DESC, 2 ASC;
Warning
Avoid ORDER BY positional numbers in production code. They break silently when columns are reordered. Always use explicit column names.
ORDER BY with Expressions and Aliases

SQL
-- Sort by a calculated value
SELECT product_name, price, quantity,
       price * quantity AS line_total
FROM order_items
ORDER BY price * quantity DESC;

-- MySQL allows aliases in ORDER BY (evaluated after SELECT)
SELECT product_name, price * quantity AS line_total
FROM order_items
ORDER BY line_total DESC;

-- Sort by string length
SELECT username, LENGTH(username) AS len
FROM users
ORDER BY len DESC;

-- Sort by day of week (1=Sunday in MySQL)
SELECT event_name, event_date
FROM events
ORDER BY DAYOFWEEK(event_date), event_date;
Note
Unlike WHERE, MySQL allows column aliases in ORDER BY because ORDER BY is evaluated after SELECT in the logical execution order.
NULL Ordering — The MySQL Workaround

In MySQL, NULL values are treated as lower than any non-NULL value for sorting purposes. This means NULLs appear first in ASC order and last in DESC order.

MySQL does not support the standard SQL NULLS FIRST / NULLS LAST syntax directly (it was added only in MySQL 8.0.26 for window functions). Use the IS NULL trick instead.

SQL
-- Default: NULLs first in ASC order
SELECT name, manager_id FROM employees ORDER BY manager_id ASC;
-- manager_id: NULL, NULL, 1, 2, 3

-- Force NULLs LAST in ASC order
SELECT name, manager_id
FROM employees
ORDER BY (manager_id IS NULL) ASC, manager_id ASC;
-- (manager_id IS NULL) returns 0 for non-null, 1 for null
-- So 0s (non-null) sort first, 1s (null) sort last
-- Result: 1, 2, 3, NULL, NULL

-- Force NULLs FIRST in DESC order (they would normally be last)
SELECT name, manager_id
FROM employees
ORDER BY (manager_id IS NULL) DESC, manager_id DESC;
-- Result: NULL, NULL, 3, 2, 1

-- Force NULLs LAST in DESC order (the default already does this)
SELECT name, manager_id
FROM employees
ORDER BY manager_id DESC;
-- Result: 3, 2, 1, NULL, NULL (NULLs naturally last in DESC)

Goal

ORDER BY expression

NULLs first, ASC (default)

ORDER BY col ASC

NULLs last, ASC

ORDER BY (col IS NULL) ASC, col ASC

NULLs first, DESC

ORDER BY (col IS NULL) DESC, col DESC

NULLs last, DESC (default)

ORDER BY col DESC

FIELD() for Custom Enum-Like Sort Order

FIELD(val, v1, v2, v3, ...) returns the 1-based position of val in the list, or 0 if not found. This lets you define a completely custom sort order — ideal for status columns and priority queues.

SQL
-- Sort orders by a business-defined status priority
SELECT id, status, total, created_at
FROM orders
ORDER BY FIELD(status, 'urgent', 'pending', 'processing', 'shipped', 'delivered', 'cancelled');
-- 'urgent' = 1, 'pending' = 2, ..., items not in list = 0 (sort first)

-- Push unlisted statuses to the end with a secondary sort
SELECT id, status, total
FROM orders
ORDER BY
  FIELD(status, 'urgent', 'pending', 'processing', 'shipped', 'delivered', 'cancelled') = 0 ASC,
  FIELD(status, 'urgent', 'pending', 'processing', 'shipped', 'delivered', 'cancelled') ASC;

-- Custom day-of-week sort starting Monday
SELECT event_name, event_day
FROM events
ORDER BY FIELD(event_day, 'Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday');
Tip
FIELD() returns 0 for values not in the list, making them sort before listed values by default. The two-expression trick above pushes them to the end.
ORDER BY with LIMIT — Top-N and Pagination

Combining ORDER BY with LIMIT is the standard pattern for pagination, top-N queries, and most-recent-N queries.

SQL
-- Top 5 most expensive products
SELECT product_name, price
FROM products
ORDER BY price DESC
LIMIT 5;

-- Most recent 10 orders
SELECT id, customer_id, total, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 10;

-- Page 3 of results (10 per page): skip 20, take 10
SELECT id, title
FROM articles
ORDER BY published_at DESC
LIMIT 10 OFFSET 20;

-- Leaderboard: top 100 players with ties handled by secondary sort
SELECT player_name, score, games_played
FROM leaderboard
ORDER BY score DESC, games_played ASC
LIMIT 100;
Warning
Without ORDER BY, LIMIT returns an arbitrary set of rows. The result may change between executions. Always pair LIMIT with ORDER BY.
ORDER BY with LIMIT — Optimizer Optimization

When MySQL sees ORDER BY col DESC LIMIT N, and there is a suitable index on col, the optimizer can satisfy the query by reading just the first N entries from the index in reverse order — it does not need to sort the entire table. This makes top-N queries extremely fast.

Without an index, MySQL sorts the entire result set (filesort) before applying LIMIT.

SQL
-- Without index: sorts all 5M rows, then returns top 10 (slow)
EXPLAIN SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- Extra: Using filesort  <-- sorts everything

-- With index: reads 10 entries from index in reverse, done (fast)
ALTER TABLE orders ADD INDEX idx_created_at (created_at);
EXPLAIN SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- type: index, Extra: NULL  <-- walks index backward
Performance: Filesort Algorithm Deep Dive

When MySQL cannot use an index for ORDER BY, it uses a filesort. MySQL has two filesort algorithms:

Single-pass: reads all columns needed by the query into a sort buffer, sorts, then outputs. Faster when rows are wide but uses more sort buffer memory.

Two-pass: reads only the sort key + row pointer into the buffer, sorts, then re-reads the rows from disk to fetch the actual columns. More disk I/O but uses less memory.

MySQL chooses between them based on max_length_for_sort_data and row size. Increasing sort_buffer_size prevents spilling to disk.

SQL
-- Check sort buffer size
SHOW VARIABLES LIKE 'sort_buffer_size';       -- default: 256K
SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- default: 4096 bytes

-- Increase sort buffer for large sorts (session level)
SET SESSION sort_buffer_size = 4 * 1024 * 1024;  -- 4 MB

-- Check if a query is spilling to disk
SHOW STATUS LIKE 'Sort_merge_passes';  -- > 0 means disk spill occurred
Covering Index for ORDER BY

If all columns in the SELECT list, WHERE clause, and ORDER BY clause are present in one index, MySQL reads only the index — never touching the table rows. EXPLAIN shows Using index.

SQL
-- Covering index: status (equality) + created_at (range/sort) + id + total (SELECT)
ALTER TABLE orders
  ADD INDEX idx_cover (status, created_at, id, total);

SELECT id, total
FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC
LIMIT 10;
id | select_type | type | key       | rows | Extra
 1 | SIMPLE      | ref  | idx_cover | 12   | Using index
Tip
A covering index that includes all WHERE, ORDER BY, and SELECT columns is the fastest possible query plan. Design composite indexes with this in mind for hot query paths.
Composite Index Column Order for ORDER BY

The golden rule for composite indexes with ORDER BY:

  1. Put equality WHERE columns first (they narrow the range).
  2. Put the ORDER BY column last (so sorted order in the index matches the query).
  3. A range condition (>, <, BETWEEN) breaks the ability to use the index for ORDER BY.

SQL
-- Good: equality on status, then ORDER BY created_at
-- Index: (status, created_at) — MySQL walks the index in order
SELECT id FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC;

-- Bad: range on created_at breaks ORDER BY index usage
-- Index: (status, created_at) can't be used for ORDER BY here
SELECT id FROM orders
WHERE status = 'pending'
  AND created_at > '2024-01-01'
ORDER BY total DESC;  -- total is not in the index after a range
ORDER BY in Subqueries and Views
Note
In MySQL 8.0+, ORDER BY inside a subquery or derived table that has no LIMIT is silently ignored. The outer query determines the final order. Always add ORDER BY at the outermost level.

SQL
-- ORDER BY in derived table without LIMIT is ignored in MySQL 8.0+
SELECT * FROM (
  SELECT id, name FROM products ORDER BY name  -- this ORDER BY is ignored
) AS sub
ORDER BY name;  -- this ORDER BY is what actually sorts

-- If you need ORDER BY inside a subquery, add LIMIT
SELECT * FROM (
  SELECT id, name FROM products ORDER BY name LIMIT 999999999
) AS sub;  -- ORDER BY now honored (with LIMIT present)
Stable Sort Guarantee (MySQL 8.0)

MySQL 8.0 guarantees a stable sort: when rows compare as equal on all ORDER BY columns, they appear in a consistent (though unspecified) order. In MySQL 5.7 and earlier, equal rows could appear in different sequences between executions. To get a fully deterministic result, always include a unique column (like the primary key) as the tiebreaker.

SQL
-- Fully deterministic: department sort, salary tiebreaker, PK final tiebreaker
SELECT id, first_name, department_id, salary
FROM employees
ORDER BY department_id ASC, salary DESC, id ASC;
-- id is the PK -- guarantees same row order on every run
Practical Use Cases

SQL
-- Leaderboard (score desc, earlier join date wins ties)
SELECT username, score, joined_at
FROM players
ORDER BY score DESC, joined_at ASC
LIMIT 50;

-- Social feed (promoted posts first, then newest)
SELECT id, title, promoted, published_at
FROM posts
ORDER BY promoted DESC, published_at DESC
LIMIT 20;

-- Invoice aging report (oldest unpaid first)
SELECT invoice_id, customer, due_date, amount
FROM invoices
WHERE paid = 0
ORDER BY due_date ASC;

-- Admin user list (locked accounts last, then alpha)
SELECT username, account_locked, created_at
FROM users
ORDER BY account_locked ASC, username ASC;
Summary Table

Scenario

Recommended Approach

Sort ascending

ORDER BY col ASC (or just ORDER BY col)

Sort descending

ORDER BY col DESC

Multiple sort keys

ORDER BY col1, col2 DESC

Custom sort order

ORDER BY FIELD(col, val1, val2, ...)

NULLs last in ASC

ORDER BY (col IS NULL) ASC, col ASC

NULLs first in DESC

ORDER BY (col IS NULL) DESC, col DESC

Top-N query

ORDER BY col DESC LIMIT N

Pagination

ORDER BY col LIMIT n OFFSET m

Deterministic order

Add primary key as final tiebreaker

Avoid filesort

Add index matching ORDER BY column(s)

Covering index

Include WHERE + ORDER BY + SELECT cols in index

Best Practices
  • Always specify ORDER BY when the order of results matters — never rely on implicit ordering.

  • Use column names, not ordinal positions, in ORDER BY for maintainable code.

  • Add indexes on columns frequently used in ORDER BY, especially combined with WHERE filters.

  • Use covering indexes on hot queries that combine WHERE, ORDER BY, and a small SELECT list.

  • Check EXPLAIN output for "Using filesort" and evaluate whether an index would help.

  • Pair ORDER BY with LIMIT to efficiently implement top-N and pagination queries.

  • Use FIELD() for domain-specific sort orders like status priority queues.

  • Always include a unique column (PK) as the final ORDER BY tiebreaker for deterministic paging.

  • Use the IS NULL trick to control NULL placement since MySQL lacks NULLS FIRST/LAST syntax.