【Oracle】存储过程

news2025/6/6 22:00:40

在这里插入图片描述

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

在这里插入图片描述

文章目录

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

-- 创建简单的存储过程
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 带变量的存储过程

-- 创建包含变量的存储过程
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)

-- 创建带输入参数的存储过程
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)

-- 创建带输出参数的存储过程
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)

-- 创建带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 显式游标

-- 使用显式游标的存储过程
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循环

-- 使用游标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 预定义异常处理

-- 完善的异常处理存储过程
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 用户定义异常

-- 创建带用户定义异常的存储过程
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存储过程
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 动态查询构建器

-- 通用查询构建器存储过程
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

-- 高性能批量处理存储过程
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 错误处理的批量操作

-- 带错误处理的批量数据导入存储过程
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进行调试

-- 带调试信息的存储过程
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 性能监控和分析

-- 性能监控存储过程
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优化技巧

-- 优化前后对比的存储过程
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 内存和资源优化

-- 资源优化的存储过程
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 存储过程权限控制

-- 创建带权限检查的存储过程
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天的工资变更

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2400801.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单元测试-断言常见注解

目录 1.断言 2.常见注解 3.依赖范围 1.断言 断言练习 package com.gdcp;import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;//测试类 public class UserServiceTest {Testpublic void testGetGender(){UserService userService new UserService…

TDengine 在电力行业如何使用 AI ?

在你面前摆着一堆机器运行日志、传感器读数、电表数据&#xff0c;几十万、几百万条每秒增长的数据流&#xff0c;你会怎么处理&#xff1f;是加人、加脚本&#xff0c;还是干脆放弃实时分析&#xff1f; 过去&#xff0c;时序数据是工业的“副产品”&#xff1a;只是存着、查…

OpenCV C/C++ 视频播放器 (支持调速和进度控制)

OpenCV C/C 视频播放器 (支持调速和进度控制) 本文将引导你使用 C 和 OpenCV 库创建一个功能稍复杂的视频播放器。该播放器不仅能播放视频&#xff0c;还允许用户通过滑动条来调整播放速度&#xff08;加速/减速&#xff09;以及控制视频的播放进度。 使用opencv打开不会压缩画…

【Linux庖丁解牛】—自定义shell的编写!

1. 打印命令行提示符 在我们使用系统提供的shell时&#xff0c;每次都会打印出一行字符串&#xff0c;这其实就是命令行提示符&#xff0c;那我们自定义的shell当然也需要这一行字符串。 这一行字符串包含用户名&#xff0c;主机名&#xff0c;当前工作路径&#xff0c;所以&a…

Linux运维笔记:1010实验室电脑资源规范使用指南

文章目录 一. 检查资源使用情况&#xff0c;避免冲突1. 检查在线用户2. 检查 CPU 使用情况3. 检查 GPU 使用情况4. 协作建议 二. 备份重要文件和数据三. 定期清理硬盘空间四. 退出 ThinLinc 时注销&#xff0c;释放内存五. 校外使用时配置 VPN注意事项 总结 实验室的电脑配备了…

【Docker 从入门到实战全攻略(二):核心概念 + 命令详解 + 部署案例】

5. Docker Compose Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。通过一个 YAML 文件来配置应用服务&#xff0c;然后使用一个命令即可创建并启动所有服务。 基本命令 docker-compose up # 创建并启动所有服务 docker-compose down # 停止并移除容器、网络等…

【conda配置深度学习环境】

好的&#xff01;我们从头开始配置一个基于Conda的虚拟环境&#xff0c;覆盖深度学习&#xff08;如PyTorch&#xff09;和传统机器学习&#xff08;如XGBoost&#xff09;&#xff0c;并适配你的显卡&#xff08;假设为NVIDIA&#xff0c;若为AMD请告知&#xff09;。以下是完…

力扣4.寻找两个正序数组的中位数

文章目录 题目介绍题解 题目介绍 题解 题解链接&#xff1a;题解 核心思路&#xff1a;通过二分查找的确定分割点使左右两部分元素数量相等。 class Solution {public double findMedianSortedArrays(int[] nums1, int[] nums2) {int n1 nums1.length;int n2 nums2.length…

【相机基础知识与物体检测】更新中

