admin 管理员组

文章数量: 887021


2024年1月15日发(作者:joblib库安装)

第一章 PL/SQL一览

一、理解PL/SQL的主要特性

了解PL/SQL最好的方法就是从简单的实例入手。下面的程序是用于处理一个网球拍订单的。首先声明一个NUMBER类型的变量来存放现有的球拍数量。然后从数据表inventory中把球拍的数量检索出来。如果数量大于零,程序就会更新inventory表,并向purchase_record表插入一条购买记录,如果数量不大于零,程序会向purchase_record表插入一条脱销(out-of-stock)记录。

DECLARE

qty_on_hand NUMBER(5);

BEGIN

SELECT quantity

INTO qty_on_hand

FROM inventory

WHERE product = 'TENNIS RACKET'

FOR UPDATE OF quantity;

IF qty_on_hand > 0 THEN -- check quantity

UPDATE inventory

SET quantity = quantity - 1

WHERE product = 'TENNIS RACKET';

INSERT INTO purchase_record

VALUES ('Tennis racket purchased', SYSDATE);

ELSE

INSERT INTO purchase_record

VALUES ('Out of tennis rackets', SYSDATE);

END IF;

COMMIT;

END;

在PL/SQL中,可以使用SQL语句来操作Oracle中的数据,并使用流程控制语句来处理数据。我们还可以声明常量和变量,定义函数和过程并捕获运行时错误。因此,PL/SQL是一种把SQL对数据操作的优势和过程化语言数据处理优势结合起来的语言。

1、块结构

PL/SQL是一种块结构的语言,它的基本组成单元是一些逻辑块,而这些块又能嵌套任意数量子块。通常,每一个逻辑块都承担一部分工作任务, PL/SQL这种将问题分而治之(divide-and-conquer)的方法称为逐步求精(stepwise refinement)。块能够让我们把逻辑相关的声明和语句组织起来,声明的内容对于块来说是本地的,在块结构退出时它们会自动销毁。

如下图所示,一个块分为三个部分:声明,处理,异常控制。其中,只有处理部分是必需的。首先程序处理声明部分,然后被声明的内容就可以在执行部分使用,当异常发生时,就可以在异常控制部分中对抛出的异常进行捕捉、处理。

我们还可以在处理部分和异常控制部分嵌套子块,但声明部分中不可以嵌套子块。不过我们仍可以在声明部分定义本地的子程序,但这样的子程序只能由定义它们的块来调用。

2、变量与常量

PL/SQL允许我们声明常量和变量,但是常量和变量必须是在声明后才可以使用,向前引用(forward

reference)是不允许的。

变量声明

变量可以是任何SQL类型,如CHAR,DATE或NUMBER等,也可以是PL/SQL类型,BOOLEAN或BINARY_INTEGER等。声明方法如下:

part_no NUMBER(4);

in_stock BOOLEAN;

我们还可以用TABLE、VARRAY和RECORD这些复合类型来声明嵌套表、变长数组(缩写为varray)和记录。

变量赋值

我们可以用三种方式为变量赋值,第一种,直接使用赋值操作符":=":

tax := price * tax_rate;

valid_id := FALSE;

bonus := current_salary * 0.10;

wages := gross_pay(emp_id,

st_hrs,

ot_hrs

) - deductions;

第二种,利用数据库中查询的结果为变量赋值:

SELECT sal * 0.10

INTO bonus

FROM emp

WHERE empno = emp_id;

第三种,把变量作为一个OUT或IN OUT模式的参数传递给子程序,然后由子程序为其赋值。如下例所示,IN OUT参数可以为被调用的子程序传递初始值然后子程序将更新后的新值返回给调用程序:

DECLARE

my_sal REAL(7,2);

PROCEDURE adjust_salary (emp_id INT, salary IN OUT REAL) IS ...

BEGIN

SELECT AVG(sal)

INTO my_sal

FROM emp;

adjust_salary(7788, my_sal); -- assigns a new value to my_sal

声明常量

声明常量跟声明变量类似,但是要加一个CONSTANT关键字,并在声明时为其赋上初始值。下例中,我们声明一个名为credit_limit的常量:

credit_limit CONSTANT REAL := 5000.00;

3、游标

Oracle使用工作区(work area)来执行SQL语句,并保存处理信息。PL/SQL可以让我们使用游标来为工作区命名,并访问存储的信息。游标的类型有两种:隐式和显式。 PL/SQL会为所有的SQL数据操作声明一个隐式的游标,包括只返回一条记录的查询操作。对于返回多条记录的查询,我们可以显式地声明一个游标来处理每一条记录。如下例:

DECLARE

CURSOR c1 IS

SELECT empno, ename, job

FROM emp

WHERE deptno = 20;

由多行查询返回的行集合称为结果集(result set)。它的大小就是满足我们查询条件的行的个数。如下图所示,显式游标"指向"当前行的记录,这可以让我们的程序每次处理一条记录。

多行查询处理有些像文件处理。例如,一个COBOL程序打开一个文件,处理记录,然后关闭文件。同样,一个PL/SQL程序打开一个游标,处理查询出来的行,然后关闭游标。就像文件指针能标记打开文件中的当前位置一样,游标能标记出结构集的当前位置。

我们可以使用OPEN,FETCH和CLOSE语句来控制游标,OPEN用于打开游标并使游标指向结果集的第一行,FETCH会检索当前行的信息并把游标指移向下一行,当最后一行也被处理完后,CLOSE就会关闭游标。

4、游标FOR循环

在大多需要使用显式游标的情况下,我们都可以用一个简单的游标FOR循环来代替OPEN,FETCH和CLOSE语句。首先,游标FOR循环会隐式地声明一个代表当前行的循环索引(loop index)变量。下一步,它会打开游标,反复从结果集中取得数据并放到循环索引的各个域(field)中。当所有行都被处理过以后,它就会关闭游标。下面的例子中,游标FOR循环隐式地声明了一个emp_rec记录:

DECLARE

CURSOR c1 IS

SELECT ename, sal, hiredate, deptno

FROM emp;

...

BEGIN

FOR emp_rec IN c1 LOOP

...

salary_total := salary_total + emp_;

END LOOP;

END;

为了使用每一个查询到的行中的每一个字段,我们可以使用点标志(dot notation),它的作用就像一个域选择器。

5、游标变量

游标变量的使用方式和游标类似,但更加灵活,因为它不绑定于某个特定的查询,所以可用于打开任何返回类型相兼容的查询语句。游标变量是真正的 PL/SQL变量,我们可以为它赋值,把它传递给子程序。如下例,我们把游标变量作为存储过程open_cv的一个参数传进去,程序执行时,可以根据 choice值的不同,灵活地打开不同的查询内容。

PROCEDURE open_cv(generic_cv IN OUT genericcurtyp, choice NUMBER) IS

BEGIN

IF choice = 1 THEN

OPEN generic_cv FOR

SELECT *

FROM emp;

ELSIF choice = 2 THEN

OPEN generic_cv FOR

SELECT *

FROM dept;

ELSIF choice = 3 THEN

OPEN generic_cv FOR

SELECT *

FROM salgrade;

END IF;

...

END;

6、属性

PL/SQL的变量和游标都有着让我们能够直接引用它们各自的数据类型或结构的属性。数据库字段和表也有类似的属性。"%"是一个属性的指示符。

%TYPE

%TYPE可以提供一个变量或数据库字段的数据类型,这在声明存放数据库值的变量时是非常有用的。假设我们要声明一个存放表books中的字段my_title的字段的变量时,就可以这样使用%TYPE属性:

my_title %TYPE;

这样声明my_title有两个优点,第一,我们不必知道title具体类型;第二,如果我们改变了数据库中对该字段的数据类型定义的话,my_title的数据类型会在运行时做出相应的改变。

%ROWTYPE

