必须用子查询或cte包裹row_number(),因其不能直接用于where;order by不可省略以保证结果稳定;partition by用于分组,非group by;取唯一首行应选row_number()而非rank()或dense_rank()。

用 ROW_NUMBER() 实现每组取第一行的通用写法
直接上最稳妥的模式:先用 ROW_NUMBER() 按分组和排序规则编号,再外层筛选 rn = 1。关键不是“怎么写”,而是“为什么必须套一层子查询或 CTE”——因为窗口函数不能直接出现在 WHERE 子句中。
- 必须用子查询或 CTE 包裹,否则报错
Window function is not allowed in WHERE clause -
ORDER BY在ROW_NUMBER()内不可省略,即使你只关心“任意一行”,也得显式写个确定性排序(比如ORDER BY id),否则结果不稳定 - 分组字段写在
PARTITION BY后,不是GROUP BY;两者语义完全不同,混用会得到意外结果
ROW_NUMBER() 和 RANK()/DENSE_RANK() 的区别在哪
三者都生成序号,但处理并列的方式不同,直接影响“第一行”的定义:
-
ROW_NUMBER():严格递增,相同排序值也强制不同编号(1,2,3),适合“取唯一一行”场景 -
RANK():并列时跳号(1,1,3),若你按时间排序且多条同时间,可能多个rn = 1,但RANK() = 1也可能返回多行 -
DENSE_RANK():并列不跳号(1,1,2),同样存在多行并列第一的风险
所以只要明确要“每组只取一条”,就坚持用 ROW_NUMBER(),别被名字误导去选 RANK()。
性能和索引注意事项
ROW_NUMBER() 的开销主要来自排序。如果 PARTITION BY 字段和 ORDER BY 字段没有联合索引,大数据量下容易触发磁盘排序,响应明显变慢。
- 理想索引是
(group_col, order_col),例如CREATE INDEX idx_user_status_created ON orders (status, created_at) - MySQL 8.0+、PostgreSQL、SQL Server 都能较好利用该索引加速窗口函数;SQLite 则基本不优化,慎用于大表
- 避免在
ORDER BY中使用函数(如ORDER BY UPPER(name)),会导致索引失效
常见错误:NULL 值导致分组异常或排序错乱
当 PARTITION BY 字段含 NULL,多数数据库会把所有 NULL 归为同一组;而 ORDER BY 中的 NULL 默认排在最前(PostgreSQL)或最后(MySQL),行为不一致。
- 如果业务上
NULL表示“未分类”,通常希望它单独成组,可用COALESCE(group_col, uuid_generate_v4())(PostgreSQL)或IFNULL(group_col, CONCAT('null_', id))(MySQL)兜底 - 显式控制
NULL排序:加NULLS LAST(PostgreSQL/Oracle)或用ORDER BY col IS NULL, col(MySQL 兼容写法) - 别依赖
SELECT *+ROW_NUMBER()—— 如果原表有重复列名或计算字段,外层查询可能因别名冲突报错
真正麻烦的不是语法写不对,而是没意识到 PARTITION BY 和 ORDER BY 字段的空值、重复、类型隐式转换,会让“第一行”在不同环境里指向完全不同的记录。










