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

PL/SQL 的变量必须先声明后使用。语法是「名字 类型 [:= 初值]」,赋值用 :=,= 是比较。 最值得养成的习惯是 %TYPE:让变量类型跟着表列走,表结构改了你一行代码都不用动。

📝 本章和前面几章的关系

5.1 节把块的骨架搭好了,这一篇往里填「变量」。 后面游标、过程、函数全都要声明变量,所以这一篇是硬基础。 %TYPE 和 %ROWTYPE 这两个写法,Oracle 特有,别的库基本没有—— 它们是 PL/SQL 最实用的一处方言。

一、基本类型:四种够用九成

NUMBER 数字、VARCHAR2 变长字符串、DATE 日期时间、BOOLEAN 布尔。 这四个覆盖日常绝大多数场景。

类型 含义 要留意什么
NUMBER(p,s) 总长 p 位、小数 s 位 不写 p、s 就是浮点,能存很大但会丢精度
VARCHAR2(n) 变长字符串,最长 n 字节 SQL 里上限 4000,PL/SQL 里能到 32767
DATE 日期加时间,精确到秒 没有「只有日期」的类型,时分秒默认 0
BOOLEAN 真/假/空 只能给 PL/SQL 用,不能存进表

BOOLEAN 存不进表,这一条最容易栽。 表里要存真假,一般用 CHAR(1) 存 'Y'/'N',或者 NUMBER(1) 存 1/0。 这是 Oracle 的历史遗留——SQL 标准里一直没定布尔类型,Oracle 干脆把它限制在 PL/SQL 里。 23c 才加了 BOOLEAN 列类型,教材讲的 19c 还没有。 VARCHAR2 里的 2 不是版本号。 它是「第 2 代」,因为第一代 VARCHAR 的行为有争议(空串算不算 NULL), Oracle 用 VARCHAR2 把语义定死了,并且承诺以后不再改。 所以写 VARCHAR2,别写 VARCHAR——后者行为将来可能变。

二、精度是硬限制:NUMBER(p,s) 怎么算

NUMBER(7,2) 是「总共 7 位有效数字,其中 2 位小数」。 所以整数部分最多 5 位,最大能存 99999.99。 超了会怎样,分两种: 小数位超了是「悄悄四舍五入」(123.456 变成 123.46,不报错); 总位数超了才是「溢出」(999999.99 报 ORA-01438)。 这两种行为在报表上长得都很正常。 金额字段的精度一定要一次定对—— 定小了,某天一笔大额进来直接报错;定大了,索引变大、比较变慢。 经验值:金额用 NUMBER(12,2) 起步(能存到百亿级),科学计算用不带精度的 NUMBER(浮点, 范围大但会丢精度),计数用 NUMBER(10) 或 INTEGER。

三、%TYPE 与 %ROWTYPE:跟着表走

v_sal emp.sal%TYPE 的意思是「v_sal 的类型跟 emp.sal 一模一样」。 编译时 Oracle 去数据字典里查一下,把类型抄过来。 好处就一个字:不写死。 你写 v_sal NUMBER(7,2),哪天 DBA 把 sal 改成 NUMBER(9,2),你的代码就溢出; 写 v_sal emp.sal%TYPE,代码一个字不用动。 v_row emp%ROWTYPE 是「一整行」,v_row.ename、v_row.sal 都能直接取。 它就是一个结构体,配合游标 FETCH ... INTO v_row 特别顺手。 注意 %TYPE 不带 NOT NULL 约束——它只抄类型,不抄约束。 所以 emp.ename 是 NOT NULL,v_name emp.ename%TYPE 仍然可以是 NULL。 想连约束一起要,得写 NOT NULL := 初值。

四、常量:CONSTANT 必须当场给值

v_pi CONSTANT NUMBER := 3.14159;——声明常量,就必须在声明时赋值。 这是唯一一处「声明即初始化」强制成立的地方。 后面再赋值是编译错,不是运行时错。 这个区别很大:编译错在「部署之前」就发现了,运行时错要等「跑到那一行」。 所以常量该用就用。 它是「这条规矩由编译器帮你守」,而注释只是「请你自己记得」。 能用编译器守的,别交给记性。

五、表达式:日期也能算

PL/SQL 的表达式基本就是 SQL 的表达式,加减乘除、比较、逻辑,都一样。唯一要留意的是日期。 日期加一个数,加的是「天」。 SYSDATE + 1 是明天,SYSDATE + 1/24 是一小时后,SYSDATE + 1/1440 是一分钟后。 因为日期在 Oracle 里就是一个数,小数部分就是时分秒。 所以「加一个月」不能用 +30。 月份长度不一样,1 月 31 日加 30 天得 3 月 2 日, 而 ADD_MONTHS 会给你 2 月 28 日——那才是「加一个月」的本意。 还有一个坑:NULL 参与任何运算,结果还是 NULL。 v_x := NULL; v_y := v_x + 1; 之后 v_y 是 NULL,不是 1。 要兜底只能用 NVL。

六、动手 Demo:算一遍精度,看一眼 %TYPE 的取数来源

