当前位置: 首页 > article >正文

【Oracle】存储过程

在这里插入图片描述

个人主页: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
BEGINDBMS_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块中调用
BEGINhello_world;
END;
/

2.1.2 带变量的存储过程

-- 创建包含变量的存储过程
CREATE OR REPLACE PROCEDURE employee_count_info
ASv_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_salaryFROM 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 THENDBMS_OUTPUT.PUT_LINE('工资水平: 较高');ELSIF v_avg_salary > 5000 THENDBMS_OUTPUT.PUT_LINE('工资水平: 中等');ELSEDBMS_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
)
ASv_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_nameINTO v_first_name, v_last_name, v_email, v_salary, v_hire_date,v_department_name, v_job_title, v_manager_nameFROM employees eLEFT JOIN departments d ON e.department_id = d.department_idLEFT JOIN jobs j ON e.job_id = j.job_idLEFT JOIN employees m ON e.manager_id = m.employee_idWHERE 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) || ' 年');EXCEPTIONWHEN NO_DATA_FOUND THENDBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');WHEN OTHERS THENDBMS_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
BEGINSELECT 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_salaryFROM employeesWHERE department_id = p_department_id;-- 如果没有找到员工,设置默认值IF p_emp_count = 0 THENp_avg_salary := 0;p_total_salary := 0;p_max_salary := 0;p_min_salary := 0;END IF;EXCEPTIONWHEN OTHERS THENp_emp_count := -1;p_avg_salary := 0;p_total_salary := 0;p_max_salary := 0;p_min_salary := 0;
END calculate_employee_stats;
/-- 调用带输出参数的存储过程
DECLAREv_count NUMBER;v_avg_salary NUMBER;v_total_salary NUMBER;v_max_salary NUMBER;v_min_salary NUMBER;v_dept_id NUMBER := 60; -- IT部门
BEGINcalculate_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
)
ASv_current_salary NUMBER;v_new_salary NUMBER;v_min_salary NUMBER;v_max_salary NUMBER;v_job_id VARCHAR2(10);
BEGIN-- 获取员工当前信息SELECT salary, job_idINTO v_current_salary, v_job_idFROM employeesWHERE employee_id = p_employee_id;-- 获取职位工资范围SELECT min_salary, max_salaryINTO v_min_salary, v_max_salaryFROM jobsWHERE job_id = v_job_id;-- 如果输入的是百分比(小于1),转换为实际金额IF p_salary_change < 1 AND p_salary_change > -1 THENp_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 THENv_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 THENv_new_salary := v_max_salary;p_salary_change := v_new_salary - v_current_salary;p_result_message := '工资已调整到职位最高标准: $' || v_max_salary;ELSEp_result_message := '工资调整成功: $' || v_current_salary || ' -> $' || v_new_salary;END IF;-- 更新员工工资UPDATE employeesSET salary = v_new_salary,last_updated = SYSDATEWHERE employee_id = p_employee_id;-- 返回实际调整金额p_salary_change := v_new_salary - v_current_salary;EXCEPTIONWHEN NO_DATA_FOUND THENp_result_message := '错误: 员工ID ' || p_employee_id || ' 不存在';p_salary_change := 0;WHEN OTHERS THENp_result_message := '错误: ' || SQLERRM;p_salary_change := 0;ROLLBACK;
END adjust_salary;
/-- 测试IN OUT参数
DECLAREv_salary_change NUMBER := 0.1; -- 10%增长v_message VARCHAR2(200);v_emp_id NUMBER := 107;
BEGINDBMS_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 ISSELECT employee_id, first_name, last_name, salaryFROM employeesWHERE department_id = p_department_idORDER BY salary;-- 游标记录类型emp_rec emp_cursor%ROWTYPE;-- 其他变量v_counter NUMBER := 0;v_total_increase NUMBER := 0;v_old_salary NUMBER;v_new_salary NUMBER;BEGINDBMS_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 employeesSET salary = v_new_salary,last_updated = SYSDATEWHERE 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 THENDBMS_OUTPUT.PUT_LINE('平均增长: $' || ROUND(v_total_increase / v_counter, 2));COMMIT;DBMS_OUTPUT.PUT_LINE('工资调整已提交');ELSEDBMS_OUTPUT.PUT_LINE('未找到符合条件的员工');END IF;EXCEPTIONWHEN OTHERS THENIF emp_cursor%ISOPEN THENCLOSE 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
)
ASv_total_employees NUMBER := 0;v_total_salary NUMBER := 0;v_dept_name VARCHAR2(50);
BEGIN-- 获取部门名称IF p_department_id IS NOT NULL THENSELECT department_name INTO v_dept_nameFROM departments WHERE department_id = p_department_id;DBMS_OUTPUT.PUT_LINE('=== ' || v_dept_name || ' 部门员工报告 ===');ELSEDBMS_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_serviceFROM employees eLEFT JOIN jobs j ON e.job_id = j.job_idWHERE (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 THENDBMS_OUTPUT.PUT_LINE('平均工资: $' || ROUND(v_total_salary / v_total_employees, 2));END IF;EXCEPTIONWHEN NO_DATA_FOUND THENDBMS_OUTPUT.PUT_LINE('未找到指定部门或部门无员工');WHEN OTHERS THENDBMS_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
)
ASv_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_emailFROM employees WHERE employee_id = p_employee_id;-- 构建动态更新语句IF p_first_name IS NOT NULL THENv_sql_stmt := v_sql_stmt || ', first_name = :first_name';END IF;IF p_last_name IS NOT NULL THENv_sql_stmt := v_sql_stmt || ', last_name = :last_name';END IF;IF p_email IS NOT NULL THEN-- 检查邮箱唯一性SELECT COUNT(*) INTO v_update_countFROM employees WHERE email = p_email AND employee_id != p_employee_id;IF v_update_count > 0 THENRAISE_APPLICATION_ERROR(-20001, '邮箱地址已被其他员工使用');END IF;v_sql_stmt := v_sql_stmt || ', email = :email';END IF;IF p_salary IS NOT NULL THENIF p_salary <= 0 THENRAISE_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_countFROM departments WHERE department_id = p_department_id;IF v_update_count = 0 THENRAISE_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 = SYSDATEWHERE employee_id = p_employee_id;v_update_count := SQL%ROWCOUNT;IF v_update_count > 0 THENCOMMIT;DBMS_OUTPUT.PUT_LINE('员工信息更新成功');DBMS_OUTPUT.PUT_LINE('员工ID: ' || p_employee_id);DBMS_OUTPUT.PUT_LINE('更新行数: ' || v_update_count);ELSEDBMS_OUTPUT.PUT_LINE('没有记录被更新');END IF;EXCEPTIONWHEN NO_DATA_FOUND THENDBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');WHEN DUP_VAL_ON_INDEX THENROLLBACK;DBMS_OUTPUT.PUT_LINE('错误: 违反唯一性约束,可能是邮箱重复');WHEN VALUE_ERROR THENROLLBACK;DBMS_OUTPUT.PUT_LINE('错误: 数据类型或长度错误');WHEN INVALID_NUMBER THENROLLBACK;DBMS_OUTPUT.PUT_LINE('错误: 无效的数字格式');WHEN OTHERS THENROLLBACK;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) THENRAISE invalid_transfer_date;END IF;-- 获取员工当前信息BEGINSELECT e.department_id, e.first_name || ' ' || e.last_name,d.department_nameINTO v_current_dept_id, v_employee_name, v_old_dept_nameFROM employees eLEFT JOIN departments d ON e.department_id = d.department_idWHERE e.employee_id = p_employee_id;EXCEPTIONWHEN NO_DATA_FOUND THENRAISE employee_not_found;END;-- 检查是否转移到相同部门IF v_current_dept_id = p_new_department_id THENRAISE same_department;END IF;-- 验证新部门是否存在SELECT COUNT(*), MAX(department_name)INTO v_dept_count, v_new_dept_nameFROM departments WHERE department_id = p_new_department_id;IF v_dept_count = 0 THENRAISE department_not_found;END IF;-- 检查员工是否是部门经理SELECT COUNT(*)INTO v_manager_countFROM departmentsWHERE manager_id = p_employee_id;IF v_manager_count > 0 THENRAISE 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 employeesSET department_id = p_new_department_id,last_updated = SYSDATEWHERE 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'));EXCEPTIONWHEN employee_not_found THENDBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');WHEN department_not_found THENDBMS_OUTPUT.PUT_LINE('错误: 目标部门ID ' || p_new_department_id || ' 不存在');WHEN invalid_transfer_date THENDBMS_OUTPUT.PUT_LINE('错误: 转移日期不能早于今天');WHEN same_department THENDBMS_OUTPUT.PUT_LINE('错误: 员工已经在目标部门中');WHEN employee_is_manager THENDBMS_OUTPUT.PUT_LINE('错误: 无法转移部门经理,请先指定新经理');WHEN OTHERS THENROLLBACK;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
)
ASv_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 ISSELECT column_name, data_typeFROM all_tab_columnsWHERE table_name = UPPER(p_table_name)AND owner = UPPER(p_owner)AND data_type IN ('NUMBER', 'INTEGER', 'FLOAT')ORDER BY column_id;BEGINDBMS_OUTPUT.PUT_LINE('=== 表 ' || p_owner || '.' || p_table_name || ' 统计信息 ===');-- 检查表是否存在v_sql := 'SELECT COUNT(*) FROM ' || p_owner || '.' || p_table_name;BEGINEXECUTE IMMEDIATE v_sql INTO v_count;DBMS_OUTPUT.PUT_LINE('总记录数: ' || v_count);EXCEPTIONWHEN OTHERS THENDBMS_OUTPUT.PUT_LINE('错误: 表不存在或无访问权限');RETURN;END;IF v_count = 0 THENDBMS_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 LOOPBEGIN-- 构建动态SQLv_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'));EXCEPTIONWHEN OTHERS THENDBMS_OUTPUT.PUT_LINE(RPAD(col_rec.column_name, 20) ||RPAD(col_rec.data_type, 15) ||'计算错误: ' || SUBSTR(SQLERRM, 1, 30));END;END LOOP;EXCEPTIONWHEN OTHERS THENDBMS_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'
)
ASv_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 THENv_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 THENv_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 THENv_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 THENv_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 THENv_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 THENv_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;-- 组合完整SQLv_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;LOOPFETCH 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;EXCEPTIONWHEN OTHERS THENIF emp_cursor%ISOPEN THENCLOSE 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 amountp_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 ISSELECT employee_id, salary, first_name || ' ' || last_nameFROM employeesWHERE department_id = p_department_idORDER BY employee_id;BEGINDBMS_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') THENRAISE_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_namesLIMIT 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 LOOPIF p_adjustment_type = 'PCT' THENv_new_salaries(i) := ROUND(v_old_salaries(i) * (1 + p_adjustment_value / 100), 2);ELSE -- AMTv_new_salaries(i) := v_old_salaries(i) + p_adjustment_value;END IF;-- 确保工资不为负数IF v_new_salaries(i) < 0 THENv_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.COUNTUPDATE employeesSET salary = v_new_salaries(i),last_updated = SYSDATEWHERE employee_id = v_emp_ids(i);-- 记录处理的员工信息FOR i IN 1..v_emp_ids.COUNT LOOPDBMS_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 THENDBMS_OUTPUT.PUT_LINE('平均调整: $' || ROUND(v_total_adjustment / v_total_processed, 2));END IF;EXCEPTIONWHEN OTHERS THENIF emp_cursor%ISOPEN THENCLOSE 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 ISSELECT employee_id, first_name, last_name, email, hire_date,job_id, salary, department_idFROM temp_employee_importWHERE processed_flag IS NULLORDER BY employee_id;BEGINDBMS_OUTPUT.PUT_LINE('=== 开始批量导入员工数据 ===');DBMS_OUTPUT.PUT_LINE('批次大小: ' || p_batch_size);DBMS_OUTPUT.PUT_LINE('');-- 创建临时导入表(如果不存在)BEGINEXECUTE 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('创建临时导入表');EXCEPTIONWHEN OTHERS THENIF 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进行批量插入,并收集错误BEGINFORALL i IN 1..v_employees.COUNT SAVE EXCEPTIONSINSERT 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.COUNTUPDATE temp_employee_importSET processed_flag = 'S'WHERE employee_id = v_employees(i).employee_id;EXCEPTIONWHEN OTHERS THEN-- 处理批量操作中的错误IF SQLCODE = -24381 THEN -- FORALL with SAVE EXCEPTIONSv_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 LOOPDECLAREv_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);BEGINDBMS_OUTPUT.PUT_LINE('  错误 ' || i || ': 员工ID ' || v_employees(v_error_index).employee_id ||' - ' || v_error_msg);-- 更新错误信息UPDATE temp_employee_importSET processed_flag = 'E',error_message = v_error_msgWHERE employee_id = v_employees(v_error_index).employee_id;END;END LOOP;-- 标记成功的记录FOR i IN 1..v_employees.COUNT LOOPDECLAREv_is_error BOOLEAN := FALSE;BEGIN-- 检查是否是错误记录FOR j IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOPIF SQL%BULK_EXCEPTIONS(j).ERROR_INDEX = i THENv_is_error := TRUE;EXIT;END IF;END LOOP;-- 如果不是错误记录,标记为成功IF NOT v_is_error THENUPDATE temp_employee_importSET 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) || '%');EXCEPTIONWHEN OTHERS THENIF import_cursor%ISOPEN THENCLOSE import_cursor;END IF;ROLLBACK;DBMS_OUTPUT.PUT_LINE('导入过程发生严重错误: ' || SQLERRM);
END bulk_import_employees;
/-- 创建错误报告存储过程
CREATE OR REPLACE PROCEDURE show_import_errors
AS
BEGINDBMS_OUTPUT.PUT_LINE('=== 导入错误报告 ===');FOR rec IN (SELECT employee_id, first_name, last_name, error_messageFROM temp_employee_importWHERE processed_flag = 'E'ORDER BY employee_id) LOOPDBMS_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
)
ASv_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) ISBEGINIF p_debug_mode THENDBMS_OUTPUT.PUT_LINE('[DEBUG] ' || TO_CHAR(SYSDATE, 'HH24:MI:SS') || ' - ' || p_message);END IF;END debug_print;BEGINdebug_print('开始计算员工 ' || p_employee_id || ' 的薪资');-- 获取基本工资SELECT salary, NVL(commission_pct, 0)INTO v_base_salary, v_commission_pctFROM employeesWHERE 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 0ENDINTO v_bonus_pctFROM employeesWHERE 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));-- 更新员工薪资信息(假设有扩展表)BEGINUPDATE employee_compensationSET 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 = SYSDATEWHERE employee_id = p_employee_id;IF SQL%ROWCOUNT = 0 THENdebug_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('薪资记录更新成功');EXCEPTIONWHEN OTHERS THENdebug_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));EXCEPTIONWHEN NO_DATA_FOUND THENdebug_print('员工不存在: ' || p_employee_id);DBMS_OUTPUT.PUT_LINE('错误: 员工ID ' || p_employee_id || ' 不存在');WHEN OTHERS THENdebug_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
ASv_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 ISv_value NUMBER;BEGINSELECT value INTO v_valueFROM v$mystat m, v$statname nWHERE m.statistic# = n.statistic#AND n.name = p_stat_name;RETURN v_value;EXCEPTIONWHEN NO_DATA_FOUND THENRETURN 0;END;v_start_cpu NUMBER;v_start_logical_reads NUMBER;v_start_physical_reads NUMBER;BEGINDBMS_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 LOOPDECLAREv_count NUMBER;v_avg_salary NUMBER;BEGINSELECT COUNT(*), AVG(salary)INTO v_count, v_avg_salaryFROM employees eJOIN departments d ON e.department_id = d.department_idWHERE 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
)
ASv_start_time TIMESTAMP;v_end_time TIMESTAMP;v_count NUMBER;BEGINDBMS_OUTPUT.PUT_LINE('=== 员工分析性能对比 ===');IF p_use_optimized THENDBMS_OUTPUT.PUT_LINE('使用优化版本...');v_start_time := SYSTIMESTAMP;-- 优化版本:使用分析函数和单次查询FOR rec IN (SELECT department_name,employee_count,avg_salary,max_salary,min_salary,salary_rankFROM (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 rnFROM departments dLEFT JOIN employees e ON d.department_id = e.department_id)WHERE rn = 1 AND employee_count > 0ORDER BY salary_rank) LOOPv_count := v_count + 1;END LOOP;ELSEDBMS_OUTPUT.PUT_LINE('使用未优化版本...');v_start_time := SYSTIMESTAMP;-- 未优化版本:多次查询FOR dept_rec IN (SELECT department_id, department_name FROM departments) LOOPDECLAREv_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_salaryFROM employeesWHERE department_id = dept_rec.department_id;IF v_emp_count > 0 THENv_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
)
ASTYPE 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;BEGINDBMS_OUTPUT.PUT_LINE('=== 资源优化批处理 ===');DBMS_OUTPUT.PUT_LINE('表名: ' || p_table_name);DBMS_OUTPUT.PUT_LINE('批次大小: ' || p_batch_size);-- 构建查询SQLv_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;-- 批量获取IDFOR i IN 1..p_batch_size LOOPFETCH 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);-- 批量处理BEGINFORALL i IN 1..v_rows_processedEXECUTE 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 THENDBMS_OUTPUT.PUT_LINE('已处理 ' || v_batch_count || ' 批,暂停1秒...');DBMS_LOCK.SLEEP(1);END IF;EXCEPTIONWHEN OTHERS THENROLLBACK;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);EXCEPTIONWHEN OTHERS THENIF v_cursor%ISOPEN THENCLOSE 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
)
ASv_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 ISv_count NUMBER;BEGIN-- 检查用户是否有特定权限SELECT COUNT(*)INTO v_countFROM user_role_privsWHERE 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) ISPRAGMA AUTONOMOUS_TRANSACTION;BEGININSERT 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') THENlog_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_salaryFROM employeesWHERE employee_id = p_employee_id;-- 业务规则检查IF p_new_salary <= 0 THENlog_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 THENlog_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_salaryFROM employees eJOIN jobs j ON e.job_id = j.job_idWHERE e.employee_id = p_employee_id;IF p_new_salary > v_max_allowed_salary THENlog_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 employeesSET salary = p_new_salary,last_updated = SYSDATEWHERE 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));EXCEPTIONWHEN NO_DATA_FOUND THENlog_salary_change(p_employee_id, NULL, p_new_salary, v_current_user, p_reason, 'EMPLOYEE_NOT_FOUND');DBMS_OUTPUT.PUT_LINE('错误: 员工不存在');WHEN OTHERS THENROLLBACK;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
BEGINDBMS_OUTPUT.PUT_LINE('=== 工资变更审计报告 ===');DBMS_OUTPUT.PUT_LINE('查询范围: 最近 ' || p_days_back || ' 天');IF p_employee_id IS NOT NULL THENDBMS_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_dateFROM salary_audit_log lLEFT JOIN employees e ON l.employee_id = e.employee_idWHERE (p_employee_id IS NULL OR l.employee_id = p_employee_id)AND l.change_date >= SYSDATE - p_days_backORDER BY l.change_date DESC) LOOPDBMS_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天的工资变更

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