在PL/SQL中,记录用于将逻辑相关数据组织起来。一个记录是由许多相关域的组合。%ROWTYPE属性返回一个记录类型,其数据类型和数据表的数据结构相一致。这样的记录类型可以完全保存从数据表中查询(SELECT)或从游标/游标变量取出(FETCH)的行记录。

行中的字段和记录中的域对应的名称和数据类型都完全一致。下面的例子中,我们声明一个dept_rec的记录。它的域名称和数据类型与表dept中的字段名称和数据类型就完全一样。

DECLARE

dept_rec dept%ROWTYPE; -- declare record variable

我们可以使用"."来引用记录中的域。

my_deptno := dept_;

假设我们声明了一个用于检索雇员的名字、工资、雇用日期和职称的游标,我们就可以使用%ROWTYPE来声明一个类型相同的记录,如下例:

DECLARE

CURSOR c1 IS

SELECT ename, sal, hiredate, job

FROM emp;

emp_rec c1%ROWTYPE; -- declare record variable that represents

-- a row fetched from the emp table

当我们执行语句

FETCH c1 INTO emp_rec;

表emp中ename字段的值就会赋给emp_rec的ename域,sal字段值赋给sal域,依此类推。

7、控制结构

流程控制是PL/SQL对SQL的最重要的扩展。PL/SQL不仅能让我们操作Oracle数据,还能让我们使用条件、循环和顺序控制语句来处理数据,如IF-THEN-ELSE,CASE,FOR-LOOP,WHILE-LOOP,EXIT-WHEN和GOTO等。

条件控制

我们经常需要根据环境来采取可选择的行动。IF-THEN-ELSE语句能让我们按照条件来执行一系列语句。IF用于检查条件;THEN决定在条件值为true的情况下执行;ELSE在条件值为false或null的情况才执行。

看一下下面的例子,这个程序用于处理银行事务。在我们从账户3取走$500元之前,它会先确认是否有足够的资金供我们支配。如果余额足够,程序就会更新账户(accounts)表的信息,否则的话,程序会向审核(audit)表插入一条余额不足的提示信息。

DECLARE

acct_balance NUMBER(11, 2);

acct CONSTANT NUMBER(4) := 3;

debit_amt CONSTANT NUMBER(5, 2) := 500.00;

BEGIN

SELECT bal

INTO acct_balance

FROM accounts

WHERE account_id = acct

FOR UPDATE OF bal;

IF acct_balance >= debit_amt THEN

UPDATE accounts

SET bal = bal - debit_amt

WHERE account_id = acct;

ELSE

INSERT INTO temp

VALUES (acct, acct_balance, 'Insufficient funds');

-- insert account, current balance, and message

END IF;

COMMIT;

END;

要从多个条件值中选出对应的操作时,我们可以使用CASE结构。CASE表达式会计算条件值,然后相应的操作(这个操作有可能是一个完整的PL/SQL块)。

CASE

WHEN shape = 'square' THEN

area := side * side;

WHEN shape = 'circle' THEN

BEGIN

area := pi *(radius * radius);

DBMS__line('Value is not exact because pi is irrational.');

END;

WHEN shape = 'rectangle' THEN

area := LENGTH * width;

ELSE

BEGIN

DBMS__line('No formula to calculate area of a' || shape);

RAISE PROGRAM_ERROR;

END;

END CASE;

循环控制

LOOP语句能让我们多次执行一系列语句。LOOP循环以关键字LOOP开头,END LOOP结尾i。下面语句就是最简单的LOOP循环:

LOOP

-- sequence of statements

END LOOP;

FOR-LOOP语句可以指定整数的范围,然后范围内每一个数字执行一次。例如,下面的循环将向数据库表插入500个数字和它们的平方根:

FOR num IN 1 .. 500 LOOP

INSERT INTO roots

VALUES (num, SQRT(num));

END LOOP;

WHILE-LOOP语句会按照某个条件值执行。每次循环条件值都会被计算一次,如果条件值为true,循环内的语句就会被执行,然后再次回到循环顶部,重新计算条件值。如果条件值为false或是null的话,循环就会停止,控制权被移交给下一语句。

下面的例子中,我们要找出第一个工资高于$2500的且行政级别高于雇员编号7499雇员的员工:

DECLARE

salary %TYPE := 0;

mgr_num %TYPE;

last_name %TYPE;

starting_empno %TYPE := 7499;

BEGIN

SELECT mgr

INTO mgr_num

FROM emp

WHERE empno = starting_empno;

WHILE salary <= 2500 LOOP

SELECT sal, mgr, ename

INTO salary, mgr_num, last_name

FROM emp

WHERE empno = mgr_num;

END LOOP;

INSERT INTO temp

VALUES (NULL, salary, last_name);

COMMIT;

EXCEPTION

WHEN NO_DATA_FOUND THEN

INSERT INTO temp

VALUES (NULL, NULL, 'Not found');

COMMIT;

END;

EXIT-WHEN语句可以在需要继续执行循环的情况下退出循环。当遇到EXIT语句时,WHEN子句中的条件值就会被计算。如果条件为true,循环就会结束,控制权交给下一条语句。下面的例子中,在total的值超过2500时,循环就会结束:

LOOP

...

total := total + salary;

EXIT WHEN total > 25000; -- exit loop if condition is true

END LOOP;

-- control resumes here

顺序控制

GOTO语句能让我们无条件地跳转到一个标签。标签就是一个用双尖括号夹起来的未声明标示符,它必须在一个可执行语句或是PL/SQL块之前。执行时,GOTO语句将控制权交给用标签作了标记的语句或块,如下例所示:

IF rating > 90 THEN

GOTO calc_raise; -- branch to label

END IF;

<>

IF job_title = 'SALESMAN' THEN -- control resumes here

amount := commission * 0.25;

ELSE

amount := salary * 0.10;

END IF;

8、模块化

模块化可以让我们把程序分成多个部分,这样可以把复杂的问题划分开来,更好的解决问题。PL/SQL为我们提供了块、子程序和包三个程序单元来用于模块化处理。

子程序

子程序有两种,分别是函数和过程。子程序很像一段小型的程序,由可选的声明部分,执行控制部分和可选的异常控制部分组成。

PROCEDURE award_bonus(emp_id NUMBER) IS

bonus REAL;

comm_missing EXCEPTION;

BEGIN -- executable part starts here

SELECT comm * 0.15

INTO bonus

FROM emp

WHERE empno = emp_id;

IF bonus IS NULL THEN

RAISE comm_missing;

ELSE

UPDATE payroll

SET pay = pay + bonus

WHERE empno = emp_id;

END IF;

EXCEPTION -- exception-handling part starts here

WHEN comm_missing THEN

...

END award_bonus;

调用时,这个过程接受一个雇员编号。它使用编号来查询雇员的commission,同时计算出15%的奖金。然后,它检查bonus总值。如果奖金为空,就会抛出异常,否则雇员的工资记录就会被更新。

PL/SQL可以让我们把逻辑相关的类型、变量、游标和子程序放在一个包内,这样更加清楚易理解。包通常有两部分组成:包说明部分和包体部分。包说明部分是应用程序的接口,它"声明"了类型、常量、变量、异常、游标和可以使用的子程序。包体用于实现包说明部分声明的子程序和游标。

下面的例子是把两个雇用相关的过程进行打包:

CREATE PACKAGE emp_actions AS -- package specification

PROCEDURE hire_employee(empno NUMBER, ename CHAR, ...);

PROCEDURE fire_employee(emp_id NUMBER);

END emp_actions;

CREATE PACKAGE BODY emp_actions AS -- package body

PROCEDURE hire_employee(empno NUMBER, ename CHAR, ...); IS

BEGIN

INSERT INTO emp

VALUES (empno, ename, ...);

END hire_employee;

PROCEDURE fire_employee(emp_id NUMBER) IS

BEGIN

DELETE FROM emp

WHERE empno = emp_id;

END fire_employee;

END emp_actions;

