第三章 PL/SQL基础

PL/SQL基础

一、字符集

在PL/SQL程序中,允许出现的字符集包括:

  • 大小写字母(A-Z和a-z)
  • 数字(0-9)
  • 符号( ) + - * / < > = ! ~ ^ ; : . ’ @ % , " # $ & _ | { } ? [ ]
  • 制表符、空格和回车符
  • PL/SQL对大小写不敏感

所以,除了在字符串字符中,小写字母和它对应的大写字母是等价的。

二、词法单元

PL/SQL包含很多词法单元 (lexical unit),先类比我们日常交流的语言逻辑:当我们说一句话时,会先把 “字母” 组合成 “词语”,再用 “词语” 构成句子。在 PL/SQL 中,“词法单元” 就是相当于 “词语” 的角色 —— 它是 PL/SQL 代码中 “最小的、有独立含义的语法单位”,是编译器在 “词法分析阶段” 从代码文本中拆分出来的基本组件。PL/SQL 编译器执行代码前,会先做两步基础工作:

  • 词法分析:把连续的代码文本(如v_sal := 5000 + 1000;)拆成一个个独立的 “词法单元”,如:v_sal、:=、5000、+、1000、;
  • 语法分析:再根据 PL/SQL 语法规则,把这些 “词法单元” 组合成合法的语句(如赋值语句、循环语句)。

大致可以分为以下几类:

  • 分隔符:用于 “分隔其他词法单元” 或 “标记代码边界” 的符号,本身无运算 / 命名含义。例如:;(语句结束符)、()(参数列表分隔符)、,(项分隔符)、///(注释分隔符)
  • 标识符:用于给 “变量、常量、子程序、表、列” 等命名的符号,是最常用的词法单元。例如:v_sal(变量名)、emp(表名)、get_sal(函数名)
  • 文字:直接表示 “具体值” 的单元(如数字、字符串、日期),是代码中 “固定的值”。例如:5000(数字字面量)、‘Oracle’(字符串字面量)、TO_DATE(‘2024-01-01’,‘YYYY-MM-DD’)(日期字面量)
  • 注释:用于标注代码说明的文本,编译器会忽略其含义,但属于独立的词法单元。例如:– 这是单行注释、/* 这是多行注释 */

为改善可读性,我们可以用空格将词法单元分隔开。实际上,我们必须将相邻的两个标识符用空格或标点符号隔开。下面这样的写法是不允许的,因为关键字END和IF连到一起了:

1
IF x > y THEN high := x; ENDIF; -- not allowed

还有,除了字符串和注释以外,我们不可以在词法单元中嵌入空格。例如,像下面的赋值符号中间就不用被分开:

1
count : = count + 1; -- not allowed

为了让层次结构清楚,我们可以用回车符来换行,空格或制表符来进行缩进。比较一下下面两段IF语句的可读性:

1
2
3
4
5
6
7
IF x>y THEN max:=x;ELSE max:=y;END IF;  

IF x > y THEN
 MAX    := x;
ELSE
 MAX    := y;
END IF;

1. 分隔符

分隔符是对PL/SQL有着特殊意义的简单或复合的符号。例如,我们使用加号和减号这样的分隔符来表现数学运算。

简单分隔符只有一个字符:

