Skip to content

PL/SQL 块结构:DECLARE、BEGIN、EXCEPTION、END

你有没有这种感觉:SQL 查询已经足够强大了,但处理复杂业务逻辑时,还是需要写很多重复的 SQL?

Oracle 的答案是:PL/SQL

PL/SQL(Procedural Language/SQL)是 Oracle 对 SQL 的扩展,它让你可以在数据库端编写过程式代码——变量、循环、条件判断、异常处理,一个都不能少。

今天,我们从 PL/SQL 最基础的部分讲起:块结构


PL/SQL 块结构详解

PL/SQL 程序由块(Block)组成,每个块包含三个部分:

┌─────────────────────────────────────────────────────────────┐
│                      PL/SQL 块结构                          │
│                                                             │
│  DECLARE        -- 可选:声明变量、常量、游标               │
│      v_var VARCHAR2(20);                                   │
│  BEGIN          -- 必需:程序逻辑                           │
│      -- SQL 语句                                            │
│      -- PL/SQL 语句                                        │
│  EXCEPTION     -- 可选:异常处理                           │
│      WHEN OTHERS THEN                                      │
│          -- 处理异常                                        │
│  END;          -- 必需:程序结束                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

最小结构:只有 BEGIN 和 END

sql
BEGIN
    DBMS_OUTPUT.PUT_LINE('Hello, Oracle!');
END;
/

注意:PL/SQL 块以 / 结束,表示执行。


DECLARE:声明部分

变量声明

sql
DECLARE
    -- 变量声明:变量名 数据类型 [NOT NULL] [:= 初始值]
    v_emp_name VARCHAR2(50);
    v_salary NUMBER(10, 2);
    v_hire_date DATE;
    v_count NUMBER := 0;  -- 带初始值
    v_pi CONSTANT NUMBER := 3.14159;  -- 常量
BEGIN
    NULL;  -- 空语句,什么都不做
END;
/

数据类型

sql
DECLARE
    v_num1 NUMBER;           -- 数字
    v_num2 NUMBER(10,2);     -- 整数或定点数
    v_char VARCHAR2(100);    -- 字符串
    v_date DATE;             -- 日期
    v_bool BOOLEAN;          -- 布尔型
    v_clob CLOB;             -- 大对象
    v_blob BLOB;             -- 二进制大对象
BEGIN
    NULL;
END;
/

使用 %TYPE 属性

使用 %TYPE 可以让变量类型与表字段或变量类型保持一致:

sql
DECLARE
    -- 与 employees 表的 last_name 字段类型相同
    v_name employees.last_name%TYPE;
    -- 与已声明变量类型相同
    v_name2 v_name%TYPE;
BEGIN
    NULL;
END;
/

使用 %ROWTYPE 属性

使用 %ROWTYPE 可以声明一个记录类型,与表行结构一致:

sql
DECLARE
    -- 整个 employees 表的行结构
    v_emp employees%ROWTYPE;
BEGIN
    SELECT * INTO v_emp
    FROM employees
    WHERE employee_id = 100;
    
    DBMS_OUTPUT.PUT_LINE('员工姓名: ' || v_emp.first_name);
END;
/

RECORD 类型

自定义记录类型:

sql
DECLARE
    TYPE t_emp_record IS RECORD (
        emp_id    NUMBER,
        emp_name  VARCHAR2(100),
        emp_salary NUMBER(10, 2)
    );
    v_emp t_emp_record;
BEGIN
    v_emp.emp_id := 100;
    v_emp.emp_name := 'John';
    v_emp.emp_salary := 5000;
END;
/

BEGIN:执行部分

基本语句

sql
DECLARE
    v_name VARCHAR2(50);
    v_salary NUMBER(10, 2);
BEGIN
    -- 赋值语句
    v_name := '张三';
    v_salary := 8000.50;
    
    -- SELECT INTO(必须返回一行)
    SELECT first_name, salary
    INTO v_name, v_salary
    FROM employees
    WHERE employee_id = 100;
    
    -- 输出
    DBMS_OUTPUT.PUT_LINE('姓名: ' || v_name || ', 工资: ' || v_salary);
END;
/

条件判断

