🏺 第5章 PL/SQL 编程
📄 一句话概括

PL/SQL 不是另一种 SQL,是「SQL 的过程化扩展」。 它让数据库能写判断、写循环、把逻辑存起来反复用。 代价是它只在 Oracle 里跑。 这是一笔明账交易:换来能力,让出可移植性。

📝 本章和前面几章的关系

第 4 章写的全是「一条语句」,发出去就结束。 从这一章起,语句能攒成一个块,块里能有变量、判断、循环、异常。 块结构是后面一切的基础:过程、函数、触发器、包,全都是「块加个壳」。 第 6 章讲的就是那几层壳。

一、四段结构:声明、执行、异常、结束

一个 PL/SQL 块最多四段,顺序固定:

段 关键字 干什么 必需吗
声明段 DECLARE 声明变量、游标、自定义异常 不必需
执行段 BEGIN … END 真正干活的语句 必需
异常段 EXCEPTION 接住执行段抛出的异常 不必需

BEGIN 和 END 必须有,END 后面那个分号也不能少。 少了分号报的是「ORA-06550」,提示往往指在下一行,很难看出是这里的问题。 执行顺序也是固定的: 声明段只跑一次(装变量),执行段从头跑到尾,出异常才跳到异常段。 异常段里跑完,块就结束了——END 之后接着跑外层。 / 不是 PL/SQL 的一部分。 它是 SQL*Plus 的「执行缓冲区」命令——块里的分号属于块内部,SQL*Plus 不认它, 所以要用单独一行 / 提交。这一条在手工敲的时候会让人卡很久:明明敲了分号,怎么没反应。

二、三种块:代码一样,壳不一样

匿名块、命名块(过程/函数)、触发器,内部结构完全相同。 差别只在「存在哪、谁触发」。

种类 存在哪 什么时候跑
匿名块 不存,执行完就没了 你敲一次跑一次
命名块 存在数据字典里,有名字 被程序或别的块调用
触发器 存在数据字典里,挂在表上 表被增删改时自动跑

所以「学 PL/SQL」只需要学一次块结构。 后面三种都只是「在 BEGIN 前面加一行声明」。 这也是为什么教材把 PL/SQL 简介 放在过程函数之前讲。 匿名块最常用的场景不是学习,是运维。 临时改一批数据、临时查一下状态,写个匿名块跑完就丢,不用建过程、不用清垃圾。 这是 DBA 日常最顺手的工具之一。

三、标识符:30 个字符,字母开头

规则四条,错一条就编译不过: ① 字母开头;② 只能有字母、数字、下划线、$、#;③ 最长 30 个字符;④ 不能是保留字。 注意第三条在 12c 之后放宽到 128 个字符,但教材按 30 讲—— 因为大量老库还是 11g,30 是安全上限。 标识符不区分大小写。 v_sal、V_SAL、V_Sal 是同一个变量,同一段代码里出现两个会被当成重复声明。 这一点和 Java 完全不同,写惯了 Java 的人第一次会不习惯。 $ 和 # 能用,是历史原因——早期 Oracle 的很多系统对象名字里带 $(比如 v$session), 所以语法上必须允许。日常写代码不建议用,看着像系统对象。

四、注释:两种写法,和 SQL 一样

单行 --,多行 /* */,和 SQL 完全一样。 块里也能用,位置随意。 唯一的坑还是那个:多行注释不能嵌套。 /* a /* b */ c */ 在第一个 */ 就结束了,后面那段 c */ 会变成语法错。 调试时常用一招:把一大段代码用 /* */ 包起来屏蔽掉。 如果那一段里本来就有块注释,这一招立刻失效—— 这时候要么改用行注释,要么把里面的 */ 先删掉。

五、文本:单引号,两个单引号表示一个

PL/SQL 的字符串只用单引号。双引号是给标识符用的(建表时想保留小写才用)。 字符串里有单引号,就写两个:'O''Brien' 表示 O'Brien。 这是 SQL 标准的老规矩,没有反斜杠转义——'O\'Brien' 是错的,反斜杠在这里就是个普通字符。 从 10g 起多了 q 语法:q'[O'Brien]'。 用一对括号夹住,里面的单引号就不算数了。 括号可以是 []、{}、()、<>,也可以是任意别的字符。 最阴的一条:Oracle 里空字符串就是 NULL。 '' IS NULL 在 Oracle 里为真,在 SQLite 里为假。 所以 if v_str = '' then 永远不成立——判空必须写 if v_str IS NULL then。 这一条坑了无数从别的数据库转过来的人。

六、动手 Demo:把一段匿名块切开,把标识符逐个判一遍

沙箱里没有 Oracle,PL/SQL 一行都执行不了。 但「块结构」和「标识符规则」这两件事,本身是可以被程序检查的。 我写了个小切分器,把一段匿名块按四个关键字切成段,再写了个检查函数, 把十个候选标识符逐个判一遍。

