上一篇【第11篇】PL/SQL子程序——存储过程与函数的创建与应用
下一篇【第13篇】Oracle实例架构深度解析——SGA、进程与实例启动全流程


摘要

本文深入讲解PL/SQL的两大高级特性:异常处理机制和游标操作。涵盖预定义异常、非预定义异常、用户自定义异常的捕获与传播,以及隐式游标、显式游标、游标FOR循环、REF游标的全面使用。通过健壮的数据处理程序实战案例,帮助读者编写出生产级别的PL/SQL代码。


一、异常处理基础

PL/SQL的异常处理机制能优雅地捕获运行时错误,防止程序因错误而崩溃,并提供有意义的错误信息。

1.1 异常的分类

Oracle中的异常分为三类:

类型 说明 示例
预定义异常 Oracle预先命名的常见错误 NO_DATA_FOUND、TOO_MANY_ROWS
非预定义异常 有错误号但Oracle未命名 ORA-02291(外键违约)
用户自定义异常 程序员定义的业务规则异常 薪资超出范围、状态不合法

1.2 异常处理结构

BEGIN
    -- 正常执行代码
    ...
EXCEPTION
    WHEN 异常名1 THEN
        -- 处理异常1
    WHEN 异常名2 OR 异常名3 THEN
        -- 处理异常2或3
    WHEN OTHERS THEN
        -- 处理所有其他异常(捕获网)
END;

二、预定义异常

Oracle定义了约20个常用预定义异常,直接使用其名称即可:

DECLARE
    v_emp     employees%ROWTYPE;
    v_salary  employees.salary%TYPE;
BEGIN
    -- NO_DATA_FOUND:SELECT INTO没有返回行
    BEGIN
        SELECT * INTO v_emp FROM employees WHERE employee_id = 99999;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            DBMS_OUTPUT.PUT_LINE('员工不存在');
    END;
    
    -- TOO_MANY_ROWS:SELECT INTO返回多行
    BEGIN
        SELECT salary INTO v_salary FROM employees WHERE department_id = 60;
    EXCEPTION
        WHEN TOO_MANY_ROWS THEN
            DBMS_OUTPUT.PUT_LINE('返回多行,请使用游标');
    END;
    
    -- ZERO_DIVIDE:除以零
    BEGIN
        DECLARE v_result NUMBER;
        BEGIN
            v_result := 100 / 0;
        EXCEPTION
            WHEN ZERO_DIVIDE THEN
                DBMS_OUTPUT.PUT_LINE('除以零错误');
        END;
    END;
    
    -- VALUE_ERROR:数值转换错误或超出范围
    BEGIN
        DECLARE v_n NUMBER;
        BEGIN
            v_n := TO_NUMBER('ABC');  -- 非数字字符串转NUMBER
        EXCEPTION
            WHEN VALUE_ERROR THEN
                DBMS_OUTPUT.PUT_LINE('数值转换错误');
        END;
    END;
    
    -- INVALID_NUMBER:SQL中的数值转换错误
    -- DUP_VAL_ON_INDEX:违反UNIQUE约束
    -- ROWTYPE_MISMATCH:游标变量类型不匹配
    -- CURSOR_ALREADY_OPEN:游标已打开再次打开
    -- INVALID_CURSOR:无效游标操作
    -- LOGIN_DENIED:登录失败
    -- NOT_LOGGED_ON:未登录
    -- PROGRAM_ERROR:内部PL/SQL错误
    -- TIMEOUT_ON_RESOURCE:等待资源超时
    -- STORAGE_ERROR:内存不足
    -- SELF_IS_NULL:对象类型的NULL实例调用方法
    
END;
/

三、非预定义异常

-- 步骤1:声明一个EXCEPTION类型变量
-- 步骤2:用PRAGMA EXCEPTION_INIT将错误号与变量关联
-- 步骤3:在EXCEPTION段捕获

DECLARE
    -- 声明非预定义异常
    ex_fk_violation    EXCEPTION;
    ex_not_null_vio    EXCEPTION;
    
    -- 关联Oracle错误号
    PRAGMA EXCEPTION_INIT(ex_fk_violation, -2291);  -- ORA-02291: integrity constraint violated
    PRAGMA EXCEPTION_INIT(ex_not_null_vio, -1400);  -- ORA-01400: cannot insert NULL
    