只有在包说明中声明的内容对应用程序是可见的,而包体中的内容是不可见,无法访问的。包被编译后会存放到数据库中,当我们首次调用包中的子程序时,整个包就会被加载到内存当中,所以,后续的调用不再涉及到磁盘的读取问题。因此,包可以提高效率改善性能。

9、数据抽象

数据抽象可以让我们把必要的属性提取出来,忽略那些非必须的细节问题,有助于我们更好地解决问题。一旦我们设计好一个数据结构,就可以不再考虑细节内容,而专注于操作这个数据结构的算法问题的研究。

集合

集合类型TABLE和VARRAY可以让我们声明索引表、嵌套表和变长数组(略称varray)。集合是类型相同的元素有序组合。在集合中,每个元素都有唯一一个能够确定该元素在集合中位置的下标索引。下面是嵌套表的一个例子:

DECLARE

TYPE staff IS TABLE OF employee;

staffer employee;

FUNCTION new_hires(hiredate DATE)

RETURN staff IS

BEGIN

...

END;

BEGIN

staffer := new_hires('10-NOV-98')(5);

...

END;

集合有些像三代语言中的数组,并且可以作为参数进行传递。

记录

我们知道,可以使用%ROWTYPE属性获取数据表中一行的记录类型,其实我们还可以定义自己的记录类型。

记录包含名称不可重复的域,域可以有不同的数据类型。假设我们设计了一个雇员记录类型,其中有名字、工资和雇用日期,这些项虽然类型不同,但逻辑上都是相关联的。看一下下面的例子:

TYPE timerec IS RECORD(

hours SMALLINT,

minutes SMALLINT

);

TYPE meetingtyp IS RECORD(

date_held DATE,

DURATION timerec, -- nested record

LOCATION VARCHAR2(20),

purpose VARCHAR2(50)

);

这里要注意的是,记录里可以嵌套记录类型。也就是说,记录本身也可以作为另一个记录的组成部分。

对象类型

PL/SQL中的面向对象编程是基于对象类型的。对象类型把数据和用于数据操作的函数和过程封装起来。其中,对象类型中的变量称为属性,函数和过程称为方法。对象类型是把大系统划分成多个逻辑实体来降低问题的复杂度,这就能使我们创建模块化、可维护和重用性好的组件了。我们在用CREATE TABLE定义对象类型的时候,常常是创建一个对真实世界对象的抽象的模板。如下面的银行账户例子中显示,模板只指定了应用程序的环境中会使用到的属性和方法:

CREATE TYPE bank_account AS OBJECT(

acct_number INTEGER(5),

balance REAL,

status VARCHAR2(10),

MEMBER PROCEDURE OPEN(amount IN REAL),

MEMBER PROCEDURE verify_acct(num IN INTEGER),

MEMBER PROCEDURE CLOSE(num IN INTEGER, amount OUT REAL),

MEMBER PROCEDURE deposit(num IN INTEGER, amount IN REAL),

MEMBER PROCEDURE withdraw(num IN INTEGER, amount IN REAL),

MEMBER FUNCTION curr_bal(num IN INTEGER)

RETURN REAL

);

运行时,当数据结构被赋值之后,我们就可以创建抽象的银行账户了。我们可以按照需求创建任意个实例(称为对象)。每个对象都有账号,余额和状态。

10、信息隐藏

有了信息隐藏,我们就可以只关心给定的设计级别的算法和数据结构设计。信息隐藏能把高阶的设计决定从频繁改变的低阶设计细节分离出来。

算法

我们可以通过自顶而下(top-down)的设计来实现算法隐藏。一旦我们明确了低阶过程的实现目的并定义好相应的接口说明,就可以忽略实现细节部分。例如,我们只需要知道将一个雇员的工资金额传递给过程raise_salary就可以提高该雇员的工资。任何对raise_salary方法的变动,对于应用程序来说,都是透明的。

数据结构

我们可以通过数据封装来实现信息隐藏。开发一组操作数据结构的工具子程序,就可以让使用它的用户和开发人员分离。这样一来,开发人员只需了解如何使用这些子程序来操作数据,并不需要知道数据真正的含义。

使用PL/SQL包,我们就可以指定哪些子程序是公有哪些是私有,更好的提供封装,简化维护。

11、错误控制

PL/SQL能够轻松的发现并处理预定义和用户定义的错误条件(即异常)。错误发生时,异常就会被抛出。也就是说,正常的执行会终止,程序控制权将交给PL/SQL块或子程序的异常处理部分。为控制被抛出的异常,我们需要单独编写异常控制句柄(即异常控制程序)。

预定义异常会被系统隐式地抛出,例如,用一个数字除以零,PL/SQL就会自动抛出预定义异常ZERO_DIVIDE。对于用户自定义异常,必须由我们显式地使用RAISE语句抛出。

我们可以在任何PL/SQL块或子程序的声明部分定义自己的异常。在执行部分,我们检查那些需要特别对待的条件,如果错误条件满足,就可以使用 RAISE抛出异常。在下面的例子中,我们要计算售货员的奖金。奖金的多少取决于他的工资(salary)和佣金(commission)。所以,如果佣金为空的话,我们就要抛出异常comm_missing。

DECLARE

...

comm_missing EXCEPTION; -- declare exception

BEGIN

...

IF commission IS NULL THEN

RAISE comm_missing; -- raise exception

END IF;

bonus := (salary * 0.10) +(commission * 0.15);

EXCEPTION

WHEN comm_missing THEN -- process the exception

...

END;

二、PL/SQL架构

PL/SQL的编码和运行时系统是一项技术,而不是一个独立的产品。可以把这项技术想象成一个能够编译并运行PL/SQL块和子程序的引擎。这个引擎可以安装在Oracle服务器上或安装在Oracle Forms,Oracle Reports这样的开发工具中。所以,PL/SQL可以在两种环境中存在:

1. Oracle数据库服务器

2. Oracle开发工具

这两种环境是独立的。PL/SQL虽被绑定到Oracle服务器上,但在某些工具中是无法使用的。在这两种环境下,PL/SQL引擎都能接受有效的 PL/SQL块或子程序。下图是PL/SQL引擎处理匿名块的过程,引擎会处理过程化语句,而把SQL语句发送给Oracle服务器端的SQL语句执行程序(SQL Statement

Executor)来处理。

1、Oracle数据库中

缺乏本地PL/SQL引擎的应用开发工具就必须依赖于Oracle来处理PL/SQL块和子程序。Oracle服务器除了能够处理SQL语句外,还会处理PL/SQL块和子程序,它会将块与子程序传给它本地的PL/SQL引擎。

匿名块

匿名PL/SQL块能被嵌到Oracle预编译程序(Oracle Precompiler)或是OCI程序中。运行的时候,不含PL/SQL引擎的程序会把这些块发送到Oracle服务器编译并执行。

存储过程

子程序可以独立编译并存储在Oracle数据库。使用CREATE语句显式创建的子程序就是一个"存储"子程序。一旦编译并保存到数据词典中,它就成了一个模式对象(schema object),可以被许多连到数据库的应用程序调用。

定义在包内的存储子程序称为打包子程序(packaged subprogram);单独定义的存储子程序称为独立子程序(standalone subprogram);而在另外一个子程序或PL/SQL块内定义的存储子程序称为本地子程序,这样的子程序不能被其他应用程序调用,只供本地使用。

存储过程执行效率高,耗内存少,应用集成,安全性好。例如,我们设计出一套存储过程和函数时,应用程序就可以调用这些函数和方法,这样就能避免大量的冗余代码提高效能。

我们可以从数据库触发器、其他存储子程序、Oracle预编译程序、OCI程序或是SQL*Plus等,调用存储子程序。例如,我们可以像下面这样从SQL*Plus中调用独立子程序create_dept:

SQL> CALL create_dept('FINANCE', 'NEW YORK');

