# 排序之后只取前几行：ORDER BY、LIMIT 到底决定了什么

并列时谁排在前、NULL 落在哪一头、为什么 LIMIT 取不出每组的前几名，以及“前 3 笔订单”怎么能悄悄换成另 3 笔

> ORDER BY · LIMIT · NULL 处理 · 约 6 分钟 · 11 月 01 日

## 本篇要点

1. ORDER BY 排的是筛选、分组之后的行，所以汇总查询里可以直接按 SELECT 的别名（如 city_total）排序；DESC 只作用于紧挨着它的那一列。
2. LIMIT 只在给定顺序上裁剪前 N 行，顺序必须由 ORDER BY 提供；不写 ORDER BY 时拿到哪几行由数据库自行决定，加索引或数据变化后可能不同。
3. LIMIT 必须写在 ORDER BY 之后，写反触发 ERROR 1064；OFFSET 从 0 数起，MySQL 的 LIMIT 2, 3 等价于跳过 2 行取 3 行，参数语序与 OFFSET 写法相反。
4. 排序键出现并列时，并列行的先后没有约定，LIMIT 1 拿到哪一行不稳定；要稳定就加一个能唯一区分行的排序键，或承认并列并全部返回。
5. MySQL 升序时 NULL 排最前、降序时排最后，且没有 NULLS FIRST/LAST 选项；PostgreSQL 默认相反，所以可用 ORDER BY city IS NULL, city 自行固定空值位置。
6. GROUP BY 加 MAX 只能给出每组的最大值，给不出这个值来自哪一行（MySQL 默认的 ONLY_FULL_GROUP_BY 会报 ERROR 1055）；LIMIT 作用在整条结果上而非每个分组，因此做不到“每组取前几名”。
7. 相关子查询能凑出每组最大值对应的行，但组内城市为 NULL 时整组丢失、组内并列时会返回多行；“每组按次序取第 1 行”要靠窗口函数。

---

前面几篇把数据筛出来、压成统计表、拼回一起、分层复用。还有一个几乎每次取数都要做的动作没讲：**按业务关心的次序把结果排好，只看最前面的那几行**。

这篇要回答的问题很具体：一句“给我金额最高的前 3 笔订单”，怎么写；以及为什么同一句 SQL，换个写法或换个数，你拿到的那几行可能并不是你以为的那几行。材料还是那 9 行 `orders`（环境是 MySQL 8.0，PostgreSQL 上语法基本一样，有差别的地方我会标出）：

| order_id | city | amount |
|---|---|---|
| 1001 | 杭州 | 129.00 |
| 1002 | 上海 | 58.50 |
| 1003 | 杭州 | 320.00 |
| 1004 | NULL | 76.00 |
| 1005 | 杭州 | 45.00 |
| 1006 | 上海 | 210.00 |
| 1007 | 上海 | 91.50 |
| 1008 | NULL | 19.90 |
| 1009 | 北京 | 199.00 |

## 第一步：给结果排个序

```sql
SELECT order_id, city, amount
FROM orders
ORDER BY amount DESC;
```

跑完第一行应该是 1003（320.00），最后一行是 1008（19.90）。`ORDER BY` 里的列叫**排序键**；不写 `ASC` / `DESC` 时默认升序。

要按两列排就写两个键，比如 `ORDER BY city, amount DESC`：先按城市，城市相同再按金额从大到小。注意 **`DESC` 只作用于紧挨着它的那一列**，它管不到前面的 `city`。

语句的位置不是随便放的。整条查询的顺序是 `WHERE → GROUP BY → HAVING → ORDER BY → LIMIT`，排序发生在筛选和分组之后，`ORDER BY` 排的是“已经过滤、已经汇总好”的那些行。所以在汇总查询里可以直接按别名排：

```sql
SELECT city, SUM(amount) AS city_total
FROM orders
GROUP BY city
ORDER BY city_total DESC;
```

结果是杭州 494.00、上海 360.00、北京 199.00、`city` 为空那一组 95.90。

## 第二步：只留前 3 行

在刚排好的结果后面加一行：

```sql
SELECT order_id, city, amount
FROM orders
ORDER BY amount DESC
LIMIT 3;
```

