个人主页: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 存储过程的概念与特点
1.2 存储过程的组成结构
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天的工资变更
结语
感谢您的阅读!期待您的一键三连!欢迎指正!