sql
DECLARE
    v_salary NUMBER(10, 2);
    v_level VARCHAR2(20);
BEGIN
    SELECT salary INTO v_salary
    FROM employees
    WHERE employee_id = 100;
    
    -- IF-THEN-ELSIF-ELSE
    IF v_salary >= 10000 THEN
        v_level := '高薪';
    ELSIF v_salary >= 5000 THEN
        v_level := '中等';
    ELSE
        v_level := '普通';
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('薪资等级: ' || v_level);
END;
/

CASE 语句

sql
DECLARE
    v_dept_id NUMBER := 50;
    v_dept_name VARCHAR2(50);
BEGIN
    -- CASE 表达式
    v_dept_name := CASE v_dept_id
        WHEN 10 THEN '行政部'
        WHEN 20 THEN '市场部'
        WHEN 30 THEN '销售部'
        ELSE '其他部门'
    END;
    
    DBMS_OUTPUT.PUT_LINE('部门: ' || v_dept_name);
END;
/

循环

sql
DECLARE
    v_counter NUMBER := 1;
BEGIN
    -- 基本循环
    LOOP
        DBMS_OUTPUT.PUT_LINE('计数器: ' || v_counter);
        v_counter := v_counter + 1;
        EXIT WHEN v_counter > 5;  -- 退出条件
    END LOOP;
END;
/
sql
BEGIN
    -- WHILE 循环
    DECLARE
        v_counter NUMBER := 1;
    BEGIN
        WHILE v_counter <= 5 LOOP
            DBMS_OUTPUT.PUT_LINE('WHILE: ' || v_counter);
            v_counter := v_counter + 1;
        END LOOP;
    END;
END;
/
sql
BEGIN
    -- FOR 循环
    FOR i IN 1..5 LOOP
        DBMS_OUTPUT.PUT_LINE('FOR: ' || i);
    END LOOP;
    
    -- REVERSE 反向
    FOR i IN REVERSE 1..5 LOOP
        DBMS_OUTPUT.PUT_LINE('REVERSE: ' || i);
    END LOOP;
END;
/

GOTO 语句(不推荐)

sql
BEGIN
    GOTO skip_output;  -- 跳转到标签
    DBMS_OUTPUT.PUT_LINE('这条不会执行');
    <<skip_output>>
    DBMS_OUTPUT.PUT_LINE('跳到这里执行');
END;
/

注意:GOTO 语句会破坏程序结构,不推荐使用。


EXCEPTION:异常处理

什么是异常?

异常是程序执行过程中发生的错误。PL/SQL 提供了完善的异常处理机制。

异常分类:
┌─────────────────────────────────────────────────────────────┐
│                      Oracle 异常                            │
│                                                             │
│  ┌───────────────────────┐  ┌───────────────────────────┐   │
│  │    系统异常            │  │    用户自定义异常         │   │
│  │    (预定义)           │  │                           │   │
│  │                       │  │    RAISE_APPLICATION_ERROR│   │
│  │  TOO_MANY_ROWS       │  │    RAISE                   │   │
│  │  NO_DATA_FOUND       │  │                           │   │
│  │  DUP_VAL_ON_INDEX    │  │                           │   │
│  │  ...                 │  │                           │   │
│  └───────────────────────┘  └───────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

预定义异常

sql
DECLARE
    v_name VARCHAR2(50);
    v_salary NUMBER(10, 2);
BEGIN
    -- NO_DATA_FOUND:查询没有返回数据
    SELECT first_name INTO v_name
    FROM employees
    WHERE employee_id = 9999;  -- 不存在的员工
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('员工不存在');
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('返回数据过多');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('其他错误: ' || SQLERRM);
END;
/

TOO_MANY_ROWS 处理

sql
DECLARE
    v_name VARCHAR2(50);
BEGIN
    -- 返回多行会触发 TOO_MANY_ROWS
    SELECT first_name INTO v_name
    FROM employees
    WHERE department_id = 50;
    
EXCEPTION
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('部门有多名员工,请使用游标');
END;
/

用户自定义异常

sql
DECLARE
    -- 声明异常
    e_invalid_salary EXCEPTION;
    v_salary NUMBER(10, 2) := -1000;
