在做列表页、报表导出这类功能时,几乎都会遇到分页取数的需求。DB2从早期版本开始就提供了FETCH FIRST n ROWS ONLY子句,用来限制查询返回的最大行数,写法简洁直观。不过很多初学者只停留在会用的层面,一旦遇到深分页、排序不稳定、结果重复或遗漏等问题就容易踩坑。这篇文章从基本语法讲起,逐步展开分页查询的几种实现方式和背后的性能考量。

FETCH FIRST ROWS ONLY的基本语法与执行逻辑
FETCH FIRST n ROWS ONLY必须写在SQL语句的末尾,紧跟在ORDER BY之后(如果有的话)。它的作用是告诉优化器:这条查询最多只需要返回前n行。最基本的写法如下:
-- 取前10条记录 SELECT EMPNO, FIRSTNME, SALARY FROM EMPLOYEE ORDER BY SALARY DESC FETCH FIRST 10 ROWS ONLY;
这种写法最大的价值在于性能。DB2优化器在识别到FETCH FIRST子句后,会倾向于选择能够提前终止扫描的执行计划。举个例子,如果SALARY列上有索引,配合降序扫描,数据库可能只需要读取前10条索引条目就停止工作,而不需要把整张表全部扫完再排序。这一点和先取全量结果再由应用程序截断的做法有本质区别。
还有两个细节值得注意。第一,如果不写ORDER BY,"前10条"的顺序是不确定的,任何能满足条件的行都可能被返回,所以分页场景下务必配合ORDER BY使用。第二,FETCH FIRST 0 ROWS ONLY在语法上是合法的,会返回空结果集,某些ORM框架会用它来做查询合法性校验。
实现翻页:OFFSET与FETCH的组合写法
单纯的FETCH FIRST只能取第一页,要翻到第二页、第三页,就需要配合OFFSET子句跳过前面的行。DB2 LUW从9.5版本开始支持这种标准写法:
-- 每页10条,取第3页的数据 SELECT EMPNO, FIRSTNME, SALARY FROM EMPLOYEE ORDER BY SALARY DESC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
这段SQL的逻辑是:先按工资降序排列,跳过前面20行,再取出10行。OFFSET和FETCH NEXT可以缩写,比如OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY与LIMIT 10 OFFSET 20(需要开启兼容参数)效果相同。注意ROWS和ROW关键字可以互换,单数复数都合法。
这种写法的问题出在深分页上。假设每页10条,要取第1000页,数据库必须先扫描并跳过9990行,页码越深,消耗的CPU和IO越多,响应时间会明显劣化。对于只是偶尔翻几页的普通后台管理系统,这种写法完全够用;但面向C端的高并发接口,就需要考虑后面介绍的优化方案。
另一个容易被忽视的坑是排序字段不唯一。如果ORDER BY的列存在大量重复值(比如按部门号排序),不同页之间可能出现同一行被重复返回或某些行被跳过的现象。这是因为排序不稳定,每次执行时分页边界的行顺序可能不同。解决办法是追加一个唯一列作为兜底排序,例如ORDER BY DEPTNO, EMPNO,确保排序结果全序且确定。
使用ROW_NUMBER窗口函数实现分页
在OFFSET语法不可用(比如老版本DB2 for z/OS)或者需要更灵活控制的场景下,窗口函数是另一种主流方案:
SELECT EMPNO, FIRSTNME, SALARY
FROM (
SELECT EMPNO, FIRSTNME, SALARY,
ROW_NUMBER() OVER (ORDER BY SALARY DESC, EMPNO) AS RN
FROM EMPLOYEE
) T
WHERE RN BETWEEN 21 AND 30;内层查询先为每一行计算行号,外层再按区间筛选。这种写法的好处是不依赖特定语法版本,跨平台兼容性好,而且可以在RN上做更复杂的条件,比如取每页的同时额外多取几行做缓冲。缺点是可读性略差,且某些DB2版本中,外层的过滤条件未必能下推到内层,导致先给所有行编号再过滤,性能不如OFFSET写法。
类似的还有ROW_NUMBER的变种——用子查询定位上一页最后一条记录的位置,然后直接从那里往后取。这种"游标式分页"或者叫"延迟关联"的思路,是解决深分页的经典手段:
-- 上一页最后一条记录的 SALARY=5000, EMPNO=1005 SELECT EMPNO, FIRSTNME, SALARY FROM EMPLOYEE WHERE (SALARY < 5000) OR (SALARY = 5000 AND EMPNO > 1005) ORDER BY SALARY DESC, EMPNO FETCH FIRST 10 ROWS ONLY;
这里的关键在于复合排序条件的改写:先比主排序字段,主字段相等时再比唯一键。只要(SALARY, EMPNO)上有合适的索引,无论翻到第几页,数据库都只需要从索引的特定位置开始读取10条记录,扫描代价与页码深度无关,深分页的性能问题就从根本上解决了。这也是各大平台的推荐做法,唯一的前置条件是客户端需要记住上一页末尾的排序值,跳页场景下要先做一次定位查询。
版本差异与索引优化建议
不同DB2产品线对分页语法的支持程度不同。DB2 LUW从9.5开始支持OFFSET FETCH,10.1之后支持得更完善;DB2 for z/OS要到V11才正式支持OFFSET FETCH语法,更早的版本只能用FETCH FIRST配合ROW_NUMBER或者三层嵌套子查询实现。此外,DB2 LUW开启了兼容Oracle或MySQL的参数后,还可以直接使用ROWNUM或LIMIT写法。写分页SQL前先确认目标库的版本和兼容模式,能避免不少莫名其妙的语法报错。
索引设计对分页性能的影响往往比写法本身更大。实践中建议遵循几条原则:排序字段和过滤字段尽量组合成复合索引,让索引的顺序与ORDER BY一致,这样数据库可以走索引顺序扫描,配合FETCH FIRST实现提前终止;排序列务必带上唯一键,保证分页结果确定;对于延迟关联写法,索引列的顺序要和WHERE中的复合条件完全对应,否则优化器可能放弃索引而回退到全表排序。可以用EXPLAIN查看执行计划,确认是否出现了SORT算力消耗,如果排序操作仍然存在,说明索引没有被有效利用,需要调整索引结构或查询写法。
最后提醒一点,FETCH FIRST中的行数不仅可以是常量,部分版本还支持使用主机变量或表达式,这在需要动态控制返回行数的程序中很实用。分页本身没有多高深,但把排序确定性、深分页代价和索引利用这三个问题处理好,才能真正写出经得起生产环境检验的分页查询。
DB2分页查询FETCH FIRST ROWS ONLYROW_NUMBER修改时间:2026-09-12 14:40:40