在这里插入图片描述

相关文章:

【Oracle】存储过程

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 存储过程基础概述1.1 存储过程的概念与特点1.2 存储过程的组成结构1.3 存储过程的优势 2. 基础存储过程2.1 简单存储过程2.1.1 创建第一个存储过程2.1.2 带变量的存储过程 2.2 带参数的存储过程2.2.1 输入参…...

CppCon 2015 学习A Few Good Types

代码重构前后&#xff0c;用现代C更安全、更简洁的方式来处理数组和长度问题&#xff0c;并且利用静态分析&#xff08;SA&#xff0c;Static Analysis&#xff09;工具来捕获潜在错误。 代码重构前&#xff08;Before&#xff09; void f(_In_reads_(num) Thing* things, un…...

winrm登录失败,指定的凭据被服务器拒绝

winrm登录失败&#xff0c;指定的凭据被服务器拒绝。 异常提示&#xff1a;the specified credentials were rejected by the server 在windows power shell执行 set-executionpolicy remotesigned winrm quickconfig winrm set winrm/config/service/auth {Basic"true…...

单元测试-断言常见注解

目录 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;只是存着、查…...

Java抽象工厂模式详解

Java 抽象工厂模式是一种创建型设计模式&#xff0c;它提供了一种方式&#xff0c;可以将一组具有共同主题的单个工厂封装起来&#xff0c;而不必指定它们具体的类。这种模式属于创建型模式&#xff0c;它隐藏了对象创建的逻辑&#xff0c;将客户端与具体类的实现解耦。 一、核…...

