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

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

order_idcityamount
1001杭州129.00
1002上海58.50
1003杭州320.00
1004NULL76.00
1005杭州45.00
1006上海210.00
1007上海91.50
1008NULL19.90
1009北京199.00

第一步:给结果排个序

1SELECT order_id, city, amount
2FROM orders
3ORDER 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 排的是“已经过滤、已经汇总好”的那些行。所以在汇总查询里可以直接按别名排:

1SELECT city, SUM(amount) AS city_total
2FROM orders
3GROUP BY city
4ORDER BY city_total DESC;

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

第二步:只留前 3 行

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

1SELECT order_id, city, amount
2FROM orders
3ORDER BY amount DESC
4LIMIT 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”是谁并不确定

换个业务问题:哪个城市订单最多?

1SELECT city, COUNT(*) AS cnt
2FROM orders
3GROUP BY city
4ORDER 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 里可以自己造一个排序键:

1SELECT order_id, city, amount
2FROM orders
3ORDER BY city IS NULL, city;

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

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

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

先用 GROUP BY 试:

1SELECT order_id, city, MAX(amount)
2FROM orders
3GROUP 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):

1SELECT order_id, city, amount
2FROM orders o
3WHERE 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 的活。