在 DM8 数据库开发中,处理单行数据时,SELECT...INTO
语句足以满足需求 —— 例如查询某个员工的工资、获取某张表的记录数。但面对多行结果集仅靠基础 SQL 语句就显得力不从心。此时,游标(Cursor) 作为 DMSQL 程序的核心工具,成为解决多行数据处理问题的关键。
游标本质是数据库服务器为查询结果集创建的临时指针,它允许开发者逐条遍历结果集中的记录,实现复杂的业务逻辑(如条件判断、数据修改、跨表关联)。
SELECT...INTO
)仅能处理单行结果,若查询返回多行,会触发TOO_MANY_ROWS
异常;若无数据,会触发NO_DATA_FOUND
异常。游标本质是结果集的内存映射。
其生命周期包含 4 个阶段:
CURSOR cur IS SELECT * FROM EMPLOYEE
),此时仅存储查询逻辑,未执行。DM8 将游标分为 4 类,不同类型适用于不同场景,选型直接影响代码效率与可维护性:
游标类型 | 核心特点 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
静态游标(隐式) | 系统自动管理,无需手动声明 / 关闭 | 简单 DML / 查询(如UPDATE 后判断影响行数) |
代码简洁,无需手动控制生命周期 | 灵活性低,无法处理多行查询 |
静态游标(显式) | 手动声明 / 打开 / 提取 / 关闭,查询固定 | 固定查询逻辑的多行处理(如批量统计) | 可控性强,支持游标属性判断 | 代码量多,需手动管理资源 |
动态游标 | 打开时才绑定查询语句,支持动态 SQL | 查询逻辑不确定(如表名 / 列名由参数决定) | 灵活性高,适配动态场景 | 无法提前编译,性能略低 |
游标变量 / 引用游标 | 指向游标对象的指针,支持结果集传递 | 子程序间共享结果集(如存储过程返回多行数据) | 可传递、可复用 | 需理解指针逻辑,调试难度高 |
选型思考:
UPDATE
后确认是否修改成功),用隐式静态游标;静态游标是最常用的游标类型,其查询逻辑在编译时确定。分为隐式游标和显式游标
可通过隐式游标属性获取执行信息,核心属性如下:
SQL%FOUND
:若语句影响 / 查询到数据,返回TRUE
;否则FALSE
;SQL%NOTFOUND
:与SQL%FOUND
相反;SQL%ROWCOUNT
:DML 语句影响的行数,或SELECT...INTO
返回的行数;SQL%ISOPEN
:始终为FALSE
(系统执行完语句后立即关闭游标)。示例:用隐式游标判断 DML 执行结果
更新 “程序员” 职位的员工薪资(加薪 5%),并判断是否更新成功。
-- 隐式游标:判断UPDATE执行结果
BEGIN
-- 执行DML语句,系统自动创建隐式游标SQL
UPDATE RESOURCES.EMPLOYEE
SET SALARY = SALARY * 1.05 -- 加薪5%
WHERE TITLE = '程序员';
-- 通过隐式游标属性判断结果
IF SQL%FOUND THEN
DBMS_OUTPUT.PUT_LINE('薪资更新成功!');
DBMS_OUTPUT.PUT_LINE('共更新 ' || SQL%ROWCOUNT || ' 名员工');
COMMIT; -- 确认更新
ELSE
DBMS_OUTPUT.PUT_LINE('未找到职位为"程序员"的员工,无需更新');
ROLLBACK; -- 回滚空操作
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('更新异常:' || SQLCODE || ' - ' || SQLERRM);
ROLLBACK;
END;
优缺点:
当需要逐行处理多行数据时,显式静态游标是首选。步骤为:定义→打开→提取→关闭。
遍历 “技术部门” 员工,打印姓名、职位、薪资,并统计平均薪资。
-- 显式游标:批量生成技术部门薪资报告
DECLARE
-- 1. 定义显式游标:关联技术部门员工查询
CURSOR tech_emp_cursor IS
SELECT NAME, TITLE, SALARY
FROM RESOURCES.EMPLOYEE
WHERE MANAGERID = 4 -- 技术经理的下属(MANAGERID=4)
ORDER BY SALARY DESC;
-- 定义变量存储游标数据
v_name VARCHAR(50);
v_title VARCHAR(50);
v_salary DEC(19,4);
-- 统计变量
v_total_salary DEC(19,4) := 0; -- 总薪资
v_emp_count INT := 0; -- 员工数量
v_avg_salary DEC(19,4); -- 平均薪资
BEGIN
-- 2. 打开游标
OPEN tech_emp_cursor;
-- 判断游标是否打开成功
IF tech_emp_cursor%ISOPEN THEN
DBMS_OUTPUT.PUT_LINE('================ 技术部门薪资报告 ================');
DBMS_OUTPUT.PUT_LINE('姓名 职位 薪资');
DBMS_OUTPUT.PUT_LINE('----------------------------------------');
-- 3. 提取数据:循环遍历,直到无数据
LOOP
FETCH tech_emp_cursor INTO v_name, v_title, v_salary;
EXIT WHEN tech_emp_cursor%NOTFOUND; -- 退出条件:无数据
-- 处理数据:打印与统计
DBMS_OUTPUT.PUT_LINE(RPAD(v_name, 10) || RPAD(v_title, 10) || v_salary);
v_total_salary := v_total_salary + v_salary;
v_emp_count := v_emp_count + 1;
END LOOP;
-- 计算平均薪资
IF v_emp_count > 0 THEN
v_avg_salary := v_total_salary / v_emp_count;
DBMS_OUTPUT.PUT_LINE('----------------------------------------');
DBMS_OUTPUT.PUT_LINE('部门总人数:' || v_emp_count);
DBMS_OUTPUT.PUT_LINE('总薪资:' || v_total_salary);
DBMS_OUTPUT.PUT_LINE('平均薪资:' || ROUND(v_avg_salary, 2));
ELSE
DBMS_OUTPUT.PUT_LINE('----------------------------------------');
DBMS_OUTPUT.PUT_LINE('技术部门无员工数据');
END IF;
ELSE
RAISE_APPLICATION_ERROR(-20001, '游标打开失败!');
END IF;
-- 4. 关闭游标,释放资源
CLOSE tech_emp_cursor;
EXCEPTION
WHEN INVALID_CURSOR THEN
DBMS_OUTPUT.PUT_LINE('游标异常:无效的游标操作(可能未打开或已关闭)');
-- 异常时确保游标关闭(避免资源泄漏)
IF tech_emp_cursor%ISOPEN THEN
CLOSE tech_emp_cursor;
END IF;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('其他异常:' || SQLCODE || ' - ' || SQLERRM);
IF tech_emp_cursor%ISOPEN THEN
CLOSE tech_emp_cursor;
END IF;
END;
在示例中,我们用到了%ISOPEN
、%NOTFOUND
、%ROWCOUNT
三个核心属性,它们是控制游标逻辑的关键:
属性 | 作用场景 | 注意事项 |
---|---|---|
%ISOPEN |
判断游标是否打开(避免重复打开 / 关闭) | 打开前为FALSE ,打开后为TRUE ,关闭后为FALSE |
%NOTFOUND |
循环退出条件(无数据可提取时为TRUE ) |
首次FETCH 前为NULL ,需用EXIT WHEN 判断 |
%ROWCOUNT |
统计已提取的行数(如报告中的员工数量) | 首次FETCH 前为0 ,每次FETCH 成功后 + 1 |
%FOUND |
判断是否提取到数据(与%NOTFOUND 相反) |
可替代%NOTFOUND ,但逻辑需调整(EXIT WHEN NOT %FOUND ) |
静态游标的查询语句在编译时固定,但若查询逻辑不确定(如表名、列名由用户输入决定),静态游标就无法满足需求。此时需用动态游标—— 在 “打开游标” 时才绑定查询语句,实现动态 SQL 的逐行处理。
动态游标的关键是OPEN...FOR...USING
语法,通过USING
子句绑定参数,避免 SQL 注入:
-- 1. 定义动态游标(仅声明类型,不绑定查询)
CURSOR 动态游标名;
-- 2. 构建动态SQL语句(含参数占位符?)
v_sql VARCHAR(1000) := 'SELECT 列1, 列2 FROM 表名 WHERE 条件 = ?';
-- 3. 打开动态游标,绑定SQL与参数
OPEN 动态游标名 FOR v_sql USING 参数1;
-- 4. 提取数据(与静态游标一致)
FETCH 动态游标名 INTO 变量1, 变量2;
-- 5. 关闭游标
CLOSE 动态游标名;
关键注意事项:
?
作为占位符,不可直接拼接字符串(避免 SQL 注入);USING
子句的参数个数、类型必须与?
完全匹配,否则触发INVALID_NUMBER
等异常;用户传入部门经理 ID(p_managerid
),动态查询该经理下属的员工信息(姓名、薪资、职位),支持模糊查询职位。
-- 动态游标:按部门经理ID查询下属员工
CREATE OR REPLACE PROCEDURE query_emp_by_manager(
p_managerid INT, -- 输入参数:部门经理ID
p_title_like VARCHAR(50) -- 输入参数:职位模糊查询(如'经理'、'代表')
) AS
-- 1. 定义动态游标
cur_dynamic CURSOR;
-- 变量:存储查询结果与动态SQL
v_name VARCHAR(50);
v_salary DEC(19,4);
v_title VARCHAR(50);
v_sql VARCHAR(1000); -- 动态SQL语句
BEGIN
-- 2. 构建动态SQL(用?作为参数占位符)
v_sql := 'SELECT NAME, SALARY, TITLE
FROM RESOURCES.EMPLOYEE
WHERE MANAGERID = ? -- 第一个参数:经理ID
AND TITLE LIKE ? -- 第二个参数:职位模糊查询
ORDER BY SALARY DESC';
-- 3. 打开动态游标,绑定SQL与参数(USING子句顺序需与?一致)
OPEN cur_dynamic FOR v_sql
USING p_managerid, '%' || p_title_like || '%'; -- 拼接模糊查询符号%
-- 4. 提取数据
DBMS_OUTPUT.PUT_LINE('---------------- 部门经理ID=' || p_managerid || ' 的下属员工 ----------------');
DBMS_OUTPUT.PUT_LINE('姓名 薪资 职位');
DBMS_OUTPUT.PUT_LINE('----------------------------------------');
LOOP
FETCH cur_dynamic INTO v_name, v_salary, v_title;
EXIT WHEN cur_dynamic%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(RPAD(v_name, 10) || RPAD(v_salary, 12) || v_title);
END LOOP;
-- 处理无数据场景
IF cur_dynamic%ROWCOUNT = 0 THEN
DBMS_OUTPUT.PUT_LINE('----------------------------------------');
DBMS_OUTPUT.PUT_LINE('未找到符合条件的员工');
END IF;
-- 5. 关闭游标
CLOSE cur_dynamic;
EXCEPTION
WHEN INVALID_CURSOR THEN
DBMS_OUTPUT.PUT_LINE('游标异常:无效的游标操作');
IF cur_dynamic%ISOPEN THEN
CLOSE cur_dynamic;
END IF;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('查询异常:' || SQLCODE || ' - ' || SQLERRM);
IF cur_dynamic%ISOPEN THEN
CLOSE cur_dynamic;
END IF;
END;
-- 调用存储过程:查询经理ID=1(总经理)下属的“经理”职位员工
CALL query_emp_by_manager(1, '经理');
为什么推荐使用动态游标
动态 SQL 若直接拼接参数(如v_sql := 'WHERE MANAGERID = ' || p_managerid
),会面临 SQL 注入风险。例如:若用户传入p_managerid = '1 OR 1=1'
,拼接后的 SQL 会变成WHERE MANAGERID = 1 OR 1=1
,导致查询所有员工,泄露敏感数据。
而USING
子句通过参数绑定避免了这一问题 —— 参数会被当作 “值” 而非 “SQL 片段” 处理,即使传入恶意字符串,也只会作为条件值匹配,不会执行恶意 SQL。
结论:动态游标必须使用USING
子句绑定参数,禁止直接拼接 SQL 字符串。
在复杂业务中,常需在子程序(存储过程 / 函数)间传递多行结果集(如:子过程 A 查询员工数据,子过程 B 处理数据,主程序 C 汇总结果)。此时,普通游标无法满足需求,需用游标变量或引用游标。
游标变量本质是指向游标对象的指针,它不存储结果集,仅存储游标地址。多个游标变量可指向同一游标,实现结果集共享。
-- 1. 定义游标变量(两种方式)
-- 方式1:直接赋值为已定义的游标
CURSOR 源游标名 IS SELECT ...;
游标变量名 CURSOR := 源游标名;
-- 方式2:打开时动态绑定查询(与动态游标类似)
游标变量名 CURSOR;
OPEN 游标变量名 FOR SELECT ... USING 参数
get_emp_cursor
获取员工游标变量;-- 子过程:返回薪资大于指定值的员工游标变量
CREATE OR REPLACE PROCEDURE get_emp_cursor(
p_min_salary DEC(19,4), -- 输入参数:最小薪资
p_emp_cursor OUT CURSOR -- 输出参数:游标变量(传递结果集)
) AS
BEGIN
-- 打开游标变量,绑定查询语句
OPEN p_emp_cursor FOR
SELECT EMPLOYEEID, NAME, SALARY
FROM RESOURCES.EMPLOYEE
WHERE SALARY > p_min_salary
ORDER BY SALARY DESC;
END;
/
-- 主程序:调用子过程获取游标变量,遍历结果
DECLARE
-- 定义游标变量(接收子过程返回的结果集)
v_emp_cursor CURSOR;
v_empid INT;
v_name VARCHAR(50);
v_salary DEC(19,4);
BEGIN
-- 调用子过程,获取薪资>60000的员工游标变量
CALL get_emp_cursor(60000, v_emp_cursor);
-- 遍历游标变量
DBMS_OUTPUT.PUT_LINE('薪资大于60000的员工列表:');
DBMS_OUTPUT.PUT_LINE('工号 | 姓名 | 薪资');
DBMS_OUTPUT.PUT_LINE('------------------------');
LOOP
FETCH v_emp_cursor INTO v_empid, v_name, v_salary;
EXIT WHEN v_emp_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_empid || ' | ' || v_name || ' | ' || v_salary);
END LOOP;
-- 关闭游标变量
CLOSE v_emp_cursor;
EXCEPTION
WHEN INVALID_CURSOR THEN
DBMS_OUTPUT.PUT_LINE('游标变量异常:无效操作');
IF v_emp_cursor%ISOPEN THEN
CLOSE v_emp_cursor;
END IF;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('主程序异常:' || SQLCODE || ' - ' || SQLERRM);
IF v_emp_cursor%ISOPEN THEN
CLOSE v_emp_cursor;
END IF;
END;
引用游标(REF CURSOR
)通过定义 “游标类型”,明确结果集结构,提高代码可读性与安全性。
-- 1. 定义REF CURSOR类型(指定结果集结构)
TYPE 游标类型名 IS REF CURSOR RETURN 表名%ROWTYPE; -- 与表结构一致
-- 或指定自定义记录类型
TYPE 记录类型名 IS RECORD(列1 类型1, 列2 类型2);
TYPE 游标类型名 IS REF CURSOR RETURN 记录类型名;
-- 2. 声明引用游标变量
引用游标变量名 游标类型名;
-- 3. 打开引用游标(与动态游标一致)
OPEN 引用游标变量名 FOR SELECT ...;
需求:创建存储过程,返回 “员工姓名、部门(通过 MANAGERID 关联)、薪资” 的自定义结果集,用引用游标传递。
-- 1. 定义REF CURSOR类型(自定义结果集结构)
CREATE OR REPLACE PACKAGE emp_package AS
-- 自定义记录类型:员工信息(含部门)
TYPE emp_dept_record IS RECORD(
emp_name VARCHAR(50), -- 员工姓名
dept_name VARCHAR(50), -- 部门名称(根据MANAGERID映射)
salary DEC(19,4) -- 薪资
);
-- 定义REF CURSOR类型,返回自定义记录
TYPE emp_dept_cursor IS REF CURSOR RETURN emp_dept_record;
-- 存储过程:返回员工-部门关联结果集
PROCEDURE get_emp_dept_cursor(
p_dept_name_like VARCHAR(50), -- 输入参数:部门名称模糊查询
p_result_cursor OUT emp_dept_cursor -- 输出参数:引用游标
);
END emp_package;
/
-- 2. 实现存储过程
CREATE OR REPLACE PACKAGE BODY emp_package AS
PROCEDURE get_emp_dept_cursor(
p_dept_name_like VARCHAR(50),
p_result_cursor OUT emp_dept_cursor
) AS
BEGIN
-- 打开引用游标,关联查询(映射部门名称)
OPEN p_result_cursor FOR
SELECT
e.NAME AS emp_name,
-- 根据MANAGERID映射部门名称
CASE
WHEN e.MANAGERID IS NULL THEN '总经办'
WHEN e.MANAGERID = 1 THEN '销售部/技术部'
WHEN e.MANAGERID = 2 THEN '销售部'
WHEN e.MANAGERID = 4 THEN '技术部'
ELSE '其他部门'
END AS dept_name,
e.SALARY
FROM RESOURCES.EMPLOYEE e
WHERE
CASE
WHEN e.MANAGERID IS NULL THEN '总经办'
WHEN e.MANAGERID = 1 THEN '销售部/技术部'
WHEN e.MANAGERID = 2 THEN '销售部'
WHEN e.MANAGERID = 4 THEN '技术部'
ELSE '其他部门'
END LIKE '%' || p_dept_name_like || '%'
ORDER BY e.SALARY DESC;
END get_emp_dept_cursor;
END emp_package;
/
-- 3. 主程序:调用存储过程,遍历引用游标
DECLARE
-- 声明引用游标变量
v_result_cursor emp_package.emp_dept_cursor;
-- 声明变量存储自定义记录
v_emp_dept emp_package.emp_dept_record;
BEGIN
-- 调用存储过程,查询“销售部”相关员工
emp_package.get_emp_dept_cursor('销售部', v_result_cursor);
DBMS_OUTPUT.PUT_LINE('---------------- 销售部相关员工 ----------------');
DBMS_OUTPUT.PUT_LINE('员工姓名 | 部门名称 | 薪资');
DBMS_OUTPUT.PUT_LINE('----------------------------------------');
-- 遍历引用游标
LOOP
FETCH v_result_cursor INTO v_emp_dept;
EXIT WHEN v_result_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(
RPAD(v_emp_dept.emp_name, 8) || ' | ' ||
RPAD(v_emp_dept.dept_name, 8) || ' | ' ||
v_emp_dept.salary
);
END LOOP;
CLOSE v_result_cursor;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('异常:' || SQLCODE || ' - ' || SQLERRM);
IF v_result_cursor%ISOPEN THEN
CLOSE v_result_cursor;
END IF;
END;
REF CURSOR RETURN
明确结果集结构,若FETCH
变量与结构不匹配,编译时即报错;REF CURSOR
类型,多个子程序可复用,减少代码冗余;emp_dept_record
)明确列含义,比匿名变量更易维护。文章
阅读量
获赞