MySQL 8.0+支持ROW_NUMBER()窗口函数实现组内排序,需确认版本≥8.0.2且未禁用窗口函数,正确语法为OVER(PARTITION BY...ORDER BY...),推荐用CTE而非子查询提升性能,取每组首行应选ROW_NUMBER()而非RANK()或DENSE_RANK()。

MySQL 8.0 中直接用 ROW_NUMBER() 就能搞定组内排序,不需要变量、子查询或临时表。前提是你的 MySQL 版本确实是 8.0+(含 8.0.2 及以上),且 SQL 模式没禁用窗口函数。
确认是否支持 ROW_NUMBER()
执行这条语句,看会不会报错:
SELECT ROW_NUMBER() OVER() AS rn FROM DUAL LIMIT 1;
如果返回 rn = 1,说明窗口函数可用;如果报错 FUNCTION xxx.ROW_NUMBER does not exist 或类似提示,大概率是版本低于 8.0,或者启用了旧兼容模式(如 sql_mode 包含 NO_ENGINE_SUBSTITUTION 以外的限制项,但更常见的是版本问题)。
检查版本最准的方式是:
SELECT VERSION();
输出必须是 8.0.x 或更高(如 8.0.33、8.4.0)。
ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...) 的写法要点
这是组内排序的核心语法,PARTITION BY 定义“组”,ORDER BY 定义“组内怎么排”。常见错误是把 ORDER BY 放在外部查询里,导致序号乱掉。
-
PARTITION BY后只能跟字段名,不能是表达式(如PARTITION BY YEAR(create_time)在部分旧 8.0 小版本中会报错,建议先算好年份字段再分组) -
ORDER BY支持多字段,比如ORDER BY salary DESC, hiredate ASC,顺序很重要:先按薪资降序,同薪再按入职时间升序 - 如果
ORDER BY字段有NULL,默认NULL排在最前(ASC)或最后(DESC),可显式加NULLS FIRST或NULLS LAST(MySQL 8.0.22+ 支持) - 别在
WHERE中引用ROW_NUMBER()别名——它在WHERE阶段还没计算,得用子查询或 CTE
取每组第一条记录:为什么推荐 CTE 而不是子查询嵌套
典型需求:每个部门工资最高的员工(只取一个,哪怕并列也只留一行)。
正确写法(CTE):
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER(PARTITION BY dname ORDER BY salary DESC) AS rn FROM employee ) SELECT dname, ename, salary FROM ranked WHERE rn = 1;
不推荐的写法(子查询嵌套):
SELECT * FROM employee e1 WHERE 1 = ( SELECT COUNT(*) FROM employee e2 WHERE e2.dname = e1.dname AND e2.salary >= e1.salary );
区别在于:
- CTE 方式只需扫描
employee表一次,ROW_NUMBER()在内存中完成分组排序 - 子查询方式对每一行都触发一次关联扫描,数据量大时性能断崖式下降
- CTE 逻辑直白:
先编号,再过滤;子查询逻辑绕,且容易漏掉NULL或边界情况
和 RANK()、DENSE_RANK() 的关键区别在哪
三者都可用于分组排序,但处理并列的方式不同,直接影响“取第一”的结果:
-
ROW_NUMBER():严格递增,相同值也给不同序号(1,2,3,4)。适合“只取一条”场景,不会漏或多 -
RANK():并列则跳号(1,1,3,3,5)。若你用RANK() ... WHERE rn = 1,可能拿到多条;但用WHERE rn <= 3时,实际返回的行数可能少于 3 -
DENSE_RANK():并列不跳号(1,1,2,2,3)。适合“取前 N 名”且允许并列的情况
所以,只要需求是“每个组只留一条记录”,无条件选 ROW_NUMBER()。其他两个函数更适合排名展示类场景,而不是去重或取顶。
真正容易被忽略的点是:很多人写了 ROW_NUMBER() 却忘了加 PARTITION BY,结果得到的是全表连续编号,不是组内编号——这个错误在调试时往往要翻半天数据才能发现。


















