结论:ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)可直接实现每组从1开始编号,无需自连接或变量;缺ORDER BY必报错,PARTITION BY字段需为确定列,NULL视为独立分组,多字段用逗号分隔,性能依赖(user_id, created_at)等联合索引。

SQL里用ROW_NUMBER()配合PARTITION BY实现分组内重置序号
直接说结论:靠ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)就能做到每组从1开始编号,不需要自连接或变量模拟。
关键在PARTITION BY把数据切块,ORDER BY决定组内排序逻辑——没ORDER BY会报错,这点很多人踩坑。
-
PARTITION BY字段必须是实际存在的列,不能是表达式(如PARTITION BY YEAR(created_at)在MySQL 8.0+才支持,旧版得先算出年份列) - 如果想按时间倒序编号(最新一条为1),
ORDER BY created_at DESC,别漏DESC - 多个字段分组写成
PARTITION BY dept_id, status,逗号分隔,不加括号
为什么RANK()和DENSE_RANK()不适合“自动重置序号”场景
它们解决的是并列排名问题,不是纯递增序号。ROW_NUMBER()保证严格1、2、3…,而RANK()遇到相同值会跳号(如1,2,2,4),DENSE_RANK()不跳但也不“重置”——它只是去重后连续编号,和分组无关。
比如同一组里两行score=95:
-
ROW_NUMBER()→ 1, 2(不管值是否相同) -
RANK()→ 1, 1(并列第1),下一行变3 -
DENSE_RANK()→ 1, 1(并列第1),下一行变2
要的是“每组独立1起始的流水号”,只用ROW_NUMBER()。
PARTITION BY字段为空值时的分组行为
NULL会被当作一个独立分组。比如PARTITION BY category,所有category IS NULL的行会挤在同一个组里编号,而不是被忽略或散开。
- 想排除空值?提前用
WHERE category IS NOT NULL - 想把NULL归入某类(如“未知”)?用
COALESCE(category, 'Unknown')包装后再PARTITION BY - 不同数据库对NULL分组的一致性高,PostgreSQL/MySQL 8.0+/SQL Server都这么处理
性能注意点:PARTITION BY字段没索引会很慢
窗口函数本身不走索引,但ORDER BY部分如果涉及大表排序,没索引就会触发磁盘临时文件,尤其当分组多、每组数据量大时。
- 典型慢查询:对千万级订单表按
user_id分组再按created_at排序编号 - 优化方法:给
(user_id, created_at)建联合索引(顺序不能反) - MySQL 8.0之前不支持窗口函数,强行用变量
@row := IF(@u = user_id, @row + 1, 1)模拟,但结果不可靠(执行计划变动可能错乱)
真正稳定又高效的方案,还是升级到支持窗口函数的版本,老老实实用ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)——那个ORDER BY不是可选项,是强制要求,也是最容易漏写的部分。