BEGIN
    -- 尝试插入违反外键约束的数据
    INSERT INTO employees(employee_id, last_name, email, hire_date, job_id, department_id)
    VALUES(300, 'TestUser', 'TEST', SYSDATE, 'IT_PROG', 99999);  -- 部门99999不存在
    
    COMMIT;
    
EXCEPTION
    WHEN ex_fk_violation THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('错误:违反外键约束(所引用的部门不存在)');
    WHEN ex_not_null_vio THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('错误:必填字段不能为空');
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('未知错误:' || SQLCODE || ' - ' || SQLERRM);
END;
/

四、用户自定义异常

-- 用于实现业务规则验证,让错误信息更有意义
DECLARE
    -- 声明用户自定义异常
    ex_salary_too_low   EXCEPTION;
    ex_salary_too_high  EXCEPTION;
    ex_invalid_dept     EXCEPTION;
    
    v_new_salary  NUMBER := 1000;  -- 测试数据
    v_dept_id     NUMBER := 60;
    v_emp_count   NUMBER;
    
BEGIN
    -- 业务规则1:薪资不能低于最低工资标准
    IF v_new_salary < 2000 THEN
        RAISE ex_salary_too_low;
    END IF;
    
    -- 业务规则2:薪资不能超过最高薪资的150%
    IF v_new_salary > 24000 * 1.5 THEN
        RAISE ex_salary_too_high;
    END IF;
    
    -- 业务规则3:部门人数不能超过20人
    SELECT COUNT(*) INTO v_emp_count FROM employees WHERE department_id = v_dept_id;
    IF v_emp_count >= 20 THEN
        RAISE ex_invalid_dept;
    END IF;
    
    -- 如果通过所有验证,执行更新
    DBMS_OUTPUT.PUT_LINE('验证通过,执行更新...');
    
EXCEPTION
    WHEN ex_salary_too_low THEN
        DBMS_OUTPUT.PUT_LINE('业务错误:薪资(' || v_new_salary || ')低于最低标准2000元');
    WHEN ex_salary_too_high THEN
        DBMS_OUTPUT.PUT_LINE('业务错误:薪资(' || v_new_salary || ')超出允许范围');
    WHEN ex_invalid_dept THEN
        DBMS_OUTPUT.PUT_LINE('业务错误:部门' || v_dept_id || '员工人数已达上限');
END;
/

五、RAISE_APPLICATION_ERROR

RAISE_APPLICATION_ERROR用于在存储过程/函数中抛出自定义错误,可以传递给调用方(包括客户端应用程序)。

-- 语法:RAISE_APPLICATION_ERROR(error_number, message [, keep_errors])
-- error_number:必须在-20999到-20000之间
-- message:错误描述(最长2048字节)

CREATE OR REPLACE PROCEDURE transfer_funds(
    p_from_acct  IN NUMBER,
    p_to_acct    IN NUMBER,
    p_amount     IN NUMBER
)
IS
    v_balance    NUMBER;
BEGIN
    -- 验证金额
    IF p_amount <= 0 THEN
        RAISE_APPLICATION_ERROR(-20010, '转账金额必须大于0,当前:' || p_amount);
    END IF;
    
    -- 查询余额
    SELECT balance INTO v_balance FROM accounts WHERE account_id = p_from_acct;
    
    -- 验证余额充足
    IF v_balance < p_amount THEN
        RAISE_APPLICATION_ERROR(-20011, 
            '账户' || p_from_acct || '余额不足。' ||
            '当前余额:' || v_balance || ',' ||
            '转账金额:' || p_amount);
    END IF;
    
    -- 执行转账
    UPDATE accounts SET balance = balance - p_amount WHERE account_id = p_from_acct;
    UPDATE accounts SET balance = balance + p_amount WHERE account_id = p_to_acct;
    
    COMMIT;
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE_APPLICATION_ERROR(-20012, '账户' || p_from_acct || '不存在');
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;  -- 重新抛出原始异常
END transfer_funds;
/

六、SQLCODE和SQLERRM

DECLARE
    v_emp_id NUMBER := 99999;