子程序以分析、编译过的形式保存在数据库中。所以,在被调用时,它们会被立即加载并传递到PL/SQL引擎。并且,它们还会利用共享内存,这样,每次只要有一个子程序需要被加载到内存,就能被多个用户调用执行。

数据库触发器

数据库触发器是与数据库中某个数据表、视图或事件相关联的存储子程序。举一个例子,我们可以让Oracle数据库在INSERT、UPDATE或 DELETE表达式影响一个表之前或之后来自动激活一个触发器。触发器的用途之一就是审核数据修改。例如,下面的表级(table-level)触发器会在emp表的salaries字段更新后被激活。

CREATE TRIGGER audit_sal

AFTER UPDATE OF sal

ON emp

FOR EACH ROW

BEGIN

INSERT INTO emp_audit

VALUES ...

END;

触发器执行部分可以包括过程语句和SQL数据操作语句。除了表级触发器以外,还包含替代触发器(instead-of triggers for views)和系统触发器(system-event trigger)。

2、Oracle工具中

在包含PL/SQL引擎的条件下,应用开发工具就能够处理PL/SQL块和子程序。开发工具会把块传给它的本地PL/SQL引擎。引擎会在应用程序段执行所有的过程语句,只把SQL语句发送给Oracle。因此,大多部分工作会在应用程序端完成,而不是在服务器端。进一步说,如果块中不包含任何 SQL语句的话,这个引擎会在应用程序端执行全部的代码。

三、PL/SQL的优势

PL/SQL是一个可移植、高效的事务处理语言:

1. 支持SQL

2. 支持面向对象编程

3. 良好的性能

4. 效率高

5. 可移植

6. 与Oracle集成

7. 高度安全

1、SQL的支持

SQL因为它的灵活、强大和易学,已经成为标准的数据库语言。只要几个像SELECT、INSERT、UPDATE和DELETE这样简单的命令就能轻松地操作关系数据库中的数据。SQL是非过程化的,这就是说我们可以决定做我们想做的,但不能决定如何做。Oracle会决定处理我们请求的最佳方案。

PL/SQL能让我们使用所有的SQL数据操作,游标控制和事务控制命令;也可以使用所有的SQL函数,操作符和伪列。所以,我们可以灵活安全地操作Oracle数据。PL/SQL完全支持SQL数据类型,这就减少了我们的应用程序和数据库间数据传递时的类型转换。

PL/SQL也支持动态SQL语句,这样能够让我们的应用程序更加灵活通用。程序可以在运行时处理SQL数据定义、数据控制和会话控制语句。

2、面向对象的支持

对象类型是理想的面向对象建模工具,它能帮助我们创建复杂的应用程序。除了能创建模块化,易维护和重用性高的软件组件外,对象类型还可以让不同开发组的程序员并发地开发组件。

对象类型通过对数据操作的封装,把数据维护代码从SQL脚本和PL/SQL块中提取出来,放到独立的方法中去。同样,对象类型也可以隐藏实现,这样我们就在不影响客户端程序的情况下改变实现细节。

此外,真实世界的复杂实体和关系也能够映射到对象类型中去。这样我们的程序就能更好将模拟的内容反映出来。

3、良好的性能

如果没有PL/SQL的话,Oracle就必须每次接收一条SQL语句,然后处理。每条SQL语句都会调用一次Oracle,这就造成很大的运行开销。在网络环境中,这种现象就更加明显。如下图所示,如果应用程序与数据库之间交互频繁,那么就可以在向Oracle发送SQL语句之前使用PL/SQL 块和子程序把SQL语句组合起来。

有了PL/SQL,整块的语句就可以一次传递给Oracle,这样就能减少应用程序和Oracle的通信,减少网络开销,如下图所示,如果我们的应用程序与数据库的交互操作较多,那么就可以用PL/SQL块和子程序将SQL语句组织起来一次性地发送给Oracle执行。

PL/SQL块和子程序能够在编译成可执行的形式存放起来,所以调用存储过程是快速和高效的。而且,存储过程是在服务器端执行的,减少网络流量改善响应时间。可执行的代码会被自动地放到缓存然后在多个用户间共享。

我们也可以为Oracle工具提供PL/SQL的过程处理功能来改善性能。这样我们就可以直接利用工具来进行计算而不用调用Oracle服务器。这样会节省大量的时间和网络开销。

4、高效性

对于像Oracle Forms和Oracle Reports这样的非过程化工具,在加入了PL/SQL功能后,我们也可以使用熟悉的过程语句来建立应用程序。例如,我们可以在Oracle Form触发器中使用整块整块的PL/SQL块,而不必多次调用触发器,宏等。所有环境中的PL/SQL都是一样的。一旦我们掌握了PL/SQL,那么我们就可以在任何支持PL/SQL的工具中使用它。

5、可移植性

用PL/SQL编写的应用程序都可移植到Oracle运行的操作系统和平台。换句话说,PL/SQL程序可以在任何Oracle能够运行的地方而运行,因此,我们不必为每一个新环境定制一套新的PL/SQL程序。

6、与SQL紧密结合

PL/SQL和SQL语言是紧密结合的。PL/SQL支持所有的SQL数据类型和NULL。这样一来,我们操作Oracle数据就变得方便快捷。% TYPE和%ROWTYPE属性进一步的拉近PL/SQL和SQL的关系。例如,我们可以通过%TYPE属性,在已定义的数据库字段的基础上声明同类型的变量。如果数据库定义发生改

变的话,在下一次编译或运行时,变量的声明类型也会随之变化。这就减少了代码维护的开销,使程序自动地适应于新的业务需求。

7、高度安全

PL/SQL存储过程能使客户端和服务器端的逻辑分离,避免让客户端操作敏感的Oracle数据。用PL/SQL编写的触发器可以有选择性的允许应用程序更新数据,并可以根据已有的内容来审核用户的插入操作。

另外,我们还可以让用户只通过调用定义者权限的存储过程,来严格控制用户对Oracle数据的访问。例如,我们可以授权用户来调用更新数据表的存储过程,但不授权他们直接访问数据表的权限。

第二章 PL/SQL基础

一、字符集

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

1. 大小写字母(A-Z和a-z)

2. 数字(0-9)

3. 符号( ) + - * / < > = ! ~ ^ ; : . ’ @ % , " # $ & _ | { } ? [ ]

4. 制表符、空格和回车符

PL/SQL对大小写不敏感,所以,除了在字符串和字符中,小写字母和它对应的大写字母是等价的。

二、词法单元

PL/SQL包含很多词法单元(lexical unit),大致可以e分为以下几类:

1. 分隔符(简单符号和复合符号)

2. 标识符,其中包括关键字

3. 文字

4. 注释

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

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

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

count : = count + 1; -- not allowed

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

IF x > y tdEN

MAX := x;

IF x>y tdEN max:=x;ELSE max:=y;END IF; ELSE

MAX := y;

END IF;

1、分隔符

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

符号

+

含义

加法操作符

%

属性指示符

.

/

(

)

:

,

*

"

=

<

>

字符串分隔符

组件选择器

触法操作符

表达式或列表分隔符

表达式或列表分隔符

主变量指示符

分隔符

多应用程序操作符

引用标识符分隔符

关系操作符

关系操作符

关系操作符

@

远程访问指示符

;

-

语句终结符

减号/负号操作符

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

符号 含义

:=

赋值操作符

=>

管联操作符

||

连接操作符

**

求幂操作符

<<

标签分隔符(开始)

>>

标签分隔符(结束)

/*

多行注视分隔符(开始)

*/

多行注视分隔符(结束)

..

范围操作符

<>

关系操作符

!=

关系操作符

~=

关系操作符

^=

关系操作符

<=

关系操作符

>=

关系操作符

--

单行注释提示符

2、标识符

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

1. X

2. t2

3. phone#

4. credit_limit

5. LastName

6. oracle$number

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

1. mine&yours -- 不允许使用连字符(not allowed because of ampersand)

