row_number()必须配合partition by和order by才能分组取首行;单独使用仅全局编号,漏写order by会导致结果不可控,需嵌套子查询筛选rn=1,并为分组与排序字段建立联合索引。

ROW_NUMBER() 必须配合 PARTITION BY 和 ORDER BY 才能分组取首行
单独写 ROW_NUMBER() 不会自动分组,它默认对整个结果集编号。要按某字段(比如 user_id)分组并取每组第一条,必须显式写 PARTITION BY user_id,再用 ORDER BY 明确“哪条算第一”——比如按时间倒序取最新记录,就得 ORDER BY created_at DESC。
常见错误是漏掉 ORDER BY,此时数据库仍会执行,但排序不可控,返回的“第一条”随机,线上容易出数据不一致问题。
- 没
PARTITION BY→ 全表编号,不是分组 -
ORDER BY字段含 NULL → NULL 可能排最前或最后,取决于数据库(PostgreSQL 默认 NULLS LAST,MySQL 8.0 默认等同于最小值),建议显式写ORDER BY status DESC NULLS LAST(PostgreSQL)或用COALESCE(status, '')(MySQL/SQL Server) - 多个字段排序时,确保组合能唯一确定顺序,否则相同排序值的行编号不确定
子查询 + WHERE rn = 1 是最通用、可读性最强的写法
窗口函数不能直接在 WHERE 中过滤编号,必须嵌套一层:外层查 rn = 1,内层生成 rn。这是跨数据库(MySQL 8.0+、PostgreSQL、SQL Server、Oracle)都支持的写法。
示例:取每个用户的最新订单
SELECT user_id, order_id, amount, created_at
FROM (
SELECT user_id, order_id, amount, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;
- 别名
t在 MySQL 中不可省略(会报错Every derived table must have its own alias) - PostgreSQL 允许省略别名,但加了更清晰,也兼容其他引擎
- 不要试图用
HAVING替代WHERE——HAVING是聚合语义,这里没GROUP BY,语法不合法
注意 ORDER BY 中的字段是否允许为 NULL,否则可能漏掉有效记录
如果用来排序的字段(如 updated_at)存在 NULL 值,而你又用了 ORDER BY updated_at DESC,某些数据库(如 PostgreSQL)默认把 NULL 排在最前面,导致 NULL 行被误标为 rn = 1;另一些(如 SQL Server)默认 NULL 最小,排最后,反而可能跳过本该取的记录。
- 安全做法:用
COALESCE(updated_at, '1970-01-01')把 NULL 统一转为极小值(MySQL/SQL Server) - PostgreSQL 可用
ORDER BY updated_at DESC NULLS LAST确保 NULL 不抢第一 - 若业务上 NULL 表示“未生效”,那它本就不该参与“取最新”逻辑,应提前在
WHERE过滤掉:WHERE updated_at IS NOT NULL
性能关键点:PARTITION BY 和 ORDER BY 字段必须有联合索引
ROW_NUMBER() 的开销集中在排序阶段。如果 PARTITION BY user_id ORDER BY created_at DESC 没对应索引,数据库会强制对全表排序,大数据量下秒变慢查询。
建索引示例(以 PostgreSQL / MySQL 8.0+ 为例):
CREATE INDEX idx_user_created ON orders (user_id, created_at DESC);
- 索引字段顺序必须和
PARTITION BY、ORDER BY严格一致:先分组字段,再排序字段 - MySQL 8.0+ 支持降序索引(
DESC关键字有效),5.7 及以前会忽略DESC,只建升序索引,此时ORDER BY ... DESC仍可能触发文件排序 - 如果排序字段有高重复率(如大量订单
status = 'pending'),考虑加入主键(如id)做二级排序,避免排序不稳定:ORDER BY status DESC, id DESC,对应索引也要包含id