看到 1003、1006、1009 三行就对了。`LIMIT 3` 的含义是**在排好的结果上砍下前 3 行**，它自己不带任何顺序。

顺序写反会立刻报错：把 `LIMIT` 放到 `ORDER BY` 前面，MySQL 给 `ERROR 1064 (42000): You have an error in your SQL syntax`。语句各部分的先后是语法规定死的。

要跳过开头几行就用 `OFFSET`：`LIMIT 3 OFFSET 2` 是跳过前 2 行、再取 3 行，得到 199.00、129.00、91.50；偏移从 **0** 数起。MySQL 的简写 `LIMIT 2, 3` 含义相同，但两个参数是“先偏移、后条数”，语序与 `OFFSET` 写法相反，容易写反。

最要紧的一件事：**不写 `ORDER BY` 直接 `LIMIT 3`，那 3 行是任取的**。数据库只保证“给你 3 行”，不保证是最小的、最早的或最新的。今天可能是这 3 行，明天加了索引、数据变多，就换成另外 3 行。`LIMIT` 只做裁剪，顺序得你给。

## 第三步：排序键出现并列时，“前 3”是谁并不确定

换个业务问题：哪个城市订单最多？

```sql
SELECT city, COUNT(*) AS cnt
FROM orders
GROUP BY city
ORDER BY cnt DESC;
```

杭州 3 笔、上海 3 笔、`city` 为空那一组 2 笔、北京 1 笔。现在在末尾加 `LIMIT 1`：你会拿到“3 笔”，但**不能保证是杭州还是上海**。两行在排序键 `cnt` 上完全相等，这种情况叫**并列**，数据库对并列的行没有任何先后约定，它按执行计划给出的顺序截断。换版本、加索引、数据量变化，都可能让你这次看到杭州、下次看到上海。

修法取决于业务上要什么。并列必须拆开，就再加一个排序键，比如 `ORDER BY cnt DESC, city`——但中文列的先后由数据库的排序规则（collation）决定，既不是拼音序也不一定合你的预期，换个字符集还可能变；要稳定，用一个你能解释清楚的键（比如城市编码）更可靠。并列就是并列，那就别只要一行，把两家都交出去。

## 第四步：NULL 排在哪一头

执行 `ORDER BY city`（升序），那两条 `city` 为 NULL 的订单排在最前面；改成 `ORDER BY city DESC`，它们排到最后。这是 MySQL 的默认行为——手册写得很直白：升序时 NULL 排在其他值之前，降序时排在其后。

换库这个默认就变了：PostgreSQL 默认升序时 NULL 排在最后，并且支持 `NULLS FIRST` / `NULLS LAST` 显式指定；MySQL 8.0 的 `ORDER BY` 没有这个写法。所以只要“空值算不算最前”会影响结论，就不要依赖默认。MySQL 里可以自己造一个排序键：

```sql
SELECT order_id, city, amount
FROM orders
ORDER BY city IS NULL, city;
```

`city IS NULL` 的结果是 0 或 1，升序排时 0（非空的行）在前，空值那两行沉到最后；想放最前就给它加 `DESC`。如果空值压根不该参与（“城市未知”不是一个城市），更干脆的做法是在 `WHERE` 里筛掉。

## 第五步：LIMIT 取不出“每组的前几名”

真正的需求常常是：**每个城市金额最高的那一笔订单，要 order_id、city、amount 三列**。

先用 `GROUP BY` 试：

```sql
SELECT order_id, city, MAX(amount)
FROM orders
GROUP BY city;
```

MySQL 8.0 会直接拦下来：`ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ... which is not functionally dependent on columns in GROUP BY clause`。这不是数据库刁难。对一个城市那一行来说，`MAX(amount)` 是明确的 320.00，但“320.00 来自哪一笔订单”它无从知道——聚合把 3 行压成 1 行时，其他列的信息就丢了。

`LIMIT` 同样帮不上。`ORDER BY amount DESC LIMIT 1` 得到的是**整张表**裁出来的一行（1003）；`LIMIT` 作用在整条查询的最终结果上，不是每个分组各裁一次。所以“每组各取一行”这件事，`LIMIT` 在结构上做不到。

能凑出正确结果的写法是相关子查询（子查询里引用了外层的列 `o.city`）：