符号 含义 PL/SQL 示例
+ 加法操作符 v_total := 5000 + 1000;(计算总和)
% 属性指示符 v_emp_rec emp%ROWTYPE;(声明与emp表结构一致的记录类型)
' 字符串分隔符 v_name := '张三';(定义字符串常量)
. 组件选择器 v_empno := emp.empno;(访问表的字段)
/ 触发操作符 BEGIN ... END; /(执行PL/SQL匿名块)
( 表达式或列表分隔符 SELECT * FROM emp WHERE deptno IN (10, 20);(包裹列表项)
) 表达式或列表分隔符 v_result := ADD(10, 20);(闭合函数参数列表)
: 主变量指示符 UPDATE emp SET sal = :new_sal WHERE empno = 1001;(引用宿主变量)
, 分隔符 INSERT INTO emp (empno, ename) VALUES (1001, '张三');(分隔字段和值)
* 多应用程序操作符 SELECT * FROM emp;(查询表中所有字段)
" 引用标识符分隔符 SELECT "EmpName" FROM "Employee";(访问大小写敏感的标识符)
= 关系操作符 IF v_sal = 5000 THEN ...(判断是否相等)
< 关系操作符 WHERE sal < 6000(查询薪资小于6000的记录)
> 关系操作符 WHILE i > 0 LOOP ...(当i大于0时执行循环)
@ 远程访问指示符 SELECT * FROM emp@remote_db;(访问远程数据库表)
; 语句终结符 SELECT sal FROM emp;(结束SQL语句)
- 减号/负号操作符 v_diff := 8000 - 3000;(计算差值);v_neg := -500;(表示负数)

复合分割符由两个字符组成:

符号 含义 PL/SQL 示例
:= 赋值操作符 v_name := 'Li Si';(给变量v_name赋值)
=> 关联操作符 v_result := calc_sal(p_empno => 1001, p_bonus => 2000);实际参数与函数 / 过程中定义的形式参数进行显式关联,将实际参数 1001 传递给函数 calc_sal 中定义的形式参数 p_empno
|| 连接操作符 v_msg := '员工姓名:' || '张三' || ',部门:研发部';(拼接多个字符串)
** 求幂操作符 v_power := 3 ** 4;(计算3的4次方,结果为81)
« 标签分隔符(开始) «sal_loop»
FOR i IN 1..5 LOOP
v_sal := v_sal + 1000;
END LOOP sal_loop;(定义循环标签)
» 标签分隔符(结束) 同上方示例,与<<sal_loop>>搭配,用END LOOP sal_loop;闭合标签
/* 多行注释分隔符(开始) /* 批量更新员工薪资:
1. 研发部薪资+15%
2. 财务部薪资+10% */
UPDATE emp SET sal = sal * 1.15 WHERE deptno = 10;
(标记多行注释起始)
*/ 多行注释分隔符(结束) 同上方示例,用*/标记多行注释结束,后续代码正常执行
.. 范围操作符 FOR i IN 10..20 LOOP
DBMS_OUTPUT.PUT_LINE(i);
END LOOP;(循环输出10到20的整数)
<> 关系操作符 SELECT * FROM emp WHERE deptno <> 30;(查询非30号部门的员工)
!= 关系操作符 IF v_status != 'ACTIVE' THEN DBMS_OUTPUT.PUT_LINE('员工状态异常'); END IF;(判断状态是否非“激活”)
~= 关系操作符 SELECT * FROM emp WHERE ename ~= 'Zhang';(查询姓名不等于“Zhang”的员工,部分Oracle版本支持)
^= 关系操作符 SELECT * FROM emp WHERE sal ^= 5000;(查询薪资不等于5000的员工,部分Oracle版本支持)
<= 关系操作符 SELECT * FROM emp WHERE hire_date <= TO_DATE('2023-01-01', 'YYYY-MM-DD');(查询2023年1月1日前入职的员工)
>= 关系操作符 IF v_sal >= 10000 THEN v_level := '高级'; END IF;(判断薪资是否大于等于10000,设置员工级别)
单行注释提示符 v_basic := 8000; -- 基础薪资赋值(后续可根据绩效调整)(标记单行注释,注释内容不执行)

2. 标识符

我们可以使用标识符来为PL/SQL程序中的常量、变量、异常、游标、游标变量、子程序和包命名。下面是一些标识符的例子:

  • X
  • t2
  • phone#
  • credit_limit
  • LastName
  • oracle$number

2.1 标识符命名规则