这一段做三件事:按 NUMBER(7,2) 的规则判四个数能不能存下、 把 emp 表的列类型读出来(%TYPE 就是从这里抄的)、验证日期加减的单位是天。 精度判断我是真按规则算的:整数位数比 p - s 大就溢出,小数位数比 s 大就舍入。

# NUMBER(7,2) 能放多大:整数 5 位、小数 2 位
def fit(v, p=7, s=2):
    ip, _, dp = v.partition('.')
    ip = ip.lstrip('0') or '0'
    if len(ip) > p - s:
        return '溢出'
    if len(dp) > s:
        return '舍入成 %.2f' % round(float(v), s)
    return '放得下'

for v in ['12345.67', '999999.99', '123.456', '0.005']:
    print(v, '->', fit(v))

# %TYPE 的意思:照着表里那一列的类型来
import sqlite3
c = sqlite3.connect(':memory:')
c.execute('create table emp (empno int, ename text, sal real)')
print(c.execute("select name, type from pragma_table_info('emp')").fetchall())

# CONSTANT:Python 只能靠约定,PL/SQL 是编译期报错
PI = 3.14159
PI = 3.14                # 没有任何语言级拦截
print('PI =', PI)

# 日期就是数:+1 是明天,+1/24 是一小时后
import datetime
now = datetime.datetime(2026, 10, 30, 9, 0, 0)
print(now + datetime.timedelta(days=1))
print(now + datetime.timedelta(hours=1))
print(now + datetime.timedelta(minutes=1))

实跑输出:

--------------------------------------------------------------------
第二十二课:数据类型、变量和常量——赋值是 :=,不是 =
--------------------------------------------------------------------

  **PL/SQL 的变量必须先声明、后使用。**
  **声明写在 `DECLARE` 段,语法是「名字 类型 [:= 初值]」。**
  **记住这一点:赋值用 `:=`,`=` 是比较。**  **这一条和 SQL 完全不同**——SQL 里 `=` 是比较,
  赋值在 `update` 里。  **混用会得到一个「语法错误」,而且报错位置常常让你误以为是别的地方。**


  基本类型:四种够用九成
  --------------------------------------------------------------------

  SQL> select name, means, note from typ
  NAME        MEANS                      NOTE
  -------------------------------------------------------------------------------------------
  NUMBER(p,s) 数字,总长 p 位、小数 s 位 不写 p、s 就是浮点,能存很大但会丢精度
  VARCHAR2(n) 变长字符串,最长 n 字节    n 默认按字节算;SQL 里上限 4000,PL/SQL 里能到 32767
  DATE        日期加时间,精确到秒       没有「只有日期」的类型,时分秒默认为 0
  BOOLEAN     真/假/空                 只能给 PL/SQL 用,不能存进表
  已选择 4 行
  **`BOOLEAN` 值得单独提一句:它只能活在 PL/SQL 里,存不进表。**
  **表里要存真假,一般用 `CHAR(1)` 存 `'Y'`/`'N'`,或者 `NUMBER(1)` 存 1/0。**
  **这是 Oracle 一个历史遗留的设计,很多新手在这上面卡过一次。**

  NUMBER(7,2):精度是硬限制
  --------------------------------------------------------------------
  **`NUMBER(7,2)` 的意思是「总共 7 位有效数字,其中 2 位是小数」。**  **所以整数部分最多 5 位**,
  最大能存 99999.99。
  这四个数放进去会怎样:
  **12345.67** → 存得下(整数 5 位、小数 2 位,正好放得下)
  **999999.99** → 溢出(整数部分 6 位,超过 5 位上限)
  **123.456** → 存得下(小数位 3 超过 2,舍入成 123.46)
  **0.005** → 存得下(小数位 3 超过 2,舍入成 0.01)

  **注意 `123.456` 那一行:它不报错,是悄悄四舍五入成 123.46。**
  **超出小数位是「舍入」,超出总位数才是「溢出」——两种处理完全不同。**
  **这两种行为在报表上长得都很正常,所以金额字段的精度一定要一次定对。**

  %TYPE 与 %ROWTYPE:跟着表走
  --------------------------------------------------------------------
  **`v_sal emp.sal%TYPE` 的意思是「`v_sal` 的类型跟 `emp.sal` 一模一样」。**
  **好处是表结构变了,变量自动跟着变,不用改代码。**
  **`v_row emp%ROWTYPE` 是「一整行」**,`v_row.ename`、`v_row.sal` 都能直接取。
  **它就是一个结构体。**

  **这台机器的 `emp` 表长这样(`%TYPE` 就是从这里取的):**

  SQL> select name as 列名, typ as 类型, nn as 非空 from colinfo
  列名     类型    非空
  ---------------------
  empno    INTEGER 否
  ename    TEXT    是
  job      TEXT    否
  mgr      INTEGER 否
  hiredate TEXT    否
  sal      REAL    否
  comm     REAL    否
  deptno   INTEGER 否
  已选择 8 行

  **`%TYPE` 的真正价值在「不写死」。**
  **你写 `v_sal NUMBER(7,2)`,哪天 DBA 把 `sal` 改成 `NUMBER(9,2)`,你的代码就溢出。**
  **写 `v_sal emp.sal%TYPE`,代码一个字不用动。**  **这是 PL/SQL 里最值得养成的一个习惯。**

  常量:CONSTANT 必须当场给值
  --------------------------------------------------------------------
  **`v_pi CONSTANT NUMBER := 3.14159;`——声明时常量,就必须在声明时赋值。**
  **后面再赋值就是编译错**,不是运行时错。

  **Python 里这种「改常量」根本没有语言级的拦截,只能靠约定。**  **PL/SQL 是在编译期就报错**,
  这就是强类型语言和脚本语言的一处硬差别。
  **所以常量该用就用**——它是「这条规矩由编译器帮你守」,  **而注释只是「请你自己记得」。**

  表达式:日期也能算
  --------------------------------------------------------------------
  **PL/SQL 的表达式基本就是 SQL 的表达式**,加减乘除、比较、逻辑,都一样。
  **唯一要留意的是日期:日期加一个数,加的是「天」。**
  **`SYSDATE + 1` 是明天,`SYSDATE + 1/24` 是一小时后,`SYSDATE + 1/1440` 是一分钟后。**
  **因为日期在 Oracle 里就是一个数(第几天),小数部分就是时分秒。**
  **所以「加一个月」不能用 `+30`**——月份长度不一样。**要用 `ADD_MONTHS`。**