matlab实现高斯烟羽模型算法

高斯烟羽模型的matlab代码 Code.m , 441 Cross.m , 1329 fit.m , 2080 fitness.m , 2160 fitness1.m , 2191 gaosiyanyu.m , 1936 jixian.m , 169 main.m , 155 mGA.m , 10415 mGA_new.fig , 7218 mGA_new.m , 18196 mPSO.m , 6681 Mutation.m , 1234 point.m , 1976 Select.m…...

SpringBoot parent依赖高版本覆盖低版本问题

问题 在Spring Boot项目中&#xff0c;有时候我们会遇到这样的情况&#xff1a;当我们引入了多个依赖库&#xff0c;而这些库中有相同的依赖项但版本不同。这种情况下&#xff0c;高版本的依赖可能会覆盖低版本的依赖&#xff0c;导致项目运行时出现不期望的行为或错误。为了解…...

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

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

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

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

C++抽象类与多态实战解析

这段 C 代码演示了 抽象类&#xff08;Abstract Class&#xff09; 和 多态&#xff08;Polymorphism&#xff09; 的使用&#xff0c;它定义了一个表示教师的抽象基类 Teacher&#xff0c;并派生出两个具体的子类&#xff1a;EnglishTeacher&#xff08;英语老师&#xff09;和…...

