【Oracle】存储过程

个人主页:Guiat
归属专栏:Oracle

文章目录

  • [1. 存储过程基础概述](#1. 存储过程基础概述)
    • [1.1 存储过程的概念与特点](#1.1 存储过程的概念与特点)
    • [1.2 存储过程的组成结构](#1.2 存储过程的组成结构)
    • [1.3 存储过程的优势](#1.3 存储过程的优势)
  • [2. 基础存储过程](#2. 基础存储过程)
    • [2.1 简单存储过程](#2.1 简单存储过程)
      • [2.1.1 创建第一个存储过程](#2.1.1 创建第一个存储过程)
      • [2.1.2 带变量的存储过程](#2.1.2 带变量的存储过程)
    • [2.2 带参数的存储过程](#2.2 带参数的存储过程)
      • [2.2.1 输入参数 (IN)](#2.2.1 输入参数 (IN))
      • [2.2.2 输出参数 (OUT)](#2.2.2 输出参数 (OUT))
      • [2.2.3 输入输出参数 (IN OUT)](#2.2.3 输入输出参数 (IN OUT))
  • [3. 高级存储过程特性](#3. 高级存储过程特性)
    • [3.1 游标的使用](#3.1 游标的使用)
      • [3.1.1 显式游标](#3.1.1 显式游标)
      • [3.1.2 游标FOR循环](#3.1.2 游标FOR循环)
    • [3.2 异常处理](#3.2 异常处理)
      • [3.2.1 预定义异常处理](#3.2.1 预定义异常处理)
      • [3.2.2 用户定义异常](#3.2.2 用户定义异常)
  • [4. 存储过程的高级应用](#4. 存储过程的高级应用)
    • [4.1 动态SQL](#4.1 动态SQL)
      • [4.1.1 使用EXECUTE IMMEDIATE](#4.1.1 使用EXECUTE IMMEDIATE)
      • [4.1.2 动态查询构建器](#4.1.2 动态查询构建器)
    • [4.2 批量处理](#4.2 批量处理)
      • [4.2.1 BULK COLLECT和FORALL](#4.2.1 BULK COLLECT和FORALL)
      • [4.2.2 错误处理的批量操作](#4.2.2 错误处理的批量操作)
  • [5. 存储过程的调试与优化](#5. 存储过程的调试与优化)
    • [5.1 调试技术](#5.1 调试技术)
      • [5.1.1 使用DBMS_OUTPUT进行调试](#5.1.1 使用DBMS_OUTPUT进行调试)
      • [5.1.2 性能监控和分析](#5.1.2 性能监控和分析)
    • [5.2 存储过程优化](#5.2 存储过程优化)
      • [5.2.1 SQL优化技巧](#5.2.1 SQL优化技巧)
      • [5.2.2 内存和资源优化](#5.2.2 内存和资源优化)
  • [6. 存储过程的安全性](#6. 存储过程的安全性)
    • [6.1 权限管理](#6.1 权限管理)
      • [6.1.1 存储过程权限控制](#6.1.1 存储过程权限控制)

正文

1. 存储过程基础概述

存储过程是预编译的SQL和PL/SQL代码块,存储在数据库中,可以重复调用执行。它是Oracle数据库中实现复杂业务逻辑的重要工具。

1.1 存储过程的概念与特点

Oracle存储过程 预编译代码 服务器端执行 可重用性 安全性 编译一次多次执行 执行效率高 减少网络传输 在数据库服务器执行 减少客户端负载 集中业务逻辑 模块化编程 代码复用 维护简便 权限控制 SQL注入防护 数据访问封装

1.2 存储过程的组成结构

存储过程结构 声明部分 DECLARE 执行部分 BEGIN 异常处理 EXCEPTION 结束标记 END 变量声明 游标声明 嵌套过程声明 SQL语句 PL/SQL语句 业务逻辑 预定义异常 用户定义异常 异常处理逻辑

1.3 存储过程的优势

  • 性能优化:预编译,执行效率高
  • 网络优化:减少客户端与服务器间的通信
  • 安全性:封装数据访问,防止SQL注入
  • 维护性:集中管理业务逻辑
  • 可重用性:一次编写,多处调用

2. 基础存储过程

2.1 简单存储过程

2.1.1 创建第一个存储过程

sql 复制代码
-- 创建简单的存储过程
CREATE OR REPLACE PROCEDURE hello_world
AS
BEGIN
    DBMS_OUTPUT.PUT_LINE('Hello, Oracle World!');
    DBMS_OUTPUT.PUT_LINE('当前时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
END hello_world;
/

-- 调用存储过程
SET SERVEROUTPUT ON
EXEC hello_world;

-- 或者使用CALL语句
CALL hello_world();

-- 在PL/SQL块中调用
BEGIN
    hello_world;
END;
/

2.1.2 带变量的存储过程

sql 复制代码
-- 创建包含变量的存储过程
CREATE OR REPLACE PROCEDURE employee_count_info
AS
    v_total_count NUMBER;
    v_active_count NUMBER;
    v_avg_salary NUMBER;
    v_max_salary NUMBER;
    v_min_salary NUMBER;
BEGIN
    -- 获取员工统计信息
    SELECT COUNT(*),
           COUNT(CASE WHEN hire_date >= ADD_MONTHS(SYSDATE, -12) THEN 1 END),
           ROUND(AVG(salary), 2),
           MAX(salary),
           MIN(salary)
    INTO v_total_count, v_active_count, v_avg_salary, v_max_salary, v_min_salary
    FROM employees;
    
    -- 输出统计信息
    DBMS_OUTPUT.PUT_LINE('=== 员工统计信息 ===');
    DBMS_OUTPUT.PUT_LINE('总员工数: ' || v_total_count);
    DBMS_OUTPUT.PUT_LINE('近一年入职: ' || v_active_count);
    DBMS_OUTPUT.PUT_LINE('平均工资: $' || v_avg_salary);
    DBMS_OUTPUT.PUT_LINE('最高工资: $' || v_max_salary);
    DBMS_OUTPUT.PUT_LINE('最低工资: $' || v_min_salary);
    
    -- 工资分析
    IF v_avg_salary > 8000 THEN
        DBMS_OUTPUT.PUT_LINE('工资水平: 较高');
    ELSIF v_avg_salary > 5000 THEN
        DBMS_OUTPUT.PUT_LINE('工资水平: 中等');
    ELSE
        DBMS_OUTPUT.PUT_LINE('工资水平: 偏低');
    END IF;
    
END employee_count_info;
/

-- 执行存储过程
EXEC employee_count_info;

2.2 带参数的存储过程

2.2.1 输入参数 (IN)

sql 复制代码
-- 创建带输入参数的存储过程
CREATE OR REPLACE PROCEDURE get_employee_info(
    p_employee_id IN NUMBER
)
AS
    v_first_name VARCHAR2(50);
    v_last_name VARCHAR2(50);
    v_email VARCHAR2(100);
    v_salary NUMBER;
    v_hire_date DATE;
    v_department_name VARCHAR2(50);
    v_job_title VARCHAR2(50);
    v_manager_name VARCHAR2(100);
BEGIN
    -- 查询员工详细信息
    SELECT e.first_name,
           e.last_name,
           e.email,
           e.salary,
           e.hire_date,
           d.department_name,
           j.job_title,
           m.first_name || ' ' || m.last_name
    INTO v_first_name, v_last_name, v_email, v_salary, v_hire_date,
         v_department_name, v_job_title, v_manager_name
    FROM employees e
    LEFT JOIN departments d ON e.department_id = d.department_id
    LEFT JOIN jobs j ON e.job_id = j.job_id
    LEFT JOIN employees m ON e.manager_id = m.employee_id
    WHERE e.employee_id = p_employee_id;
    
    -- 显示员工信息
    DBMS_OUTPUT.PUT_LINE('=== 员工信息 ===');
    DBMS_OUTPUT.PUT_LINE('姓名: ' || v_first_name || ' ' || v_last_name);
    DBMS_OUTPUT.PUT_LINE('邮箱: ' || v_email);
    DBMS_OUTPUT.PUT_LINE('工资: $' || v_salary);
    DBMS_OUTPUT.PUT_LINE('入职日期: ' || TO_CHAR(v_hire_date, 'YYYY-MM-DD'));
    DBMS_OUTPUT.PUT_LINE('部门: ' || NVL(v_department_name, '未分配'));
    DBMS_OUTPUT.PUT_LINE('职位: ' || NVL(v_job_title, '未知'));
    DBMS_OUTPUT.PUT_LINE('经理: ' || NVL(v_manager_name, '无'));
    
    -- 计算工作年限
    DBMS_OUTPUT.PUT_LINE('工作年限: ' || 
                        ROUND(MONTHS_BETWEEN(SYSDATE, v_hire_date) / 12, 1) || ' 年');
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('发生错误: ' || SQLERRM);
END get_employee_info;
/

-- 调用带参数的存储过程
EXEC get_employee_info(100);
EXEC get_employee_info(999); -- 测试不存在的员工

2.2.2 输出参数 (OUT)

sql 复制代码
-- 创建带输出参数的存储过程
CREATE OR REPLACE PROCEDURE calculate_employee_stats(
    p_department_id IN NUMBER,
    p_emp_count OUT NUMBER,
    p_avg_salary OUT NUMBER,
    p_total_salary OUT NUMBER,
    p_max_salary OUT NUMBER,
    p_min_salary OUT NUMBER
)
AS
BEGIN
    SELECT COUNT(*),
           ROUND(AVG(salary), 2),
           SUM(salary),
           MAX(salary),
           MIN(salary)
    INTO p_emp_count, p_avg_salary, p_total_salary, p_max_salary, p_min_salary
    FROM employees
    WHERE department_id = p_department_id;
    
    -- 如果没有找到员工,设置默认值
    IF p_emp_count = 0 THEN
        p_avg_salary := 0;
        p_total_salary := 0;
        p_max_salary := 0;
        p_min_salary := 0;
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        p_emp_count := -1;
        p_avg_salary := 0;
        p_total_salary := 0;
        p_max_salary := 0;
        p_min_salary := 0;
END calculate_employee_stats;
/

-- 调用带输出参数的存储过程
DECLARE
    v_count NUMBER;
    v_avg_salary NUMBER;
    v_total_salary NUMBER;
    v_max_salary NUMBER;
    v_min_salary NUMBER;
    v_dept_id NUMBER := 60; -- IT部门
BEGIN
    calculate_employee_stats(
        p_department_id => v_dept_id,
        p_emp_count => v_count,
        p_avg_salary => v_avg_salary,
        p_total_salary => v_total_salary,
        p_max_salary => v_max_salary,
        p_min_salary => v_min_salary
    );
    
    DBMS_OUTPUT.PUT_LINE('=== 部门 ' || v_dept_id || ' 统计信息 ===');
    DBMS_OUTPUT.PUT_LINE('员工数量: ' || v_count);
    DBMS_OUTPUT.PUT_LINE('平均工资: $' || v_avg_salary);
    DBMS_OUTPUT.PUT_LINE('工资总额: $' || v_total_salary);
    DBMS_OUTPUT.PUT_LINE('最高工资: $' || v_max_salary);
    DBMS_OUTPUT.PUT_LINE('最低工资: $' || v_min_salary);
END;
/

2.2.3 输入输出参数 (IN OUT)

sql 复制代码
-- 创建带IN OUT参数的存储过程
CREATE OR REPLACE PROCEDURE adjust_salary(
    p_employee_id IN NUMBER,
    p_salary_change IN OUT NUMBER,
    p_result_message OUT VARCHAR2
)
AS
    v_current_salary NUMBER;
    v_new_salary NUMBER;
    v_min_salary NUMBER;
    v_max_salary NUMBER;
    v_job_id VARCHAR2(10);
BEGIN
    -- 获取员工当前信息
    SELECT salary, job_id
    INTO v_current_salary, v_job_id
    FROM employees
    WHERE employee_id = p_employee_id;
    
    -- 获取职位工资范围
    SELECT min_salary, max_salary
    INTO v_min_salary, v_max_salary
    FROM jobs
    WHERE job_id = v_job_id;
    
    -- 如果输入的是百分比(小于1),转换为实际金额
    IF p_salary_change < 1 AND p_salary_change > -1 THEN
        p_salary_change := v_current_salary * p_salary_change;
    END IF;
    
    -- 计算新工资
    v_new_salary := v_current_salary + p_salary_change;
    
    -- 检查工资范围
    IF v_new_salary < v_min_salary THEN
        v_new_salary := v_min_salary;
        p_salary_change := v_new_salary - v_current_salary;
        p_result_message := '工资已调整到职位最低标准: $' || v_min_salary;
    ELSIF v_new_salary > v_max_salary THEN
        v_new_salary := v_max_salary;
        p_salary_change := v_new_salary - v_current_salary;
        p_result_message := '工资已调整到职位最高标准: $' || v_max_salary;
    ELSE
        p_result_message := '工资调整成功: $' || v_current_salary || ' -> $' || v_new_salary;
    END IF;
    
    -- 更新员工工资
    UPDATE employees
    SET salary = v_new_salary,
        last_updated = SYSDATE
    WHERE employee_id = p_employee_id;
    
    -- 返回实际调整金额
    p_salary_change := v_new_salary - v_current_salary;
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        p_result_message := '错误: 员工ID ' || p_employee_id || ' 不存在';
        p_salary_change := 0;
    WHEN OTHERS THEN
        p_result_message := '错误: ' || SQLERRM;
        p_salary_change := 0;
        ROLLBACK;
END adjust_salary;
/

-- 测试IN OUT参数
DECLARE
    v_salary_change NUMBER := 0.1; -- 10%增长
    v_message VARCHAR2(200);
    v_emp_id NUMBER := 107;
BEGIN
    DBMS_OUTPUT.PUT_LINE('调整前工资变化: ' || v_salary_change);
    
    adjust_salary(
        p_employee_id => v_emp_id,
        p_salary_change => v_salary_change,
        p_result_message => v_message
    );
    
    DBMS_OUTPUT.PUT_LINE('实际工资变化: $' || v_salary_change);
    DBMS_OUTPUT.PUT_LINE('结果: ' || v_message);
    
    COMMIT;
END;
/

3. 高级存储过程特性

3.1 游标的使用

3.1.1 显式游标

sql 复制代码
-- 使用显式游标的存储过程
CREATE OR REPLACE PROCEDURE process_department_salaries(
    p_department_id IN NUMBER,
    p_increase_percent IN NUMBER DEFAULT 5
)
AS
    -- 声明游标
    CURSOR emp_cursor IS
        SELECT employee_id, first_name, last_name, salary
        FROM employees
        WHERE department_id = p_department_id
        ORDER BY salary;
    
    -- 游标记录类型
    emp_rec emp_cursor%ROWTYPE;
    
    -- 其他变量
    v_counter NUMBER := 0;
    v_total_increase NUMBER := 0;
    v_old_salary NUMBER;
    v_new_salary NUMBER;
    
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 部门 ' || p_department_id || ' 工资调整 ===');
    DBMS_OUTPUT.PUT_LINE('调整比例: ' || p_increase_percent || '%');
    DBMS_OUTPUT.PUT_LINE('');
    
    -- 打开游标
    OPEN emp_cursor;
    
    LOOP
        -- 提取数据
        FETCH emp_cursor INTO emp_rec;
        
        -- 检查是否还有数据
        EXIT WHEN emp_cursor%NOTFOUND;
        
        -- 处理当前员工
        v_counter := v_counter + 1;
        v_old_salary := emp_rec.salary;
        v_new_salary := ROUND(v_old_salary * (1 + p_increase_percent / 100), 2);
        
        -- 更新工资
        UPDATE employees
        SET salary = v_new_salary,
            last_updated = SYSDATE
        WHERE employee_id = emp_rec.employee_id;
        
        -- 累计增长金额
        v_total_increase := v_total_increase + (v_new_salary - v_old_salary);
        
        -- 显示调整信息
        DBMS_OUTPUT.PUT_LINE(v_counter || '. ' || emp_rec.first_name || ' ' || 
                            emp_rec.last_name || ': $' || v_old_salary || 
                            ' -> $' || v_new_salary || 
                            ' (+$' || (v_new_salary - v_old_salary) || ')');
    END LOOP;
    
    -- 关闭游标
    CLOSE emp_cursor;
    
    -- 显示汇总信息
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE('=== 调整汇总 ===');
    DBMS_OUTPUT.PUT_LINE('处理员工数: ' || v_counter);
    DBMS_OUTPUT.PUT_LINE('总增长金额: $' || ROUND(v_total_increase, 2));
    
    IF v_counter > 0 THEN
        DBMS_OUTPUT.PUT_LINE('平均增长: $' || ROUND(v_total_increase / v_counter, 2));
        COMMIT;
        DBMS_OUTPUT.PUT_LINE('工资调整已提交');
    ELSE
        DBMS_OUTPUT.PUT_LINE('未找到符合条件的员工');
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        IF emp_cursor%ISOPEN THEN
            CLOSE emp_cursor;
        END IF;
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('错误: ' || SQLERRM);
END process_department_salaries;
/

-- 测试游标存储过程
EXEC process_department_salaries(20, 8); -- IT部门加薪8%

3.1.2 游标FOR循环

sql 复制代码
-- 使用游标FOR循环的存储过程
CREATE OR REPLACE PROCEDURE generate_employee_report(
    p_department_id IN NUMBER DEFAULT NULL
)
AS
    v_total_employees NUMBER := 0;
    v_total_salary NUMBER := 0;
    v_dept_name VARCHAR2(50);
BEGIN
    -- 获取部门名称
    IF p_department_id IS NOT NULL THEN
        SELECT department_name INTO v_dept_name
        FROM departments WHERE department_id = p_department_id;
        DBMS_OUTPUT.PUT_LINE('=== ' || v_dept_name || ' 部门员工报告 ===');
    ELSE
        DBMS_OUTPUT.PUT_LINE('=== 全公司员工报告 ===');
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE(RPAD('员工姓名', 20) || RPAD('职位', 15) || 
                        RPAD('工资', 10) || RPAD('入职日期', 12) || '工作年限');
    DBMS_OUTPUT.PUT_LINE(RPAD('-', 70, '-'));
    
    -- 游标FOR循环
    FOR emp_rec IN (
        SELECT e.first_name || ' ' || e.last_name AS full_name,
               j.job_title,
               e.salary,
               e.hire_date,
               ROUND(MONTHS_BETWEEN(SYSDATE, e.hire_date) / 12, 1) AS years_service
        FROM employees e
        LEFT JOIN jobs j ON e.job_id = j.job_id
        WHERE (p_department_id IS NULL OR e.department_id = p_department_id)
        ORDER BY e.salary DESC
    ) LOOP
        -- 处理每个员工记录
        v_total_employees := v_total_employees + 1;
        v_total_salary := v_total_salary + emp_rec.salary;
        
        DBMS_OUTPUT.PUT_LINE(
            RPAD(emp_rec.full_name, 20) ||
            RPAD(NVL(emp_rec.job_title, 'N/A'), 15) ||
            RPAD('$' || emp_rec.salary, 10) ||
            RPAD(TO_CHAR(emp_rec.hire_date, 'YYYY-MM-DD'), 12) ||
            emp_rec.years_service || '年'
        );
    END LOOP;
    
    -- 显示汇总信息
    DBMS_OUTPUT.PUT_LINE(RPAD('-', 70, '-'));
    DBMS_OUTPUT.PUT_LINE('总员工数: ' || v_total_employees);
    DBMS_OUTPUT.PUT_LINE('工资总额: $' || v_total_salary);
    IF v_total_employees > 0 THEN
        DBMS_OUTPUT.PUT_LINE('平均工资: $' || ROUND(v_total_salary / v_total_employees, 2));
    END IF;
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('未找到指定部门或部门无员工');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('生成报告时发生错误: ' || SQLERRM);
END generate_employee_report;
/

-- 测试游标FOR循环
EXEC generate_employee_report(60); -- IT部门报告
EXEC generate_employee_report; -- 全公司报告

3.2 异常处理

3.2.1 预定义异常处理

sql 复制代码
-- 完善的异常处理存储过程
CREATE OR REPLACE PROCEDURE safe_employee_update(
    p_employee_id IN NUMBER,
    p_first_name IN VARCHAR2 DEFAULT NULL,
    p_last_name IN VARCHAR2 DEFAULT NULL,
    p_email IN VARCHAR2 DEFAULT NULL,
    p_salary IN NUMBER DEFAULT NULL,
    p_department_id IN NUMBER DEFAULT NULL
)
AS
    v_current_email VARCHAR2(100);
    v_update_count NUMBER := 0;
    v_sql_stmt VARCHAR2(4000) := 'UPDATE employees SET last_updated = SYSDATE';
    v_where_clause VARCHAR2(100) := ' WHERE employee_id = :emp_id';
BEGIN
    -- 验证员工是否存在
    SELECT email INTO v_current_email
    FROM employees 
    WHERE employee_id = p_employee_id;
    
    -- 构建动态更新语句
    IF p_first_name IS NOT NULL THEN
        v_sql_stmt := v_sql_stmt || ', first_name = :first_name';
    END IF;
    
    IF p_last_name IS NOT NULL THEN
        v_sql_stmt := v_sql_stmt || ', last_name = :last_name';
    END IF;
    
    IF p_email IS NOT NULL THEN
        -- 检查邮箱唯一性
        SELECT COUNT(*) INTO v_update_count
        FROM employees 
        WHERE email = p_email AND employee_id != p_employee_id;
        
        IF v_update_count > 0 THEN
            RAISE_APPLICATION_ERROR(-20001, '邮箱地址已被其他员工使用');
        END IF;
        
        v_sql_stmt := v_sql_stmt || ', email = :email';
    END IF;
    
    IF p_salary IS NOT NULL THEN
        IF p_salary <= 0 THEN
            RAISE_APPLICATION_ERROR(-20002, '工资必须大于0');
        END IF;
        v_sql_stmt := v_sql_stmt || ', salary = :salary';
    END IF;
    
    IF p_department_id IS NOT NULL THEN
        -- 验证部门是否存在
        SELECT COUNT(*) INTO v_update_count
        FROM departments WHERE department_id = p_department_id;
        
        IF v_update_count = 0 THEN
            RAISE_APPLICATION_ERROR(-20003, '指定的部门不存在');
        END IF;
        
        v_sql_stmt := v_sql_stmt || ', department_id = :dept_id';
    END IF;
    
    -- 执行更新(这里简化处理,实际应用中可使用动态SQL)
    UPDATE employees 
    SET first_name = NVL(p_first_name, first_name),
        last_name = NVL(p_last_name, last_name),
        email = NVL(p_email, email),
        salary = NVL(p_salary, salary),
        department_id = NVL(p_department_id, department_id),
        last_updated = SYSDATE
    WHERE employee_id = p_employee_id;
    
    v_update_count := SQL%ROWCOUNT;
    
    IF v_update_count > 0 THEN
        COMMIT;
        DBMS_OUTPUT.PUT_LINE('员工信息更新成功');
        DBMS_OUTPUT.PUT_LINE('员工ID: ' || p_employee_id);
        DBMS_OUTPUT.PUT_LINE('更新行数: ' || v_update_count);
    ELSE
        DBMS_OUTPUT.PUT_LINE('没有记录被更新');
    END IF;
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');
    
    WHEN DUP_VAL_ON_INDEX THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('错误: 违反唯一性约束,可能是邮箱重复');
    
    WHEN VALUE_ERROR THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('错误: 数据类型或长度错误');
    
    WHEN INVALID_NUMBER THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('错误: 无效的数字格式');
    
    WHEN OTHERS THEN
        ROLLBACK;
        IF SQLCODE BETWEEN -20999 AND -20000 THEN
            -- 用户定义的应用错误
            DBMS_OUTPUT.PUT_LINE('业务错误: ' || SQLERRM);
        ELSE
            -- 其他系统错误
            DBMS_OUTPUT.PUT_LINE('系统错误: ' || SQLCODE || ' - ' || SQLERRM);
        END IF;
END safe_employee_update;
/

-- 测试异常处理
BEGIN
    -- 正常更新
    safe_employee_update(
        p_employee_id => 107,
        p_first_name => 'Diana',
        p_salary => 5200
    );
    
    -- 测试各种异常情况
    safe_employee_update(999, 'Test', 'User'); -- 员工不存在
    safe_employee_update(107, p_salary => -1000); -- 无效工资
    safe_employee_update(107, p_department_id => 999); -- 部门不存在
END;
/

3.2.2 用户定义异常

sql 复制代码
-- 创建带用户定义异常的存储过程
CREATE OR REPLACE PROCEDURE transfer_employee(
    p_employee_id IN NUMBER,
    p_new_department_id IN NUMBER,
    p_effective_date IN DATE DEFAULT SYSDATE
)
AS
    -- 用户定义异常
    employee_not_found EXCEPTION;
    department_not_found EXCEPTION;
    invalid_transfer_date EXCEPTION;
    same_department EXCEPTION;
    employee_is_manager EXCEPTION;
    
    -- 变量声明
    v_current_dept_id NUMBER;
    v_dept_count NUMBER;
    v_manager_count NUMBER;
    v_employee_name VARCHAR2(100);
    v_old_dept_name VARCHAR2(50);
    v_new_dept_name VARCHAR2(50);
    
BEGIN
    -- 验证转移日期
    IF p_effective_date < TRUNC(SYSDATE) THEN
        RAISE invalid_transfer_date;
    END IF;
    
    -- 获取员工当前信息
    BEGIN
        SELECT e.department_id, 
               e.first_name || ' ' || e.last_name,
               d.department_name
        INTO v_current_dept_id, v_employee_name, v_old_dept_name
        FROM employees e
        LEFT JOIN departments d ON e.department_id = d.department_id
        WHERE e.employee_id = p_employee_id;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE employee_not_found;
    END;
    
    -- 检查是否转移到相同部门
    IF v_current_dept_id = p_new_department_id THEN
        RAISE same_department;
    END IF;
    
    -- 验证新部门是否存在
    SELECT COUNT(*), MAX(department_name)
    INTO v_dept_count, v_new_dept_name
    FROM departments 
    WHERE department_id = p_new_department_id;
    
    IF v_dept_count = 0 THEN
        RAISE department_not_found;
    END IF;
    
    -- 检查员工是否是部门经理
    SELECT COUNT(*)
    INTO v_manager_count
    FROM departments
    WHERE manager_id = p_employee_id;
    
    IF v_manager_count > 0 THEN
        RAISE employee_is_manager;
    END IF;
    
    -- 记录转移历史
    INSERT INTO employee_transfer_history (
        transfer_id,
        employee_id,
        old_department_id,
        new_department_id,
        transfer_date,
        created_by,
        created_date
    ) VALUES (
        employee_transfer_seq.NEXTVAL,
        p_employee_id,
        v_current_dept_id,
        p_new_department_id,
        p_effective_date,
        USER,
        SYSDATE
    );
    
    -- 执行转移
    UPDATE employees
    SET department_id = p_new_department_id,
        last_updated = SYSDATE
    WHERE employee_id = p_employee_id;
    
    COMMIT;
    
    -- 输出成功信息
    DBMS_OUTPUT.PUT_LINE('=== 员工转移成功 ===');
    DBMS_OUTPUT.PUT_LINE('员工: ' || v_employee_name);
    DBMS_OUTPUT.PUT_LINE('从: ' || NVL(v_old_dept_name, '未分配部门'));
    DBMS_OUTPUT.PUT_LINE('到: ' || v_new_dept_name);
    DBMS_OUTPUT.PUT_LINE('生效日期: ' || TO_CHAR(p_effective_date, 'YYYY-MM-DD'));
    
EXCEPTION
    WHEN employee_not_found THEN
        DBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');
    
    WHEN department_not_found THEN
        DBMS_OUTPUT.PUT_LINE('错误: 目标部门ID ' || p_new_department_id || ' 不存在');
    
    WHEN invalid_transfer_date THEN
        DBMS_OUTPUT.PUT_LINE('错误: 转移日期不能早于今天');
    
    WHEN same_department THEN
        DBMS_OUTPUT.PUT_LINE('错误: 员工已经在目标部门中');
    
    WHEN employee_is_manager THEN
        DBMS_OUTPUT.PUT_LINE('错误: 无法转移部门经理,请先指定新经理');
    
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('转移失败: ' || SQLERRM);
END transfer_employee;
/

-- 创建转移历史表(如果不存在)
CREATE TABLE employee_transfer_history (
    transfer_id NUMBER PRIMARY KEY,
    employee_id NUMBER,
    old_department_id NUMBER,
    new_department_id NUMBER,
    transfer_date DATE,
    created_by VARCHAR2(30),
    created_date DATE
);

CREATE SEQUENCE employee_transfer_seq START WITH 1 INCREMENT BY 1;

-- 测试用户定义异常
EXEC transfer_employee(107, 20); -- 正常转移
EXEC transfer_employee(999, 20); -- 员工不存在
EXEC transfer_employee(107, 999); -- 部门不存在
EXEC transfer_employee(107, 20); -- 相同部门

4. 存储过程的高级应用

4.1 动态SQL

4.1.1 使用EXECUTE IMMEDIATE

sql 复制代码
-- 动态SQL存储过程
CREATE OR REPLACE PROCEDURE dynamic_table_stats(
    p_table_name IN VARCHAR2,
    p_owner IN VARCHAR2 DEFAULT USER
)
AS
    v_sql VARCHAR2(4000);
    v_count NUMBER;
    v_avg_value NUMBER;
    v_max_value NUMBER;
    v_min_value NUMBER;
    v_column_name VARCHAR2(128);
    v_data_type VARCHAR2(128);
    
    -- 游标:获取数值列
    CURSOR num_columns_cur IS
        SELECT column_name, data_type
        FROM all_tab_columns
        WHERE table_name = UPPER(p_table_name)
          AND owner = UPPER(p_owner)
          AND data_type IN ('NUMBER', 'INTEGER', 'FLOAT')
        ORDER BY column_id;
        
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 表 ' || p_owner || '.' || p_table_name || ' 统计信息 ===');
    
    -- 检查表是否存在
    v_sql := 'SELECT COUNT(*) FROM ' || p_owner || '.' || p_table_name;
    
    BEGIN
        EXECUTE IMMEDIATE v_sql INTO v_count;
        DBMS_OUTPUT.PUT_LINE('总记录数: ' || v_count);
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('错误: 表不存在或无访问权限');
            RETURN;
    END;
    
    IF v_count = 0 THEN
        DBMS_OUTPUT.PUT_LINE('表为空,无统计数据');
        RETURN;
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE('数值列统计:');
    DBMS_OUTPUT.PUT_LINE(RPAD('列名', 20) || RPAD('数据类型', 15) || 
                        RPAD('平均值', 12) || RPAD('最大值', 12) || '最小值');
    DBMS_OUTPUT.PUT_LINE(RPAD('-', 70, '-'));
    
    -- 遍历数值列
    FOR col_rec IN num_columns_cur LOOP
        BEGIN
            -- 构建动态SQL
            v_sql := 'SELECT ROUND(AVG(' || col_rec.column_name || '), 2), ' ||
                     'MAX(' || col_rec.column_name || '), ' ||
                     'MIN(' || col_rec.column_name || ') ' ||
                     'FROM ' || p_owner || '.' || p_table_name ||
                     ' WHERE ' || col_rec.column_name || ' IS NOT NULL';
            
            EXECUTE IMMEDIATE v_sql INTO v_avg_value, v_max_value, v_min_value;
            
            DBMS_OUTPUT.PUT_LINE(
                RPAD(col_rec.column_name, 20) ||
                RPAD(col_rec.data_type, 15) ||
                RPAD(NVL(TO_CHAR(v_avg_value), 'N/A'), 12) ||
                RPAD(NVL(TO_CHAR(v_max_value), 'N/A'), 12) ||
                NVL(TO_CHAR(v_min_value), 'N/A')
            );
            
        EXCEPTION
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE(
                    RPAD(col_rec.column_name, 20) ||
                    RPAD(col_rec.data_type, 15) ||
                    '计算错误: ' || SUBSTR(SQLERRM, 1, 30)
                );
        END;
    END LOOP;
    
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('执行错误: ' || SQLERRM);
END dynamic_table_stats;
/

-- 测试动态SQL
EXEC dynamic_table_stats('EMPLOYEES');
EXEC dynamic_table_stats('DEPARTMENTS');

4.1.2 动态查询构建器

sql 复制代码
-- 通用查询构建器存储过程
CREATE OR REPLACE PROCEDURE flexible_employee_search(
    p_department_id IN NUMBER DEFAULT NULL,
    p_job_id IN VARCHAR2 DEFAULT NULL,
    p_min_salary IN NUMBER DEFAULT NULL,
    p_max_salary IN NUMBER DEFAULT NULL,
    p_hire_date_from IN DATE DEFAULT NULL,
    p_hire_date_to IN DATE DEFAULT NULL,
    p_order_by IN VARCHAR2 DEFAULT 'employee_id',
    p_order_direction IN VARCHAR2 DEFAULT 'ASC'
)
AS
    v_sql VARCHAR2(4000);
    v_where_clause VARCHAR2(2000) := '';
    v_conditions NUMBER := 0;
    
    -- 定义REF CURSOR类型
    TYPE emp_cursor_type IS REF CURSOR;
    emp_cursor emp_cursor_type;
    
    -- 记录类型
    v_employee_id NUMBER;
    v_full_name VARCHAR2(100);
    v_email VARCHAR2(100);
    v_salary NUMBER;
    v_hire_date DATE;
    v_department_name VARCHAR2(50);
    v_job_title VARCHAR2(50);
    
BEGIN
    -- 构建基本查询
    v_sql := 'SELECT e.employee_id, ' ||
             'e.first_name || '' '' || e.last_name AS full_name, ' ||
             'e.email, e.salary, e.hire_date, ' ||
             'd.department_name, j.job_title ' ||
             'FROM employees e ' ||
             'LEFT JOIN departments d ON e.department_id = d.department_id ' ||
             'LEFT JOIN jobs j ON e.job_id = j.job_id';
    
    -- 构建WHERE条件
    IF p_department_id IS NOT NULL THEN
        v_where_clause := v_where_clause || 
                         CASE WHEN v_conditions > 0 THEN ' AND ' ELSE ' WHERE ' END ||
                         'e.department_id = ' || p_department_id;
        v_conditions := v_conditions + 1;
    END IF;
    
    IF p_job_id IS NOT NULL THEN
        v_where_clause := v_where_clause || 
                         CASE WHEN v_conditions > 0 THEN ' AND ' ELSE ' WHERE ' END ||
                         'e.job_id = ''' || p_job_id || '''';
        v_conditions := v_conditions + 1;
    END IF;
    
    IF p_min_salary IS NOT NULL THEN
        v_where_clause := v_where_clause || 
                         CASE WHEN v_conditions > 0 THEN ' AND ' ELSE ' WHERE ' END ||
                         'e.salary >= ' || p_min_salary;
        v_conditions := v_conditions + 1;
    END IF;
    
    IF p_max_salary IS NOT NULL THEN
        v_where_clause := v_where_clause || 
                         CASE WHEN v_conditions > 0 THEN ' AND ' ELSE ' WHERE ' END ||
                         'e.salary <= ' || p_max_salary;
        v_conditions := v_conditions + 1;
    END IF;
    
    IF p_hire_date_from IS NOT NULL THEN
        v_where_clause := v_where_clause || 
                         CASE WHEN v_conditions > 0 THEN ' AND ' ELSE ' WHERE ' END ||
                         'e.hire_date >= DATE ''' || TO_CHAR(p_hire_date_from, 'YYYY-MM-DD') || '''';
        v_conditions := v_conditions + 1;
    END IF;
    
    IF p_hire_date_to IS NOT NULL THEN
        v_where_clause := v_where_clause || 
                         CASE WHEN v_conditions > 0 THEN ' AND ' ELSE ' WHERE ' END ||
                         'e.hire_date <= DATE ''' || TO_CHAR(p_hire_date_to, 'YYYY-MM-DD') || '''';
        v_conditions := v_conditions + 1;
    END IF;
    
    -- 组合完整SQL
    v_sql := v_sql || v_where_clause || ' ORDER BY ' || p_order_by || ' ' || p_order_direction;
    
    DBMS_OUTPUT.PUT_LINE('=== 员工搜索结果 ===');
    DBMS_OUTPUT.PUT_LINE('查询SQL: ' || v_sql);
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE(RPAD('ID', 6) || RPAD('姓名', 20) || RPAD('邮箱', 25) || 
                        RPAD('工资', 10) || RPAD('入职日期', 12) || RPAD('部门', 15) || '职位');
    DBMS_OUTPUT.PUT_LINE(RPAD('-', 100, '-'));
    
    -- 执行动态查询
    OPEN emp_cursor FOR v_sql;
    
    LOOP
        FETCH emp_cursor INTO v_employee_id, v_full_name, v_email, v_salary, 
                              v_hire_date, v_department_name, v_job_title;
        EXIT WHEN emp_cursor%NOTFOUND;
        
        DBMS_OUTPUT.PUT_LINE(
            RPAD(v_employee_id, 6) ||
            RPAD(SUBSTR(v_full_name, 1, 19), 20) ||
            RPAD(SUBSTR(v_email, 1, 24), 25) ||
            RPAD('$' || v_salary, 10) ||
            RPAD(TO_CHAR(v_hire_date, 'YYYY-MM-DD'), 12) ||
            RPAD(SUBSTR(NVL(v_department_name, 'N/A'), 1, 14), 15) ||
            SUBSTR(NVL(v_job_title, 'N/A'), 1, 20)
        );
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE(RPAD('-', 100, '-'));
    DBMS_OUTPUT.PUT_LINE('找到 ' || emp_cursor%ROWCOUNT || ' 条记录');
    
    CLOSE emp_cursor;
    
EXCEPTION
    WHEN OTHERS THEN
        IF emp_cursor%ISOPEN THEN
            CLOSE emp_cursor;
        END IF;
        DBMS_OUTPUT.PUT_LINE('查询执行错误: ' || SQLERRM);
END flexible_employee_search;
/

-- 测试动态查询构建器
-- 搜索IT部门,工资在5000-10000之间的员工
EXEC flexible_employee_search(p_department_id => 60, p_min_salary => 5000, p_max_salary => 10000);

-- 搜索2005年后入职的员工,按工资降序排列
EXEC flexible_employee_search(p_hire_date_from => DATE '2005-01-01', p_order_by => 'salary', p_order_direction => 'DESC');

4.2 批量处理

4.2.1 BULK COLLECT和FORALL

sql 复制代码
-- 高性能批量处理存储过程
CREATE OR REPLACE PROCEDURE bulk_salary_adjustment(
    p_department_id IN NUMBER,
    p_adjustment_type IN VARCHAR2, -- 'PCT' for percentage, 'AMT' for amount
    p_adjustment_value IN NUMBER,
    p_batch_size IN NUMBER DEFAULT 1000
)
AS
    -- 声明集合类型
    TYPE emp_id_array IS TABLE OF employees.employee_id%TYPE;
    TYPE salary_array IS TABLE OF employees.salary%TYPE;
    TYPE name_array IS TABLE OF VARCHAR2(100);
    
    -- 声明集合变量
    v_emp_ids emp_id_array;
    v_old_salaries salary_array;
    v_new_salaries salary_array;
    v_emp_names name_array;
    
    -- 其他变量
    v_total_processed NUMBER := 0;
    v_total_adjustment NUMBER := 0;
    v_batch_count NUMBER := 0;
    
    -- 游标声明
    CURSOR emp_cursor IS
        SELECT employee_id, salary, first_name || ' ' || last_name
        FROM employees
        WHERE department_id = p_department_id
        ORDER BY employee_id;
        
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 批量工资调整开始 ===');
    DBMS_OUTPUT.PUT_LINE('部门ID: ' || p_department_id);
    DBMS_OUTPUT.PUT_LINE('调整类型: ' || CASE p_adjustment_type 
                                        WHEN 'PCT' THEN '百分比' 
                                        WHEN 'AMT' THEN '固定金额' 
                                        ELSE '未知' END);
    DBMS_OUTPUT.PUT_LINE('调整值: ' || p_adjustment_value);
    DBMS_OUTPUT.PUT_LINE('批次大小: ' || p_batch_size);
    DBMS_OUTPUT.PUT_LINE('');
    
    -- 验证调整类型
    IF p_adjustment_type NOT IN ('PCT', 'AMT') THEN
        RAISE_APPLICATION_ERROR(-20001, '无效的调整类型,必须是PCT或AMT');
    END IF;
    
    -- 打开游标并批量处理
    OPEN emp_cursor;
    
    LOOP
        -- 批量获取数据
        FETCH emp_cursor BULK COLLECT INTO v_emp_ids, v_old_salaries, v_emp_names
        LIMIT p_batch_size;
        
        -- 退出条件
        EXIT WHEN v_emp_ids.COUNT = 0;
        
        v_batch_count := v_batch_count + 1;
        DBMS_OUTPUT.PUT_LINE('处理第 ' || v_batch_count || ' 批,记录数: ' || v_emp_ids.COUNT);
        
        -- 初始化新工资数组
        v_new_salaries := salary_array();
        v_new_salaries.EXTEND(v_emp_ids.COUNT);
        
        -- 计算新工资
        FOR i IN 1..v_emp_ids.COUNT LOOP
            IF p_adjustment_type = 'PCT' THEN
                v_new_salaries(i) := ROUND(v_old_salaries(i) * (1 + p_adjustment_value / 100), 2);
            ELSE -- AMT
                v_new_salaries(i) := v_old_salaries(i) + p_adjustment_value;
            END IF;
            
            -- 确保工资不为负数
            IF v_new_salaries(i) < 0 THEN
                v_new_salaries(i) := 0;
            END IF;
            
            v_total_adjustment := v_total_adjustment + (v_new_salaries(i) - v_old_salaries(i));
        END LOOP;
        
        -- 批量更新
        FORALL i IN 1..v_emp_ids.COUNT
            UPDATE employees
            SET salary = v_new_salaries(i),
                last_updated = SYSDATE
            WHERE employee_id = v_emp_ids(i);
        
        -- 记录处理的员工信息
        FOR i IN 1..v_emp_ids.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('  ' || v_emp_names(i) || ': $' || v_old_salaries(i) || 
                               ' -> $' || v_new_salaries(i) || 
                               ' (变化: $' || (v_new_salaries(i) - v_old_salaries(i)) || ')');
        END LOOP;
        
        v_total_processed := v_total_processed + v_emp_ids.COUNT;
        
        -- 提交当前批次
        COMMIT;
        DBMS_OUTPUT.PUT_LINE('第 ' || v_batch_count || ' 批处理完成并提交');
        DBMS_OUTPUT.PUT_LINE('');
        
    END LOOP;
    
    CLOSE emp_cursor;
    
    -- 显示汇总信息
    DBMS_OUTPUT.PUT_LINE('=== 批量调整完成 ===');
    DBMS_OUTPUT.PUT_LINE('总处理员工数: ' || v_total_processed);
    DBMS_OUTPUT.PUT_LINE('总调整金额: $' || ROUND(v_total_adjustment, 2));
    DBMS_OUTPUT.PUT_LINE('处理批次数: ' || v_batch_count);
    
    IF v_total_processed > 0 THEN
        DBMS_OUTPUT.PUT_LINE('平均调整: $' || ROUND(v_total_adjustment / v_total_processed, 2));
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        IF emp_cursor%ISOPEN THEN
            CLOSE emp_cursor;
        END IF;
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('批量处理失败: ' || SQLERRM);
        DBMS_OUTPUT.PUT_LINE('已回滚所有更改');
END bulk_salary_adjustment;
/

-- 测试批量处理
EXEC bulk_salary_adjustment(60, 'PCT', 10, 5); -- IT部门加薪10%,每批5人
EXEC bulk_salary_adjustment(20, 'AMT', 500, 3); -- 市场部每人加薪$500,每批3人

4.2.2 错误处理的批量操作

sql 复制代码
-- 带错误处理的批量数据导入存储过程
CREATE OR REPLACE PROCEDURE bulk_import_employees(
    p_batch_size IN NUMBER DEFAULT 100
)
AS
    -- 集合类型定义
    TYPE emp_record_type IS RECORD (
        employee_id NUMBER,
        first_name VARCHAR2(50),
        last_name VARCHAR2(50),
        email VARCHAR2(100),
        hire_date DATE,
        job_id VARCHAR2(10),
        salary NUMBER,
        department_id NUMBER
    );
    
    TYPE emp_array_type IS TABLE OF emp_record_type;
    v_employees emp_array_type;
    
    -- 错误处理相关
    TYPE error_array_type IS TABLE OF NUMBER;
    v_error_indexes error_array_type;
    v_success_count NUMBER := 0;
    v_error_count NUMBER := 0;
    v_total_processed NUMBER := 0;
    
    -- 游标:从临时导入表读取数据
    CURSOR import_cursor IS
        SELECT employee_id, first_name, last_name, email, hire_date,
               job_id, salary, department_id
        FROM temp_employee_import
        WHERE processed_flag IS NULL
        ORDER BY employee_id;
        
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 开始批量导入员工数据 ===');
    DBMS_OUTPUT.PUT_LINE('批次大小: ' || p_batch_size);
    DBMS_OUTPUT.PUT_LINE('');
    
    -- 创建临时导入表(如果不存在)
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE temp_employee_import AS SELECT * FROM employees WHERE 1=0';
        EXECUTE IMMEDIATE 'ALTER TABLE temp_employee_import ADD processed_flag VARCHAR2(1)';
        EXECUTE IMMEDIATE 'ALTER TABLE temp_employee_import ADD error_message VARCHAR2(500)';
        DBMS_OUTPUT.PUT_LINE('创建临时导入表');
    EXCEPTION
        WHEN OTHERS THEN
            IF SQLCODE != -955 THEN -- 表已存在
                RAISE;
            END IF;
    END;
    
    OPEN import_cursor;
    
    LOOP
        -- 批量获取数据
        FETCH import_cursor BULK COLLECT INTO v_employees LIMIT p_batch_size;
        EXIT WHEN v_employees.COUNT = 0;
        
        DBMS_OUTPUT.PUT_LINE('处理批次,记录数: ' || v_employees.COUNT);
        
        -- 使用FORALL进行批量插入,并收集错误
        BEGIN
            FORALL i IN 1..v_employees.COUNT SAVE EXCEPTIONS
                INSERT INTO employees (
                    employee_id, first_name, last_name, email, hire_date,
                    job_id, salary, department_id, last_updated
                ) VALUES (
                    v_employees(i).employee_id,
                    v_employees(i).first_name,
                    v_employees(i).last_name,
                    v_employees(i).email,
                    v_employees(i).hire_date,
                    v_employees(i).job_id,
                    v_employees(i).salary,
                    v_employees(i).department_id,
                    SYSDATE
                );
            
            -- 所有记录都成功插入
            v_success_count := v_success_count + v_employees.COUNT;
            
            -- 标记为已处理
            FORALL i IN 1..v_employees.COUNT
                UPDATE temp_employee_import
                SET processed_flag = 'S'
                WHERE employee_id = v_employees(i).employee_id;
                
        EXCEPTION
            WHEN OTHERS THEN
                -- 处理批量操作中的错误
                IF SQLCODE = -24381 THEN -- FORALL with SAVE EXCEPTIONS
                    v_error_count := v_error_count + SQL%BULK_EXCEPTIONS.COUNT;
                    v_success_count := v_success_count + (v_employees.COUNT - SQL%BULK_EXCEPTIONS.COUNT);
                    
                    DBMS_OUTPUT.PUT_LINE('批次中有 ' || SQL%BULK_EXCEPTIONS.COUNT || ' 个错误');
                    
                    -- 处理每个错误
                    FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
                        DECLARE
                            v_error_index NUMBER := SQL%BULK_EXCEPTIONS(i).ERROR_INDEX;
                            v_error_code NUMBER := SQL%BULK_EXCEPTIONS(i).ERROR_CODE;
                            v_error_msg VARCHAR2(500) := SQLERRM(-v_error_code);
                        BEGIN
                            DBMS_OUTPUT.PUT_LINE('  错误 ' || i || ': 员工ID ' || 
                                               v_employees(v_error_index).employee_id ||
                                               ' - ' || v_error_msg);
                            
                            -- 更新错误信息
                            UPDATE temp_employee_import
                            SET processed_flag = 'E',
                                error_message = v_error_msg
                            WHERE employee_id = v_employees(v_error_index).employee_id;
                        END;
                    END LOOP;
                    
                    -- 标记成功的记录
                    FOR i IN 1..v_employees.COUNT LOOP
                        DECLARE
                            v_is_error BOOLEAN := FALSE;
                        BEGIN
                            -- 检查是否是错误记录
                            FOR j IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
                                IF SQL%BULK_EXCEPTIONS(j).ERROR_INDEX = i THEN
                                    v_is_error := TRUE;
                                    EXIT;
                                END IF;
                            END LOOP;
                            
                            -- 如果不是错误记录,标记为成功
                            IF NOT v_is_error THEN
                                UPDATE temp_employee_import
                                SET processed_flag = 'S'
                                WHERE employee_id = v_employees(i).employee_id;
                            END IF;
                        END;
                    END LOOP;
                ELSE
                    -- 其他错误
                    RAISE;
                END IF;
        END;
        
        v_total_processed := v_total_processed + v_employees.COUNT;
        COMMIT;
        
        DBMS_OUTPUT.PUT_LINE('批次处理完成');
        DBMS_OUTPUT.PUT_LINE('');
        
    END LOOP;
    
    CLOSE import_cursor;
    
    -- 显示最终统计
    DBMS_OUTPUT.PUT_LINE('=== 导入完成 ===');
    DBMS_OUTPUT.PUT_LINE('总处理记录: ' || v_total_processed);
    DBMS_OUTPUT.PUT_LINE('成功导入: ' || v_success_count);
    DBMS_OUTPUT.PUT_LINE('失败记录: ' || v_error_count);
    DBMS_OUTPUT.PUT_LINE('成功率: ' || ROUND(v_success_count / v_total_processed * 100, 2) || '%');
    
EXCEPTION
    WHEN OTHERS THEN
        IF import_cursor%ISOPEN THEN
            CLOSE import_cursor;
        END IF;
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('导入过程发生严重错误: ' || SQLERRM);
END bulk_import_employees;
/

-- 创建错误报告存储过程
CREATE OR REPLACE PROCEDURE show_import_errors
AS
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 导入错误报告 ===');
    
    FOR rec IN (
        SELECT employee_id, first_name, last_name, error_message
        FROM temp_employee_import
        WHERE processed_flag = 'E'
        ORDER BY employee_id
    ) LOOP
        DBMS_OUTPUT.PUT_LINE('员工ID: ' || rec.employee_id ||
                           ', 姓名: ' || rec.first_name || ' ' || rec.last_name ||
                           ', 错误: ' || rec.error_message);
    END LOOP;
END;
/

5. 存储过程的调试与优化

5.1 调试技术

5.1.1 使用DBMS_OUTPUT进行调试

sql 复制代码
-- 带调试信息的存储过程
CREATE OR REPLACE PROCEDURE debug_salary_calculation(
    p_employee_id IN NUMBER,
    p_debug_mode IN BOOLEAN DEFAULT FALSE
)
AS
    v_base_salary NUMBER;
    v_bonus_pct NUMBER;
    v_commission_pct NUMBER;
    v_total_compensation NUMBER;
    v_tax_rate NUMBER := 0.25; -- 25%税率
    v_net_salary NUMBER;
    
    PROCEDURE debug_print(p_message VARCHAR2) IS
    BEGIN
        IF p_debug_mode THEN
            DBMS_OUTPUT.PUT_LINE('[DEBUG] ' || TO_CHAR(SYSDATE, 'HH24:MI:SS') || 
                               ' - ' || p_message);
        END IF;
    END debug_print;
    
BEGIN
    debug_print('开始计算员工 ' || p_employee_id || ' 的薪资');
    
    -- 获取基本工资
    SELECT salary, NVL(commission_pct, 0)
    INTO v_base_salary, v_commission_pct
    FROM employees
    WHERE employee_id = p_employee_id;
    
    debug_print('基本工资: $' || v_base_salary);
    debug_print('佣金比例: ' || (v_commission_pct * 100) || '%');
    
    -- 计算奖金比例(基于工作年限)
    SELECT CASE 
             WHEN MONTHS_BETWEEN(SYSDATE, hire_date) >= 60 THEN 0.15  -- 5年以上15%
             WHEN MONTHS_BETWEEN(SYSDATE, hire_date) >= 36 THEN 0.10  -- 3年以上10%
             WHEN MONTHS_BETWEEN(SYSDATE, hire_date) >= 12 THEN 0.05  -- 1年以上5%
             ELSE 0
           END
    INTO v_bonus_pct
    FROM employees
    WHERE employee_id = p_employee_id;
    
    debug_print('奖金比例: ' || (v_bonus_pct * 100) || '%');
    
    -- 计算总薪酬
    v_total_compensation := v_base_salary * (1 + v_bonus_pct + v_commission_pct);
    debug_print('税前总薪酬: $' || ROUND(v_total_compensation, 2));
    
    -- 计算税后薪资
    v_net_salary := v_total_compensation * (1 - v_tax_rate);
    debug_print('税后薪资: $' || ROUND(v_net_salary, 2));
    debug_print('税额: $' || ROUND(v_total_compensation - v_net_salary, 2));
    
    -- 更新员工薪资信息(假设有扩展表)
    BEGIN
        UPDATE employee_compensation
        SET base_salary = v_base_salary,
            bonus_amount = v_base_salary * v_bonus_pct,
            commission_amount = v_base_salary * v_commission_pct,
            gross_salary = v_total_compensation,
            tax_amount = v_total_compensation - v_net_salary,
            net_salary = v_net_salary,
            calculation_date = SYSDATE
        WHERE employee_id = p_employee_id;
        
        IF SQL%ROWCOUNT = 0 THEN
            debug_print('员工薪资记录不存在,插入新记录');
            INSERT INTO employee_compensation (
                employee_id, base_salary, bonus_amount, commission_amount,
                gross_salary, tax_amount, net_salary, calculation_date
            ) VALUES (
                p_employee_id, v_base_salary, v_base_salary * v_bonus_pct,
                v_base_salary * v_commission_pct, v_total_compensation,
                v_total_compensation - v_net_salary, v_net_salary, SYSDATE
            );
        END IF;
        
        debug_print('薪资记录更新成功');
        
    EXCEPTION
        WHEN OTHERS THEN
            debug_print('更新薪资记录时出错: ' || SQLERRM);
            -- 创建表(如果不存在)
            EXECUTE IMMEDIATE '
                CREATE TABLE employee_compensation (
                    employee_id NUMBER PRIMARY KEY,
                    base_salary NUMBER,
                    bonus_amount NUMBER,
                    commission_amount NUMBER,
                    gross_salary NUMBER,
                    tax_amount NUMBER,
                    net_salary NUMBER,
                    calculation_date DATE
                )';
            debug_print('创建薪资表并重试');
            
            INSERT INTO employee_compensation (
                employee_id, base_salary, bonus_amount, commission_amount,
                gross_salary, tax_amount, net_salary, calculation_date
            ) VALUES (
                p_employee_id, v_base_salary, v_base_salary * v_bonus_pct,
                v_base_salary * v_commission_pct, v_total_compensation,
                v_total_compensation - v_net_salary, v_net_salary, SYSDATE
            );
    END;
    
    COMMIT;
    debug_print('薪资计算完成');
    
    -- 输出结果
    DBMS_OUTPUT.PUT_LINE('=== 员工 ' || p_employee_id || ' 薪资计算结果 ===');
    DBMS_OUTPUT.PUT_LINE('基本工资: $' || v_base_salary);
    DBMS_OUTPUT.PUT_LINE('奖金: $' || ROUND(v_base_salary * v_bonus_pct, 2));
    DBMS_OUTPUT.PUT_LINE('佣金: $' || ROUND(v_base_salary * v_commission_pct, 2));
    DBMS_OUTPUT.PUT_LINE('税前总额: $' || ROUND(v_total_compensation, 2));
    DBMS_OUTPUT.PUT_LINE('税后薪资: $' || ROUND(v_net_salary, 2));
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        debug_print('员工不存在: ' || p_employee_id);
        DBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');
    WHEN OTHERS THEN
        debug_print('发生错误: ' || SQLERRM);
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('计算失败: ' || SQLERRM);
END debug_salary_calculation;
/

-- 测试调试功能
EXEC debug_salary_calculation(100, TRUE);  -- 开启调试模式
EXEC debug_salary_calculation(101, FALSE); -- 关闭调试模式

5.1.2 性能监控和分析

sql 复制代码
-- 性能监控存储过程
CREATE OR REPLACE PROCEDURE performance_test_procedure
AS
    v_start_time TIMESTAMP;
    v_end_time TIMESTAMP;
    v_elapsed_time INTERVAL DAY TO SECOND;
    v_cpu_time NUMBER;
    v_logical_reads NUMBER;
    v_physical_reads NUMBER;
    
    -- 获取会话统计信息
    FUNCTION get_session_stat(p_stat_name VARCHAR2) RETURN NUMBER IS
        v_value NUMBER;
    BEGIN
        SELECT value INTO v_value
        FROM v$mystat m, v$statname n
        WHERE m.statistic# = n.statistic#
          AND n.name = p_stat_name;
        RETURN v_value;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RETURN 0;
    END;
    
    v_start_cpu NUMBER;
    v_start_logical_reads NUMBER;
    v_start_physical_reads NUMBER;
    
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 性能测试开始 ===');
    
    -- 记录开始时间和统计信息
    v_start_time := SYSTIMESTAMP;
    v_start_cpu := get_session_stat('CPU used by this session');
    v_start_logical_reads := get_session_stat('session logical reads');
    v_start_physical_reads := get_session_stat('physical reads');
    
    -- 执行测试操作
    DBMS_OUTPUT.PUT_LINE('执行测试操作...');
    
    -- 模拟复杂查询
    FOR i IN 1..1000 LOOP
        DECLARE
            v_count NUMBER;
            v_avg_salary NUMBER;
        BEGIN
            SELECT COUNT(*), AVG(salary)
            INTO v_count, v_avg_salary
            FROM employees e
            JOIN departments d ON e.department_id = d.department_id
            WHERE e.salary > 5000;
        END;
    END LOOP;
    
    -- 记录结束时间和统计信息
    v_end_time := SYSTIMESTAMP;
    v_cpu_time := get_session_stat('CPU used by this session') - v_start_cpu;
    v_logical_reads := get_session_stat('session logical reads') - v_start_logical_reads;
    v_physical_reads := get_session_stat('physical reads') - v_start_physical_reads;
    
    -- 计算执行时间
    v_elapsed_time := v_end_time - v_start_time;
    
    -- 输出性能统计
    DBMS_OUTPUT.PUT_LINE('=== 性能统计结果 ===');
    DBMS_OUTPUT.PUT_LINE('执行时间: ' || 
        EXTRACT(SECOND FROM v_elapsed_time) + 
        EXTRACT(MINUTE FROM v_elapsed_time) * 60 + 
        EXTRACT(HOUR FROM v_elapsed_time) * 3600 || ' 秒');
    DBMS_OUTPUT.PUT_LINE('CPU时间: ' || v_cpu_time || ' 厘秒');
    DBMS_OUTPUT.PUT_LINE('逻辑读: ' || v_logical_reads || ' 块');
    DBMS_OUTPUT.PUT_LINE('物理读: ' || v_physical_reads || ' 块');
    DBMS_OUTPUT.PUT_LINE('缓存命中率: ' || 
        ROUND((v_logical_reads - v_physical_reads) / v_logical_reads * 100, 2) || '%');
    
END performance_test_procedure;
/

-- 执行性能测试
EXEC performance_test_procedure;

5.2 存储过程优化

5.2.1 SQL优化技巧

sql 复制代码
-- 优化前后对比的存储过程
CREATE OR REPLACE PROCEDURE optimized_employee_analysis(
    p_use_optimized IN BOOLEAN DEFAULT TRUE
)
AS
    v_start_time TIMESTAMP;
    v_end_time TIMESTAMP;
    v_count NUMBER;
    
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 员工分析性能对比 ===');
    
    IF p_use_optimized THEN
        DBMS_OUTPUT.PUT_LINE('使用优化版本...');
        v_start_time := SYSTIMESTAMP;
        
        -- 优化版本:使用分析函数和单次查询
        FOR rec IN (
            SELECT department_name,
                   employee_count,
                   avg_salary,
                   max_salary,
                   min_salary,
                   salary_rank
            FROM (
                SELECT d.department_name,
                       COUNT(e.employee_id) OVER (PARTITION BY d.department_id) AS employee_count,
                       ROUND(AVG(e.salary) OVER (PARTITION BY d.department_id), 2) AS avg_salary,
                       MAX(e.salary) OVER (PARTITION BY d.department_id) AS max_salary,
                       MIN(e.salary) OVER (PARTITION BY d.department_id) AS min_salary,
                       RANK() OVER (ORDER BY AVG(e.salary) OVER (PARTITION BY d.department_id) DESC) AS salary_rank,
                       ROW_NUMBER() OVER (PARTITION BY d.department_id ORDER BY e.employee_id) AS rn
                FROM departments d
                LEFT JOIN employees e ON d.department_id = e.department_id
            )
            WHERE rn = 1 AND employee_count > 0
            ORDER BY salary_rank
        ) LOOP
            v_count := v_count + 1;
        END LOOP;
        
    ELSE
        DBMS_OUTPUT.PUT_LINE('使用未优化版本...');
        v_start_time := SYSTIMESTAMP;
        
        -- 未优化版本:多次查询
        FOR dept_rec IN (SELECT department_id, department_name FROM departments) LOOP
            DECLARE
                v_emp_count NUMBER;
                v_avg_salary NUMBER;
                v_max_salary NUMBER;
                v_min_salary NUMBER;
            BEGIN
                -- 每个部门都要执行一次查询
                SELECT COUNT(*), 
                       NVL(ROUND(AVG(salary), 2), 0),
                       NVL(MAX(salary), 0),
                       NVL(MIN(salary), 0)
                INTO v_emp_count, v_avg_salary, v_max_salary, v_min_salary
                FROM employees
                WHERE department_id = dept_rec.department_id;
                
                IF v_emp_count > 0 THEN
                    v_count := v_count + 1;
                END IF;
            END;
        END LOOP;
    END IF;
    
    v_end_time := SYSTIMESTAMP;
    
    DBMS_OUTPUT.PUT_LINE('处理部门数: ' || v_count);
    DBMS_OUTPUT.PUT_LINE('执行时间: ' || 
        ROUND(EXTRACT(SECOND FROM (v_end_time - v_start_time)) * 1000, 2) || ' 毫秒');
    
END optimized_employee_analysis;
/

-- 性能对比测试
EXEC optimized_employee_analysis(FALSE); -- 未优化版本
EXEC optimized_employee_analysis(TRUE);  -- 优化版本

5.2.2 内存和资源优化

sql 复制代码
-- 资源优化的存储过程
CREATE OR REPLACE PROCEDURE resource_optimized_batch_process(
    p_table_name IN VARCHAR2,
    p_batch_size IN NUMBER DEFAULT 1000
)
AS
    TYPE id_array_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    v_ids id_array_type;
    v_sql VARCHAR2(4000);
    v_cursor_id INTEGER;
    v_rows_processed NUMBER;
    v_total_processed NUMBER := 0;
    v_batch_count NUMBER := 0;
    
    -- 使用REF CURSOR减少内存占用
    TYPE ref_cursor_type IS REF CURSOR;
    v_cursor ref_cursor_type;
    v_id NUMBER;
    
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 资源优化批处理 ===');
    DBMS_OUTPUT.PUT_LINE('表名: ' || p_table_name);
    DBMS_OUTPUT.PUT_LINE('批次大小: ' || p_batch_size);
    
    -- 构建查询SQL
    v_sql := 'SELECT id FROM ' || p_table_name || ' WHERE processed_flag IS NULL ORDER BY id';
    
    OPEN v_cursor FOR v_sql;
    
    LOOP
        -- 清空数组
        v_ids.DELETE;
        v_rows_processed := 0;
        
        -- 批量获取ID
        FOR i IN 1..p_batch_size LOOP
            FETCH v_cursor INTO v_id;
            EXIT WHEN v_cursor%NOTFOUND;
            
            v_ids(i) := v_id;
            v_rows_processed := v_rows_processed + 1;
        END LOOP;
        
        EXIT WHEN v_rows_processed = 0;
        
        v_batch_count := v_batch_count + 1;
        DBMS_OUTPUT.PUT_LINE('处理第 ' || v_batch_count || ' 批,记录数: ' || v_rows_processed);
        
        -- 批量处理
        BEGIN
            FORALL i IN 1..v_rows_processed
                EXECUTE IMMEDIATE 
                    'UPDATE ' || p_table_name || 
                    ' SET processed_flag = ''Y'', processed_date = SYSDATE WHERE id = :1'
                USING v_ids(i);
            
            v_total_processed := v_total_processed + v_rows_processed;
            
            -- 定期提交以释放锁和日志空间
            COMMIT;
            
            -- 可选:在批次间暂停以减少系统负载
            IF MOD(v_batch_count, 10) = 0 THEN
                DBMS_OUTPUT.PUT_LINE('已处理 ' || v_batch_count || ' 批,暂停1秒...');
                DBMS_LOCK.SLEEP(1);
            END IF;
            
        EXCEPTION
            WHEN OTHERS THEN
                ROLLBACK;
                DBMS_OUTPUT.PUT_LINE('批次 ' || v_batch_count || ' 处理失败: ' || SQLERRM);
                -- 继续处理下一批
        END;
        
    END LOOP;
    
    CLOSE v_cursor;
    
    DBMS_OUTPUT.PUT_LINE('=== 处理完成 ===');
    DBMS_OUTPUT.PUT_LINE('总处理记录: ' || v_total_processed);
    DBMS_OUTPUT.PUT_LINE('处理批次: ' || v_batch_count);
    
EXCEPTION
    WHEN OTHERS THEN
        IF v_cursor%ISOPEN THEN
            CLOSE v_cursor;
        END IF;
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('处理过程发生错误: ' || SQLERRM);
END resource_optimized_batch_process;
/

6. 存储过程的安全性

6.1 权限管理

6.1.1 存储过程权限控制

sql 复制代码
-- 创建带权限检查的存储过程
CREATE OR REPLACE PROCEDURE secure_salary_update(
    p_employee_id IN NUMBER,
    p_new_salary IN NUMBER,
    p_reason IN VARCHAR2
)
AS
    v_current_user VARCHAR2(30);
    v_user_role VARCHAR2(30);
    v_current_salary NUMBER;
    v_max_allowed_salary NUMBER;
    v_is_authorized BOOLEAN := FALSE;
    
    -- 权限检查函数
    FUNCTION check_user_permission(p_operation VARCHAR2) RETURN BOOLEAN IS
        v_count NUMBER;
    BEGIN
        -- 检查用户是否有特定权限
        SELECT COUNT(*)
        INTO v_count
        FROM user_role_privs
        WHERE granted_role IN ('HR_MANAGER', 'SALARY_ADMIN')
           OR granted_role = 'DBA';
        
        RETURN v_count > 0;
    END;
    
    -- 审计日志记录
    PROCEDURE log_salary_change(
        p_emp_id NUMBER,
        p_old_salary NUMBER,
        p_new_salary NUMBER,
        p_changed_by VARCHAR2,
        p_reason VARCHAR2,
        p_status VARCHAR2
    ) IS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        INSERT INTO salary_audit_log (
            log_id, employee_id, old_salary, new_salary,
            changed_by, change_reason, change_status,
            change_date
        ) VALUES (
            salary_audit_seq.NEXTVAL, p_emp_id, p_old_salary, p_new_salary,
            p_changed_by, p_reason, p_status, SYSDATE
        );
        COMMIT;
    END;
    
BEGIN
    -- 获取当前用户信息
    v_current_user := USER;
    
    DBMS_OUTPUT.PUT_LINE('=== 安全工资更新 ===');
    DBMS_OUTPUT.PUT_LINE('操作用户: ' || v_current_user);
    DBMS_OUTPUT.PUT_LINE('员工ID: ' || p_employee_id);
    DBMS_OUTPUT.PUT_LINE('新工资: $' || p_new_salary);
    
    -- 权限检查
    IF NOT check_user_permission('SALARY_UPDATE') THEN
        log_salary_change(p_employee_id, NULL, p_new_salary, v_current_user, 
                         p_reason, 'PERMISSION_DENIED');
        RAISE_APPLICATION_ERROR(-20001, '权限不足:用户无工资更新权限');
    END IF;
    
    -- 获取当前工资
    SELECT salary INTO v_current_salary
    FROM employees
    WHERE employee_id = p_employee_id;
    
    -- 业务规则检查
    IF p_new_salary <= 0 THEN
        log_salary_change(p_employee_id, v_current_salary, p_new_salary, 
                         v_current_user, p_reason, 'INVALID_AMOUNT');
        RAISE_APPLICATION_ERROR(-20002, '工资金额必须大于0');
    END IF;
    
    -- 检查工资增长限制(不能超过50%)
    IF p_new_salary > v_current_salary * 1.5 THEN
        log_salary_change(p_employee_id, v_current_salary, p_new_salary, 
                         v_current_user, p_reason, 'EXCESSIVE_INCREASE');
        RAISE_APPLICATION_ERROR(-20003, '工资增长不能超过50%');
    END IF;
    
    -- 检查职位工资上限
    SELECT j.max_salary INTO v_max_allowed_salary
    FROM employees e
    JOIN jobs j ON e.job_id = j.job_id
    WHERE e.employee_id = p_employee_id;
    
    IF p_new_salary > v_max_allowed_salary THEN
        log_salary_change(p_employee_id, v_current_salary, p_new_salary, 
                         v_current_user, p_reason, 'EXCEEDS_JOB_LIMIT');
        RAISE_APPLICATION_ERROR(-20004, '工资超过职位上限: $' || v_max_allowed_salary);
    END IF;
    
    -- 执行更新
    UPDATE employees
    SET salary = p_new_salary,
        last_updated = SYSDATE
    WHERE employee_id = p_employee_id;
    
    -- 记录成功的更改
    log_salary_change(p_employee_id, v_current_salary, p_new_salary, 
                     v_current_user, p_reason, 'SUCCESS');
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('工资更新成功');
    DBMS_OUTPUT.PUT_LINE('原工资: $' || v_current_salary);
    DBMS_OUTPUT.PUT_LINE('新工资: $' || p_new_salary);
    DBMS_OUTPUT.PUT_LINE('变化: $' || (p_new_salary - v_current_salary));
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        log_salary_change(p_employee_id, NULL, p_new_salary, v_current_user, 
                         p_reason, 'EMPLOYEE_NOT_FOUND');
        DBMS_OUTPUT.PUT_LINE('错误: 员工不存在');
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('更新失败: ' || SQLERRM);
END secure_salary_update;
/

-- 创建审计日志表
CREATE TABLE salary_audit_log (
    log_id NUMBER PRIMARY KEY,
    employee_id NUMBER,
    old_salary NUMBER,
    new_salary NUMBER,
    changed_by VARCHAR2(30),
    change_reason VARCHAR2(500),
    change_status VARCHAR2(50),
    change_date DATE
);

CREATE SEQUENCE salary_audit_seq START WITH 1 INCREMENT BY 1;

-- 创建审计查询存储过程
CREATE OR REPLACE PROCEDURE show_salary_audit(
    p_employee_id IN NUMBER DEFAULT NULL,
    p_days_back IN NUMBER DEFAULT 30
)
AS
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 工资变更审计报告 ===');
    DBMS_OUTPUT.PUT_LINE('查询范围: 最近 ' || p_days_back || ' 天');
    IF p_employee_id IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE('员工ID: ' || p_employee_id);
    END IF;
    DBMS_OUTPUT.PUT_LINE('');
    
    FOR rec IN (
        SELECT l.log_id, l.employee_id, 
               e.first_name || ' ' || e.last_name AS employee_name,
               l.old_salary, l.new_salary, l.changed_by, l.change_reason,
               l.change_status, l.change_date
        FROM salary_audit_log l
        LEFT JOIN employees e ON l.employee_id = e.employee_id
        WHERE (p_employee_id IS NULL OR l.employee_id = p_employee_id)
          AND l.change_date >= SYSDATE - p_days_back
        ORDER BY l.change_date DESC
    ) LOOP
        DBMS_OUTPUT.PUT_LINE('记录ID: ' || rec.log_id);
        DBMS_OUTPUT.PUT_LINE('员工: ' || NVL(rec.employee_name, '未知') || 
                           ' (ID: ' || rec.employee_id || ')');
        DBMS_OUTPUT.PUT_LINE('工资变化: $' || NVL(rec.old_salary, 0) || 
                           ' -> $' || rec.new_salary);
        DBMS_OUTPUT.PUT_LINE('操作人: ' || rec.changed_by);
        DBMS_OUTPUT.PUT_LINE('状态: ' || rec.change_status);
        DBMS_OUTPUT.PUT_LINE('时间: ' || TO_CHAR(rec.change_date, 'YYYY-MM-DD HH24:MI:SS'));
        DBMS_OUTPUT.PUT_LINE('原因: ' || NVL(rec.change_reason, '未提供'));
        DBMS_OUTPUT.PUT_LINE('---');
    END LOOP;
END show_salary_audit;
/

-- 测试安全存储过程
EXEC secure_salary_update(100, 25000, '年度调薪');
EXEC show_salary_audit(100, 7); -- 查看员工100最近7天的工资变更

结语

感谢您的阅读!期待您的一键三连!欢迎指正!

相关推荐
潘yi.2 小时前
NoSQL之Redis配置与优化
数据库·redis·nosql
zdkdchao2 小时前
hbase资源和数据权限控制
大数据·数据库·hbase
伤不起bb2 小时前
NoSQL 之 Redis 配置与优化
linux·运维·数据库·redis·nosql
leo__5202 小时前
PostgreSQL配置文件修改及启用方法
数据库·postgresql
南風_入弦3 小时前
优化09-表连接
数据库·oracle
Snk0xHeart4 小时前
极客大挑战 2019 EasySQL 1(万能账号密码,SQL注入,HackBar)
数据库·sql·网络安全
····懂···5 小时前
数据库OCP专业认证培训
数据库·oracle·ocp
学习中的码虫5 小时前
数据库-MySQL
数据库
Karry的巡洋舰6 小时前
【数据库】安全性
数据库·oracle
软件测试小仙女6 小时前
鸿蒙APP测试实战:从HDC命令到专项测试
大数据·软件测试·数据库·人工智能·测试工具·华为·harmonyos