参考&#xff1a; 黑马机器人 | 相机标定&物体检测https://robot.czxy.com/docs/camera/ 01-相机基础 相机基础概述 相机是机器视觉的基础&#xff0c;相机直接产生了相机数据。所有视觉算法都是作用在相机数据上的。相机数据的好坏&#xff0c;或者对相机数据的理解方式…

【前端】性能优化和分类

本页知识点参考&#xff1a;https://zhuanlan.zhihu.com/p/514222781 1. 加载性能优化 1.1 网站性能优化 content方法&#xff1a; 1&#xff09;减少HTTP请求&#xff1a;合并文件&#xff0c;CSS精灵&#xff0c;inline Image 2&#xff09;减少DNS查询&#xff1a;DNS缓存&…

PPO和GRPO算法

verl 是现在非常火的 rl 框架&#xff0c;而且已经支持了多个 rl 算法&#xff08;ppo、grpo 等等&#xff09;。 过去对 rl 的理解很粗浅&#xff08;只知道有好多个角色&#xff0c;有的更新权重&#xff0c;有的不更新&#xff09;&#xff0c;也曾硬着头皮看了一些论文和知…

rk3588 上运行smolvlm-realtime-webcam,将视频转为文字描述

smolvlm-realtime-webcam 是一个开源项目&#xff0c;结合了轻量级多模态模型 SmolVLM 和本地推理引擎 llama.cpp&#xff0c;能够在本地实时处理摄像头视频流&#xff0c;生成自然语言描述&#xff0c; 开源项目地址 https://github.com/ngxson/smolvlm-realtime-webcamhttps…

Rust 学习笔记:Box<T>

Rust 学习笔记&#xff1a;Box Rust 学习笔记&#xff1a;Box<T\>Box\<T> 简介使用 Box\<T\> 在堆上存储数据启用带有 box 的递归类型关于 cons 列表的介绍计算非递归类型的大小使用 Box\<T\> 获取大小已知的递归类型 Rust 学习笔记&#xff1a;Box<…

操作系统学习(十三)——Linux

一、Linux Linux 是一种类 Unix 的自由开源操作系统内核&#xff0c;由芬兰人 Linus Torvalds 于 1991 年首次发布。如今它广泛应用于服务器、桌面、嵌入式设备、移动设备&#xff08;如 Android&#xff09;等领域。 设计思想&#xff1a; 原则描述模块化与可移植性Linux 内…

NLP学习路线图(二十二): 循环神经网络(RNN)

在自然语言处理&#xff08;NLP&#xff09;的广阔天地中&#xff0c;序列数据是绝对的核心——无论是流淌的文本、连续的语音还是跳跃的时间序列&#xff0c;都蕴含着前后紧密关联的信息。传统神经网络如同面对一幅打散的拼图&#xff0c;无法理解词语间的顺序关系&#xff0c…

每日一C(1)C语言的内存分布

目录 代码区 常量区 全局/静态区 初始化数据段&#xff08;.data&#xff09; 未初始化数据段&#xff08;.bss&#xff09; 堆区 栈区 总结 今天我们学习的是C语言的内存分布&#xff0c;以及这些分区所存储的内容和其特点。今天的思维导图如下。 C语言作为一款直接处…

Photoshop使用钢笔绘制图形

1、绘制脸部路径 选择钢笔工具&#xff0c;再选择“路径”。 基于两个点绘制一个弯曲的曲线 使用Alt键移动单个点&#xff0c;该点决定了后续的曲线方向 继续绘制第3个点 最后一个点首尾是同一个点&#xff0c;使用钢笔保证是闭合回路。 以同样的方式绘制2个眼睛外框。 使用椭…

应用层协议:HTTP

目录 HTTP&#xff1a;超文本传输协议 1.1 HTTP报文 1.1.1 请求报文 1.1.2 响应报文 1.2 HTTP请求过程和原理 1.2.1 请求过程 1、域名&#xff08;DNS&#xff09;解析 2、建立TCP连接&#xff08;三次握手&#xff09; 3、发送HTTP请求 4、服务器处理请求 5、返回H…

复习——C++

1、scanf和scanf_s区别 2、取地址&#xff0c;输出 char ba; char* p&b; cout<<*p; cout<<p; p(char*)"abc"; cout<<*p; cout<<p; cout<<(void*)p; 取地址&#xff0c;把b的地址给p 输出*p&#xff0c;是输出p的空间内的值…