OpenAI API 流式传输

OpenAI API 流式传输教程 &#x1f30a; 本教程将详细解释 OpenAI API 如何进行数据流式传输&#xff0c;从基本的文本块到复杂的工具调用指令。流式传输允许你逐步从模型接收数据&#xff0c;这对于构建响应灵敏的用户界面和处理长输出非常有用。 1. 基础知识&#xff1a;Ser…...

嵌入式分析利器:DuckDB与SqlSugar实战

​ 一、DuckDB 的核心特性与适用场景 DuckDB 是一款 嵌入式分析型数据库&#xff08;OLAP&#xff09; &#xff0c;专为高效查询设计&#xff0c;主要特点包括&#xff1a; 列式存储与向量化引擎 数据按列存储&#xff0c;提升聚合统计效率&#xff08;如 SUM/AVG&#xf…...

嵌入式学习笔记 - freeRTOS任务设计要点

一 中断函数中不允许操作任务 因为中断函数使用的上下文环境是MSP环境&#xff0c;而非PSP环境&#xff0c;不允许挂起任务&#xff0c;不允许阻塞任务的任何操作。 可以使用FromISR函数进行操作。 二 中断的频率与处理时间 中断的处理时间要远低于任务的运行时间&#xff…...

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

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

12:点云处理—调平,角度,平面度,高度,体积