2. debit-amount -- 不允许使用连字符(not allowed because of hyphen)

3. on/off -- 不允许使用斜线(not allowed because of slash)

4. user id -- 不允许使用空格(not allowed because of space)

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

1. money$$$tree

2. SN##

3. try_again_

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

1. lastname

2. LastName -- 与lastname相同

3. LASTNAME -- 与lastname和Lastname相同

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

保留关键字

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

DECLARE

end BOOLEAN; -- not allowed; causes compilation error

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

DECLARE

end_of_game BOOLEAN; -- allowed

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

预定义标识

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

引用标识符

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

1. "X+Y"

2. "last name"

3. "on/off switch"

4. "employee(s)"

5. "*** header info ***"

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

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

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

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

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

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

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

还有一种做法就是可以建立视图来为原来的字段名更换一个新名。

3、文字

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

数字文字

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

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之间,就会产生编译错误:

DECLARE

n NUMBER;

BEGIN

n := 10E127; -- causes a 'numeric overflow or underflow' error

字符文字

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

'Z', '%', '7', ' ', 'z', '('

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

字符串文字

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

'Hello, world!'

'XYZ Corporation'

'10-NOV-91'

'He said "Life is like licking honey from a tdorn."'

'$1,000,000'

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

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

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

'baker'

'Baker'

布尔(Boolean)文字

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

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

DECLARE

d1 DATE := DATE '1998-12-25';

t1 TIMESTAMP := TIMESTAMP '1997-10-22 13:01:01';

t2 TIMESTAMP WItd TIME ZONE := TIMESTAMP '1997-01-31 09:26:56.66 +02:00';

-- tdree years and two montds

-- (For greater precision, we would use tde day-to-second interval)

i1 INTERVAL YEAR TO MONtd := INTERVAL '3-2' YEAR TO MONtd;

-- Five days, four hours, tdree minutes, two and 1/100 seconds

i2 INTERVAL DAY TO SECOND := INTERVAL '5 04:03:02.01' DAY TO SECOND;

...

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

current_timestamp - current_timestape

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

1. (interval_expression) DAY TO SECOND

2. (interval_expression) YEAR TO MONtd

4、注释

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

单行注释

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

-- begin processing

SELECT sal INTO salary

FROM emp -- get current salary

WHERE empno = emp_id;

bonus := salary * 0.15; -- compute bonus amount

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

-- DELETE FROM emp WHERE comm IS NULL;

多行注释

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

BEGIN

...

/* Compute a 15% bonus for top-rated employees. */

IF rating > 90 tdEN

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;

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

/*

LOOP

FETCH c1

INTO emp_rec;

EXIT WHEN c1%NOTFOUND;

...

END LOOP;

*/

三、声明

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

birtdday DATE;

emp_count SMALLINT := 0;

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

pi REAL := 3.14159;

radius REAL := 1;

area REAL := pi * radius ** 2;

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

birtdday DATE;

birtdday DATE := NULL;

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

credit_limit CONSTANT REAL := 5000.00;

常量在声明的时候必须进行初始化,否则就会产生编译错误。

1、使用DEFAULT

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

blood_type CHAR := 'o';

就可以用DEFAULT来替换:

blood_type CHAR DEFAULT 'o';

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

2、使用NOT NULL

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

acct_id INTEGER(4) NOT NULL := 9999;

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

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

NATURALN和POSITIVEN是PL/SQL提供的两个不可为空的预定义子数据类型。下面这两个声明是等价的:

emp_count NATURAL NOT NULL := 0;

emp_count NATURALN := 0;

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

line_items POSITIVEN; -- not allowed; not initialized

3、使用%TYPE

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

credit REAL(7, 2);

debit credit%TYPE;

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

my_dname %TYPE;

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

DECLARE

my_empno %TYPE;

...

BEGIN

my_empno := NULL; -- tdis works

4、使用%ROWTYPE

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

DECLARE

emp_rec emp%ROWTYPE;

CURSOR c1 IS

SELECT deptno, dname, loc FROM dept;

dept_rec c1%ROWTYPE;

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

emp_ := 'JOHNSON';

emp_ := emp_ * 1.15;

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

CREATE PACKAGE emp_actions AS

CURSOR c1 RETURN emp%ROWTYPE; -- declare cursor specification

...

END emp_actions;

CREATE PACKAGE BODY emp_actions AS

CURSOR c1 RETURN emp%ROWTYPE IS -- define cursor body

SELECT * FROM emp WHERE sal > 3000;

...

END emp_actions;

聚合赋值

用%ROWTYPE作声明的时候是不可以进行初始化赋值的,但是有两种方法可以一次性为所有字段赋值。方法一:假如两个记录类型的声明引用了同一数据表或游标,那么它们就可以相互赋值,如:

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;

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

dept_rec2 := dept_rec3; -- not allowed

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

DECLARE

dept_rec dept%ROWTYPE;

...

BEGIN

SELECT * INTO dept_rec FROM dept WHERE deptno = 30;

...

END;

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

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

使用别名

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

DECLARE

CURSOR my_cursor IS

SELECT sal + NVL(comm, 0) 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;

IF my_ > 2000 tdEN

INSERT INTO temp VALUES (NULL, my_, my_);

END IF;

END LOOP;

CLOSE my_cursor;

END;

5、声明的约束

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

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

mini INTEGER := 15;

但是,PL/SQL允许向前声明子程序。

对于同样数据类型的每一个变量,都必须单独声明:

i SMALLINT;

j SMALLINT;

k SMALLINT;

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

i, j, k SMALLINT; -- not allowed

四、PL/SQL命名规范

同样的命名规约适用于所有的PL/SQL程序,规约涉及的内容包括常量、变量、游标、异常、过程、函数和包。命名可能是简单的,加以限定的,远程的或是既加以限定又是远程的。例如,我们也许可能用到以下几种调用过程raise_salary的方式:

raise_salary(...); -- simple

emp__salary(...); -- qualified

raise_salary@newyork(...); -- remote

emp__salary@newyork(...); -- qualified and remote

第一种情况,我们只是简单的使用程序名称。第二种情况,我们必须使用点标志(dot notation)来引用过程,因为它是保存在emp_actions包中的。第三种情况,使用远程访问指示符,就能引用数据库连接newyork,因为过程是存放在远程数据库的。第四中情况,我们在过程名称加上限定修饰词并引用数据库连接。

同义词

我们可以创建同义词来隐藏远程模式对象的位置,其中包括表、视图、序列、存储函数、包、和对象类型。但是,我们不能为子程序或包中声明的内容创建同义词,其中包括常量、变量、游标变量、异常和打包子程序。

作用域

同一作用域内声明的标识符都必须是唯一的。所以,即使它们的数据类型不同,变量和参数也不能享用同一名称。下例中,第二个声明是不允许的:

valid_id BOOLEAN;

valid_id VARCHAR2 (5); -- not allowed duplicate identifier

大小写敏感

像所有的标识符一样,常量、变量和参数的名称都是大小写不敏感的。例如,PL/SQL认为下面的名称都是相同的:

zip_code INTEGER;

zip_code INTEGER; -- same as zip_code

命名解析

在SQL语句中,数据库字段名称的优先级要高于本地变量和形式参数。例如,下面的DELETE语句会从emp表删除所有的雇员信息,而不只是名字为"KING"的雇员:

DECLARE

ename VARCHAR2 (10) := 'KING';

BEGIN

DELETE FROM emp

WHERE ename = ename;

...

在这种情况下,为了避免产生歧义,可以像下面这样在本地变量和形式参数的前面加上类似于"my_"这样的前缀:

DECLARE

my_ename VARCHAR2(10);

或是使用块标签来进行引用限定:

<

>

DECLARE

ename VARCHAR2 (10) := 'KING';

BEGIN

DELETE FROM emp

WHERE ename = ;

...