```sql
SELECT order_id, city, amount
FROM orders o
WHERE amount = (SELECT MAX(amount) FROM orders WHERE city = o.city);
```

跑出来是 1003、1006、1009 三行，每个城市一笔。但它有两个必须知道的坑：`city` 为空的两笔**一笔都出不来**——`city = o.city` 在两边都是 NULL 时结果是 NULL 而不是 true，跟前面讲过的“NULL 不能用等号判断”是同一件事；一组里有两笔同额最高时，两行都会返回，“每组一笔”悄悄变成“每组 N 笔”。

要做到“每组按某个次序取第 1 行”，需要一种既保留明细行、又能在组内排名的工具，那是窗口函数（`ROW_NUMBER`）要解决的问题，留到下一篇。

## 交稿前的自查

- 有 `LIMIT` 就必须有 `ORDER BY`，否则那几行是任取的。
- 排序键能不能把每一行区分开？有并列，就别指望 `LIMIT` 固定给你某几行。
- `DESC` 写在哪一列上，它只管紧挨着的那一列。
- NULL 默认排在哪一头，换个数据库还成不成立。
- 你要的是“整张表的前 N”，还是“每组的前 N”——后者不是 `LIMIT` 的活。

## 思维导图

```mermaid
flowchart LR
  A["排序后取前几行"] --> B["ORDER BY：按什么排"]
  B --> B1["默认升序，DESC 只管紧邻的一列"]
  B --> B2["排的是 WHERE / GROUP BY 之后的行，可用别名"]
  A --> C["LIMIT：裁下前 N 行"]
  C --> C1["必须写在 ORDER BY 之后，写反报 1064"]
  C --> C2["OFFSET 从 0 数起，LIMIT 2,3 语序相反"]
  C --> C3["没有 ORDER BY 时 LIMIT 是任取"]
  A --> D["结果不可靠的三种情况"]
  D --> D1["排序键并列：进哪一行由数据库决定"]
  D --> D2["NULL 位置：MySQL 升序在前，PostgreSQL 默认在后"]
  D --> D3["无排序直接 LIMIT"]
  A --> E["LIMIT 结构性做不到"]
  E --> E1["每组各取前几名"]
  E1 --> E2["GROUP BY + MAX 丢明细行，报 1055"]
  E1 --> E3["相关子查询能凑，但漏 NULL、并列多行"]
  E1 --> E4["下一步：窗口函数 ROW_NUMBER"]
```

## 术语表

- 排序键：ORDER BY 后面用来决定先后的一列或几列值；排序键相同的行构成并列，它们之间没有约定顺序。
- 并列（tie）：多行在排序键上取值完全相同。此时数据库不保证谁在前，所以“前 N 行”里包含哪些并列行是不确定的。
- LIMIT / OFFSET：LIMIT 在已排好的结果上砍下前 N 行，OFFSET 决定跳过开头多少行（从 0 数起）。两者都不产生顺序。
- ONLY_FULL_GROUP_BY：MySQL 8.0 默认开启的 SQL 模式，禁止 SELECT 里出现既不在 GROUP BY 中、也没有被聚合函数包住的列，因为该列该取哪一行的值无法确定。

## 来源

1. [MySQL 8.0 Reference Manual: SELECT Statement — ORDER BY 与 LIMIT {[offset,] row_count | row_count OFFSET offset} 的语法与参数含义](https://dev.mysql.com/doc/refman/8.0/en/select.html)
2. [MySQL 8.0 Reference Manual: NULL Values — 升序时 NULL 排在其他值之前、降序时排在其后的默认行为](https://dev.mysql.com/doc/refman/8.0/en/null-values.html)
3. [PostgreSQL Documentation: LIMIT and OFFSET — 为什么 LIMIT 必须配一个能定出唯一顺序的 ORDER BY](https://www.postgresql.org/docs/current/queries-limit.html)
4. [PostgreSQL Documentation: SELECT — NULLS FIRST/LAST 与 FETCH FIRST n ROWS WITH TIES 的语法](https://www.postgresql.org/docs/current/sql-select.html)

---

原文：https://pangzhengboyin.com/articles/order-by-limit-top-n-rows-b9e924d6

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具：每篇 5–10 分钟，边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
