Since the first lesson of the module we've been repeating the same warning: the result set has no guaranteed order. It's time to take control. ORDER BY is the only way to make a query return rows in a particular order, and although it looks like the simplest clause in SQL, it hides half a dozen details that separate a query that works by accident from one that always works: what happens with ties, where nulls go, why a capital letter can end up somewhere odd, and why sorting by column number is a time bomb.
Contents
- Without
ORDER BYthere's no guaranteed order ASCandDESC- Sorting by several columns and breaking ties
- Sorting by alias, by expression and by ordinal position
- Nulls:
NULLS FIRSTandNULLS LAST - Sorting text: collation and accents
- Sorting dates
ORDER BYin the logical execution order- Common Mistakes and Tips
- Exercises
- Conclusion
- Without
ORDER BY there's no guaranteed order
ORDER BY there's no guaranteed orderLet's make it explicit once and for all. This query:
returns, today, on your machine, products 1 to 20. And that gives you a false sense of security, because the SQL standard promises no order when there's no ORDER BY, and neither does PostgreSQL. What you're seeing is the order in which the engine found the rows as it walked the data file.
Real circumstances in which that order changes without you touching the query:
| Situation | What happens |
|---|---|
A row is updated with UPDATE |
PostgreSQL writes a new version of the row at the end of the table; that row now comes back last |
| The optimizer decides to use an index | The rows come out in the index's order, not the physical one |
| The table is large and parallelism kicks in | Several processes read different chunks and deliver interleaved results |
VACUUM FULL is run or the table is rebuilt |
The physical order changes completely |
A table is added with a JOIN (module 3) |
The order depends on the join algorithm chosen |
You can check the first case yourself, if you dare modify the data (remember that the script from 01-06 is idempotent and you can reload it):
Product 1 will now appear at the end of the list. Nothing has failed, nothing warns you: the order simply was never guaranteed.
Absolute rule: if the order of the rows matters for your report, your application or your pagination, write
ORDER BY. There are no shortcuts, there are no exceptions, and "but it always comes out right" is not an argument.
ASC and DESC
ASC and DESCORDER BY is written at the end of the query and states which column to sort by and in which direction:
| id | name | price |
|---|---|---|
| 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 6 | Aloe vera face cream 50 ml | 18.90 |
| 20 | Spirulina capsules 120 units | 16.40 |
| 8 | Almond body oil 200 ml | 14.25 |
| 13 | Soy wax candles (pack of 2) | 13.75 |
| 1 | Extra virgin olive oil 500 ml | 12.50 |
| 10 | Concentrated eco laundry detergent 1 L | 11.20 |
| 12 | Reusable cotton bags (pack of 5) | 9.90 |
| 3 | Raw orange blossom honey 500 g | 9.75 |
| 7 | Rosemary solid shampoo 80 g | 8.40 |
| 19 | Natural stick deodorant 50 g | 7.80 |
| 11 | Loofah scrubber (pack of 3) | 5.50 |
| 17 | Cold-pressed orange juice 1 L | 5.40 |
| 16 | Ginger kombucha 750 ml | 4.95 |
| 9 | Calendula lip balm 15 ml | 4.60 |
| 2 | Organic brown rice 1 kg | 3.90 |
| 18 | Bamboo toothbrush | 3.50 |
| 14 | Organic chamomile tea 20 bags | 3.25 |
| 4 | Spelt pasta 500 g | 2.80 |
| 5 | Organic crushed tomato 400 g | 1.95 |
The whole catalogue from most to least expensive. There's a business answer for you already: the matcha is the most expensive product (€22.00) and the crushed tomato the cheapest (€1.95).
| Keyword | Meaning | Is it the default? |
|---|---|---|
ASC |
Ascending: lowest to highest, A to Z, oldest to most recent date | Yes |
DESC |
Descending: the other way round | No |
Since ASC is the default, these two are identical:
Writing ASC explicitly isn't mandatory, but in queries with several columns and mixed directions it helps readability a lot.
- Sorting by several columns and breaking ties
When two rows have the same value in the sort column, their relative order is not defined. It's the same problem as in section 1, on a smaller scale.
The five rows of category 1 do come out together, but in what order among themselves? Whichever the engine sees fit. To pin it down you add more columns to the ORDER BY, separated by commas: the second breaks ties in the first, the third breaks ties in the second, and so on. Each column can carry its own ASC or DESC.
| id | name | category_id | price |
|---|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 1 | 12.50 |
| 3 | Raw orange blossom honey 500 g | 1 | 9.75 |
| 2 | Organic brown rice 1 kg | 1 | 3.90 |
| 4 | Spelt pasta 500 g | 1 | 2.80 |
| 5 | Organic crushed tomato 400 g | 1 | 1.95 |
| 6 | Aloe vera face cream 50 ml | 2 | 18.90 |
| 8 | Almond body oil 200 ml | 2 | 14.25 |
| 7 | Rosemary solid shampoo 80 g | 2 | 8.40 |
| 9 | Calendula lip balm 15 ml | 2 | 4.60 |
| 13 | Soy wax candles (pack of 2) | 3 | 13.75 |
| 10 | Concentrated eco laundry detergent 1 L | 3 | 11.20 |
| 12 | Reusable cotton bags (pack of 5) | 3 | 9.90 |
| 11 | Loofah scrubber (pack of 3) | 3 | 5.50 |
| 15 | Ceremonial matcha green tea 30 g | 4 | 22.00 |
| 17 | Cold-pressed orange juice 1 L | 4 | 5.40 |
| 16 | Ginger kombucha 750 ml | 4 | 4.95 |
| 14 | Organic chamomile tea 20 bags | 4 | 3.25 |
| 19 | Natural stick deodorant 50 g | 5 | 7.80 |
| 18 | Bamboo toothbrush | 5 | 3.50 |
| 20 | Spirulina capsules 120 units | 6 | 16.40 |
A perfectly presentable catalogue: grouped by category and, within each one, from the most to the least expensive item.
Three columns, with customer data:
| id | name | last_name | city | country |
|---|---|---|---|---|
| 9 | Camille | Dubois | Lyon | France |
| 10 | Julien | Moreau | Paris | France |
| 7 | Sofia | Moreira Costa | Lisbon | Portugal |
| 8 | Tiago | Almeida Nunes | Porto | Portugal |
| 11 | Elena | Navarro Puig | Alicante | Spain |
| 5 | Ana | Belmonte Roca | Barcelona | Spain |
| 13 | Núria | Bosch Ferrer | Barcelona | Spain |
| 3 | Marta | Sanchis Gil | Castellón | Spain |
| 4 | Javier | Ortega Ruiz | Madrid | Spain |
| 12 | Diego | Ramos Herrera | Seville | Spain |
| 15 | Inés | Carrasco Vega | Valencia | Spain |
| 2 | Carlos | Ferrer Ibáñez | Valencia | Spain |
| 6 | Pau | Llorens Vidal | Valencia | Spain |
| 1 | Lucía | Martínez Soler | Valencia | Spain |
| 14 | Hugo | Iglesias Pardo | Zaragoza | Spain |
Read it from the outside in: first everybody from France, then Portugal, then Spain; within each country the cities in alphabetical order, and within each city the surnames. The two customers from Barcelona end up sorted by surname (Belmonte before Bosch) and the four from Valencia do too (Carrasco, Ferrer, Llorens, Martínez).
Professional tip: always finish the
ORDER BYwith a unique column. Adding, idas the last criterion guarantees the order is deterministic: the same query returns exactly the same order today and a year from now. It's essential for the pagination you'll see in 02-06, and for automated tests not to fail at random.
- Sorting by alias, by expression and by ordinal position
4.1. By alias
Here the debt from 02-02 is settled: since ORDER BY runs after SELECT, the alias already exists and can be used.
| name | price | cost | margin |
|---|---|---|---|
| Ceremonial matcha green tea 30 g | 22.00 | 12.50 | 9.50 |
| Aloe vera face cream 50 ml | 18.90 | 9.50 | 9.40 |
| Spirulina capsules 120 units | 16.40 | 8.70 | 7.70 |
| Almond body oil 200 ml | 14.25 | 7.10 | 7.15 |
| Soy wax candles (pack of 2) | 13.75 | 6.90 | 6.85 |
| Reusable cotton bags (pack of 5) | 9.90 | 4.30 | 5.60 |
| Concentrated eco laundry detergent 1 L | 11.20 | 6.00 | 5.20 |
| Rosemary solid shampoo 80 g | 8.40 | 3.60 | 4.80 |
| Extra virgin olive oil 500 ml | 12.50 | 7.80 | 4.70 |
| Natural stick deodorant 50 g | 7.80 | 3.30 | 4.50 |
| Raw orange blossom honey 500 g | 9.75 | 5.40 | 4.35 |
| Loofah scrubber (pack of 3) | 5.50 | 2.20 | 3.30 |
| Calendula lip balm 15 ml | 4.60 | 1.80 | 2.80 |
| Cold-pressed orange juice 1 L | 5.40 | 2.60 | 2.80 |
| Ginger kombucha 750 ml | 4.95 | 2.30 | 2.65 |
| Bamboo toothbrush | 3.50 | 1.20 | 2.30 |
| Organic chamomile tea 20 bags | 3.25 | 1.40 | 1.85 |
| Organic brown rice 1 kg | 3.90 | 2.10 | 1.80 |
| Spelt pasta 500 g | 2.80 | 1.35 | 1.45 |
| Organic crushed tomato 400 g | 1.95 | 0.90 | 1.05 |
Notice the real tie in rows 13 and 14: the lip balm (product 9) and the orange juice (product 17) both leave exactly €2.80 of margin. The trailing , id is what makes 9 come out before 17 reproducibly; without it, the order between the two would be the engine's business.
And notice that we've sorted by id without projecting it. That's perfectly legal (unlike what happened with SELECT DISTINCT in 02-04): ORDER BY can use any column from the FROM's tables, whether or not it appears in the result.
4.2. By expression
You can also repeat the full expression instead of using the alias. The result is identical:
| name | price | cost |
|---|---|---|
| Bamboo toothbrush | 3.50 | 1.20 |
| Calendula lip balm 15 ml | 4.60 | 1.80 |
| Loofah scrubber (pack of 3) | 5.50 | 2.20 |
| Natural stick deodorant 50 g | 7.80 | 3.30 |
| Rosemary solid shampoo 80 g | 8.40 | 3.60 |
(First 5 of 20 rows.)
This is the ranking by percentage margin, not by absolute margin, and it gives a very different result: the bamboo toothbrush, which only leaves €2.30 per unit, is the product that contributes the highest margin percentage (65.7 %). Sorting by the right metric matters as much as calculating it correctly.
4.3. By ordinal position
SQL lets you sort by the number of the column within the SELECT:
It's equivalent to ORDER BY price DESC, because price is the second column in the list.
It works, it's standard and it's very widespread in quick, exploratory queries. But it's fragile and it shouldn't reach code that gets saved:
| Risk | Scenario |
|---|---|
Somebody adds a column at the start of the SELECT |
ORDER BY 2 starts sorting by something else, with no error |
| Somebody reorders the column list | The same |
| Whoever reads the query | Has to count columns to understand what it does |
Combined with SELECT * |
The meaning depends on the table's definition, which can change |
Course rule:
ORDER BY 2to explore inpsql; a column name or alias in any query you're going to save.
A nuance that catches people out: the ordinal only works in ORDER BY (and in GROUP BY, module 4). In WHERE it means nothing, and ORDER BY 2 + 1 does not sort by the third column: PostgreSQL reads that as a constant expression (the number 3) and ignores it as a sort criterion, because it's the same for every row.
- Nulls:
NULLS FIRST and NULLS LAST
NULLS FIRST and NULLS LASTA NULL isn't greater or smaller than anything: it's unknown. But to sort you have to put it somewhere, so each engine makes an arbitrary decision. PostgreSQL considers NULL to be the largest value.
That's where its defaults come from:
| Direction | Where nulls go in PostgreSQL |
|---|---|
ASC |
At the end (equivalent to NULLS LAST) |
DESC |
At the start (equivalent to NULLS FIRST) |
Let's see it with orders.employee_id, which is NULL on the ten orders that came in through the web:
| id | customer_id | employee_id | status |
|---|---|---|---|
| 2 | 2 | 4 | delivered |
| 6 | 5 | 4 | cancelled |
| 10 | 9 | 4 | delivered |
| 16 | 4 | 4 | shipped |
| 4 | 4 | 5 | delivered |
| 8 | 7 | 5 | delivered |
| 12 | 10 | 5 | delivered |
| 18 | 5 | 5 | paid |
| 14 | 12 | 6 | delivered |
| 20 | 9 | 6 | pending |
| 1 | 1 | (null) | delivered |
| 3 | 3 | (null) | delivered |
| 5 | 1 | (null) | delivered |
| 7 | 6 | (null) | delivered |
| 9 | 8 | (null) | delivered |
| 11 | 2 | (null) | delivered |
| 13 | 11 | (null) | delivered |
| 15 | 1 | (null) | delivered |
| 17 | 7 | (null) | shipped |
| 19 | 6 | (null) | delivered |
The ten orders handled by sales reps (4, 5 and 6) first, and the ten web orders at the end. If you want the nulls at the top, you ask for it explicitly:
Now orders 1, 3, 5, 7, 9, 11, 13, 15, 17 and 19 head the list, followed by sales rep 4's, then 5's and then 6's.
The same with customers.referred_by_id:
| id | name | last_name | referred_by_id |
|---|---|---|---|
| 1 | Lucía | Martínez Soler | (null) |
| 4 | Javier | Ortega Ruiz | (null) |
| 6 | Pau | Llorens Vidal | (null) |
| 7 | Sofia | Moreira Costa | (null) |
| 9 | Camille | Dubois | (null) |
| 12 | Diego | Ramos Herrera | (null) |
| 14 | Hugo | Iglesias Pardo | (null) |
| 2 | Carlos | Ferrer Ibáñez | 1 |
| 3 | Marta | Sanchis Gil | 1 |
| 15 | Inés | Carrasco Vega | 1 |
| 5 | Ana | Belmonte Roca | 2 |
| 13 | Núria | Bosch Ferrer | 5 |
| 11 | Elena | Navarro Puig | 6 |
| 8 | Tiago | Almeida Nunes | 7 |
| 10 | Julien | Moreau | 9 |
The seven customers who arrived on their own at the top, and below them the eight who came by recommendation, grouped by who brought them in: Lucía has brought three (Carlos, Marta and Inés), which makes her GreenStore's best referrer.
5.1. A comparison across engines
This is one of the dialect differences that causes the most headaches when migrating queries:
| Engine | NULL is considered |
ORDER BY col ASC |
ORDER BY col DESC |
Does it accept NULLS FIRST/LAST? |
|---|---|---|---|---|
| PostgreSQL | The largest | Nulls at the end | Nulls at the start | Yes |
| Oracle | The largest | Nulls at the end | Nulls at the start | Yes |
| MySQL / MariaDB | The smallest | Nulls at the start | Nulls at the end | No |
| SQLite | The smallest | Nulls at the start | Nulls at the end | Yes (since 3.30) |
| SQL Server | The smallest | Nulls at the start | Nulls at the end | No |
In other words: the same query ORDER BY employee_id puts the ten web orders at the end in PostgreSQL and at the start in MySQL. If your report shows the first ten rows, you'll see completely different data.
On engines that don't accept NULLS FIRST/LAST you emulate it with an auxiliary sort column, taking advantage of the fact that a boolean sorts false before true:
-- Portable to MySQL and SQL Server: nulls last even in ASC
ORDER BY (employee_id IS NULL), employee_id;The expression employee_id IS NULL is false (0) for the rows with a value and true (1) for the null ones, so sorting by it pushes the nulls to the end. It's a trick worth keeping in your pocket.
- Sorting text: collation and accents
Sorting numbers or dates is arithmetic. Sorting text is culture, and that's where things get interesting.
A collation is the set of rules that decides whether 'apple' comes before or after 'Banana', whether 'café' and 'CAFE' count as equal when sorting, and whether 'ch' counts as one letter or two. Every PostgreSQL database is created with a default collation, which you can check:
The two usual scenarios:
| Collation | How it sorts | Effect on case and accents |
|---|---|---|
C or POSIX |
By byte value in UTF-8 | Capitals come before all lowercase letters, and accented letters come after all of A-Z. apple ends up after Zulema |
en_US.UTF-8, es_ES.UTF-8, ICU |
Linguistic rules | Capitals and lowercase interleave (apple before Banana); accents only break ties; Ñ sits between N and O |
You can check it on your installation with a direct comparison:
SELECT 'apple' < 'Banana' AS database_collation,
('apple' COLLATE "C") < ('Banana' COLLATE "C") AS c_collation;On a database created with a linguistic collation (the usual case):
| database_collation | c_collation |
|---|---|
| true | false |
'apple' < 'Banana' is true with linguistic rules —the letter a comes before b, and case only breaks ties— and false with the C collation, because in UTF-8 the capital B is encoded as byte 0x42, well below the 0x61 of lowercase a. The same query, two different orders, depending on how the database was created.
To force a particular collation in a query:
| id | name | last_name | city |
|---|---|---|---|
| 8 | Tiago | Almeida Nunes | Porto |
| 5 | Ana | Belmonte Roca | Barcelona |
| 13 | Núria | Bosch Ferrer | Barcelona |
| 15 | Inés | Carrasco Vega | Valencia |
| 9 | Camille | Dubois | Lyon |
| 2 | Carlos | Ferrer Ibáñez | Valencia |
| 14 | Hugo | Iglesias Pardo | Zaragoza |
| 6 | Pau | Llorens Vidal | Valencia |
| 1 | Lucía | Martínez Soler | Valencia |
| 10 | Julien | Moreau | Paris |
| 7 | Sofia | Moreira Costa | Lisbon |
| 11 | Elena | Navarro Puig | Alicante |
| 4 | Javier | Ortega Ruiz | Madrid |
| 12 | Diego | Ramos Herrera | Seville |
| 3 | Marta | Sanchis Gil | Castellón |
COLLATE "en-US-x-icu" applies English rules through the ICU library, which PostgreSQL has bundled since version 10. Notice Moreau before Moreira: they share More, and then a comes before i.
You can see the collations available on your server with:
Practical notes about collations:
- Collation also affects
WHERE,LIKEand indexes. An index created with one collation is no use for sorting with another (module 8). - Sorting with an explicit
COLLATEis slower than using the default collation, because it stops the index being reused. - Changing the collation of an existing database is a major operation: every text index has to be rebuilt. It's a decision you make when creating the database.
- If you need an order "without accents or case", the modern route in PostgreSQL is non-deterministic collations (
CREATE COLLATION ... deterministic = false); the classic route is sorting byLOWER(unaccent(column)), with functions from module 6.
- Sorting dates
Dates sort chronologically, with no surprises, because the DATE type is a number on the inside:
| id | customer_id | order_date | status | shipping_cost |
|---|---|---|---|---|
| 20 | 9 | 2026-02-21 | pending | 12.50 |
| 19 | 6 | 2026-02-09 | paid | 4.95 |
| 18 | 5 | 2026-01-27 | paid | 4.95 |
| 17 | 7 | 2026-01-13 | shipped | 9.90 |
| 16 | 4 | 2025-12-19 | shipped | 4.95 |
| 15 | 1 | 2025-12-02 | delivered | 0.00 |
| 14 | 12 | 2025-11-14 | delivered | 4.95 |
| 13 | 11 | 2025-10-22 | delivered | 4.95 |
| 12 | 10 | 2025-10-01 | delivered | 12.50 |
| 11 | 2 | 2025-09-09 | delivered | 0.00 |
(First 10 of 20 rows.)
The orders from the most recent to the oldest: the natural order of any work queue. In this dataset the date grows with the id, so sorting by date descending coincides with sorting by id descending; in a real database that needn't hold (an order can be recorded with a backdated date) and relying on it would be a mistake.
Important warning: this works because the columns are of type DATE. If a date were stored as text, the order would be alphabetical:
| Format stored as text | Resulting alphabetical order |
|---|---|
'2026-01-13' (ISO) |
Matches the chronological one, by a fortunate coincidence |
'13/01/2026' (European) |
Disastrous: 13 January 2026 would sit next to 13 March 1998 |
It's one more reason to store dates in date-typed columns, as was decided in 01-04 and 01-06.
ORDER BY in the logical execution order
ORDER BY in the logical execution orderORDER BY is the second-to-last step: it runs after the rows have been filtered, projected and deduplicated, and only LIMIT comes afterwards.
flowchart LR
A["1 · FROM"] --> B["2 · WHERE"]
B --> C["3 · SELECT<br/>aliases are born here"]
C --> D["3b · DISTINCT"]
D --> E["4 · ORDER BY<br/>✅ can use aliases<br/>✅ and unprojected columns"]
E --> F["5 · LIMIT<br/>(02-06)"]
Consequences you've already seen in action:
Can ORDER BY... |
Answer | Why |
|---|---|---|
Use a SELECT alias? |
Yes | The alias already exists: SELECT ran before |
Use a column that isn't in the SELECT? |
Yes (except with DISTINCT) |
The table's columns are still available |
| Use a calculated expression? | Yes | It's evaluated over the result's rows |
| Use the column ordinal? | Yes | It's a convenience from the standard |
Use an alias with SELECT DISTINCT over another column? |
No | The restriction seen in 02-04 |
And a performance consideration you'll pick up again in module 8: sorting costs. PostgreSQL can avoid the work if an index already returns the rows in the requested order; if not, it has to sort the whole set in memory (or on disk, if it doesn't fit). An ORDER BY over a table of millions of rows without a suitable index is one of the most frequent causes of slow queries.
One last note: if a query with ORDER BY is used as a subquery (module 7) or inside a view (module 10), that order doesn't necessarily propagate to the outer query. The ORDER BY that rules is the outermost one.
Common Mistakes and Tips
- Relying on the order without
ORDER BY. It works until the day it doesn't. It's the most expensive mistake in this lesson. - Forgetting the tie-breaker. Two rows with the same value can come out in any order, and that order can change between runs. Always finish with a unique column.
- Writing
DESConce and thinking it affects the whole list.ORDER BY a, b DESCsortsaascending andbdescending. If you want both descending:ORDER BY a DESC, b DESC. - Sorting by ordinal in production code.
ORDER BY 2breaks silently when somebody reorders theSELECT. - Assuming nulls go where you think. PostgreSQL puts them at the end in
ASC; MySQL, at the start. If it matters, writeNULLS FIRSTorNULLS LAST. - Sorting dates stored as text. Alphabetical order, not chronological. Use the
DATEtype. - Sorting numbers stored as text.
'10' < '9'is true alphabetically. The same problem. - Being surprised by where capitals or accented letters land. It depends on the database's collation. Check it with
SHOW lc_collate;. - Tip: use
COLLATEonly when you really need it, because it stops indexes being used. - Tip: for reports, sort by what the reader is looking for. A product listing is sorted by name if they're going to look for a specific one, and by price if they're going to compare.
- Tip: check the order at the extremes. Look at the first and last row: they usually give away an inverted
ASC/DESCor misplaced nulls immediately.
Exercises
Exercise 1
Management wants the catalogue listing as it will appear on the website: active products only, grouped by category from lowest to highest, and within each category in alphabetical order by name. Show category_id, id, name and price. Explain why the result has 19 rows.
Exercise 2
HR asks for the payroll sorted by salary from highest to lowest, showing name, last_name, job_title, salary and manager_id. Add whatever is needed for the order to be deterministic and answer: where does Rosa Alcázar Vives appear and why doesn't her null manager_id affect the order?
Exercise 3
On order_lines, get the lines sorted by amount from highest to lowest, showing id, order_id, product_id, quantity and the amount rounded to two decimals. Write the query in two ways —using the alias and repeating the expression— and explain why both work here but only one of them would work in a WHERE.
Solutions
Solution 1
| category_id | id | name | price |
|---|---|---|---|
| 1 | 1 | Extra virgin olive oil 500 ml | 12.50 |
| 1 | 2 | Organic brown rice 1 kg | 3.90 |
| 1 | 5 | Organic crushed tomato 400 g | 1.95 |
| 1 | 3 | Raw orange blossom honey 500 g | 9.75 |
| 1 | 4 | Spelt pasta 500 g | 2.80 |
| 2 | 8 | Almond body oil 200 ml | 14.25 |
| 2 | 6 | Aloe vera face cream 50 ml | 18.90 |
| 2 | 9 | Calendula lip balm 15 ml | 4.60 |
| 2 | 7 | Rosemary solid shampoo 80 g | 8.40 |
| 3 | 10 | Concentrated eco laundry detergent 1 L | 11.20 |
| 3 | 11 | Loofah scrubber (pack of 3) | 5.50 |
| 3 | 12 | Reusable cotton bags (pack of 5) | 9.90 |
| 3 | 13 | Soy wax candles (pack of 2) | 13.75 |
| 4 | 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 4 | 17 | Cold-pressed orange juice 1 L | 5.40 |
| 4 | 16 | Ginger kombucha 750 ml | 4.95 |
| 4 | 14 | Organic chamomile tea 20 bags | 3.25 |
| 5 | 18 | Bamboo toothbrush | 3.50 |
| 5 | 19 | Natural stick deodorant 50 g | 7.80 |
19 rows because WHERE active discards product 20 (Spirulina capsules), the only discontinued one. And that's why category 6 doesn't appear in the listing: it was its only product.
Two observations about the order:
- Within each category, the
idis no longer increasing: category 2 starts with product 8 because "Almond body oil" comes alphabetically before "Aloe vera", "Calendula" and "Rosemary". It's the proof that the order is dictated by theORDER BYand not by the table. - In category 4, "Ceremonial matcha" comes before "Cold-pressed orange juice" because they share the initial
Cand the second letter breaks the tie (ebeforeo); accents play no part here because they sit beyond the point where the order has already been decided. If some product began with a lowercase or an accented letter, in a database with theCcollation it would appear at the very end of the whole listing. That's the effect from section 6.
Solution 2
| name | last_name | job_title | salary | manager_id |
|---|---|---|---|---|
| Rosa | Alcázar Vives | General manager | 62000.00 | (null) |
| Andrés | Company Talens | Sales manager | 41000.00 | 1 |
| Beatriz | Nadal Ripoll | Logistics manager | 39500.00 | 1 |
| Daniel | Vercher Lluch | Data analyst | 35000.00 | 1 |
| Óscar | Peris Blasco | Sales rep | 28500.00 | 2 |
| Laia | Puig Sanchis | Sales rep | 27800.00 | 2 |
| Marc | Estévez Roig | Customer support | 24500.00 | 2 |
| Irene | Salvador Mira | Warehouse operator | 22000.00 | 3 |
Rosa Alcázar Vives appears first, and not because of her null manager_id but because she has the highest salary (€62,000). Her null manager_id has no influence whatsoever on the order, because that column takes no part in the ORDER BY: it appears in the result but not as a sort criterion. It's an important distinction: nulls only alter the order of the columns you sort by.
The trailing , id guarantees determinism. In this data no salary repeats, so it changes nothing today; the day two sales reps are hired on the same pay, the listing will still always come out the same.
Solution 3
The version with an alias:
SELECT id,
order_id,
product_id,
quantity,
ROUND(quantity * unit_price * (1 - discount), 2) AS amount
FROM order_lines
ORDER BY amount DESC, id;The version with the expression repeated:
SELECT id,
order_id,
product_id,
quantity,
ROUND(quantity * unit_price * (1 - discount), 2) AS amount
FROM order_lines
ORDER BY quantity * unit_price * (1 - discount) DESC, id;Both return the same thing. The first ten rows of the 47:
| id | order_id | product_id | quantity | amount |
|---|---|---|---|---|
| 28 | 12 | 15 | 2 | 44.00 |
| 18 | 8 | 1 | 3 | 35.63 |
| 24 | 10 | 6 | 2 | 34.02 |
| 45 | 19 | 16 | 6 | 26.73 |
| 41 | 17 | 1 | 2 | 25.00 |
| 1 | 1 | 1 | 2 | 23.90 |
| 9 | 4 | 15 | 1 | 22.00 |
| 42 | 17 | 15 | 1 | 22.00 |
| 39 | 16 | 10 | 2 | 21.28 |
| 16 | 7 | 16 | 4 | 19.80 |
Why both work here. ORDER BY runs after SELECT, so by that point the alias amount already exists and the expression can also be re-evaluated. Both forms are valid and PostgreSQL generates the same plan.
Why only one would work in WHERE. WHERE runs before SELECT, so the alias hasn't been born yet: WHERE amount > 30 would give column "amount" does not exist. Only the version with the full expression can be used to filter, as you saw in 02-03. This whole lesson rests on that same asymmetry in the logical execution order.
Notice as well the tie in rows 7 and 8: lines 9 and 42 are both worth exactly €22.00 (one unit of matcha in each case). The trailing , id is what decides that 9 comes out first. Without it, the order between the two would be unpredictable, and a paginated report could end up showing the same line twice or not at all.
Conclusion
You're now in control of the order of your results:
- Without
ORDER BYthere's no guaranteed order. AnUPDATE, an index or parallelism can change it with no warning. ASC(the default) andDESCdecide the direction, and eachORDER BYcolumn carries its own.- Sorting by several columns resolves ties, and finishing with a unique column makes the order deterministic: essential for pagination and for tests not to fail at random.
- You can sort by alias, by expression and by ordinal position; the last one only for exploring, because it breaks silently.
- Nulls go at the end in
ASCand at the start inDESCin PostgreSQL, exactly the opposite of MySQL, SQLite and SQL Server.NULLS FIRST/NULLS LASTmakes it explicit. - The order of text depends on the database's collation: with
C, capitals come first and accented letters go to the very end; with linguistic or ICU rules, everything lands where you'd expect.COLLATE "en-US-x-icu"forces English rules. - Dates sort chronologically if —and only if— they're stored in date-typed columns.
ORDER BYis step 4 of the logical order: the aliases already exist, every column is still visible, and onlyLIMITlies ahead.
In the module's last lesson, Limiting Results with LIMIT, you'll add step 5 and close the full cycle of a query. You'll see why LIMIT is the first thing you write when exploring an unfamiliar table, how pagination is built with OFFSET, why that pagination degrades and drifts out of sync on large tables, and what serious applications do instead.
SQL Course
Module 1: Introduction to SQL
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