BEGIN
    UPDATE employees SET salary = salary * 1.1 WHERE employee_id = v_emp_id;
    
    IF SQL%ROWCOUNT = 0 THEN
        RAISE_APPLICATION_ERROR(-20013, '员工' || v_emp_id || '不存在');
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        -- SQLCODE:当前错误的Oracle错误号(负数)
        -- SQLERRM:当前错误的错误消息
        DBMS_OUTPUT.PUT_LINE('错误代码:' || SQLCODE);
        DBMS_OUTPUT.PUT_LINE('错误信息:' || SQLERRM);
        DBMS_OUTPUT.PUT_LINE('错误详情:' || SQLERRM(SQLCODE));
        
        -- 记录到错误日志表
        INSERT INTO error_log(log_time, error_code, error_msg, module_name)
        VALUES(SYSDATE, SQLCODE, SUBSTR(SQLERRM, 1, 512), 'salary_update');
        COMMIT;  -- 注意:在EXCEPTION中的COMMIT只提交此INSERT
        
        RAISE;  -- 重新抛出(如果需要调用层继续处理)
END;
/

七、游标(Cursor)

游标是Oracle用于处理SELECT语句返回的多行数据的机制。

7.1 隐式游标

Oracle为每条DML语句(INSERT/UPDATE/DELETE)和SELECT INTO语句自动创建隐式游标,通过SQL%属性访问。

DECLARE
    v_updated_rows NUMBER;
BEGIN
    UPDATE employees
    SET salary = salary * 1.10
    WHERE department_id = 60;
    
    -- SQL%ROWCOUNT:受影响的行数
    v_updated_rows := SQL%ROWCOUNT;
    DBMS_OUTPUT.PUT_LINE('更新了 ' || v_updated_rows || ' 行');
    
    -- SQL%FOUND:是否找到至少一行(TRUE/FALSE/NULL)
    IF SQL%FOUND THEN
        COMMIT;
        DBMS_OUTPUT.PUT_LINE('更新已提交');
    ELSE
        DBMS_OUTPUT.PUT_LINE('没有符合条件的记录');
    END IF;
    
    -- SQL%NOTFOUND:没有找到行(SQL%FOUND的反值)
    -- SQL%ISOPEN:对于隐式游标始终为FALSE(已关闭)
END;
/

7.2 显式游标

当需要处理多行数据时,使用显式游标。

DECLARE
    -- 声明游标(DECLARE区)
    CURSOR c_employees IS
        SELECT employee_id, last_name, salary, department_id
        FROM employees
        WHERE department_id IN (60, 80, 90)
        ORDER BY department_id, salary DESC;
    
    -- 声明游标记录变量
    v_emp_rec  c_employees%ROWTYPE;
    v_count    NUMBER := 0;
    
BEGIN
    -- 打开游标(分配内存,执行SELECT)
    OPEN c_employees;
    
    -- 循环获取数据
    LOOP
        -- FETCH:获取当前行并推进游标
        FETCH c_employees INTO v_emp_rec;
        
        -- 检查是否还有数据
        EXIT WHEN c_employees%NOTFOUND;  -- 等同于 NOT c_employees%FOUND
        
        v_count := v_count + 1;
        DBMS_OUTPUT.PUT_LINE(
            v_count || '. ' ||
            RPAD(v_emp_rec.last_name, 15) || 
            ' 部门:' || v_emp_rec.department_id ||
            ' 薪资:¥' || TO_CHAR(v_emp_rec.salary, '99,999')
        );
    END LOOP;
    
    -- 关闭游标(释放资源)
    CLOSE c_employees;
    
    DBMS_OUTPUT.PUT_LINE('共 ' || v_count || ' 条记录');
    
EXCEPTION
    WHEN OTHERS THEN
        -- 确保游标被关闭
        IF c_employees%ISOPEN THEN
            CLOSE c_employees;
        END IF;
        RAISE;
END;
/

7.3 带参数的游标

DECLARE
    -- 带参数的游标
    CURSOR c_dept_emp(p_dept_id NUMBER, p_min_salary NUMBER DEFAULT 0) IS
        SELECT employee_id, last_name, salary
        FROM employees
        WHERE department_id = p_dept_id
        AND salary >= p_min_salary
        ORDER BY salary DESC;
    
    v_emp c_dept_emp%ROWTYPE;
    
BEGIN
    -- 打开带参数的游标
    OPEN c_dept_emp(60, 5000);  -- IT部门,薪资>=5000
    LOOP
        FETCH c_dept_emp INTO v_emp;
        EXIT WHEN c_dept_emp%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_emp.last_name || ': ¥' || v_emp.salary);
    END LOOP;
    CLOSE c_dept_emp;
    
    DBMS_OUTPUT.PUT_LINE('---');
    
    -- 同一游标用不同参数再次打开
    OPEN c_dept_emp(80);  -- 销售部门,薪资>=0(使用默认值)
    LOOP
        FETCH c_dept_emp INTO v_emp;
        EXIT WHEN c_dept_emp%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_emp.last_name || ': ¥' || v_emp.salary);
    END LOOP;
    CLOSE c_dept_emp;