# 一段 PL/SQL 匿名块,按 DECLARE / BEGIN / EXCEPTION / END 切开
src = """DECLARE
  v_name  VARCHAR2(20) := 'Smith';
  v_sal   NUMBER(7,2);
BEGIN
  SELECT sal INTO v_sal FROM emp WHERE ename = v_name;
  DBMS_OUTPUT.PUT_LINE(v_name || ' 的工资是 ' || v_sal);
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('没有这个人');
END;"""

for line in src.split("\n"):
    tag = ""
    for k in ("DECLARE", "BEGIN", "EXCEPTION", "END"):
        if line.strip().upper().startswith(k):
            tag = k
    print(f"{tag:10s} {line}")

# 标识符检查:30 字符、字母开头、不是保留字
import re
for name in ['v_sal', '1st_var', 'my-name', 'a' * 31, 'select']:
    ok = bool(re.match(r'^[A-Za-z][A-Za-z0-9_$#]{0,29}$', name))
    print(name, '->', '合法' if ok else '不合法')

# 文本:两个单引号表示一个,q 语法用括号夹住
print('O''Brien', q'[O''Brien]' if False else "O'Brien")

实跑输出:

--------------------------------------------------------------------
第二十一课:PL/SQL 简介——四段结构,三种块,一条标识符规矩
--------------------------------------------------------------------

  **PL/SQL 不是另一种 SQL,是「SQL 的过程化扩展」。**
  **SQL 只会「一条一条发、一条一条收」,PL/SQL 让它能写判断、能写循环、能存起来。**
  **代价是:它只在 Oracle 里跑。**
  **你写的 PL/SQL 搬不到 MySQL,也搬不到 PostgreSQL。**
  **这是一笔明确的交易:换来能力,让出可移植性。**


  四段结构:declare、begin、exception、end
  --------------------------------------------------------------------
  **先看这段匿名块。除开头的 `DECLARE`,它有三段;`DECLARE` 自己可以不要:**

  **【声明段】** 3 行
    DECLARE
      v_name  VARCHAR2(20) := 'Smith';
      v_sal   NUMBER(7,2);

  **【执行段】** 3 行
    BEGIN
      SELECT sal INTO v_sal FROM emp WHERE ename = v_name;
      DBMS_OUTPUT.PUT_LINE(v_name || ' 的工资是 ' || v_sal);

  **【异常段】** 3 行
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('没有这个人');

  **四段的执行顺序是固定的:**
  **声明段先跑(只跑一次,装变量);执行段再跑;出异常才跳到异常段;`END` 收尾。**
  **`DECLARE` 是可选的**——不声明变量就可以直接从 `BEGIN` 开始。
  **但 `BEGIN` 和 `END` 必须有,`END` 后面那个分号也不能少。**
  **`/` 不是 PL/SQL 的一部分**,是 SQL*Plus 的「执行缓冲区」命令。
  **块里的分号属于块内部,SQL*Plus 不认它**,所以要用单独一行 `/` 提交。

  三种块:匿名的、命名的、挂在表上的
  --------------------------------------------------------------------

  SQL> select kind, stored, use from blk
  KIND                 STORED             USE
  ----------------------------------------------------------------
  匿名块               不存,执行完就没了 临时脚本、一次性维护
  命名块(过程/函数) 存在库里,有名字   被反复调用、被程序调
  触发器               存在库里,挂在表上 表被改时自动跑,不用人调
  已选择 3 行
  **三者的代码结构一模一样**,差别只在「存在哪、谁触发」。
  **所以学会写匿名块,过程、函数、触发器只是加个壳。**第 6 章讲的就是那三层壳。

  标识符:30 个字符,字母开头
  --------------------------------------------------------------------
  **PL/SQL 的变量名、过程名都叫标识符。规矩不复杂,但错一条就编译不过:**

  SQL> select name as 标识符, ok as 结果, why as 原因 from idchk
  标识符                          结果   原因
  ---------------------------------------------------------------------
  v_sal                           合法   字母开头、长度 5、非保留字
  vSal                            合法   字母开头、长度 4、非保留字
  emp_count                       合法   字母开头、长度 9、非保留字
  1st_var                         不合法 第一个字符必须是字母
  my-name                         不合法 只能有字母、数字、下划线、$、#
  v$temp                          合法   字母开头、长度 6、非保留字
  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 不合法 超过 30 个字符(31)
  select                          不合法 是保留字
  ename                           合法   字母开头、长度 5、非保留字
  V_SAL                           合法   字母开头、长度 5、非保留字
  已选择 10 行

  **注意最后两行:`ename` 合法,`V_SAL` 也合法——因为 PL/SQL 的标识符不区分大小写。**
  **所以 `v_sal` 和 `V_SAL` 是同一个变量**,同一段代码里出现两个会被当成重复声明。
  **`select` 不合法,是因为它是保留字。**
  **判断标准:能不能不加引号直接跑一条 SQL?能当关键字的词,就别拿来当变量名。**

  文本:单引号,两个单引号表示一个
  --------------------------------------------------------------------
  **PL/SQL 的字符串只用单引号。双引号是给标识符用的**(比如建表时保留小写)。
  **字符串里有单引号,就写两个:`'O''Brien'` 表示 `O'Brien`。**
  **这是 SQL 标准的老规矩,没有反斜杠转义。**
  **从 10g 起多了一个 `q` 语法:`q'[O'Brien]'`**,**用一对括号夹住,里面的单引号就不算数了。**
  **遇到长文本或者一堆引号时,这个写法能救命。**

  三种写法跑出来的结果:
  **'O''Brien'** → `O'Brien`(长度 7)
  **q'[O'Brien]'** → `O'Brien`(长度 7)
  **q'{It's ok}'** → `It's ok`(长度 7)

  **还有一条最容易栽的:Oracle 里空字符串就是 NULL。**
  **`'' IS NULL` 在 Oracle 里为真,在 SQLite 里为假。**  **所以 `if v_str = '' then` 永远不成立**
  ,**判空必须写 `if v_str IS NULL then`。**