下面的例子演示了如何使用子程序名称来限定对本地变量和形式参数的引用:

FUNCTION bonus (deptno IN NUMBER, ...) RETURN REAL IS

job CHAR(10);

BEGIN

SELECT ... WHERE deptno = AND job = ;

...

五、PL/SQL标识符的作用域(scope)和可见度(visiblity)

对标识符的引用可以通过它的作用域和可见度来进行解析。标识符的作用域就是我们引用标识符的程序单元区域(块,子程序或包)。一个标识符只在它的作用域内可见,我们可以在作用域内不使用限定词而直接引用它。下图演示了变量x的作用域和可见度。x首先被声明在封闭块中,然后又在子块中重新定义。

PL/SQL块中声明的标识符对于其所在块来说是本地的,对于子块来说是全局的。如果全局标识符在子块中被重新声明,那么,全局和本地声明的标识符在子块的作用域都是存在的,但是,只有本地标识符是可见的,这时如果想引用全局标识符,就需要添加限定修饰词。

虽然我们不能在同一块中两次声明同一标识符,但可以在两个不同的块中声明同一标识符。这两个标识符是互相独立的,对其中任何一个的改变都不会影响到另一个。但是,一个块不能引用同一级别中另外一个块中的变量,因为对于它来说,同级块中标识符即不是本地的,又不是全局的。

下面的例子演示了作用域规则:

DECLARE

a CHAR;

b REAL;

BEGIN

-- identifiers available here: a (CHAR), b

DECLARE

a INTEGER;

c REAL;

BEGIN

-- identifiers available here: a (INTEGER), b, c

END;

DECLARE

d REAL;

BEGIN

-- identifiers available here: a (CHAR), b, d

END;

-- identifiers available here: a (CHAR), b

END;

如果子块中重新声明了全局标识符,本地标识符优先权高于全局标识符,我们就不能再引用全局标识符,除非使用限定名(qualified name)。修饰词可以是封闭块的标签,如下例所示:

<>

DECLARE

birtddate DATE;

BEGIN

DECLARE

birtddate DATE;

BEGIN

...

IF birtddate = ate tdEN

...

END IF;

...

END;

...

END;

如下例所示,限定修饰词也可以是封闭子程序的名称:

PROCEDURE check_credit(...) IS

rating NUMBER;

FUNCTION valid(...)

RETURN BOOLEAN IS

rating NUMBER;

BEGIN

...

IF check_ < 3 tdEN ...

END;

BEGIN

...

END;

但是,在同一作用域内,标签和子程序不能使用相同的命名。

六、变量赋值

变量和常量都是在程序进入块或子程序的时候被初始化的。默认情况下,变量都是被初始化成NULL的。除非我们为变量指定一个值,否则结果是未知的。请看下面的例子:

DECLARE

count INTEGER;

BEGIN

-- COUNT began witd a value of NULL.

-- tdus tde expression ’COUNT + 1’ is also null.

-- So after tdis assignment, COUNT is still NULL.

count := count + 1;

为了避免这样的情况,就要保证在赋值之前不要使用这个变量。

我们可以使用表达式来为变量赋值,例如下面的语句为变量bonus赋值:

bonus := salary * 0.15;

这里,我们需要保证的是salary * 0.15计算结果的类型必须和bonus类型保持一致。

1、布尔型(Boolean)赋值

只有TRUE、FALSE和NULL才可以赋给布尔类型的变量。例如:

BEGIN

done := FALSE;

WHILE NOT done LOOP

...

END LOOP;

当表达式中使用关系操作符的时候,返回结果也是布尔类型的值,所以下面的语句也是允许的。

done := (count > 500);

2、利用SQL查询为PL/SQL变量赋值

我们可以使用SELECT语句让Oracle为变量赋值。对于查询字段中的每一项,在INTO子句的后面都必须有与之对应的类型兼容的变量。看一下下面这个例子:

DECLARE

emp_id %TYPE;

emp_name %TYPE;

wages NUMBER(7,2);

BEGIN

-- assign a value to emp_id here

SELECT ename, sal + comm INTO emp_name, wages

FROM emp

WHERE empno = emp_id;

...

END;

但是,上面的用法不可以为布尔类型变量赋值。

七、PL/SQL表达式与比较

表达式由操作数和操作符构成。一个操作数就是一个变量、常量、文字或是能够返回一个值的函数。下面是一个简单的数学表达式:

-X / 2 + 3

像负号(-)这样的只作用于一个操作数的操作符称为一元操作符;而像除号(/)这样作用于两个操作数的操作符称为二元操作符。PL/SQL没有三元操作符。

最简单的表达式就是一个能直接算出值的变量。PL/SQL按照指定的操作符和操作数来计算表达式的值,结果值的数据类型是由表达式所在的关联文决定的。

由于操作符的运算优先级不同,表达式的计算顺序也是不一样的。下表是默认的操作符优先级顺序。

操作符

**

+, -

*, /

+, -, ||

运算

求幂

正,负

乘,除

加,减,连接

=, <, >, <=, >=, <>, !=, ~=, ^=,

比较

IS NULL, LIKE, BETWEEN, IN

NOT

AND

OR

逻辑非

优先级高的操作符会比优先级低的操作符先求值。下例中,两个表达式都能计算出结果8来,因为除号的优先级要高于加号。优先级相同的操作符不会采取特殊的计算顺序。

5 + 12 / 4

12 / 4 + 5

我们可以使用括号控制计算顺序。例如,下面的表达式值是7,而不是11,因为括号覆盖了默认的操作符优先顺序:

(8 + 6) / 2

再看一个例子。下面的运算中,减法会在除法之前被计算,这是因为最深层的表达式总是第一个被计算的:

100 + (20 / 5 + (7 - 3))

最后,我们看看如何使用括号来改善可读性,即使不是在必须使用括号的时候:

(salary * 0.05) + (commission * 0.25)

1、逻辑操作符

逻辑操作符有AND、OR和NOT,其中AND和OR是二元操作符,而NOT是一元操作符。下面是对应操作的真值表。

x y x AND y x OR y NOT x

TRUE TRUE FALSE

TRUE FALSE

TRUE FALSE

TRUE TRUE

TRUE FALSE FALSE

TRUE NULL NULL

FALSE TRUE FALSE TRUE TRUE

FALSE FALSE FALSE FALSE TRUE

FALSE NULL FALSE

NULL TRUE NULL

NULL TRUE

TRUE NULL

NULL NULL

NULL NULL

NULL FALSE FALSE

NULL NULL NULL

如上面的真值表所示,AND只在操作符两边的操作数都是真的情况才返回TRUE。另一方面,OR操作符两边的操作数只要有一个值为真就能返回TRUE。NOT会返回操作数相反的值。例如NOT TRUE返回FALSE。

这里需要注意的地方是,由于NULL是一个不确定的值,所以NOT NULL的值也是无法确定的。

运算顺序

当我们不用括号指定计算顺序的时候,操作符的优先级就会决定操作数的计算顺序。比较下面两个表达式:

NOT (valid AND done) NOT valid AND done

如果布尔变量valid和done的值都是FALSE,那么第一个表达式的结果就为TRUE。但是,第二个表达式的结果却是FALSE,因为NOT的优先级要比AND高。因此,第二个表达式就等价于:

(NOT valid) AND done

在下面的例子中,当valid的值为FALSE,不论done值是多少,整个表达式的值总为FALSE:

valid AND done

同样,当下例中的valid的值为TRUE时,不论done值是多少,整个表达式的值总为TRUE:

valid OR done

短路计算

在计算逻辑表达式时,PL/SQL使用的是短路计算方法。也就是说,PL/SQL在结果可以确定下来的时候,就不会再继续计算表达式的值了。看一下下面这个例子:

DECLARE

...

on_hand INTEGER;

on_order INTEGER;

BEGIN

...

IF (on_hand = 0) OR ((on_order / on_hand) < 5) tdEN