END;
/

7.4 游标FOR循环(推荐!最简洁的方式)

-- 游标FOR循环自动处理:OPEN、FETCH、%NOTFOUND检查、CLOSE
-- 是处理多行查询的首选方式

-- 方式1:使用已声明的游标
DECLARE
    CURSOR c_emp IS
        SELECT employee_id, last_name, salary FROM employees WHERE department_id = 60;
BEGIN
    FOR emp_rec IN c_emp LOOP
        DBMS_OUTPUT.PUT_LINE(emp_rec.last_name || ': ¥' || emp_rec.salary);
    END LOOP;
    -- 不需要OPEN/CLOSE,完全自动
END;
/

-- 方式2:直接在FOR LOOP中写SQL(最简洁!)
BEGIN
    FOR emp_rec IN (
        SELECT e.employee_id, e.last_name, e.salary, d.department_name
        FROM employees e
        JOIN departments d ON e.department_id = d.department_id
        WHERE e.salary > 10000
        ORDER BY e.salary DESC
    ) LOOP
        DBMS_OUTPUT.PUT_LINE(
            emp_rec.department_name || ' | ' ||
            emp_rec.last_name || ' | ¥' || emp_rec.salary
        );
    END LOOP;
END;
/

7.5 游标的UPDATE/DELETE(用于行级操作)

DECLARE
    CURSOR c_emp_update IS
        SELECT employee_id, salary, commission_pct
        FROM employees
        WHERE department_id = 80
        FOR UPDATE;  -- 声明游标用于更新,加行锁
    
BEGIN
    FOR emp IN c_emp_update LOOP
        -- 根据不同条件更新不同的行
        IF emp.commission_pct >= 0.25 THEN
            UPDATE employees
            SET salary = emp.salary * 1.15
            WHERE CURRENT OF c_emp_update;  -- 更新游标当前行(最安全)
        ELSIF emp.commission_pct >= 0.15 THEN
            UPDATE employees
            SET salary = emp.salary * 1.10
            WHERE CURRENT OF c_emp_update;
        ELSE
            UPDATE employees
            SET salary = emp.salary * 1.05
            WHERE CURRENT OF c_emp_update;
        END IF;
    END LOOP;
    
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('薪资更新完成');
END;
/

7.6 REF游标(动态游标)

REF游标可以在运行时绑定不同的查询,实现动态查询。

-- 声明REF游标类型
DECLARE
    TYPE emp_cur_type IS REF CURSOR RETURN employees%ROWTYPE;  -- 强类型(指定返回类型)
    TYPE weak_cur_type IS REF CURSOR;  -- 弱类型(任何查询都可以)
    
    c_emp     emp_cur_type;
    c_any     weak_cur_type;
    v_emp     employees%ROWTYPE;
    
    v_dept_id NUMBER := 60;
    v_query   VARCHAR2(500);
    
BEGIN
    -- 使用强类型REF游标
    OPEN c_emp FOR 
        SELECT * FROM employees WHERE department_id = v_dept_id;
    
    LOOP
        FETCH c_emp INTO v_emp;
        EXIT WHEN c_emp%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_emp.last_name || ': ¥' || v_emp.salary);
    END LOOP;
    CLOSE c_emp;
    
    -- 动态SQL与弱类型REF游标
    v_query := 'SELECT * FROM employees WHERE department_id = :dept AND salary > :sal';
    OPEN c_any FOR v_query USING 60, 5000;  -- 绑定变量
    LOOP
        FETCH c_any INTO v_emp;
        EXIT WHEN c_any%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_emp.last_name || ' (动态查询)');
    END LOOP;
    CLOSE c_any;
END;
/

八、综合实战案例——健壮的批量数据处理程序

SET SERVEROUTPUT ON SIZE 1000000