1.调平 2.夹角、平面度 3.高度、体积...

Marketo 集成 8x8 Connect 短信 API 指南

一、&#x1f50d; 项目背景与目标 在营销自动化流程中&#xff0c;需要在用户完成特定行为&#xff08;如填写表单、完成注册&#xff09;后&#xff0c;自动发送一条短信进行提醒、欢迎或验证。 Marketo 原生不具备短信发送能力&#xff0c;但支持通过 Webhook 集成第三方 A…...

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

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

Elasticsearch索引(Index)介绍,它与数据库中的表有什么区别?

在Elasticsearch(ES)中,索引(Index)是存储和组织文档(Document)的逻辑容器,类似于关系型数据库(如MySQL)中的“数据库(Database)”或“表(Table)”,但设计理念和实现机制有显著差异。以下从定义、核心特性、与数据库表的对比三方面详细解析。 一、索引的定义与…...

Elasticsearch中什么是分析器(Analyzer)?它由哪些组件组成?

在Elasticsearch(ES)中,分析器(Analyzer)是处理文本的核心组件,负责将原始文本转换为适合索引和搜索的词项(Term)。它直接影响搜索的准确性和性能,是构建高效搜索系统的关键。 一、分析器的核心作用 1. 分词(Tokenization):将文本拆分为独立的词(Token)。 例如…...

