存储过程/函数:过程无返回值用 CREATE PROCEDURE,函数有返回值用 CREATE FUNCTION,参数模式分 IN / OUT / IN OUT。
触发器(Trigger):BEFORE / AFTER + INSERT / UPDATE / DELETE,行级用 FOR EACH ROW,通过 :NEW / :OLD 访问新旧值。
包(Package):规范 = 包规范(声明) + 包体(实现),用于模块化与权限控制。
1) PL/SQL 基本结构:匿名块
PL/SQL 程序的最小单元是 块(Block),最常见的是「匿名块」,结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
DECLARE -- 声明区(可选):变量、常量、游标、自定义类型、局部子程序 v_name VARCHAR2(50); v_salary NUMBER(10,2) :=8000; BEGIN -- 执行区(必填):SQL 语句和过程化语句 SELECT first_name INTO v_name FROM employees WHERE employee_id =100;
DBMS_OUTPUT.PUT_LINE('Name: '|| v_name ||', Salary: '|| v_salary); EXCEPTION -- 异常处理区(可选):捕获并处理错误 WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('Employee not found.'); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Error: '|| SQLERRM); END; /
DECLARE i NUMBER :=1; BEGIN -- 1) 基础 LOOP + EXIT WHEN DBMS_OUTPUT.PUT_LINE('--- LOOP ---'); LOOP DBMS_OUTPUT.PUT_LINE('i='|| i); EXIT WHEN i >=3; i := i +1; END LOOP;
-- 2) WHILE 循环 DBMS_OUTPUT.PUT_LINE('--- WHILE ---'); i :=1; WHILE i <=3 LOOP DBMS_OUTPUT.PUT_LINE('i='|| i); i := i +1; END LOOP;
-- 3) FOR 循环(最常用,无需手动声明/自增) DBMS_OUTPUT.PUT_LINE('--- FOR ---'); FOR j IN1..3 LOOP DBMS_OUTPUT.PUT_LINE('j='|| j); END LOOP;
-- 反向遍历:REVERSE FOR j IN REVERSE 1..3 LOOP DBMS_OUTPUT.PUT_LINE('reverse j='|| j); END LOOP; END; /
4) 游标(Cursor):遍历结果集
4.1 显式游标(完整控制)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
DECLARE CURSOR c_emps IS-- 1) 声明游标 SELECT employee_id, first_name, salary FROM employees WHERE department_id =60; r c_emps%ROWTYPE; -- 行变量 BEGIN OPEN c_emps; -- 2) 打开 LOOP FETCH c_emps INTO r; -- 3) 抓取 EXIT WHEN c_emps%NOTFOUND; DBMS_OUTPUT.PUT_LINE(r.employee_id ||' | '|| r.first_name ||' | '|| r.salary); END LOOP; CLOSE c_emps; -- 4) 关闭 END; /
4.2 FOR 循环游标(推荐,自动开关)
1 2 3 4 5 6 7 8 9
BEGIN FOR r IN (SELECT employee_id, first_name, salary FROM employees WHERE department_id =60) LOOP DBMS_OUTPUT.PUT_LINE(r.employee_id ||' | '|| r.first_name ||' | '|| r.salary); END LOOP; END; /
4.3 带参数的游标
1 2 3 4 5 6 7 8 9
DECLARE CURSOR c_dept_emps(p_dept_id NUMBER) IS SELECT employee_id, first_name FROM employees WHERE department_id = p_dept_id; BEGIN FOR r IN c_dept_emps(60) LOOP DBMS_OUTPUT.PUT_LINE(r.employee_id ||' - '|| r.first_name); END LOOP; END; /
5) 异常处理
5.1 预定义异常
异常名
触发场景
NO_DATA_FOUND
SELECT ... INTO 没查到数据
TOO_MANY_ROWS
SELECT ... INTO 返回多行
ZERO_DIVIDE
除以零
DUP_VAL_ON_INDEX
唯一约束冲突
OTHERS
兜底,匹配所有其它异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
DECLARE v_sal employees.salary%TYPE; BEGIN SELECT salary INTO v_sal FROM employees WHERE employee_id =-1; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('No employee with id=-1, defaulting to 0.'); v_sal :=0; WHEN TOO_MANY_ROWS THEN DBMS_OUTPUT.PUT_LINE('Unexpected: query returned multiple rows.'); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Error code: '|| SQLCODE ||' Msg: '|| SQLERRM); RAISE; -- 重新抛出 END; /
5.2 自定义异常 + PRAGMA EXCEPTION_INIT
1 2 3 4 5 6 7 8 9 10 11 12 13 14
DECLARE e_sal_too_low EXCEPTION; -- 把自定义异常绑定到 -20001 错误码 PRAGMA EXCEPTION_INIT(e_sal_too_low, -20001); v_sal NUMBER :=500; BEGIN IF v_sal <1000THEN RAISE_APPLICATION_ERROR(-20001, 'Salary '|| v_sal ||' below minimum 1000.'); END IF; EXCEPTION WHEN e_sal_too_low THEN DBMS_OUTPUT.PUT_LINE('Handled: '|| SQLERRM); END; /
CREATEOR REPLACE PROCEDURE raise_salary( p_emp_id IN employees.employee_id%TYPE, p_pct IN NUMBER DEFAULT0.05, -- 默认参数 p_new_sal OUT employees.salary%TYPE ) IS v_cur_sal employees.salary%TYPE; BEGIN SELECT salary INTO v_cur_sal FROM employees WHERE employee_id = p_emp_id FORUPDATE; -- 行锁,避免并发更新
v_cur_sal := v_cur_sal * (1+ p_pct);
UPDATE employees SET salary = v_cur_sal WHERE employee_id = p_emp_id;
p_new_sal := v_cur_sal; COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20001, 'Employee '|| p_emp_id ||' not found.'); WHEN OTHERS THEN ROLLBACK; RAISE; END raise_salary; /
CREATEOR REPLACE FUNCTION get_annual_sal( p_emp_id employees.employee_id%TYPE ) RETURN NUMBER IS v_sal employees.salary%TYPE; v_comm employees.commission_pct%TYPE; BEGIN SELECT salary, NVL(commission_pct, 0) INTO v_sal, v_comm FROM employees WHERE employee_id = p_emp_id;
RETURN v_sal *12* (1+ v_comm); EXCEPTION WHEN NO_DATA_FOUND THEN RETURNNULL; END get_annual_sal; /
在 SQL 中直接用:
1 2 3
SELECT employee_id, first_name, get_annual_sal(employee_id) AS annual_pay FROM employees WHERE department_id =60;
7) 触发器(Trigger):自动响应 DML/DDL 事件
7.1 行级触发器:BEFORE INSERT/UPDATE 自动填充字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
CREATEOR REPLACE TRIGGER trg_employees_biu BEFORE INSERTORUPDATEON employees FOREACHROW-- 行级:每一行触发一次 BEGIN -- 新员工自动设置入职日 IF INSERTING THEN :NEW.hire_date :=COALESCE(:NEW.hire_date, SYSDATE); END IF;
-- 薪资校验(防降薪超 20%) IF UPDATING('salary') AND :OLD.salary ISNOT NULLTHEN IF :NEW.salary < :OLD.salary *0.8THEN RAISE_APPLICATION_ERROR(-20002, 'Salary cannot drop more than 20%: '|| :OLD.salary ||' -> '|| :NEW.salary); END IF; END IF; END trg_employees_biu; /
FUNCTION get_emp(p_id employees.employee_id%TYPE) RETURN employees%ROWTYPE IS r employees%ROWTYPE; BEGIN SELECT*INTO r FROM employees WHERE employee_id = p_id; RETURN r; END get_emp;
PROCEDURE list_by_dept(p_dept_id employees.department_id%TYPE, p_list OUT t_emp_list) IS BEGIN SELECT* BULK COLLECTINTO p_list -- 批量绑定到集合,性能远好于逐行 FETCH FROM employees WHERE department_id = p_dept_id ORDERBY employee_id; END list_by_dept;
END pkg_emp_mgmt; /
调用包子程序:
1 2 3 4 5 6 7 8 9
DECLARE v_list pkg_emp_mgmt.t_emp_list; BEGIN pkg_emp_mgmt.list_by_dept(60, v_list); FOR i IN1..v_list.COUNT LOOP DBMS_OUTPUT.PUT_LINE(v_list(i).employee_id ||' - '|| v_list(i).first_name); END LOOP; END; /
DECLARE TYPE t_ids ISTABLEOF employees.employee_id%TYPE; v_ids t_ids; BEGIN SELECT employee_id BULK COLLECTINTO v_ids FROM employees WHERE salary >10000;
DECLARE TYPE t_emp_rec IS RECORD (id NUMBER, sal NUMBER); TYPE t_emp_tab ISTABLEOF t_emp_rec; v_data t_emp_tab; BEGIN SELECT employee_id, salary *1.1 BULK COLLECTINTO v_data FROM employees WHERE department_id =60;
-- 一次性把整个集合绑定给 UPDATE,只发一次 SQL FORALL i IN1..v_data.COUNT UPDATE employees SET salary = v_data(i).sal, last_updated_by =USER, last_updated_at = SYSDATE WHERE employee_id = v_data(i).id;