...

END IF;

END;

当on_hand的值是零的时候,操作符OR左面的操作数结果为TRUE,所以PL/SQL就不需要计算右边的值了。如果PL/SQL是在应用OR操作符之前计算两个操作数的值的话,那么右边的操作数就会产生一个除零的错误。不管怎样,依赖于"短路"计算不是一个好习惯。

比较操作符

比较操作符用于将一个表达式与另一个表达式进行比较。结果是TRUE或FALSE或NULL。最常见的就是我们在条件控制语句和SQL数据操作语句中的WHERE子句中使用比较操作符。例如:

IF quantity_on_hand > 0 tdEN

UPDATE inventory

SET quantity = quantity - 1

WHERE part_number = item_number;

ELSE

...

END IF;

关系操作符

关系操作符可以让我们随意比较复杂的表达式。下面的表格列出了各种关系操作符的含义。

操作符

=

含义

等于

<>, !=, ~=, ^=

不等于

<

>

<=

>=

小于

大于

小于等于

大于等于

IS NULL 操作符

如果IS NULL所作用的操作数为空,则返回结果TRUE,否则返回结果FALSE。与空值作比较,结果总是空。所以,无论什么时候跟空值作比较,都要使用IS NULL操作符:

IF variable IS NULL tdEN ...

LIKE操作符

我们可以使用LIKE操作符来判断一个字符、字符串或CLOB类型的值是不是与我们指定的样式相匹配。如果样式匹配,LIKE就会返回TRUE,否则返回FALSE。用于LIKE匹配的样式中,包含两种通配符。下划线(_):精确匹配一个字符;百分号(%):匹配零个或多个字符。如下面的例子中,如果ename的值是"JOHNSON",那么表达式结果就为TRUE:

ename LIKE 'J%SON'

BETWEEN操作符

BETWEEN操作符用于判断目标值是否在指定的目标范围内。例如,下面表达式的结果就为FALSE:

45 BETWEEN 38 AND 44

IN操作符

IN操作符是用于测试目标值是否是集合成员之一。其中,集合是可以包含NULL值的,但它们是被忽略的。例如,下面这个语句并不会删除ename值为NULL的行:

DELETE FROM emp

WHERE ename IN (NULL, 'KING', 'FORD');

此外,如果集合中包含了NULL值,下面表达式的运算结果就是FALSE。

value NOT IN set

所以,下面这个表达式也不会删除任何行:

DELETE FROM emp

WHERE ename NOT IN (NULL, 'king');

连接操作符

双竖线(||)可以当作字符连接操作符,可以将两个字符串(CHAR、VARCHAR2、CLOB或等价的Unicode支持的类型)连接起来。例如表达式

'suit' || 'case'

返回的结果就是

'suitcase'

如果操作符两边的操作数都是CHAR类型,连接操作符返回的结果就是CHAR值。如果其中一个是CLOB值,操作符就返回临时CLOB。其余情况均返回VARCHAR2类型。

2、布尔表达式

PL/SQL允许我们在SQL语句和过程语句中比较变量和常量。这样的比较称为布尔表达式,它们是由用关系操作符分割开的简单或复杂表达式组成。通常,布尔表达式是由逻辑操作符AND、OR或NOT连接。布尔表达式的运算结果总是TRUE、FALSE或NULL。

在SQL语句中,布尔表达式能让我们指定一个表中哪些行记录可以被影响。在过程语句中,布尔表达式是条件控制的基础。其中有三种布尔表达式:算术、字符和日期。

布尔算术表达式

我们可以使用关系表达式来比较两个数字等或不等。例如,下面的表达式结果就为真:

number1 := 75;

number2 := 70;

number1 > number2 -- TRUE

布尔字符表达式

我们也可以比较字符的等或不等。默认情况下,比较都是基于字符串中每个字节的二进制值的。比如,下面例子中的表达式结果就为真:

string1 := 'Katdy';

string2 := 'Katdleen';

string1 > string2 -- TRUE

设置初始化参数NLS_COMP=ANSI,就能使用初始化参数NLS_SORT指定的整理序列(collating

sequence)来进行比较。整理序列是一个字符集中表现字符的数字代码(numeric code)的内部顺序,如果一个字符的数字代码比另一个大,那这个字符就比另一个字符大。关于字符在整理序列中出现的位置,每种语言都可能有不同的定义规则。比如说,重音字母可能会因数据库的字符集的不同而排序不同,即使每一种情况下的二进制值都相同。

布尔日期表达式

对于日期类型的比较,是按照年代的顺序的。如下例,date1的值是大于date2的值的。

date1 := '01-JAN-91';

date2 := '31-DEC-90';

date1 > date2 -- TRUE

关于PL/SQL的布尔表达式使用的一些建议

一般地,不要把实型数字用于精确比较。实型数字一般都是按近似值存储的。所以,下面的表式式值并不等于TRUE:

COUNT := 1;

IF COUNT = 1.0 tdEN

...

END IF;

在作比较时使用括号是一个好习惯。例如,下面的这样的表达式形式是不允许的,因为 100 < tax 的结果是布尔型,而布尔型是不能和数字500进行比较的。

100 < tax < 500 -- not allowed

解决方法是使用下面这样的表达式:

(100 < tax) AND (tax < 500)

对于布尔型的变量来说,它的值要么为TRUE要么为FALSE,因此,对布尔型变量应用比较操作是多余的。对于下面的内容:

WHILE NOT(done = TRUE) LOOP

...

END LOOP;

可以简化为:

WHILE NOT done LOOP

...

END LOOP;

对COLB类型应用比较操作符或是用LIKE和BETWEEN这样的函数时,可能会产生临时LOB。我们就得确保有足够大的表空间来容纳这些临时LOB。

3、CASE表达式

一个CASE表达式从一个或多个供选方案中选择一个返回结果。CASE表达式使用一个选择器来决定返回哪一个分支的结果。具体的语法形式如下:

CASE selector

WHEN expression1 tdEN result1

WHEN expression2 tdEN result2

...

WHEN expressionn tdEN resultn

[ELSE resultN+1]

END;

选择器后面跟着一个或多个WHEN子句,它们会被依次验证的。一旦有一个WHEN子句满足条件的话,剩下的分支条件就不再执行了。例如:

DECLARE

grade CHAR(1) := 'B';

appraisal VARCHAR2(20);

BEGIN

appraisal := CASE grade

WHEN 'A' tdEN 'Excellent'

WHEN 'B' tdEN 'Very Good'

WHEN 'C' tdEN 'Good'

WHEN 'D' tdEN 'Fair'

WHEN 'F' tdEN 'Poor'

ELSE 'No such grade'

END;

END;

其中,ELSE子句是可选的,工作方式同IF语句中的ELSE子句相似。如果我们不提供ELSE子句,并且选择器没有匹配任何WHEN子句,表达式的返回的结果就是NULL。

这种形式的CASE表达式的另外一种使用方法就是CASE语句,其中每个WHEN子句都可以是一个完整的PL/SQL块。

搜索式CASE表达式

PL/SQL也提供了搜索式的CASE表达式,它的语法形式如下:

CASE

WHEN expression1 tdEN result1

WHEN expression2 tdEN result2

...

WHEN expressionn tdEN resultn

[ELSE resultN+1]

END;

搜索式CASE表达式没有选择器。每个WHEN子句包含一个能返回布尔值的搜索条件。例子如下:

DECLARE

grade CHAR(1);

appraisal VARCHAR2(20);

BEGIN

...

appraisal := CASE

WHEN grade = 'A' tdEN 'Excellent'

WHEN grade = 'B' tdEN 'Very Good'

WHEN grade = 'C' tdEN 'Good'

WHEN grade = 'D' tdEN 'Fair'

WHEN grade = 'F' tdEN 'Poor'

ELSE 'No such grade'

END;

...

END;

