ROW_NUMBER() 能修复 MyBatis 分页排序错乱,因其作为窗口函数在排序后严格生成唯一序号,固化顺序;需用子查询先编号再筛选范围,且 ORDER BY 必须包含唯一字段兜底,避免重复值导致编号不稳定。

ROW_NUMBER() 为什么能修复 MyBatis 分页时的排序错乱?
根本原因是:MyBatis 的 limit + offset 分页(如 MySQL 的 LIMIT 10 OFFSET 20)在多字段排序、存在重复值时,数据库执行计划可能不保证稳定顺序;而 ROW_NUMBER() 是窗口函数,在排序后**严格按指定规则生成唯一序号**,相当于把排序逻辑“固化”进结果集。
典型现象:同一页数据刷新后顺序变、上下页有重复或漏掉记录、ORDER BY create_time DESC, id DESC 下时间相同的数据每次查出来位置不一致。
MyBatis XML 中正确写法的关键点
不能直接在 SELECT 里套 ROW_NUMBER() 然后加 LIMIT——那会先分页再编号,失去意义。必须先用子查询/CTE 编号,再外层筛选行号范围。
- MySQL 8.0+ / PostgreSQL / SQL Server 都支持,但语法细节不同;MySQL 5.7 不支持窗口函数,必须升级或换方案
- MyBatis 的
或#{}参数要传入pageNum和pageSize,用于计算rn BETWEEN ? AND ? -
ORDER BY必须出现在ROW_NUMBER() OVER (ORDER BY ...)里,且和业务要求的展示顺序完全一致;如果漏掉id等唯一字段兜底,重复值仍会导致编号不稳定
示例(MySQL 8.0+):
SELECT * FROM (
SELECT
id, name, create_time,
ROW_NUMBER() OVER (ORDER BY create_time DESC, id DESC) AS rn
FROM user_info
WHERE status = #{status}
) t
WHERE t.rn BETWEEN #{startRow} AND #{endRow}
其中 #{startRow} = (pageNum - 1) * pageSize + 1,#{endRow} = pageNum * pageSize。
MyBatis-Plus 的 Page 对象怎么配合 ROW_NUMBER()?
MyBatis-Plus 默认分页插件走的是物理分页(LIMIT),不兼容窗口函数。想用 ROW_NUMBER(),必须绕过自动分页,手动写 SQL 并禁用 Interceptor 的重写逻辑。
- 在 Mapper 接口方法上加
@Select注解,或 XML 中写完整子查询 SQL,不要用selectPage方法 - 避免在 service 层调用
PageHelper.startPage(),否则会强行注入LIMIT,导致 SQL 语法错误 - 返回类型用
List或自定义 DTO,别依赖 MP 的IPage自动封装 total —— 总数得另查一次COUNT(*)(注意:不能 COUNT(rn),要 COUNT(1))
容易被忽略的性能和兼容性陷阱
窗口函数不是银弹。在大数据量下,ROW_NUMBER() 会强制全表排序再编号,比带索引的 LIMIT 更重。
- 确保
ORDER BY字段组合上有联合索引,例如(status, create_time, id),否则OVER子句会触发 filesort - Oracle 用户注意:
ROW_NUMBER()没问题,但别和ROWNUM混用;SQL Server 要用OFFSET FETCH替代时,优先考虑它而非窗口函数 - PostgreSQL 的
OFFSET在深分页(如OFFSET 100000)下也慢,此时ROW_NUMBER()+ 子查询反而更稳,但务必加WHERE条件缩小基数
真正麻烦的从来不是写对那一行 ROW_NUMBER() OVER,而是确认你的数据库版本、索引覆盖、以及 MyBatis 是否在背后偷偷改了你的 SQL。











