row_number() 是 mysql 8.0+ 分组内排序最可靠方式,需版本≥8.0.2、启用窗口函数,且 over() 中必须同时指定 partition by 和 order by;取每组首行须用 cte 先标序再过滤,不可在 where 中直接引用 rn。

ROW_NUMBER() 是 MySQL 8.0 分组内排序最直接、最可靠的手段,前提是版本 ≥ 8.0.2 且未禁用窗口函数。关联子查询或变量模拟不仅慢,还容易因 NULL、重复值或执行计划变化导致结果错乱。
确认你的 MySQL 真的支持窗口函数
别跳过这步——很多“报错说没这个函数”的情况,其实是版本不够或配置问题。
- 执行
SELECT VERSION();,输出必须是8.0.x或更高(如8.0.33、8.4.0) - 再跑一次
SELECT ROW_NUMBER() OVER() FROM DUAL LIMIT 1;,能返回数字才说明真可用 - 如果报
FUNCTION xxx.ROW_NUMBER does not exist,基本就是版本低于 8.0.2;Window 'w' lacks an ORDER BY clause则是语法写错了,不是不支持
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 必须写全,漏一个就失效
这是最常被抄错的模板。只写 PARTITION BY user_id 不加 ORDER BY,序号会随机分配;只写 ORDER BY created_at 不写 PARTITION BY,就成了全表排序,不是“组内”。
-
PARTITION BY后只能跟原始字段名,比如user_id、dept_id,不能直接写PARTITION BY YEAR(created_at)(部分旧小版本会报错) -
ORDER BY必须明确方向:ORDER BY created_at DESC才能取最新记录;默认ASC容易拿错第一行 - 如果排序字段可能重复(比如多个订单同秒创建),务必补二级排序,例如
ORDER BY created_at DESC, id DESC,否则每次执行结果可能不一致
想取每组第一条?必须用 CTE 或子查询包一层
ROW_NUMBER() 是在 SELECT 阶段计算的,而 WHERE 在它之前执行。所以你不能直接写 WHERE rn = 1。
- 推荐用 CTE:先打标,再过滤,语义清晰,MySQL 8.0 还可能做物化优化
- 示例:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn FROM orders ) SELECT id, user_id, amount, created_at FROM ranked WHERE rn = 1;
- 避免老式子查询嵌套(
SELECT * FROM (SELECT ...) t WHERE t.rn = 1),可读性差,某些场景下优化器更难生成高效执行计划
和 RANK()、DENSE_RANK() 混用时,逻辑差异直接影响业务结果
三者看着像,但行为完全不同。选错函数,Top N 就会漏人或重名。
- 要“每个部门工资最高且只取一人”,用
ROW_NUMBER();要“所有并列最高都留下”,就得换RANK()或DENSE_RANK() -
RANK()并列后跳号(1,1,3),DENSE_RANK()并列后不跳(1,1,2),ROW_NUMBER()强制唯一(1,2,3) - 如果业务要求“前 3 名”,且允许并列,
DENSE_RANK()更合理;若用于分页(如第 2 页 10 条),ROW_NUMBER()才能保证总数可控
真正麻烦的不是写法,而是排序依据是否稳定。哪怕 created_at 精确到微秒,只要没加唯一列兜底,MySQL 就可能在物理顺序上随机返回——这点在大数据量或高并发写入时特别容易暴露。