CREATE OR REPLACE PROCEDURE process_salary_review(
    p_review_year  IN NUMBER DEFAULT EXTRACT(YEAR FROM SYSDATE)
)
IS
    -- 异常声明
    ex_invalid_year     EXCEPTION;
    ex_budget_exceeded  EXCEPTION;
    
    -- 游标:查询需要调薪的员工
    CURSOR c_emp_review IS
        SELECT 
            e.employee_id,
            e.last_name,
            e.salary,
            e.hire_date,
            e.department_id,
            ROUND(MONTHS_BETWEEN(SYSDATE, e.hire_date) / 12, 1) AS years_served,
            d.department_name
        FROM employees e
        JOIN departments d ON e.department_id = d.department_id
        WHERE e.salary < (SELECT PERCENTILE_CONT(0.6) WITHIN GROUP (ORDER BY salary)
                         FROM employees)  -- 薪资在60百分位以下
        AND NOT EXISTS (  -- 当年未调薪
            SELECT 1 FROM salary_history sh
            WHERE sh.employee_id = e.employee_id
            AND EXTRACT(YEAR FROM sh.review_date) = p_review_year
        )
        ORDER BY e.department_id, e.salary ASC;
    
    -- 统计变量
    v_processed_count   NUMBER := 0;
    v_success_count     NUMBER := 0;
    v_skip_count        NUMBER := 0;
    v_total_increase    NUMBER := 0;
    v_budget_used       NUMBER := 0;
    v_budget_max        CONSTANT NUMBER := 200000;  -- 年度预算上限
    
    -- 单员工处理变量
    v_raise_pct         NUMBER;
    v_new_salary        NUMBER;
    v_increase_amount   NUMBER;
    
