根本原因是oracle order by缺乏确定性顺序,当排序字段存在重复值且未补充唯一字段(如主键或rowid)时,相同键值的行物理顺序不固定,导致rownum分配不稳定,进而引发分页数据重复。

Oracle分页查询在Java中结果重复,根本原因不是Hibernate或MyBatis框架的问题,也不是setFirstResult()和setMaxResults()用错了,而是Oracle执行排序时缺乏**确定性顺序**——只要ORDER BY字段存在重复值,且未补充唯一性字段,每次执行ROWNUM分页就可能返回不一致的行序。
Oracle的ORDER BY本身不保证稳定性
Oracle官方文档明确说明:当排序键(如create_date)相等时,数据库不承诺保持原始物理顺序。这意味着即使SQL里写了ORDER BY create_date DESC,相同时间戳的多条记录在不同查询中可能被任意排列——而ROWNUM是在排序结果集上逐行编号的,顺序一变,ROWNUM分配就变,导致第2页和第3页取到重叠数据。
- 现象典型:前端翻页时,第3页开头几条和第2页结尾几条完全一样
- 排查关键:把Hibernate生成的SQL直接粘进PL/SQL执行,参数不变但结果变动 → 问题在DB层
- 误区警惕:以为加了
ORDER BY就万事大吉,其实只是“有排序”,不是“可复现排序”
必须补一个唯一字段才能闭环
解决方法不是换框架、不是调参数,而是让ORDER BY子句具备**全序能力**——即任意两行都能比较出大小关系。最稳妥的做法是追加主键或ROWID:
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
-
ORDER BY create_date DESC, id ASC(推荐,语义清晰,索引友好) -
ORDER BY create_date DESC, ROWID ASC(通用,无需业务字段,但ROWID在表迁移后失效) - 避免用
ORDER BY create_date DESC, ROWNUM——ROWNUM是结果集生成后才赋值的,不能用于排序依据 - 如果联表查询,唯一字段必须来自驱动表(即
FROM后的主表),否则一对多会导致重复行放大
MyBatis用RowBounds也逃不开这个坑
很多人以为用RowBounds就能绕过手写ROWNUM逻辑,其实MyBatis底层仍会拼出类似三层嵌套SQL。只要Mapper里的SELECT语句没带足够排序字段,RowBounds照样分页错乱。
- 错误写法:
<select> SELECT * FROM user ORDER BY gmt_create DESC </select> - 正确写法:
<select> SELECT * FROM user ORDER BY gmt_create DESC, id ASC </select> - Hibernate HQL同理:
FROM User u ORDER BY u.createTime DESC, u.id ASC - 注意:复合排序字段顺序影响索引使用,
create_time + id组合索引要按此顺序建
真正容易被忽略的点是:这个问题在小数据量、低并发下几乎不暴露——只有当重复排序字段值较多,且分页跨多个数据块读取时,Oracle的块内扫描顺序差异才会被放大。上线前不做千条级数据压测,很可能漏掉这个隐患。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