搜索条件按顺序计算。搜索条件的布尔值决定了哪个WHEN子句被执行。如果搜索条件的值为TRUE,它对应的WHEN子句就会被执行。只要其中一个 WHEN子句被执行,后续的搜索条件就不会被计算了。如果没有匹配的条件,可选的ELSE就会被执行。如果没有匹配的WHEN子句,也没有ELSE子句,表达式的结果就为NULL。

4、在比较和条件语句中处理NULL值

在使用NULL值时,我们一定要记住下面几条规则,避免发生一些常见的错误:

1. 比较中如果有空值的话,那么计算结果总为NULL

2. 对空值应用逻辑操作符NOT,结果还是NULL

3. 条件控制语句中,如果条件的运算结果值为NULL的话,与之相关的语句就不会被执行

4. 简单CASE语句中对于空值的判断要使用WHEN expression IS NULL

下例中,我们期待的是sequence_of_statements被执行,因为x和y看起来就是不等的。但是,由于NULL是不确定的值,那么,x是否等于y也就无法确定了。所以,sequence_of_statements并不会执行。

x := 5;

y := NULL;

...

IF x != y tdEN -- yields NULL, not TRUE

sequence_of_statements; -- not executed

END IF;

同样,下例中的sequence_of_statements也不会被执行:

a := NULL;

b := NULL;

...

IF a = b tdEN -- yields NULL, not TRUE

sequence_of_statements; -- not executed

END IF;

NOT操作符

让我们再回忆一下逻辑操作符NOT,当对一个NULL值应用NOT时,结果总是NULL。因此,下面两段内容并不相同。

IF x > y tdEN IF NOT x > y tdEN

high := x; high := y;

ELSE

END IF;

ELSE

END IF;

high := y; high := x;

当IF条件值为FALSE或NULL时,ELSE部分就会被执行。如果x和y都不为NULL的话,两段程序运行的效果是一样的。但是,如果IF条件为NULL的话,第一段是给y赋值,而第二段是给x赋值。

零长度字符串

PL/SQL把零长度字符串当作空值处理,这其中包括由字符函数和布尔表达式返回的值。下面的语句均是给目标变量赋空值的操作:

null_string := TO_CHAR('');

zip_code := SUBSTR(address, 25, 0);

valid :=(NAME != '');

所以,对于检测空字符串,要使用IS NULL操作符:

IF my_string IS NULL tdEN ...

连接操作符

连接操作符会忽略空值,例如表达式

'apple' || NULL || NULL || 'sauce'

会返回

'applesauce'

函数

如果给内置函数传递空值,一般也都会返回空值,但以下几种情况除外。

函数DECODE将它的第一个参数和后面的一个或多个表达式相比较(表达式的值有可能为空),如果比较的内容相匹配,就会返回后面的结果表达式。例如在下面的例子中,如果字段rating的值为空,DECODE就会返回1000:

SELECT DECODE(rating,

NULL, 1000,

'C', 2000,

'B', 4000,

'A', 5000

)

INTO credit_limit

FROM accts

WHERE acctno = my_acctno;

函数NVL在判断出第一个参数是空的情况下,会返回第二个参数的值,否则直接返回第一个参数的值。使用方法如下:

start_date := NVL(hire_date, SYSDATE);

函数REPLACE第二个参数是NULL的时候,它就会返回第一个参数的值,不管是否有第三个参数。例如,在下面例子中,结果字符串new_string的值和old_string的值完全一样。

new_string := REPLACE(old_string, NULL, my_string);

如果第三个参数为空的话,REPLACE就会把第一个参数中出现的第二个参数删除,然后返回结果。如下面这个例子:

syllabified_name := 'gold - i - locks';

NAME := REPLACE(syllabified_name,

' - ',

NULL

);

运算的结果字符串是"goldilocks"。如果第二个和第三个参数都是NULL值,REPLACE就直接返回第一个参数。

八、内置函数

PL/SQL为我们提供了许多功能强大的数据操作函数。这些函数可以分为以下几类:

1. 错误报告

2. 数字

3. 字符

4. 类型转换

5. 日期

6. 对象引用

7. 杂项

下面的表格是各个分类的函数。

错误

SQLCODE

SQLERRM

数字 字符 转换

CHARTOROWID

CONVERT

日期

对象引用

DEREF

杂项

ABS ASCII ADD_MONtdS BFILENAME

ACOS CHR CURRENT_DATE REF DECODE

ASIN CONCAT HEXTORAW

CURRENT_TIMESTVALUAMP

DBTIMEZONE

E

TREAT

DUMP

ATAN INITCAP

ATAN2

BITAND

RAWTOHEX

ROWIDTOCHAR

TO_BLOB

TO_CHAR

TO_CLOB

EMPTY_BLOB

INSTR EXTRACT EMPTY_CLOB

INSTRB FROM_TZ

LAST_DAY

GREATEST

LEAST

NLS_CHARSET_DECL_LEN

NLS_CHARSET_ID

CEIL LENGtd

COS LENGtdB LOCALTIMESTAMP

MONtdS_BETWEEN

NEW_TIME

COSH LOWER TO_DATE

TO_MULTI_BYTE

TO_NCLOB

EXP

FLOOR

LN

LPAD NLS_CHARSET_NAME

LTRIM

NLS_INITCAP

NEXT_DAY

NUMTODSINTERVAL

NVL

TO_NUMBER SYS_CONTEXT

LOG

NLS_LOWETO_SINGLE_BNUMTOYMINTERVR YTE AL

ROUND

SESSIONTIMEZONE

SYSDATE

SYSTIMESTAMP

TO_DSINTERVAL

SYS_GUID

UID

USER

MOD NLSSORT

POWENLS_UPPER

ROUND

R

REPLACE

USERENV

VSIZE

SIGN RPAD

SIN RTRIM

SINH SOUNDEX

SQRT SUBSTR

TO_TIMESTAMP

TO_TIMESTAMP_LTZ

TO_TIMESTAMP_TZ

TO_YMINTERVAL

TAN SUBSTRB

TRANSLATE

TRIM

UPPER

TANH

TRUNC

TZ_OFFSET

TRUNC

除了错误报告(error-reporting)函数SQLCODE和SQLERRM之外,我们可以在SQL语句中使用上面所有的函数。同样,我们可以在过程表达式中使用除了对象引用函数DEFREF、REF、VALUE和杂函数(miscellaneous function)DECODE、DUMP、VSIZE之外的所有函数。

虽然SQL聚合函数(aggregate function,如AVG和COUNT)和SQL解析函数(analytic function,如CORR和LAG)没有组合到PL/SQL中,但我们仍可以在SQL语句中使用它们(但不能在过程语句中使用)。

第三章 PL/SQL数据类型

一、预定义数据类型

Oracle的数据类型可以分为四类,分别是标量类型,复合类型,引用类型和LOB类型。标量类型没有内部组件;而复合类型包含了能够被单独操作的内部组件;引用类型类似于3G语言中的指针,能够引用一个值;LOB类型的值就是一个lob定位器,能够指示出大对象(如图像)的存储位置。

下图是在PL/SQL中可以使用的预定义类型,其中标量类型又分为四类:数字、字符、布尔和日期/时间。

1、数字型

数字类型可以存储整数、实数和浮点数,可以表示数值的大小,参与计算。

BINARY_INTEGER

我们可以使用BINARY_INTEGER数据类型来存储有符号整数。它的范围是-2**31至2**31。跟PLS_INTEGER一样, BINARY_INTEGER所需的存储空间也要小于NUMBER。但是,大多数的BINARY_INTEGER操作要比PLS_INTEGER操作慢。

BINARY_INTEGER子类型

所谓的基类型,就是有子类型继承于它。子类型在基类型的基础上添加一些约束限制,也可能重新定义数值范围。为了使用方便,PL/SQL预定义了下面几个BINARY_INTEGER的子类。

1. NATURAL


本文标签: 使用 类型 语句 声明 变量