BEGIN
    -- 检查业务规则
    IF v_salary < 0 THEN
        -- 抛出异常
        RAISE e_invalid_salary;
    END IF;
    
EXCEPTION
    WHEN e_invalid_salary THEN
        DBMS_OUTPUT.PUT_LINE('错误:工资不能为负数');
END;
/

RAISE_APPLICATION_ERROR

用于抛出带错误码的自定义错误:

sql
BEGIN
    -- 模拟业务验证
    IF TRUE THEN
        -- 错误码范围:-20000 到 -20999
        RAISE_APPLICATION_ERROR(-20001, '业务错误:无效的操作');
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('错误码: ' || SQLCODE);
        DBMS_OUTPUT.PUT_LINE('错误消息: ' || SQLERRM);
        RAISE;  -- 重新抛出异常
END;
/

SQLCODE 和 SQLERRM

sql
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('错误码: ' || SQLCODE);   -- 错误码
        DBMS_OUTPUT.PUT_LINE('错误消息: ' || SQLERRM);  -- 错误信息
        -- 记录日志
        INSERT INTO error_log (error_code, error_msg, error_time)
        VALUES (SQLCODE, SQLERRM, SYSDATE);
        COMMIT;
END;
/

完整示例:员工薪资调整

sql
DECLARE
    v_emp_id NUMBER := 100;
    v_old_salary NUMBER(10, 2);
    v_new_salary NUMBER(10, 2);
    v_raise_percent NUMBER := 0.1;
    e_salary_limit EXCEPTION;
BEGIN
    -- 获取当前工资
    SELECT salary INTO v_old_salary
    FROM employees
    WHERE employee_id = v_emp_id;
    
    -- 计算新工资
    v_new_salary := v_old_salary * (1 + v_raise_percent);
    
    -- 检查业务规则:涨薪后不超过 30000
    IF v_new_salary > 30000 THEN
        RAISE e_salary_limit;
    END IF;
    
    -- 更新工资
    UPDATE employees
    SET salary = v_new_salary
    WHERE employee_id = v_emp_id;
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('员工 ' || v_emp_id || ' 工资调整完成');
    DBMS_OUTPUT.PUT_LINE('原工资: ' || v_old_salary || ', 新工资: ' || v_new_salary);
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('错误:员工不存在');
        ROLLBACK;
    WHEN e_salary_limit THEN
        DBMS_OUTPUT.PUT_LINE('错误:涨薪后超过上限 30000');
        ROLLBACK;
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('未知错误: ' || SQLERRM);
        ROLLBACK;
END;
/

面试高频问题

Q1: PL/SQL 块的三部分是什么?

DECLARE(可选)BEGIN(必须)EXCEPTION(可选)END(必须)

Q2: %TYPE 和 %ROWTYPE 的区别?

  • %TYPE:获取表字段或变量的数据类型
  • %ROWTYPE:获取整行的数据类型(记录类型)

Q3: 预定义异常有哪些?

异常说明
NO_DATA_FOUNDSELECT INTO 没有返回数据
TOO_MANY_ROWSSELECT INTO 返回多行
DUP_VAL_ON_INDEX唯一索引冲突
INVALID_NUMBER字符串转数字失败
ZERO_DIVIDE除以零
OTHERS捕获所有异常

Q4: WHEN OTHERS THEN NULL; 是好习惯吗?

不是。这种方式会静默吞掉异常,不推荐。应该记录日志或重新抛出异常:

sql
-- 不好
EXCEPTION
    WHEN OTHERS THEN NULL;

-- 推荐
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('错误: ' || SQLERRM);
        RAISE;  -- 重新抛出
END;
/

总结

PL/SQL 块结构核心要点:

部分关键字说明
声明DECLARE变量、常量、游标、异常
执行BEGIN程序逻辑
异常EXCEPTION错误处理
结束END程序结束
sql
DECLARE
    -- 声明变量
    v_var VARCHAR2(50);
BEGIN
    -- 执行逻辑
    NULL;
EXCEPTION
    -- 异常处理
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
/

理解块结构,是学习 PL/SQL 的基础。下一节,我们来学习存储过程和函数。


下一步

基于 VitePress 构建