四个数里只有一个溢出:999999.99。 它整数部分 6 位,超过 7 - 2 = 5 位的上限。 其余三个都「放得下」,其中两个是被舍入过的。 123.456 变成 123.46,0.005 变成 0.01——都没报错。 这就是「小数位超了只是舍入」的直接证据。 在 Oracle 里它连警告都不给,数据就这么悄悄变了。 pragma_table_info('emp') 打出八列的类型。 emp.sal%TYPE 编译时就是来这儿查的——查到 REAL,就把变量定成对应的数字类型。 这就是「跟着表走」的机制。 注意这台机器上 sal 的类型是 REAL,Oracle 里是 NUMBER(7,2)。 类型名不同,但「%TYPE 会去查字典」这个机制是一样的。 常量那一段是反例:Python 里 PI = 3.14 直接把原来的值覆盖了,什么都没拦。 PL/SQL 里同样的代码是编译错。 这是强类型语言和脚本语言最直观的一处差别。 日期那三行:加 1 天、加 1 小时、加 1 分钟。 注意 1 小时写的是 hours=1,在 PL/SQL 里要写成 +1/24—— 因为单位是「天」,所以任何时间间隔都要除以当天的份数。

30 秒实验

  1. 把 fit 的 p、s 改成 (5,2),再跑那四个数。 这次 12345.67 也应该溢出了——整数部分 5 位,正好卡在 5 - 2 = 3 的上限外面。
  2. 加一个数 0.0001 试试。 小数位 4 位,舍入成 0.00——这就是「钱算着算着变成 0」的来源:精度定得太小。
  3. 用 pragma_table_info('dept') 查一下 deptno 的类型,再对比 emp.deptno。 两张表的 deptno 类型必须一致,否则连接时会做隐式转换,索引就废了。
  4. 把 datetime.timedelta(days=1) 换成 timedelta(days=30),对比「加 30 天」和「加一个月」。 2 月那一个月会让你看到差别。

常见误区

  1. 用 = 赋值。 PL/SQL 里 = 是比较,赋值是 :=。 写错会报语法错,而且报错位置常常指在别的地方。
  2. 以为 NUMBER(7,2) 能存 7 位整数。 是「总共 7 位,其中 2 位小数」, 整数只剩 5 位。 金额字段定小了,早晚有一天会溢出。
  3. 以为 %TYPE 会连 NOT NULL 一起抄过来。 不会。它只抄类型,不抄约束。
  4. 日期加一个月写 +30。 月份长度不一样。加月一律用 ADD_MONTHS,它自己处理月末。
自测三问(点开对答案) 1. `NUMBER(7,2)` 最大能存多少?`123.456` 放进去会怎样? —— 最大 **99999.99**(整数 5 位、小数 2 位)。 `123.456` **不报错,悄悄舍入成 123.46**——小数位超了只舍入,总位数超了才溢出。 2. `%TYPE` 到底省了什么? —— 省了「把类型写死」。**表列类型一变,变量自动跟着变,代码不用改。** **注意它只抄类型,不抄 `NOT NULL` 约束。** 3. 为什么「加一个月」不能用 `+30`? —— 因为月份长度不一样。 **1 月 31 日加 30 天是 3 月 2 日,而 `ADD_MONTHS` 给的是 2 月 28 日**—— 后者才是「加一个月」的本意。

下一篇讲流程控制: 两个选择(IF、CASE),三个循环(LOOP、WHILE、FOR)。 重点会放在「哪个至少跑一次」和「FOR 的范围是闭区间」这两处。

相关笔记