10-ROWNUM三层分页的演化 10-OracleDialectROWNUM三层分页的演化Oracle没有LIMIT OFFSET。分页要用ROWNUM包三层——这三层不是设计出来的是Oracle的执行顺序逼出来的。而且browise的分页还修过一个→的bug。这篇从Oracle的执行模型讲起把三层分页的每一层为什么存在、bug是怎么发生的讲透。文章目录10-OracleDialectROWNUM三层分页的演化一、先理解ROWNUM的怪脾气二、三层分页每层解决一个铁律第一层排序第二层包一层让ROWNUM在排序结果上编号第三层再包一层把ROWNUM变成普通列三、 → 的bug四、supportsOffset()两种分页的参数顺序差异五、paginateCustom与paginate为什么是两份六、countWrap总行数的三种姿态七、ORDER BY为什么被塞进中层源码browise-metadata/src/main/java/com/browise/ea/core/dialect/OracleDialect.javaL97-109一、先理解ROWNUM的怪脾气Oracle的ROWNUM是伪列——行被取出来时才赋值。它有两条铁律铁律一ROWNUM在ORDER BY之前赋值SELECTROWNUM,nameFROMusersORDERBYname-- ROWNUM是排序前的编号不是排序后的名次铁律二ROWNUM判断只能用或WHEREROWNUM10-- 永远返回0行-- 原因第一行取出来时ROWNUM1110不成立→丢弃-- 丢弃后下一行取出来ROWNUM还是1前面的行没被计数-- 110又不成立→无限循环丢弃ROWNUM 10同理不行——ROWNUM从小于某个值的行开始积累一旦要求它天生大于某个值它永远达不到。二、三层分页每层解决一个铁律目标取按 name 排序后的第 11~20 行。第一层排序SELECT...FROMusersORDERBYname原始查询排序——但直接在这里加ROWNUM条件会撞铁律一编号是排序前的。第二层包一层让ROWNUM在排序结果上编号SELECTrow_1.*,ROWNUMASrownum_FROM(第一层)row_1-- rownum_ 是排序后结果集上的连续编号 ✓ 绕开铁律一但在这里写WHERE rownum_ 11撞铁律二别名rownum_本质还是ROWNUM语义。第三层再包一层把ROWNUM变成普通列SELECT*FROM(SELECTrow_1.*,ROWNUMASrownum_FROM(...原始SQLORDERBY...)row_1)row_WHERErow_.rownum_?ANDrow_.rownum_?-- rownum_已经物化成结果集的普通数字列——任意比较都合法 ✓ 绕开铁律二OracleDialect.paginate的完整实现OverridepublicStringpaginate(Stringsql,StringorderByColumn){StringorderBy(orderByColumn!null!orderByColumn.isEmpty())? order by orderByColumn:;returnselect * from (select row_1.*, rownum as rownum_ from (sqlorderBy) row_1) row_ where row_.rownum_ ? and row_.rownum_ ?;}三层各有名字内层业务查询ORDER BY、中层编号、外层过滤。三、→的bugBROWISE-STATUS记录了这次修复。修复前的代码whererow_.rownum_?androw_.rownum_?-- 参数: minRow1, maxRow20第1页第1页rownum_ 1 AND rownum_ 20→第1行丢了用户看到的第一页从第2条开始——列表第一条数据永远神秘消失。修复 ?whererow_.rownum_?androw_.rownum_?-- 第1页: rownum_ 1 AND rownum_ 20 → 正确的1~20行这类bug为什么难以发现——只有第1页丢一行第2页21~40完全正常。人工测试翻页通常看第2页第1页的数据看起来也挺全——少了一条谁数得清呢。是数据对账列表total137但第一页只有19条暴露的。四、supportsOffset()两种分页的参数顺序差异SqlBuilder追加分页参数的代码第07篇讲过strSqldialect.paginate(strSql,orderByColumn);if(dialect.supportsOffset()){// LIMIT ? OFFSET ? 风格paramValues.add(maxRow-minRow1);// 第一个参数页大小paramValues.add(minRow-1);// 第二个参数偏移量}else{// ROWNUM风格paramValues.add(minRow);// 第一个参数起始行paramValues.add(maxRow);// 第二个参数结束行}风格SQL形态参数序ROWNUMOracle ? AND ?min, maxOFFSETMySQL/PGLIMIT ? OFFSET ?size, offset同一个params列表尾部两种方言追加的值含义完全不同。supportsOffset()这个布尔值就是方言告诉调用方我这边参数该按什么顺序给的信号。KingBasePgDialect修的就是这个——它语法是LIMIT/OFFSETsupportsOffset应返回true但最初参数按min,max追加翻页越翻越偏。修复实现supportsOffset()返回trueSqlBuilder加分支判断。五、paginateCustom与paginate为什么是两份OverridepublicStringpaginate(Stringsql,StringorderByColumn){/* 三层ROWNUM */}OverridepublicStringpaginateCustom(Stringsql,StringorderByColumn){/* 三层ROWNUM一模一样 */}Oracle方言里两个方法实现相同——但在别的方言里不同。MySQL的paginate由元数据查询调用列已知paginateCustom由rawSql调用整条SQL是黑盒包COUNT/分页的姿势可能有区别比如要不要强制内层ORDER BY。接口留两个入口Oracle恰好可以共用实现别的方言按需分化。六、countWrap总行数的三种姿态分页的另一半是COUNT。OracleDialectOverridepublicStringcountWrap(Stringsql){returnselect count(*) as rowcount1 from (sql) a;}把原查询包成子查询再count——最通用但不是最优。理论上可以剥掉ORDER BY提升一点性能但无脑包一层最安全不用解析SQL结构任何查询都能包。这和MyBatisHelper的autoCountSELECT COUNT(1) FROM (...) _t是同一个思路——browise-data模块给MyBatis路径用EA引擎给元数据路径用两处各自实现了同一个包装策略。七、ORDER BY为什么被塞进中层注意paginate的实现——sql orderBy被包在中层里select*from(selectrow_1.*,rownumasrownum_from(原始SQLorderbyxxx ←ORDERBY在内层)row_1)row_如果ORDER BY放到外层会怎样——外层先过滤rownum_再排序但rownum_是排序前编号铁律一过滤出来的行是随机20行再排序。分页必须先定序后取段——ORDER BY的位置不是风格问题是正确性问题。这也解释了getFirstConditionColumn()EaEngine里为什么给分页一个默认排序列——无ORDER BY的分页在Oracle上每页结果不确定ROWNUM编号顺序不保证翻页可能重复或丢行。引擎宁可拿第一个条件列当默认排序也不允许无序分页。✅ 亮点从ROWNUM两条铁律推出三层分页的必然结构复盘→丢首行bug的成因和为什么难发现讲透supportsOffset()决定参数顺序的接口设计、ORDER BY塞中层是正确性不是风格。适合所有要在Oracle上做分页的人。扩展方向第11篇rawSql通道的分页包装、第24篇MyBatisHelper的autoCount对照。