在DB2的存储过程和嵌入式SQL开发中,游标(CURSOR)是遍历多行结果集的标准手段。它的使用看似简单,无非是声明、打开、取数、关闭四个步骤,但实际项目中大量问题恰恰出在这些细节上:有人忘记关闭游标导致锁一直挂着,有人在不支持的结果集返回方式上反复报错,还有人遇到了COMMIT之后游标失效却找不到原因。这篇文章把DB2游标的正确用法和关闭时的注意事项一次性讲透。

DB2游标的完整生命周期与标准写法
游标在DB2中本质上是一个指向结果集的指针,必须严格按照DECLARE、OPEN、FETCH、CLOSE的顺序使用。DECLARE语句只是定义了游标对应的SELECT语句,此时并没有执行查询;OPEN时DB2才真正执行查询并建立结果集;FETCH负责逐行取出数据放到变量中;CLOSE则释放结果集和相关资源。
下面是一个在SQL PL存储过程中使用游标的完整示例,展示了标准流程和NOT FOUND处理器的配合用法:
CREATE OR REPLACE PROCEDURE demo_cursor()
BEGIN
DECLARE v_id INTEGER;
DECLARE v_name VARCHAR(50);
DECLARE v_salary DECIMAL(10,2);
DECLARE v_at_end INTEGER DEFAULT 0;
-- 声明NOT FOUND处理器,FETCH取不到数据时触发
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET v_at_end = 1;
-- 声明游标,必须在处理器声明之后
DECLARE cur_emp CURSOR FOR
SELECT id, name, salary
FROM employee
WHERE salary > 5000
ORDER BY id;
SET v_at_end = 0;
OPEN cur_emp;
emp_loop: LOOP
FETCH cur_emp INTO v_id, v_name, v_salary;
IF v_at_end = 1 THEN
LEAVE emp_loop;
END IF;
-- 这里处理每一行数据
CALL process_emp(v_id, v_name, v_salary);
END LOOP emp_loop;
CLOSE cur_emp;
END
有几个容易出错的点需要特别注意。第一,DECLARE的顺序有严格规定:变量声明在前,条件声明其次,处理器(HANDLER)声明再次,游标声明必须在所有处理器之后,否则编译直接报错。第二,FETCH列表中的变量顺序和类型必须与SELECT列一一对应,类型不匹配时不会报语法错误,而是取出错误的值,这种问题非常隐蔽。第三,循环退出条件依赖NOT FOUND处理器设置标志位,不要在循环内重复SET标志位为0,否则可能出现死循环。
WITH RETURN与HOLD选项:结果集返回和事务提交场景
很多开发者分不清什么时候该用WITH RETURN,什么时候用普通游标。WITH RETURN用于存储过程需要把结果集直接返回给调用方的场景,常见于报表类存储过程。这类游标由调用方(如应用程序或客户端工具)负责关闭,存储过程本身不应该去CLOSE它:
CREATE OR REPLACE PROCEDURE get_dept_emp(IN p_dept INTEGER)
DYNAMIC RESULT SETS 1
BEGIN
DECLARE cur1 CURSOR WITH RETURN TO CLIENT FOR
SELECT id, name, salary
FROM employee
WHERE dept_id = p_dept;
OPEN cur1;
-- 注意:这里不能CLOSE,结果集要返回给客户端
END
使用WITH RETURN时必须注意两点。一是过程定义中要写明DYNAMIC RESULT SETS 1,否则结果集不会返回;二是TO CLIENT和TO CALLER的区别,TO CLIENT把结果集直接透传给最初发起调用的客户端,适合嵌套调用的场景,TO CALLER只返回给直接调用者。
另一个高频问题是COMMIT与游标的关系。默认情况下,执行COMMIT会关闭所有已打开的游标(WITH HOLD选项的除外)。如果存储过程里在循环FETCH的过程中穿插了COMMIT操作,第二次FETCH就会报SQLCODE -501(游标未打开)之类的错误。解决办法是声明游标时加上WITH HOLD:
DECLARE cur1 CURSOR WITH HOLD FOR
SELECT account_id, balance FROM accounts WHERE status = 'A';
WITH HOLD让游标在COMMIT之后仍然保持打开状态,跨多个事务遍历结果集时必须用它。但要注意ROLLBACK仍然会关闭所有游标,包括WITH HOLD的,这一点在LUW和z/OS平台上的行为是一致的。另外,WITH HOLD的游标会持有某些锁资源直到游标关闭,长时间不关闭会影响并发,需要在业务允许的前提下权衡使用。
游标未关闭的隐患与正确关闭姿势
游标没有正确关闭会带来三类实际问题。首先是资源泄漏:每个打开的游标都会占用数据库管理器的一段内存和一个游标句柄,DB2对并发打开的游标数量有上限(由数据库配置参数和语句堆大小决定),泄漏的游标积累到一定程度会报SQLCODE -905或内存分配失败。其次是锁持有:如果游标对应的查询在隔离级别RR或RS下运行,未关闭的游标会一直持有表锁或行锁,阻塞其他事务。最后是事务日志无法及时释放,影响批量任务的吞吐。
正确的关闭做法包括以下几条:第一,存储过程结束时显式CLOSE所有非返回型游标,不要依赖过程退出时的自动清理,显式关闭能提前释放锁;第二,在异常处理中加入兜底关闭逻辑,通过EXIT HANDLER保证出错路径也会关闭游标:
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
IF cur_emp IS OPEN THEN -- 部分版本支持该判断语法
CLOSE cur_emp;
END IF;
RESIGNAL;
END
第三,如果用的是应用程序接口(如JDBC的ResultSet、CLI的SQLFetch循环),务必在finally块或try-with-resources中关闭ResultSet和Statement,因为存储过程正常返回并不代表客户端侧的游标资源已被回收。第四,排查游标泄漏时可以查询系统监控表,在LUW环境下查看MON_GET_UNIT_STATEMENT或快照中的open_cursor计数,确认游标是否随时间持续增长。
最后补充一个跨平台的差异提醒:z/OS主机环境下的COBOL嵌入式SQL使用游标时,除了OPEN、FETCH、CLOSE的标准语法外,还要注意DECLARE CURSOR可以放在DATA DIVISION中,且WITH HOLD在CICS事务环境下有额外的语义限制。遇到游标行为异常时,第一步先确认当前隔离级别和COMMIT策略,第二步检查DECLARE语句的选项组合,大多数游标问题都能从这两点定位到根因。