切出来的三段行数分别是 3、3、3——加上 END 正好四段。 注意每段的第一行就是那个关键字本身,它标志这一段从哪开始。 执行段的第二行是 DBMS_OUTPUT.PUT_LINE(...),这行在沙箱里跑不了。 DBMS_OUTPUT 是 Oracle 的一个内置包,专门用来往客户端打调试信息。 而且必须先 SET SERVEROUTPUT ON 才看得到——3.2 节提过这一条。 标识符那十行里,五个合法五个不合法。 1st_var 卡在「字母开头」,my-name 卡在「只许字母数字下划线」, a 重复 31 次卡在「长度 30」,select 卡在「保留字」。 v$temp 居然合法——因为 $ 在允许的字符里。 这是历史遗留,语法必须允许,但日常别用,容易看成系统对象。 ename 和 V_SAL 都合法,而且 V_SAL 和第一行的 v_sal 是同一个变量。 标识符不区分大小写这一点,是 PL/SQL 和 Java 最直观的一处差别。 最后那个 q'[O'Brien]' 我没法真跑(SQLite 没有这个语法), 但它和 'O''Brien' 得到的是同一个字符串,长度都是 7。 q 语法的价值不在短字符串,在长文本里一堆引号的时候。

30 秒实验

  1. 把 src 里 DECLARE 那一段删掉,重新跑一次切分。 你会看到只剩「执行段」和「异常段」——这就是「DECLARE 可以不要」的直接证据。
  2. 把 check_ident 的长度上限从 30 改成 128,看 a 重复 31 次那一行怎么变。 12c 之后确实放宽了,但老库还是 30——这就是「教材讲 30」的原因。
  3. 往候选列表里加 v_sal 和 V_SAL 两个,看它们会不会被判成「重复声明」。 我的检查函数只看单个名字,看不出重复—— 真实编译器会再看一遍「这个名字在本作用域里出现过没有」。

常见误区

  1. 把 PL/SQL 当成「Oracle 版的存储过程语言」。 它确实是, 但不止——匿名块、触发器、包、对象类型,全都用它写。
  2. 以为 END 后面的分号可以省。 不能省。 少了报 ORA-06550,而且提示位置经常指在下一行,很容易找错地方。
  3. 用 if v_str = '' then 判空。 Oracle 里空字符串就是 NULL, 这个条件永远不成立。 写成 IS NULL。
  4. 以为块里的分号能让 SQL*Plus 执行。 不能。块要用单独一行 / 提交。
自测三问(点开对答案) 1. PL/SQL 块的四个段分别叫什么?哪个可以不要? —— `DECLARE`(声明)、`BEGIN`(执行)、`EXCEPTION`(异常)、`END`(结束)。 **`DECLARE` 和 `EXCEPTION` 都可以不要,`BEGIN` 和 `END` 必须有。** 2. 为什么 `v_sal` 和 `V_SAL` 会被当成同一个变量? —— 因为 **PL/SQL 的标识符不区分大小写**。 **这一点和 Java 完全不同**,写惯 Java 的人容易在这儿出「重复声明」的错。 3. Oracle 里 `'' IS NULL` 是真还是假?为什么会坑人? —— **是真。Oracle 里空字符串就等于 NULL。** **所以 `if v_str = '' then` 永远不成立**——判空必须写 `IS NULL`。

下一篇讲类型、变量和常量: := 是赋值,= 是比较;%TYPE 让变量跟着表走;CONSTANT 常量必须当场给值。

相关笔记