SQL 数据分析 · 指南 · 2026年10月30日 · 13 分钟阅读
SQL ORDER BY 排序:升序降序、多字段排序、NULLS LAST 与取前 N 条

ORDER BY 给查询结果排序。把它写在查询的末尾,列出要排序的列,每列后面加上 ASC(升序,默认)或 DESC(降序):ORDER BY range DESC 会把航程最长的机型排在最前面。多字段排序时,第二列只在第一列相同时才起作用。没有 ORDER BY,数据库返回的行没有任何保证的顺序,所以凡是关心顺序的查询,以及每一个带 LIMIT 的查询,都需要它。最容易踩坑的两个细节:NULL 排在哪里(各数据库不同,所以要写 NULLS FIRST 或 NULLS LAST),以及取前 N 条时边界上的并列。
下面的例子都在 Nomto SQL 课程的航空公司数据库上运行,使用 PostgreSQL 语法,引擎是 DuckDB(Nomto SQL 练习场背后的引擎),每个结果表都是真实输出。
ORDER BY 语法
SELECT columns
FROM table
WHERE condition
ORDER BY column1 [ASC | DESC] [NULLS FIRST | NULLS LAST],
column2 [ASC | DESC] ...
LIMIT n;
ORDER BY 写在 WHERE、GROUP BY 和 HAVING 之后,LIMIT 之前。它也是数据库最后执行的步骤之一,所以可以使用 SELECT 里定义的别名(后面会讲)。
升序和降序
九种机型,按航程从短到长排序:
SELECT model, range
FROM aircrafts
ORDER BY range;
| model | range |
|---|---|
| Cessna 208 Caravan | 1200 |
| Bombardier CRJ-200 | 2700 |
| Sukhoi Superjet-100 | 3000 |
| Boeing 737-300 | 4200 |
| Airbus A321-200 | 5600 |
| Airbus A320-200 | 5700 |
| Airbus A319-100 | 6700 |
| Boeing 767-300 | 7900 |
| Boeing 777-300 | 11100 |
ORDER BY range 等同于 ORDER BY range ASC。要最长的排在前面,就写 ORDER BY range DESC。数字按大小排,日期和时间从早到晚排,文本按字母顺序排(有几个需要注意的地方,见「文本排序」一节)。
多字段排序
用逗号分隔多个列。行先按第一列排序;只有第一列相同时才看第二列,依此类推。每一列有自己的方向。按制造商从 A 到 Z 分组,同一制造商内部按航程从长到短:
SELECT split_part(model, ' ', 1) AS maker, model, range
FROM aircrafts
ORDER BY maker, range DESC;
| maker | model | range |
|---|---|---|
| Airbus | Airbus A319-100 | 6700 |
| Airbus | Airbus A320-200 | 5700 |
| Airbus | Airbus A321-200 | 5600 |
| Boeing | Boeing 777-300 | 11100 |
| Boeing | Boeing 767-300 | 7900 |
| Boeing | Boeing 737-300 | 4200 |
| Bombardier | Bombardier CRJ-200 | 2700 |
| Cessna | Cessna 208 Caravan | 1200 |
| Sukhoi | Sukhoi Superjet-100 | 3000 |
一个常见误区:写了 ORDER BY maker, range DESC,以为两列都是降序。DESC 只作用于紧挨在它前面的那一列,这里的 maker 仍然是升序。
按别名、列序号或表达式排序
ORDER BY 后面可以写:
SELECT里的列别名:上面的maker不是真实的列,而是一个表达式的别名;- 一个表达式:
ORDER BY split_part(model, ' ', 1)得到同样的结果; - 列序号:
ORDER BY 2 DESC按SELECT列表里的第二列排序; - 根本没有选出来的列:
SELECT city FROM airports ORDER BY timezone是合法的。
对分组结果按聚合值排序,用别名或列序号都可以:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC; -- 或 ORDER BY 2 DESC,或 ORDER BY COUNT(*) DESC
| status | flights |
|---|---|
| Arrived | 16707 |
| Scheduled | 15383 |
| On Time | 518 |
| Cancelled | 414 |
| Departed | 58 |
| Delayed | 41 |
列序号(ORDER BY 2)在探索数据时很顺手,但在保存下来的查询里很脆弱:在 SELECT 开头加一列,排序就悄悄变了。别人要读的查询里,最好写列名。
还有两个限制。别名可以单独使用,但不一定能放进表达式里:当 flights 是别名时,PostgreSQL 会拒绝 ORDER BY flights * 2。另外,用 SELECT DISTINCT 时,PostgreSQL 要求 ORDER BY 里的每个表达式都出现在选择列表中(报错「for SELECT DISTINCT, ORDER BY expressions must appear in select list」);DuckDB 宽松一些,但别依赖这一点。
用 CASE 自定义排序
按字母排状态没有意义,大家想看的是航班生命周期的顺序。用 CASE 给每个值一个编号,再按编号排序:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY CASE status
WHEN 'Scheduled' THEN 1
WHEN 'On Time' THEN 2
WHEN 'Delayed' THEN 3
WHEN 'Departed' THEN 4
WHEN 'Arrived' THEN 5
ELSE 6
END;
| status | flights |
|---|---|
| Scheduled | 15383 |
| On Time | 518 |
| Delayed | 41 |
| Departed | 58 |
| Arrived | 16707 |
| Cancelled | 414 |
ELSE 让任何意料之外的值排到最后,而不是最前。同样的技巧可以把某个值置顶:ORDER BY city = 'Moscow' DESC, city 先列出 Moscow,其余按字母排(在 PostgreSQL 和 DuckDB 里 true 排在 false 之后,所以要用 DESC)。MySQL 还有 ORDER BY FIELD(status, 'Scheduled', 'On Time', ...),这是 MySQL 特有的写法,在国内项目里很常见,但换到别的数据库就用不了。更多 CASE 用法见 SQL CASE WHEN 用法。
NULLS FIRST 和 NULLS LAST
哪些航班起飞延误最久?有 16,348 个航班还没起飞,它们的 actual_departure 以及算出来的延误时间都是 NULL:
SELECT flight_no, departure_airport, scheduled_departure,
EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60 AS delay_min
FROM flights
ORDER BY delay_min DESC NULLS LAST
LIMIT 5;
| flight_no | departure_airport | scheduled_departure | delay_min |
|---|---|---|---|
| PG0589 | PEE | 2026-07-25 15:30:00 | 277 |
| PG0164 | DME | 2026-07-25 15:25:00 | 268 |
| PG0364 | KRR | 2026-07-15 11:45:00 | 267 |
| PG0568 | OVS | 2026-08-09 16:15:00 | 260 |
| PG0096 | URJ | 2026-07-30 16:35:00 | 258 |
改成 NULLS FIRST,排在最前面的就是根本没有延误数据的航班:
| flight_no | departure_airport | scheduled_departure | delay_min |
|---|---|---|---|
| PG0402 | DME | 2026-09-05 12:25:00 | NULL |
| PG0405 | DME | 2026-08-12 09:35:00 | NULL |
| PG0404 | DME | 2026-08-12 19:05:00 | NULL |
如果两者都不写,结果取决于数据库:
| 数据库 | ASC 时 NULL 在 | DESC 时 NULL 在 |
|---|---|---|
| PostgreSQL、Oracle | 最后 | 最前 |
| DuckDB | 最后 | 最后 |
| MySQL、SQL Server、SQLite、Hive、Spark SQL | 最前 | 最后 |
所以同一条 ORDER BY delay_min DESC LIMIT 5,在 DuckDB 里返回真正的前 5 名,在 PostgreSQL 里却返回五个 NULL。PostgreSQL、Oracle、DuckDB、Spark SQL、Hive(2.1 起)和 SQLite(3.30 起)都支持 NULLS FIRST/NULLS LAST。MySQL 和 SQL Server 不支持;在那里要先按一个标记排序:MySQL 写 ORDER BY delay_min IS NULL, delay_min DESC,SQL Server 写 ORDER BY CASE WHEN delay_min IS NULL THEN 1 ELSE 0 END, delay_min DESC。NULL 的更多行为见 SQL NULL 值处理。
ORDER BY 加 LIMIT:取前 N 条
ORDER BY 加 LIMIT 就是取「前 N 名」的方法。从谢列梅捷沃机场(SVO)出发,航班最多的四个目的地:
SELECT arrival_airport, COUNT(*) AS flights
FROM flights
WHERE departure_airport = 'SVO'
GROUP BY arrival_airport
ORDER BY flights DESC, arrival_airport
LIMIT 4;
| arrival_airport | flights |
|---|---|
| LED | 305 |
| BZK | 183 |
| GOJ | 122 |
| PES | 122 |
边界上的并列
GOJ、PES 和 ULV 三个机场都是 122 个航班,而前 4 名里只放得下两个。如果去掉第二排序列 arrival_airport,数据库可以随便挑其中两个,它也确实这么做了:只用 ORDER BY flights DESC 把查询运行五次,第 3、第 4 名得到了五种不同的组合(PES 和 ULV、GOJ 和 PES、ULV 和 PES、GOJ 和 ULV、PES 和 GOJ)。每次刷新都变的报表就是一个 bug。两种解决办法:
- 加一个决胜列,让顺序唯一,就像上面那样。结果稳定了,但截断仍然是任意的:ULV 落选只是因为它的名字。
- 保留所有并列的行。 PostgreSQL 13+ 和 Oracle 支持
FETCH FIRST 4 ROWS WITH TIES,SQL Server 有TOP 4 WITH TIES。DuckDB 和 MySQL 都没有,用RANK():
SELECT arrival_airport, flights
FROM (
SELECT arrival_airport, COUNT(*) AS flights,
RANK() OVER (ORDER BY COUNT(*) DESC) AS rnk
FROM flights
WHERE departure_airport = 'SVO'
GROUP BY arrival_airport
) t
WHERE rnk <= 4
ORDER BY flights DESC, arrival_airport;
| arrival_airport | flights |
|---|---|
| LED | 305 |
| BZK | 183 |
| GOJ | 122 |
| PES | 122 |
| ULV | 122 |
五行,因为三个机场并列第 3。每组取前 N 条(比如每个机场的前 3 条航线)见 SQL 窗口函数;RANK、DENSE_RANK 和 ROW_NUMBER 在并列时的区别见 ROW_NUMBER、RANK 和 DENSE_RANK。
各数据库怎么取前 N 条
| 数据库 | 取前 3 条 |
|---|---|
| PostgreSQL、DuckDB、MySQL、SQLite、Hive、Spark SQL | ORDER BY range DESC LIMIT 3 |
| SQL Server | SELECT TOP 3 ... ORDER BY range DESC |
| 标准 SQL(PostgreSQL、Oracle 12c+、SQL Server、DuckDB) | ORDER BY range DESC FETCH FIRST 3 ROWS ONLY |
用 OFFSET 分页
OFFSET 在 LIMIT 开始计数之前跳过若干行。机型列表的第 2 页,每页三条:
SELECT model, range
FROM aircrafts
ORDER BY range DESC
LIMIT 3 OFFSET 3;
| model | range |
|---|---|
| Airbus A320-200 | 5700 |
| Airbus A321-200 | 5600 |
| Boeing 737-300 | 4200 |
标准写法 OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY 返回同样的行,在 SQL Server 里也能用(那里 OFFSET 必须配合 ORDER BY)。MySQL 还有简写 LIMIT 3, 3(先偏移量,后行数,顺序和 LIMIT 3 OFFSET 3 相反,容易写反)。分页只有在顺序唯一时才有意义:有并列时,某一行可能出现在两页上,也可能哪一页都没有。在大表上,很深的偏移量很慢,因为数据库仍然要生成并跳过前面所有的行;应用程序通常改用「游标分页」(keyset pagination,WHERE range < last_seen_value ORDER BY range DESC LIMIT 3)。
文本排序:存成文本的数字、大小写和中文
文本按字符逐个比较,所以存成文本的数字会排「错」。空客 A319 的座位号是 1A、10C 这样的文本:
SELECT seat_no FROM seats WHERE aircraft_code = '319' ORDER BY seat_no LIMIT 8;
| seat_no |
|---|
| 10A |
| 10B |
| 10C |
| 10D |
| 10E |
| 10F |
| 11A |
| 11B |
第 10 排排在第 1 排前面,因为「1」后面跟「0」比「1」后面跟「A」小。先按数字部分排,再按字母排:
SELECT seat_no, fare_conditions
FROM seats
WHERE aircraft_code = '319'
ORDER BY CAST(regexp_extract(seat_no, '^[0-9]+') AS INTEGER), right(seat_no, 1)
LIMIT 8;
| seat_no | fare_conditions |
|---|---|
| 1A | Business |
| 1C | Business |
| 1D | Business |
| 1F | Business |
| 2A | Business |
| 2C | Business |
| 2D | Business |
| 2F | Business |
在 PostgreSQL 里,把 regexp_extract(seat_no, '^[0-9]+') 换成 substring(seat_no FROM '^[0-9]+')。以文本形式导入的 ID 和金额也有同样的问题:'10' < '9'。真正的解决办法是把它们存成数字类型。
大小写是另一个意外。DuckDB 按字符编码比较文本,所以所有大写字母都排在所有小写字母前面:
SELECT x FROM (VALUES ('banana'), ('Cherry'), ('apple'), ('Apple')) t(x)
ORDER BY x;
| x |
|---|
| Apple |
| Cherry |
| apple |
| banana |
PostgreSQL 在常见的 en_US 排序规则下返回 apple、Apple、banana、Cherry;MySQL 和 SQL Server 默认的不区分大小写的排序规则会把 apple 和 Apple 视为相等。要在所有地方得到同样的顺序,就按 lower(x) 排序,再用 x 作决胜列:
| x |
|---|
| Apple |
| apple |
| banana |
| Cherry |
中文按拼音排序
中文也是同样的问题:默认按字符编码(Unicode 码位)排序,结果既不是拼音顺序,也不是笔画顺序:
SELECT x FROM (VALUES ('上海'), ('北京'), ('广州'), ('深圳')) t(x)
ORDER BY x;
| x |
|---|
| 上海 |
| 北京 |
| 广州 |
| 深圳 |
在 DuckDB 里指定中文排序规则,就会按拼音排:
SELECT x FROM (VALUES ('上海'), ('北京'), ('广州'), ('深圳')) t(x)
ORDER BY x COLLATE zh;
| x |
|---|
| 北京 |
| 广州 |
| 上海 |
| 深圳 |
其他数据库的写法:PostgreSQL(带 ICU 支持)用 ORDER BY x COLLATE "zh-x-icu";MySQL 8 可以用 ORDER BY x COLLATE utf8mb4_zh_0900_as_cs,老项目里常见的写法是 ORDER BY CONVERT(x USING gbk)(GBK 编码里常用汉字大致按拼音排列,生僻字不一定准)。
随机排序
要随机挑几行,就按随机数排序:
SELECT member_id, signup_channel
FROM loyalty_members
ORDER BY random()
LIMIT 3;
| member_id | signup_channel |
|---|---|
| 781 | web |
| 32855 | web |
| 36422 | airport |
你运行时得到的三行会不一样,这正是目的。这个函数在 PostgreSQL、DuckDB 和 SQLite 里是 random(),在 MySQL、Hive 和 Spark SQL 里是 RAND(),SQL Server 用 ORDER BY NEWID()。为了留下三行而把整张大表排一遍代价很高;要从大表抽样,PostgreSQL 和 DuckDB 提供 TABLESAMPLE(DuckDB 还有 USING SAMPLE)。
常见错误
- 依赖「自然」顺序。 没有
ORDER BY时,顺序取决于引擎怎么方便,两次运行之间或更新数据之后都可能变。 - 在子查询或视图里写
ORDER BY。 大多数数据库不保证外层查询会保留这个顺序(SQL Server 在没有TOP时直接报错)。在最外层查询里排序。 - 只给一列写了
DESC,以为所有列都降序。 每一列都要单独写方向。 LIMIT之前没有决胜列。 并列会让取前 N 条和分页都不稳定。- 忘了
NULL排在哪里。 排序列可能为空时,写上NULLS LAST或NULLS FIRST。 - 对存成文本的数字排序。 先转换类型,或者修正列的类型。
这些规则在 SQL 基础查询和 SQL 速查表里也会反复出现。
常见问题
SQL ORDER BY 默认是升序还是降序?
默认升序(ASC):最小的数字、最早的日期、字母表开头的值排在前面。DESC 反过来。NULL 默认排在哪里取决于数据库,所以用 NULLS FIRST 或 NULLS LAST 明确写出来。
ORDER BY 怎样按多个字段排序?
用逗号分隔,每列写自己的方向:ORDER BY maker ASC, range DESC。第二列只决定第一列相同的那些行之间的顺序。
ORDER BY 里能用列别名吗?
能。ORDER BY 在 SELECT 之后执行,所以 SELECT COUNT(*) AS flights ... ORDER BY flights DESC 在所有主流数据库里都能用。但 WHERE 里不能用别名,因为 WHERE 在 SELECT 之前执行。
怎样让 NULL 排在最后?
在列后面加 NULLS LAST:ORDER BY delay_min DESC NULLS LAST。MySQL 和 SQL Server 没有这个语法,要先按「是否为空」的标记排序,比如 MySQL 里写 ORDER BY delay_min IS NULL, delay_min DESC。
能按 SELECT 里没有的列排序吗?
能,除了 SELECT DISTINCT(PostgreSQL 和 SQL Server 里)以及 GROUP BY 之后已经不存在的列。在分组查询里,可以按分组列或像 COUNT(*) 这样的聚合值排序。
中文怎么按拼音排序?
指定中文排序规则:DuckDB 用 ORDER BY x COLLATE zh,PostgreSQL 用 COLLATE "zh-x-icu",MySQL 8 用 COLLATE utf8mb4_zh_0900_as_cs,或者用老办法 ORDER BY CONVERT(x USING gbk)。不指定时,中文按 Unicode 编码排序,看起来没有规律。
ORDER BY 会让查询变慢吗?
对大结果集排序要花时间,尤其是没有 LIMIT 的时候。有 ORDER BY ... LIMIT n 时,数据库会用 top-N 排序,只在内存里保留 n 行;在 PostgreSQL、MySQL 或 SQL Server 里,排序列上的索引还能让数据库完全省掉排序。
其他语言: English, Español, Português (Brasil), Tiếng Việt