BEGIN
    -- 验证输入年份
    IF p_review_year < 2000 OR p_review_year > EXTRACT(YEAR FROM SYSDATE) + 1 THEN
        RAISE ex_invalid_year;
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('==============================================');
    DBMS_OUTPUT.PUT_LINE('薪资调整处理程序');
    DBMS_OUTPUT.PUT_LINE('处理年份:' || p_review_year);
    DBMS_OUTPUT.PUT_LINE('处理时间:' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
    DBMS_OUTPUT.PUT_LINE('预算上限:¥' || TO_CHAR(v_budget_max, '999,999'));
    DBMS_OUTPUT.PUT_LINE('==============================================');
    
    -- 遍历需要调薪的员工
    FOR emp IN c_emp_review LOOP
        v_processed_count := v_processed_count + 1;
        
        -- 根据工龄确定涨薪比例
        IF emp.years_served >= 10 THEN
            v_raise_pct := 0.15;
        ELSIF emp.years_served >= 7 THEN
            v_raise_pct := 0.12;
        ELSIF emp.years_served >= 5 THEN
            v_raise_pct := 0.10;
        ELSIF emp.years_served >= 3 THEN
            v_raise_pct := 0.08;
        ELSE
            v_raise_pct := 0.05;
        END IF;
        
        v_new_salary := ROUND(emp.salary * (1 + v_raise_pct));
        v_increase_amount := v_new_salary - emp.salary;
        
        -- 检查预算
        IF v_budget_used + v_increase_amount * 12 > v_budget_max THEN
            v_skip_count := v_skip_count + 1;
            DBMS_OUTPUT.PUT_LINE(
                '[跳过] ' || RPAD(emp.last_name, 12) || 
                '  预算已用:¥' || TO_CHAR(v_budget_used, '999,999')
            );
            RAISE ex_budget_exceeded;  -- 预算耗尽,停止处理
        END IF;
        
        -- 执行调薪
        BEGIN
            UPDATE employees
            SET salary = v_new_salary
            WHERE employee_id = emp.employee_id;
            
            -- 记录调薪历史
            INSERT INTO salary_history(employee_id, old_salary, new_salary, 
                                        raise_pct, review_date, review_year)
            VALUES(emp.employee_id, emp.salary, v_new_salary, 
                   v_raise_pct, SYSDATE, p_review_year);
            
            v_budget_used := v_budget_used + v_increase_amount * 12;
            v_success_count := v_success_count + 1;
            v_total_increase := v_total_increase + v_increase_amount;
            
            DBMS_OUTPUT.PUT_LINE(
                '[成功] ' || RPAD(emp.last_name, 12) ||
                ' 部门:' || RPAD(emp.department_name, 12) ||
                ' 工龄:' || emp.years_served || '年' ||
                ' ¥' || TO_CHAR(emp.salary, '99,999') ||
                ' → ¥' || TO_CHAR(v_new_salary, '99,999') ||
                ' (+' || ROUND(v_raise_pct * 100) || '%)'
            );
            
        EXCEPTION
            WHEN OTHERS THEN
                -- 单个员工处理失败不影响其他员工
                v_skip_count := v_skip_count + 1;
                DBMS_OUTPUT.PUT_LINE(
                    '[失败] ' || emp.last_name || 
                    ' 错误:' || SUBSTR(SQLERRM, 1, 100)
                );
        END;
    END LOOP;
    
    COMMIT;
    
    -- 输出汇总
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE('==============================================');
    DBMS_OUTPUT.PUT_LINE('处理完成汇总');
    DBMS_OUTPUT.PUT_LINE('总候选人数:' || v_processed_count);
    DBMS_OUTPUT.PUT_LINE('成功调薪:' || v_success_count || ' 人');
    DBMS_OUTPUT.PUT_LINE('跳过/失败:' || v_skip_count || ' 人');
    DBMS_OUTPUT.PUT_LINE('月薪增加总量:¥' || TO_CHAR(v_total_increase, '999,999'));
    DBMS_OUTPUT.PUT_LINE('预计年度预算使用:¥' || TO_CHAR(v_budget_used, '999,999'));
    DBMS_OUTPUT.PUT_LINE('==============================================');
    
EXCEPTION
    WHEN ex_invalid_year THEN
        DBMS_OUTPUT.PUT_LINE('错误:无效的年份 ' || p_review_year);
    WHEN ex_budget_exceeded THEN
        COMMIT;  -- 提交已处理的
        DBMS_OUTPUT.PUT_LINE('');
        DBMS_OUTPUT.PUT_LINE('预算已耗尽,已处理 ' || v_success_count || ' 人后停止');
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('严重错误,已回滚所有操作:' || SQLERRM);
        RAISE;
END process_salary_review;
/

九、常见问题

Q1:游标FOR循环和普通LOOP的性能差异?

游标FOR循环默认使用批量取数(BULK COLLECT),每次获取多行,性能通常优于逐行FETCH。在处理大数据量时,可以使用显式的BULK COLLECT进一步优化:

DECLARE
    TYPE emp_tab IS TABLE OF employees%ROWTYPE;
    v_emps    emp_tab;
    v_limit   CONSTANT NUMBER := 1000;
    
    CURSOR c_emp IS SELECT * FROM employees WHERE department_id = 60;
BEGIN
    OPEN c_emp;
    LOOP
        FETCH c_emp BULK COLLECT INTO v_emps LIMIT v_limit;  -- 每次取1000行
        EXIT WHEN v_emps.COUNT = 0;
        
        FOR i IN 1..v_emps.COUNT LOOP
            -- 处理v_emps(i)
            DBMS_OUTPUT.PUT_LINE(v_emps(i).last_name);
        END LOOP;
    END LOOP;
    CLOSE c_emp;
END;
/

Q2:WHEN OTHERS THEN RAISE 有什么用?

EXCEPTION
    WHEN OTHERS THEN
        -- 记录日志
        INSERT INTO error_log VALUES(SYSDATE, SQLCODE, SQLERRM, 'module_name');
        COMMIT;
        RAISE;  -- 重新抛出,让上层调用者也能感知到错误
-- 如果不RAISE,错误被吞掉,调用者以为执行成功了!

十、总结

本文系统讲解了PL/SQL高级特性:

  • 异常处理:预定义、非预定义、用户自定义异常,RAISE_APPLICATION_ERROR,SQLCODE/SQLERRM
  • 隐式游标:SQL%ROWCOUNT、SQL%FOUND等属性
  • 显式游标:声明、OPEN、FETCH、CLOSE、带参数游标
  • 游标FOR循环:最简洁、推荐的多行处理方式
  • REF游标:动态查询的游标类型
  • BULK COLLECT:批量取数优化

至此,PL/SQL基础三篇(基础语法、子程序、异常与游标)已全部完成,下一个系列将深入Oracle体系结构的内部机制。


上一篇【第11篇】PL/SQL子程序——存储过程与函数的创建与应用
下一篇【第13篇】Oracle实例架构深度解析——SGA、进程与实例启动全流程


参考资料

  • 《Oracle 11g数据库管理员指南》— 刘宪军著
  • Oracle官方文档:Database PL/SQL Language Reference - Exception Handling
  • Oracle官方文档:Database PL/SQL Language Reference - Cursors
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