标识符可以由字母、数字、下划线(_)、美元符号($)和数字符号(#)组成。而像连字符,斜线(/)等符号都是不允许使用的。如下例:

  • mine&yours – 不允许使用连字符(not allowed because of ampersand)
  • debit-amount – 不允许使用连字符(not allowed because of hyphen)
  • on/off – 不允许使用斜线(not allowed because of slash)
  • user id – 不允许使用空格(not allowed because of space)

而使用美元符号、下划线和数字符号都是允许的:

  • money$$$tree
  • SN##
  • try_again_

我们也可以使用大小写混合的形式来编写标识符。但是要记住,除了字符串和字符以外,PL/SQL对大小写是不敏感的。所以,只在大小写上有区别的标识符,PL/SQL会把它们当做同一标识处理,如下例:

  • lastname
  • LastName – 与lastname相同
  • LASTNAME – 与lastname和Lastname相同

标识符的长度不能超过30。对于标识符的命名尽可能代表某种含义,避免使用像cpm这样的命名,而是使用cost_per_tdousand这样意义明确的命名方式。

2.2 保留关键字

对于某些标识符,我们称它们为保留关键字(reserved word),因为对于PL/SQL来说,它们有着特殊含义,不可以被重新定义。例如BEGIN和END,它们代表块或子程序的起始和结束而被PL/SQL 保留下来。在下面的例子中,我们可以看到,如果重定义一个关键字的话,就会产生一个编译错误:

1
2
DECLARE
 END BOOLEAN; -- not allowed; causes compilation error

但像下面这样把保留关键字嵌套在标识符中使用是允许的:

1
2
DECLARE
 end_of_game BOOLEAN; -- allowed

通常,保留关键字都是以大写形式存在的,这样能够增强可读性。但是,跟其他PL/SQL标识符一样,保留关键字也可以使用小写或大小写混合的形式。

2.3 预定义标识

在包STANDARD中声明的全局标识符(如INVALID_NUMBER)是可以被重新声明的。但是,不建议重新声明预定义标识符,因为这样做的结果会使本地声明覆盖全局声明。

直接使用STANDARD包预定义的INVALID_NUMBER,能正确捕获 “无效数字” 异常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DECLARE
    v_num NUMBER;
BEGIN
    -- 尝试把字符串'ABC'转成数字,会触发INVALID_NUMBER异常
    v_num := TO_NUMBER('ABC');
EXCEPTION
    -- 捕获系统预定义的INVALID_NUMBER异常(全局标识符)
    WHEN INVALID_NUMBER THEN
        DBMS_OUTPUT.PUT_LINE('捕获到异常:无效数字格式'); -- 会正常执行这句
END;
/

你在本地代码中重新声明了INVALID_NUMBER(比如把它定义成一个普通变量),此时 “本地声明会覆盖全局声明”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DECLARE
    -- 重新声明:把INVALID_NUMBER定义成一个本地变量(覆盖系统全局的异常常量)
    INVALID_NUMBER VARCHAR2(20) := '这是本地变量';
    v_num NUMBER;
BEGIN
    v_num := TO_NUMBER('ABC'); -- 依然会触发“无效数字”异常
EXCEPTION
    -- 此时,代码会找“本地声明的INVALID_NUMBER”,但它是变量不是异常常量,所以捕获失败!
    WHEN INVALID_NUMBER THEN
        DBMS_OUTPUT.PUT_LINE('捕获到异常:无效数字格式'); -- 这句不会执行
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('捕获到未知异常:' || SQLERRM); -- 会执行这句,因为异常没被正确捕获
END;
/

2.4 引用标识符

为了获取更多的灵活性,PL/SQL允许我们用双引号将标识符夹起来。这样的标识符很少使用,但有时它们非常有用。它们可以包含任何可打印字符,其中空格也包含在内,但是,不可以包含双引号。因此,下面这些引用标识符都是有效的:

  • “X+Y”
  • “last name”
  • “on/off switch”
  • “employee(s)”
  • “*** header info ***”

除了双引号以外,引用标识符最多可以包含30个字符。虽然把PL/SQL保留关键字作为引用标识符是被允许的,但这并不是一个好的编程习惯。

有些PL/SQL保留关键字并不是SQL的保留关键字。例如,我们可以在CREATE TABLE语句中使用TYPE作为字段名。但是,如果程序中的SQL语句要引用到这个字段的话,就会发生编译错误:

1
SELECT acct, type, bal INTO ... -- causes compilation error

为了避免发生这样的错误,就需要把字段名用双引号夹起来:

1
SELECT acct, "TYPE", bal INTO ...

要注意的是,字段名不能采用小写或大小写混合的形式(CREATE TABLE语句中除外)。例如,下面的语句是无效的:

1
SELECT acct, "type", bal INTO ... -- causes compilation error

因为表创建时没加双引号,字段type被自动转成大写TYPE存储;但查询时用双引号写"type",Oracle 强制按小写匹配,找不到对应的大写字段TYPE,所以编译错误。Oracle 中 “双引号是大小写开关”—— 不加双引号默认大写匹配,加了双引号必须原样匹配,还有一种做法就是可以建立视图来为原来的字段名更换一个新名。

3. 文字

文字就是一个数字、字符、字符串或布尔(Boolean)值。它本身是数据而不是对数据的引用,如数字147和布尔值FALSE都是文字。

3.1 数字文字

在算术表达式中有两种数字文字可以使用:整数和实数。整数文字不带小数点,有一个可选的符号,例子如下:

030 6 -14 0 +32767

实数文字带有小数点,也有一个可选的符号,例子如下:

6.6667 0.0 -12.0 3.14159 +8300.00 .5 25.

PL/SQL把12.0和25.这样的数字都当作实数处理,虽然它们只有整数部分值。

数字文字不能包含美元符号或是逗号,但可以使用科学记数法。只要在数字后面添加一个E(或e),再跟上一个整数即可(符号可选)。比如下面几个例子:

2E5 1.0E-7 3.14159e0 -1E38 -9.5e-3

E代表了十的幂,即权(times ten to tde power of)。E后面的整数值代表指数。**是幂操作符。

5E3 = 5 * 10**3 = 5 * 1000 = 5000

– tde double asterisk (**) is tde exponentiation operator

在上面的例子里,小数点向右移动三个位置,而在下面这个例子中,我们把E后面的数字改成-3,就能让小数点向左移动三个位置

5E-3 = 5 * 10**-3 = 5 * 0.001 = 0.005

再举一个例子。如果字符文字的范围不在1E-130到10E125之间,就会产生编译错误:

1
2
3
4
DECLARE
 n NUMBER;
BEGIN
 n := 10E127;   -- causes a 'numeric overflow or underflow' error

3.2 字符文字

字符文字就是由单引号夹起来的一个单独的字符。字符文字包括PL/SQL字符集中所有的可打印字符:字母、数字、空格和特殊符号。如下例所示:

‘Z’, ‘%’, ‘7’, ’ ‘, ‘z’, ‘(’

对于字符文字来说,PL/SQL是大小写敏感的。例如,PL/SQL会把’Z’和’z’当成不同的字符。字符'0’到'9’虽不与整数文字等价,但它们可以被应用于算术表达式中,因为它们会被隐式地转换成整数。

3.3 字符串文字

字符值可以用标识符来表示,或是写成字符串文字,字符串文字就是由单引号夹起来的零个或多个字符,如下例所示:

1
2
3
4
5
'Hello, world!'
'XYZ Corporation'
'10-NOV-91'
'He said "Life is like licking honey from a tdorn."'
'$1,000,000'

除了空字符串(’’)之外,所有的字符串文字都是CHAR类型。如果我们想表现一个单引号字符串的话,可以用两个连续的单引号来表示:

1
'Don''t leave witdout saving your work.'

PL/SQL对字符串是大小写敏感的。例如,下面两个字符串是不相同的:

1
2
'baker'
'Baker'

3.4 布尔(Boolean)文字

布尔文字可以用值TRUE、FALSE和NULL(表示缺失、未知或不可用的值)来表示。记住,布尔文字本身就是值,而不是字符串。

3.5 日期和时间文字

日期因类型的不同,有很多表现形式,比如下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DECLARE
    -- 1. 日期类型(仅包含年月日)
    d1 DATE := DATE '1998-12-25';  -- 1998年12月25日
    
    -- 2. 时间戳类型(包含年月日时分秒)
    t1 TIMESTAMP := TIMESTAMP '1997-10-22 13:01:01';  -- 1997年10月22日13点01分01秒
    
    -- 3. 带时区的时间戳类型
    t2 TIMESTAMP WITH TIME ZONE := TIMESTAMP '1997-01-31 09:26:56.66 +02:00';  -- 东二区时间
    
    -- 4. 年到月的间隔类型(表示3年2个月)
    i1 INTERVAL YEAR TO MONTH := INTERVAL '3-2' YEAR TO MONTH;  -- '3-2'中3是年,2是月
    
    -- 5. 天到秒的间隔类型(表示5天4小时3分2.01秒)
    i2 INTERVAL DAY TO SECOND := INTERVAL '5 04:03:02.01' DAY TO SECOND;  -- '5'是天,后面是时分秒
BEGIN
    -- 示例:时间间隔的计算与类型指定
    DBMS_OUTPUT.PUT_LINE('当前时间戳差值(默认天到秒): ' || (CURRENT_TIMESTAMP - CURRENT_TIMESTAMP));
    
    -- 显式指定间隔类型
    DBMS_OUTPUT.PUT_LINE('显式指定天到秒间隔: ' || (CURRENT_TIMESTAMP - CURRENT_TIMESTAMP) DAY TO SECOND);
    -- 注意:年到月间隔通常用于日期差计算(如两个DATE的差)
    DBMS_OUTPUT.PUT_LINE('日期差转年到月间隔: ' || (d1 - DATE '1995-10-25') YEAR TO MONTH);
END;
/

PL/SQL 中,时间间隔用于表示 “两个时间点之间的差值”,必须明确类型:

  • YEAR TO MONTH:只能表示 “年和月” 的间隔(如 3 年 2 个月),不能包含天、时、分、秒。格式:INTERVAL ‘年-月’ YEAR TO MONTH(例如'3-2’表示 3 年 2 个月)。
  • DAY TO SECOND:只能表示 “天、时、分、秒” 的间隔(如 5 天 4 小时),不能包含年、月。格式:INTERVAL ‘天 时:分:秒.毫秒’ DAY TO SECOND(例如'5 04:03:02.01’表示 5 天 4 时 3 分 2.01 秒)。

我们可以指定间隔值是YEAR TO MONTH类型还是DAY TO SECOND类型。如:current_timestamp - current_timestamp

上面表达式的结果值类型默认是INTERVAL DAY TO SECONDE。我们还可以使用下面的方法来指定间隔类型:

(interval_expression) DAY TO SECOND
(interval_expression) YEAR TO MONTH

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
DECLARE
    -- 定义两个TIMESTAMP类型变量(包含时分秒)
    t1 TIMESTAMP := TIMESTAMP '2024-01-01 10:00:00';  -- 时间点1:2024年1月1日10:00:00
    t2 TIMESTAMP := TIMESTAMP '2023-09-15 08:30:00';  -- 时间点2:2023年9月15日8:30:00
    -- 定义两个DATE类型变量(仅包含年月日)
    d1 DATE := DATE '2024-01-01';                    -- 日期1:2024年1月1日
    d2 DATE := DATE '2023-09-15';                    -- 日期2:2023年9月15日
BEGIN
    -- 1. TIMESTAMP相减(默认返回DAY TO SECOND间隔)
    -- 计算逻辑:t1 - t2 = 间隔天数 + 小时:分钟:秒
    -- 实际计算:从2023-09-15 08:30:00到2024-01-01 10:00:00的总时长
    -- 结果示例:108 01:30:00.000000(108天1小时30分钟)
    DBMS_OUTPUT.PUT_LINE('TIMESTAMP差(天时分秒): ' || (t1 - t2));
    
    -- 2. 显式指定DAY TO SECOND格式(与默认结果一致)
    -- 格式说明:DAY TO SECOND强制按“天 时:分:秒.小数秒”显示
    -- 结果与上一行相同:108 01:30:00.000000
    DBMS_OUTPUT.PUT_LINE('显式天到秒: ' || (t1 - t2) DAY TO SECOND);
    
    -- 3. DATE类型计算年-月间隔(精确方式)
    -- 步骤1:MONTHS_BETWEEN(d1, d2)计算实际月数差
    -- 计算逻辑:(2024-01-01) - (2023-09-15) ≈ 3.548个月(精确到小数)
    -- 步骤2:TRUNC取整数部分 → 3个月
    -- 步骤3:NUMTOYMINTERVAL转换为YEAR TO MONTH间隔 → 0年3个月
    -- 最终结果:+000000000-03(显示为0-3)
    DBMS_OUTPUT.PUT_LINE(
        '日期差(年-月): ' || 
        NUMTOYMINTERVAL(TRUNC(MONTHS_BETWEEN(d1, d2)), 'MONTH') 
    );

    -- 补充:展示中间计算过程(方便理解)
    DBMS_OUTPUT.PUT_LINE('--- 中间计算过程 ---');
    DBMS_OUTPUT.PUT_LINE('d1 - d2的天数差: ' || (d1 - d2));  -- 结果:108(天)
    DBMS_OUTPUT.PUT_LINE('MONTHS_BETWEEN(d1, d2)的月数差: ' || MONTHS_BETWEEN(d1, d2));  -- 结果:≈3.548387
    DBMS_OUTPUT.PUT_LINE('TRUNC后取整月数: ' || TRUNC(MONTHS_BETWEEN(d1, d2)));  -- 结果:3(个月)
END;
/

4. 注释

PL/SQL编译器会忽略注释,但我们不可以这样做。添加注释能让我们的程序更加易读。通常我们添加注释的目的就是描述每段代码的用途。PL/SQL支持两种注释风格:单行和多行。

4.1 单行注释

单行注释由一对连字符(–)开头。如下例:

1
2
3
4
5
-- begin processing
SELECT sal INTO salary
FROM emp -- get current salary
WHERE empno = emp_id;
bonus := salary * 0.15; -- compute bonus amount

注释可以出现在一条语句的末端。在测试或调试程序的时候,有时我们想禁用某行代码,就可以用注释给它"注掉"(comment-out),如下面的例子:

1
-- DELETE FROM emp WHERE comm IS NULL;

4.2 多行注释

多行注释由斜线星号(/)开头,星号斜线(/)结尾,可以注释多行内容。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
BEGIN
 ...
 /* Compute a 15% bonus for top-rated employees. */
 IF rating > 90 THEN
   bonus := salary * 0.15 /* bonus is based on salary */
 ELSE
   bonus := 0;
 END IF;
 ...
 /* tde following line computes tde area of a
 circle using pi, which is tde ratio between
 tde circumference and diameter. */
 area := pi * radius**2;
END;

我们可以使用多行注释注掉整块代码,如下例所示:

1
2
3
4
5
6
7
8
/*
LOOP
FETCH c1
INTO emp_rec;
EXIT WHEN c1%NOTFOUND;
...
END LOOP;
*/

三、声明

在PL/SQL中,我们可以在块、子程序或包的声明部分来声明常量或变量。声明能够分配内存空间,指定数据类型,为存储位置进行命名以便我们能够引用这块存储空间。下面来看一下声明的例子:

1
2
birtdday DATE;
emp_count SMALLINT := 0;

第一句声明了一个DATE类型的变量。第二句声明了SMALLINT类型的变量,并用赋值操作符指定了初始值零。下面再看一个稍微复杂一点的例子,用一个声明过的变量来初始化另一个变量:

1
2
3
pi       REAL := 3.14159;
radius   REAL := 1;
area     REAL := pi * radius ** 2;

默认情况下,变量是被初始化为NULL的。所以,下面两个声明是等价的:

1
2
birtdday   DATE;
birtdday   DATE := NULL;

对于常量声明要多加一个CONSTANT关键字:

1
credit_limit CONSTANT REAL := 5000.00;

1. 使用DEFAULT

我们可以使用关键字DEFAULT来替换赋值操作符为变量初始化。下面这个声明

1
blood_type   CHAR := 'o';

就可以用DEFAULT来替换:

1
blood_type CHAR DEFAULT 'o';

我们可以使用DEFAULT来初始化子程序参数、游标参数和用户定义的记录中的域。

2. 使用NOT NULL

除了在声明中做初始化操作外,还可以使用NOT NULL进行约束:

1
acct_id INTEGER(4) NOT NULL := 9999;

这样一来,我们就不能为变量acct_id指派空值了。如果这样做的话,PL/SQL就会抛出预定义异常VALUE_ERROR。NOT NULL约束后面必须跟着初始化子句。像下面这样的声明是不允许的:

1
acct_id INTEGER(5) NOT NULL;   -- not allowed; not initialized

NATURALN和POSITIVEN是PL/SQL提供的两个不可为空的预定义子数据类型,NATURALN 是一种预定义的子类型(subtype),属于 Oracle 的STANDARD包,用于约束变量只能存储非负整数(0 及正整数)。下面这两个声明是等价的:

1
2
emp_count NATURAL NOT NULL := 0;
emp_count NATURALN         := 0;

在NATURALN和POSITIVEN声明中,类型分类符后面必须跟上一个初始化子句。否则就会发生编译错误。例如,下面的声明就是不合法的:

1
line_items POSITIVEN;   -- not allowed; not initialized

3. 使用%TYPE

%TYPE属性能够为我们提供变量或数据库字段的数据类型。在下面的例子中,%TYPE提供了变量credit的数据类型:

1
2
credit   REAL(7, 2);
debit    credit%TYPE;

在引用数据库中某个字段的数据类型时,%TYPE显得更加有用。我们可以通过表名加字段来引用,或是使用所有者加表名加字段来引用:

1
my_dname scott.dept.dname%TYPE;

使用%TYPE声明my_dname有两个好处。首先,我们不必知道dname具体的数据类型。其次,如果数据库中对dname的数据类型定义发生了改变,变量my_dname的数据类型也会在运行时作出相应的改变。但是要注意的是,%TYPE只提供类型信息,并不提供NOT NULL约束信息,所以下面这段代码即时是在emp.empno不可为空的情况下也是可以运行的:

1
2
3
4
5
DECLARE
 my_empno emp.empno%TYPE;
 ...
BEGIN
 my_empno := NULL; -- this works

4. 使用%ROWTYPE

%ROWTYPE属性提供数据表(或视图)中一整行数据的类型信息。记录可以完整地保存从游标或游标变量中取出的当前行的信息。下面例子中,我们声明了两个记录,第一个保存emp表的行信息,第二个保存从游标c1取出的行信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DECLARE
    -- 声明emp表的行类型变量(存储整行emp记录)
    emp_rec  emp%ROWTYPE;
    
    -- 声明游标c1,定义查询部门信息的结果集
    CURSOR c1 IS
        SELECT deptno, dname, loc 
          FROM dept;
    
    -- 声明与游标c1结果集匹配的行类型变量(存储游标返回的一行记录)
    dept_rec  c1%ROWTYPE;

我们还可以为指定的域进行赋值操作,如下例:

1
2
emp_rec.ename := 'JOHNSON';
emp_rec.sal   := emp_rec.sal * 1.15;

%ROWTYPE同%TYPE一样,只提供类型信息,并不能保证NOT NULL约束。在最后一个例子中,我们使用%ROWTYPE来定义一个打包游标(packaged cursor):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 包声明(规范部分)
CREATE PACKAGE emp_actions AS
    -- 声明游标规范(仅定义返回类型,不包含具体查询)
    CURSOR c1 
        RETURN emp%ROWTYPE;   -- 返回emp表的整行记录类型
    
    -- 其他包元素声明(如变量、函数、过程等)
    ...

END emp_actions;
/

-- 包体(实现部分)
CREATE PACKAGE BODY emp_actions AS
    -- 定义游标主体(实现游标规范,包含具体查询逻辑)
    CURSOR c1 
        RETURN emp%ROWTYPE 
    IS
        SELECT * 
          FROM emp 
         WHERE sal > 3000;   -- 筛选薪资大于3000的员工记录
    
    -- 其他包元素的实现(如函数、过程的具体逻辑)
    ...

END emp_actions;
/

4.1 聚合赋值

用%ROWTYPE作声明的时候是不可以进行初始化赋值的,但是有两种方法可以一次性为所有字段赋值。

方法一:假如两个记录类型的声明引用了同一数据表或游标,那么它们就可以相互赋值,如:

1
2
3
4
5
6
7
8
9
DECLARE
 dept_rec1   dept%ROWTYPE;
 dept_rec2   dept%ROWTYPE;
 CURSOR c1 IS 
   SELECT deptno, dname, loc  FROM dept;
 dept_rec3   c1%ROWTYPE;
BEGIN
 ...
 dept_rec1 := dept_rec2;

但是,如果一个类型是引用的是数据表而另一个引用的是游标的话,那么,即使它们表现的内容相同,也是不能相互赋值的:

1
dept_rec2 := dept_rec3; -- not allowed

方法二:我们可以使用SELECT或FETCH语句将取得的数据赋给记录。但在表或视图中定义的字段名称顺序要与记录中的名称顺序相同。

1
2
3
4
5
6
7
DECLARE
 dept_rec dept%ROWTYPE;
 ...
BEGIN
 SELECT * INTO dept_rec FROM dept WHERE deptno = 30;
 ...
END;

但是,我们不能使用赋值语句来把字段列表中的值赋给记录。所以,下面的语法形式是不允许的:

1
record_name := (value1, value2, value3, ...); -- not allowed

4.2 使用别名

从游标中取出的数据,如果游标定义中含有表达式时,我们就需要使用别名才能正确地为%ROWTYPE类型记录赋值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
DECLARE
    -- 声明游标:计算员工总薪资(工资+奖金,奖金为NULL时按0处理)并关联员工姓名
    CURSOR my_cursor IS
        SELECT 
            sal + NVL(comm, 0) AS wages,  -- 总薪资(别名wages)
            ename 
        FROM emp;
    
    -- 声明与游标匹配的行类型变量,用于存储游标提取的一行数据
    my_rec  my_cursor%ROWTYPE;
BEGIN
    -- 打开游标
    OPEN my_cursor;
    
    -- 循环提取游标数据
    LOOP
        -- 从游标中提取一行数据到变量
        FETCH my_cursor INTO my_rec;
        
        -- 当游标无数据时退出循环
        EXIT WHEN my_cursor%NOTFOUND;
        
        -- 若总薪资大于2000,插入到temp表
        IF my_rec.wages > 2000 THEN
            INSERT INTO temp 
            VALUES (NULL, my_rec.wages, my_rec.ename);
        END IF;
    END LOOP;
    
    -- 关闭游标
    CLOSE my_cursor;
END;

5. 声明的约束

PL/SQL不允许向前引用。也就是说我们在使用变量或常量之前必须先声明。像下面这样的语句就是不合法的:

1
2
maxi   INTEGER := 2 * mini;   -- not allowed
mini   INTEGER := 15;

但是,PL/SQL允许向前声明子程序。对于同样数据类型的每一个变量,都必须单独声明:

1
2
3
i   SMALLINT;
j   SMALLINT;
k   SMALLINT;

像下面这样的声明方式是不允许的:

1
i, j, k   SMALLINT;   -- not allowed

小结

在数据库时代,特别是Oracle数据库时代,PL/SQL已经成为了数据库应用程序开发的标准语言。