Sort Results with ORDER BY
Sort results with ORDER BY in PostgreSQL — practical, hands-on steps, troubleshooting, and what to study next.
Focus: sort results with order by
You've built tables, loaded data, and filtered rows—but your result sets still feel like a shuffled deck. Without a guaranteed order, PostgreSQL returns rows in whatever physical order the planner chooses, which can change between queries. This lesson shows you how to take control and sort results with ORDER BY, so your reports, APIs, and dashboards always display data the way users expect.
The problem this lesson solves
Imagine you run a query like SELECT * FROM products; and the rows appear in a random-looking sequence. On one run, the cheapest product is at the top; on the next, an overdue inventory item. This unpredictability breaks pagination, messes up rankings, and makes debugging painful.
ORDER BY solves this by letting you specify exact sort rules—by column, by direction, or even by multiple columns. Without it, you literally have no guarantee of row order, because PostgreSQL may use a sequential scan (reading pages on disk) or a parallel worker that returns rows in a nondeterministic way. Need a top-10 leaderboard? Need the latest 5 orders for a customer? Need a stable pagination key? All of these demand ORDER BY.
Core concept / mental model
Think of ORDER BY as the director of a theater—once the actors (rows) leave the stage (the storage engine), the director arranges them in the exact line-up you request before the curtain rises (results are returned). You tell the director which property to base the line-up on (column), whether to go left-to-right (ascending) or right-to-left (descending), and what to do with ties (secondary sort keys).
Pro tip: The database does not store rows in a sorted order. ORDER BY is a processing step that happens after filtering (WHERE) and before returning results (or offsetting for pagination).
Why not trust default order?
Many beginners assume that SELECT * returns rows in insertion order. That is sometimes true for small tables, but as soon as you update, delete, or reindex, the physical order changes. Even the same query can return a different order depending on table size, indexes, or concurrent activity. Never rely on it—always use ORDER BY if order matters.
How it works step by step
- Start with a SELECT statement that defines which columns and which rows to return.
- Append ORDER BY at the end (after WHERE, GROUP BY, HAVING).
- Specify one or more columns to sort by. For each, choose
ASC(default) orDESC. - For ties, add a secondary column. The database sorts by the first key, then uses the second to break ties.
- Optionally sort by expressions—e.g.,
ORDER BY price + taxorORDER BY lower(name). - to
NULLS FIRSTorNULLS LASTto control where missing values go.
SELECT product_id, name, price
FROM products
ORDER BY price DESC, name ASC;
This returns products sorted from most expensive to least, and within the same price, alphabetically by name.
Sorting directions
ASC— smallest/number first, alphabetically A→Z, dates oldest→newest.DESC— largest/number first, Z→A, dates newest→oldest.
Multiple columns
When you list columns with commas, the database processes them left to right—first key, then second key for ties, etc.
Null handling
By default, NULL sorts larger than any value in ascending order (so NULLs appear last in ASC, first in DESC). Use NULLS FIRST or NULLS LAST to override.
Hands-on walkthrough
Let's create a sample table and run a series of ORDER BY queries. Copy-paste these into psql or your favorite SQL client.
-- Create a demo table
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
category TEXT,
price NUMERIC(8,2),
created_at DATE
);
-- Insert sample rows
INSERT INTO products (name, category, price, created_at) VALUES
('Laptop', 'Electronics', 999.99, '2024-01-15'),
('Mouse', 'Electronics', 29.99, '2024-03-10'),
('Desk', 'Furniture', 299.00, '2024-02-01'),
('Chair', 'Furniture', 149.50, '2024-04-22'),
('Monitor', 'Electronics', 450.00, '2024-05-03');
Now run the queries one by one:
-- 1) Sort by price ascending (cheapest first)
SELECT name, price FROM products ORDER BY price ASC;
-- 2) Sort by price descending (most expensive first)
SELECT name, price FROM products ORDER BY price DESC;
-- 3) Sort by category, then by name within category
SELECT category, name, price FROM products ORDER BY category ASC, name ASC;
-- 4) Sort by price descending, but put NULLs last (add a NULL later)
INSERT INTO products (name, category, price, created_at) VALUES ('Keyboard', 'Electronics', NULL, '2024-06-01');
SELECT name, price FROM products ORDER BY price DESC NULLS LAST;
Expected output (query 4):
name | price
-------------+---------
Laptop | 999.99
Monitor | 450.00
Desk | 299.00
Chair | 149.50
Mouse | 29.99
Keyboard | NULL
Exercise: Flip the order
Run the same query but change DESC to ASC. Notice how NULL jumps to the top by default? Add NULLS LAST to keep it at the bottom—your report will look more polished.
Compare options / when to choose what
| Scenario | Use | Why |
|---|---|---|
| Single column, default order | ORDER BY col |
Simplest, sorts ascending |
| Top N results (e.g., top 5 sales) | ORDER BY col DESC LIMIT 5 |
Gets highest values first |
| Ties must be deterministic | ORDER BY col1, col2 |
Secondary key breaks ties |
| Nulls should not appear at top/bottom | ORDER BY col ASC NULLS LAST |
Controls null placement |
| Sort by computed value | ORDER BY price * quantity |
No need for extra column |
| Sort by text case-insensitively | ORDER BY lower(name) |
Normalizes text sorting |
When to use multiple keys: Always use a secondary key when your primary key has duplicates (e.g., sorting by category alone will group products but within each category order is arbitrary). Adding name makes the output predictable.
When to sort by expression: If you need a sort rule based on a calculation (total cost, age in days), you can place that expression in ORDER BY without creating a new column. For complex expressions, consider creating a functional index.
Pro tip: For large tables, sorting by a column with an index can be much faster—PostgreSQL may use the index to return rows already in sorted order. We'll cover indexes in a later lesson.
Troubleshooting & edge cases
Error: column "x" must appear in the GROUP BY clause — If you use GROUP BY, your ORDER BY columns must be in the GROUP BY or in an aggregate function (like MAX(price)). Fix by adding the column to GROUP BY or wrapping in an aggregate.
Wrong order in text columns — PostgreSQL sorts text using the database collation. For case-insensitive sorting, use ORDER BY lower(name) or set a collation like C on the column.
NULLs appearing at unexpected spot — Remember the default behavior: NULLs are considered larger than any value in ASC, and smaller in DESC. If you see NULLs at the top when you expect them at the bottom, add NULLS LAST explicitly.
Sorting by a column not in the SELECT list — This is allowed in PostgreSQL (unless DISTINCT is used). It can be handy, but sometimes confuses readers—clarify with comments.
Performance problems — If your query is slow, check EXPLAIN to see if a sort step is on a large result set. Adding an index or filtering earlier (WHERE) can reduce the sort size dramatically.
What you learned & what's next
You've mastered the core of sorting results with ORDER BY: you know how to sort by one or multiple columns, control direction, handle NULLs, and sort by expressions. You also understand the importance of deterministic order for pagination and reporting.
Next up in the PostgreSQL Tutorial, you'll learn how to limit results with LIMIT and OFFSET — the perfect companion to ORDER BY for building paginated APIs and "top N" dashboards. Combined, these give you full control over which rows come back and in what order.
Pro tip: Always pair ORDER BY with LIMIT when you only need the top rows—otherwise you may sort the entire table unnecessarily.
Now try the practice recap below to test your new skills!
Practice recap
Create a small table of orders (customer, amount, order_date) and insert 10 rows. Write a query that returns the top 3 most recent orders per customer (use ORDER BY order_date DESC). Then add a second sort key to break ties by amount DESC. Check your output—do you see full names as expected? If you're comfortable, switch to NULLS LAST for a column that might have NULLs.
Practice recap
Create a table of orders (customer, amount, order_date) and insert 10 rows. Write a query that returns the top 3 most recent orders per customer using ORDER BY order_date DESC. Add a secondary sort by amount DESC to break ties. Then test NULLS LAST on a date column that can be NULL and inspect the result order.
Common mistakes
- Forgetting ORDER BY entirely and assuming the database returns rows in insertion order — that's never guaranteed.
- Using
ORDER BY DESConly on the first column and expecting the second column to also be descending (it defaults to ASC). - Assuming NULLs sort to the bottom by default — in ASC they go last, but in DESC they go first unless you add NULLS FIRST/LAST.
- Using ORDER BY on a column not present in the GROUP BY clause (if using aggregates) — you must include it or wrap it.
Variations
- Use
ORDER BYwith a CASE expression to implement custom sort logic, e.g., prioritize 'VIP' status before 'Normal'. - Sort by an expression like
ORDER BY lower(name)for case-insensitive text sorting. - Try
ORDER BYon an indexed column to leverage an index scan instead of an explicit sort.
Real-world use cases
- E-commerce product listing sorted by price ascending or descending; users expect stable ordering when filtering.
- Financial dashboard sorting transactions by date descending to show the most recent activity first.
- Leaderboard for a gaming app that sorts player scores descending, breaking ties by playtime (secondary key).
Key takeaways
- ORDER BY is the only way to guarantee a specific row order in PostgreSQL; never rely on physical storage order.
- Use ASC (default) or DESC per column; multiple columns sort left to right with secondary keys breaking ties.
- Control NULL placement with NULLS FIRST/LAST—crucial for reports where missing values should appear at the end.
- Sorting by expressions (e.g., price * quantity) allows flexible ordering without adding new columns.
- Pair ORDER BY with LIMIT for top-N problems, and consider indexes to speed up sorts on large tables.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.