使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收

1.普通文本消息的发送和接收 GetMapping("/stream")public SseEmitter streamResponse() {SseEmitter emitter new SseEmitter(0L); // 0L 表示永不超时Executors.newSingleThreadExecutor().execute(() -> {try {for (int i 1; i < 5; i) {emitter.send(&q…...

.net Avalonia 在centos部署

.NET Avalonia 在 CentOS 部署指南 在跨平台应用开发中&#xff0c;.NET Avalonia 凭借其强大的功能和灵活性受到了广泛关注。而将基于 .NET Avalonia 开发的应用程序部署到 CentOS 系统上&#xff0c;是很多开发者会面临的任务。下面就为大家详细介绍在 CentOS 上部署 .NET A…...

MyBatis深度解析:XML/注解配置与动态SQL编写实战

引言 在现代Java企业级应用开发中&#xff0c;MyBatis作为一款优秀的持久层框架&#xff0c;因其灵活性和易用性广受开发者喜爱。相比Hibernate等全自动ORM框架&#xff0c;MyBatis提供了更接近SQL的开发体验&#xff0c;同时又不失面向对象的优雅。本文将深入探讨MyBatis的核…...

面试经验 对常用 LLM 工具链(如 LlamaFactory)的熟悉程度和实践经验

面试场景&#xff1a; 你正在面试一个大型语言模型&#xff08;LLM&#xff09;工程师或研究员的职位&#xff0c;面试官想了解你对常用 LLM 工具链&#xff08;如 LlamaFactory&#xff09;的熟悉程度和实践经验。 面试经验分享&#xff1a;LlamaFactory-CLI 工具实践 面试官…...

【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;也曾硬着头皮看了一些论文和知…...

ceph 对象存储用户限额满导致无法上传文件

查看日志 kl logs -f rook-ceph-rgw-my-store-a-5cc4c4d5b5-26n6j|grep -i error|head -1Defaulted container "rgw" out of: rgw, log-collector, chown-container-data-dir (init) debug 2025-05-30T19:44:11.573+0000 7fa7b7a6d700...