一、什么是子程序
子程序就是能够接受参数并被其他程序所调用的命名PL/SQL块。PL/SQL子程序有两种类型,过程和函数。一般地,过程用于执行一个操作,而函数用于计算一个结果值。
与未命名或匿名PL/SQL块一样,子程序也有声明部分,执行部分和一个可选的异常处理部分。声明部分包含类型、游标、常量、变量、异常和嵌套子程序的声明。这些内容都是本地的,在程序退出时会自动销毁。执行部分包含赋值语句、流程控制语句和Oracle的数据操作语句。异常处理部分包含异常处理程序。思考下面用于记入借方银行账户的debit_account过程:
PROCEDURE?debit_account(acct_id?INTEGER,?amount?REAL)?IS
??old_balance???REAL;
??new_balance???REAL;
??overdrawn?????EXCEPTION;
BEGIN
??SELECT?bal
????INTO?old_balance
????FROM?accts
???WHERE?acct_no?=?acct_id;
??new_balance????:=?old_balance?-?amount;
??IF?new_balance?<?0?THEN
????RAISE?overdrawn;
??ELSE
????UPDATE?accts
???????SET?bal?=?new_balance
?????WHERE?acct_no?=?acct_id;
??END?IF;
EXCEPTION
??WHEN?overdrawn?THEN
????...
END?debit_account;
在被调用时,这个过程接受一个银行账号和借贷金额。它使用账号从accts表中查询账目结算信息。然后用借款金额计算新的账目结算。如果计算后的余额比零小,异常就会被抛出;否则,该账号相关信息就会被更新。
二、子程序的优点
子程序能提供扩展性,它能够让我们根据自己的需求来编写特定的PL/SQL。比如,我们需要一个能够创建新部门的过程,就可以像下面这样编写代码:
PROCEDURE?create_dept(new_dname?VARCHAR2,?new_loc?VARCHAR2)?IS
BEGIN
??INSERT?INTO?dept
???????VALUES?(deptno_seq.NEXTVAL,?new_dname,?new_loc);
END?create_dept;
子程序还能提供模块化,就是说它可以把一个程序定义成多个模块,更易管理。这样,我们就可以用自顶而下的设计(top-down design)和逐步求精(stepwise refinement)的方法来解决问题。
此外,子程序在提高程序的重用性和可维护方面也是很有用的。只要编译成功,子程序就可以放心地用在很多应用程序中。如果它的定义内容发生了改变,受到影响的只有子程序本身而已,这就简化了维护过程。最后,子程序还有助于逻辑的抽象。使用子程序时,我们需要知道的是它们的功能,而不是它们实现功能的细节问题。
三、理解PL/SQL过程
过程是一个能执行某个特定操作的子程序。我们可以用下面的语法来编写过程:
[CREATE?[OR?REPLACE]]
PROCEDURE?procedure_name[(parameter[,?parameter]...)]
??[AUTHID?{DEFINER?|?CURRENT_USER}]?{IS?|?AS}
??[PRAGMA?AUTONOMOUS_TRANSACTION;]
??[local?declarations]
BEGIN
??executable?statements
[EXCEPTION
??exception?handlers]
END?[name];
parameter的含义如下:
parameter_name?[IN?|?OUT?[NOCOPY]?|?IN?OUT?[NOCOPY]]?datatype
[{:=?|?DEFAULT}?expression]
CREATE子句能让我们创建保存在数据库中的独立过程。我们可以从SQL*Plus中或是在使用动态SQL的程序中执行CREATE PROCEDURE语句。
AUTHID子句决定了存储过程是按所有者权限(默认)调用还是按当前用户权限执行,也能决定在没有限定修饰词的情况下,对所引用的对象是按所有者模式进行解析还是按当前用户模式进行解析。我们可以指定CURRENT_USER来覆盖掉程序的默认行为。
编译指示AUTONOMOUS_TRANSACTION会告诉PL/SQL编译器把过程标记为自治(独立)。自治事务能让我们把主事务挂起,执行SQL操作,提交或回滚自治事务,然后再恢复主事务。
我们不能对参数的数据类型进行约束,如下例中对acct_id的声明就是不合法的,因为它对CHAR类型进行了长度限制:
PROCEDURE?reconcile?(acct_id?CHAR(5))?IS?...???--?illegal
但是,我们可以使用下面的方法间接的对字符的长度进行限制:
DECLARE
??SUBTYPE?Char5?IS?CHAR(5);
??PROCEDURE?reconcile?(acct_id?Char5)?IS?...
过程有两个部分,过程说明和过程体。说明部分由关键字PROCEDURE开头,以过程名或参数列表结尾。参数声明是可选的。没有参数的过程是不用使用圆括号的。
过程体由关键字IS(或AS)开头,并以END结尾,END后面可以跟上一个可选的过程名。过程体有三个部分:声明、执行和可选的异常处理。
声明部分包括本地声明,它处于IS和BEGIN之间。在匿名PL/SQL块使用的关键字DECLARE在这里不再需要。执行部分包括许多语句,它们被放到BEGIN和EXCEPTION(或END)之间,并且至少要有一条语句出现在过程的执行部分。NULL语句可以满足这个需求。异常处理部分包含异常处理程序,它被放在关键字EXCEPTION和END之间。
在下面的过程raise_salary中,我们会根据给定的金额来为雇员加薪:
PROCEDURE?raise_salary(emp_id?INTEGER,?amount?REAL)?IS
??current_salary???REAL;
??salary_missing???EXCEPTION;
BEGIN
??SELECT?sal
????INTO?current_salary
????FROM?emp
???WHERE?empno?=?emp_id;
??IF?current_salary?IS?NULL?THEN
????RAISE?salary_missing;
??ELSE
????UPDATE?emp
???????SET?sal?=?sal?+?amount
?????WHERE?empno?=?emp_id;
??END?IF;
EXCEPTION
??WHEN?NO_DATA_FOUND?THEN
????INSERT?INTO?emp_audit
?????????VALUES?(emp_id,?'No?such?number');
??WHEN?salary_missing?THEN
????INSERT?INTO?emp_audit
?????????VALUES?(emp_id,?'Salary?is?null');
END?raise_salary;
在调用时,过程接受雇员编号和薪资调整金额,然后用雇员编号从emp表找出指定雇员的当前工资。如果雇员编号无法找到或是当前工资为空,异常就会被抛出,否则工资就会被更新。
过程可以作为一个PL/SQL语句来调用。例如,我们可以像下面这样调用raise_salary:
raise_salary(emp_id,?amount);
四、理解PL/SQL函数
函数是一个能够计算结果值的子程序,函数除了有一个RETURN子句之外,其它结构跟过程类似。我们可以用下面的语法来编写(本地)函数:
[CREATE?[OR?REPLACE?]?]
??FUNCTION?function_name?[?(?parameter?[?,?parameter?]...?)?]?RETURN?datatype
??[?AUTHID?{?DEFINER?|?CURRENT_USER?}?]
??[?PARALLEL_ENABLE
??[?{?[CLUSTER?parameter?BY?(column_name?[,?column_name?]...?)?]?|
??[ORDER?parameter?BY?(column_name?[?,?column_name?]...?)?]?}?]
??[?(?PARTITION?parameter?BY
??{?[?{RANGE?|?HASH?}?(column_name?[,?column_name]...)]?|?ANY?}
??)?]
??]
??[DETERMINISTIC]?[?PIPELINED?[?USING?implementation_type?]?]
??[?AGGREGATE?[UPDATE?VALUE]?[WITH?EXTERNAL?CONTEXT]
??USING?implementation_type?]?{IS?|?AS}
??[?PRAGMA?AUTONOMOUS_TRANSACTION;?]
??[?local?declarations?]
BEGIN
??executable?statements
[?EXCEPTION
??exception?handlers?]
END?[?name?];
函数的语法结构与过程类似,这里就不再重复。但有几个不同点还是需要注意的。
PARALLEL_ENABLE选项能声明一个在并发DML操作的从属会话(slave session)中被安全调用的存储函数。主(logon)会话的状态不会被从属会话所共享。每个从属会话都有它自己的状态,这是在会话开始时初始化的。函数的结果不应依赖于会话(静态)变量的状态。否则结果就可能随着会话而发生变化。
提示DETERMINISTIC能帮助优化程序避免冗余的函数调用。如果存储函数的调用跟前一次调用时所使用的参数相同,优化程序就直接选出前一次的计算结果值。函数结果不应该依赖于会话变量或模式对象的状态。否则结果会随着调用而发生变化。只有DETERMINISTIC函数才允许被函数索引或是参数query_rewrite_enabled为TRUE的实体化视图调用。
我们不能对参数或是函数返回值的类型添加约束,但可以像前面的过程那样使用间接的约束方法。
思考下面的函数sal_ok,它的作用是检查工资是否超出限定范围:
FUNCTION?sal_ok(salary?REAL,?title?VARCHAR2)
??RETURN?BOOLEAN?IS
??min_sal???REAL;
??max_sal???REAL;
BEGIN
??SELECT?losal,?hisal
????INTO?min_sal,?max_sal
????FROM?sals
???WHERE?job?=?title;
??RETURN?(salary?>=?min_sal)?AND(salary?<=?max_sal);
END?sal_ok;
调用时,函数接受雇员的工资金额和职别,然后使用职别从数据表sals选择工资范围。函数标识符sal_ok由RETURN语句返回的布尔值赋值。如果salary超过限定范围,sal_ok值就是FALSE,否则就是TRUE。
函数可以作为表达式的一部分而被调用,如下例所示。函数标识符sal_ok就像一个变量一样,它的值是由传递进去的参数所决定的。
IF?sal_ok(new_sal,?new_title)?THEN?...
1、使用RETURN语句
RETURN语句能够立即结束当前执行的子程序并把控制权交还给调用者。然后程序继续执行子程序调用之后的语句。(不要把RETURN语句和函数声明中的RETURN子句搞混淆了,声明中的RETURN只是用于指明函数返回值的数据类型。)
子程序能包含几个RETURN语句。最后一个语句不一定非得是RETURN语句。只要执行RETURN语句就能立即结束当前子程序。但是,在一个子程序包含有多个出口点不是一个好习惯。
在过程中,RETURN语句不能返回值,也不能返回任何表达式。它的作用只是在过程到达正常的过程结尾之前将控制权交给调用者。
但是,在函数中,RETURN语句必须包含一个表达式,该表达式的值会在RETURN语句执行时被计算。计算的结果赋给函数标识符,标识符的作用相当于RETURN说明中的类型的变量。观察下面返回指定银行账户的余额的函数balance:
FUNCTION?balance(acct_id?INTEGER)
??RETURN?REAL?IS
??acct_bal???REAL;
BEGIN
??SELECT?bal
????INTO?acct_bal
????FROM?accts
???WHERE?acct_no?=?acct_id;
??RETURN?acct_bal;
END?balance;
下例演示了如何在RETURN语句中使用复杂的表达式:
FUNCTION?compound(years?NUMBER,?amount?NUMBER,?rate?NUMBER)
??RETURN?NUMBER?IS
BEGIN
??RETURN?amount?*?POWER((rate?/?100)?+?1,?years);
END?compound;
函数中,至少要有一条执行路径能够到达RETURN语句。否则,在运行时就会得到函数没有返回值的错误。
2、控制PL/SQL子程序的副影响
为了能在SQL中被调用,存储函数必须遵守以下"纯度"规则,这些规则能控制函数副作用:
- 当从SELECT语句或是并发的INSERT、UPDATE、DELETE语句中调用函数时,它不能修改任何数据表。
- 当从INSERT、UPDATE或DELETE语句中调用函数时,它不能查询或修改这些语句所能影响到的数据表。
- 当从SELECT、INSERT、UPDATE或DELETE语句中调用函数时,它不能执行SQL事务控制语句(如COMMIT),会话控制语句(如SET ROLE)或系统控制语句(如ALTER SYSTEM)。同样,它也不能执行数据定义语句(如CREATE),因为这些语句都是自动提交事务的。
如果函数内部任何一条SQL语句与上述规则相冲突,我们就会在运行时得到错误(语句被分析的时候)。
为了检查这些冲突项,我们可以使用编译指示RESTRICT_REFERENCES(编译器指令)。编译指示能判断函数是否读写数据表或包中的变量。例如在下面的函数中,编译指示就能判断出函数credit_ok不写数据库(WNDS)也不读取包(RNPS):
CREATE?PACKAGE?loans?AS
??...
??FUNCTION?credit_ok?RETURN?BOOLEAN;
??PRAGMA?RESTRICT_REFERENCES?(credit_ok,?WNDS,?RNPS);
END?loans;
注意:一个静态的INSERT、UPDATE或DELETE语句总是与WNDS相冲突的;如果读取了数据库字段,它也会与RNDS冲突。一个动态INSERT、UPDATE或DELETE语句总与WNDS和RNDS冲突。
五、声明PL/SQL子程序
我们可以在PL/SQL块、子程序或包中声明子程序。但是,子程序只能在其他内容声明之后再声明。
PL/SQL需要我们先声明标识然后才能引用它们。所以,在使用子程序之前必须要先声明。例如,下面对过程award_bonus的声明就是非法的,因为它在过程calc_rating未声明之前就开始调用它:
DECLARE
??...
??PROCEDURE?award_bonus?IS
??BEGIN
??calc_rating(...);???--?undeclared?identifier
????...
??END;
??PROCEDURE?calc_rating?(...)?IS
??BEGIN
????...
??END;
这种情况下,我们只要简单地把过程calc_rating放到award_bonus之前就可以了。但是简单的做法不一定总是有效的,如:两个过程相互引用或是我们就想按逻辑或字母顺序来定义他们。
我们可以使用"向前声明"来解决这个问题,向前声明由子程序说明和一个分号组成。在下面的例子中,向前声明通知PL/SQL,过程calc_rating的体部分可以在块的后面找到。
DECLARE
??PROCEDURE?calc_rating?(?...?);???--?forward?declaration
??...
虽然形式参数列表在向前声明中已经出现过了,但它也必须出现在子程序体中。子程序体可以放在向前声明之后的任何地方,但它们必须出现在同一个程序单元中。
六、子程序打包
我们可以把逻辑相关的子程序打包后放到数据库中。那样,子程序就能被许多应用程序共享。子程序说明部分放在包说明部分;子程序体放在包体,子程序体对应用程序是不可见的。因此,包能帮助我们隐藏程序的实现细节。如下例:
CREATE?PACKAGE?emp_actions?AS???--?package?spec
??PROCEDURE?hire_employee(emp_id?INTEGER,?NAME?VARCHAR2,?...);
??PROCEDURE?fire_employee(emp_id?INTEGER);
??PROCEDURE?raise_salary(emp_id?INTEGER,?amount?REAL);
??...
END?emp_actions;
CREATE?PACKAGE?BODY?emp_actions?AS???--?package?body
??PROCEDURE?hire_employee(emp_id?INTEGER,?NAME?VARCHAR2,?...)?IS
??BEGIN
????...
????INSERT?INTO?emp
?????????VALUES?(emp_id,?NAME,?...);
??END?hire_employee;
??PROCEDURE?fire_employee(emp_id?INTEGER)?IS
??BEGIN
????DELETE?FROM?emp
??????????WHERE?empno?=?emp_id;
??END?fire_employee;
??PROCEDURE?raise_salary(emp_id?INTEGER,?amount?REAL)?IS
??BEGIN
????UPDATE?emp
???????SET?sal?=?sal?+?amount
?????WHERE?empno?=?emp_id;
??END?raise_salary;
??...
END?emp_actions;
我们还能够直接在包体内定义子程序而不用在包说明部分编写它们的说明。但是,这样的子程序只能在包内的使用。
七、形参VS实参
子程序使用参数来传递信息。调用时子程序参数列表中引用的变量或表达式是实际参数(actual parameter,以下简称实参)。例如,下面的过程列出了两个实参emp_num和amout:
raise_salary(emp_num,?amount);
下面的过程调用演示了用表达式作为实参:
raise_salary(emp_num,?merit?+?cola);
子程序声明和子程序体中引用的变量是形式参数(formal parameter,以下简称形参)。例如,下面的过程声明了两个形参emp_id和amount:
PROCEDURE?raise_salary(emp_id?INTEGER,?amount?REAL)?IS
BEGIN
??UPDATE?emp
?????SET?sal?=?sal?+?amount
???WHERE?empno?=?emp_id;
END?raise_salary;
好的编程习惯就是使用不同命名的形参和实参。
调用过程raise_salary时,实参的内容会被赋值到对应的形参上,如果有必要的话,在赋值之前PL/SQL会帮助我们进行类型转换。例如,下面对raise_salary的调用就是有效的:
raise_salary(emp_num,?'2500');
实参和它对应的形参必须类型兼容,例如,PL/SQL是不能把数据从DATE类型转到REAL类型的。下面的过程调用就会引起预定义异常VALUE_ERROR,因为PL/SQL不能把第二个实参转成一个数字:
raise_salary(emp_num,?'$2500');???--?note?the?dollar?sign
八、位置标示法VS名字标示法
在调用子程序时,我们既可以使用位置标示法又可以使用名字标示法来编写实参。也就是说,我们可以按位置或名称来把实参和形参关联起来。如下例所示:
DECLARE
??acct???INTEGER;
??amt????REAL;
??PROCEDURE?credit_acct(acct_no?INTEGER,?amount?REAL)?IS?...
我们可以使用四种等价的方法来调用过程credit_acct:
BEGIN
??credit_acct(acct,?amt);???--?positional?notation
??credit_acct(amount?=>?amt,?acct_no?=>?acct);???--?named?notation
??credit_acct(acct_no?=>?acct,?amount?=>?amt);???--?named?notation
??credit_acct(acct,?amount?=>?amt);???--?mixed?notation
1、使用位置标示法
第一个过程调用使用了位置标示法。PL/SQL编译器将第一个实参acct和第一个形参acct_no关联,并把第二个实参amt和第二个形参amount关联。
2、使用名字标示法
第二个过程调用使用了名字标示法。箭头(=>)作为关联操作符,把左边的实参和右边的形参关联起来。
第三个过程调用也使用了名字标示法,而且我们可以随意安排参数的位置。所以,我们不需要知道形参的在参数列表中的顺序。
3、使用混合标示法
第四个过程调用使用了名字标示法和位置标示法。在这种情况下,位置标示法必须在名字标示法之前,不能反过来使用,像下面这样的调用方法就是不合法的:
credit_acct(acct_no?=>?acct,?amt);???--?illegal
九、指定子程序参数模式
我们可以使用参数的模式来定义形式参数的行为。一共有三种模式:IN、OUT和IN OUT。但是,最好避免在函数中使用OUT和IN OUT模式。函数的作用是用来接受零个或多个参数然后返回一个值。用函数返回多个值不是个好习惯。同样,函数应该避免产生负影响,那样会改变那些对子程序来说是非本地的变量值。
1、使用IN模式
IN模式能让我们把值传递给被调用子程序,在子程序中,IN模式参数的作用就像常量一样。因此,它不能被赋值。例如,下面的赋值语句就会引起编译错误:
PROCEDURE?debit_account(acct_id?IN?INTEGER,?amount?IN?REAL)?IS
??minimum_purchase???CONSTANT?REAL?DEFAULT?10.0;
??service_charge?????CONSTANT?REAL?DEFAULT?0.50;
BEGIN
??IF?amount?<?minimum_purchase?THEN
????amount????:=?amount?+?service_charge;???--?causes?compilation?error
??END?IF;
??...
END?debit_account;
IN模式形参对应的实参可以是常量、文字、被初始化的变量或是表达式。与OUT和IN OUT模式的参数不同,我们可以为IN模式的参数初始化一个默认值。
2、使用OUT模式
OUT模式的参数能让我们把值返回给子程序的调用者。在子程序中,OUT模式参数的作用就像变量。这也就意味着我们可以把它当作本地变量来使用,例如:
PROCEDURE?calc_bonus(emp_id?IN?INTEGER,?bonus?OUT?REAL)?IS
??hire_date???????DATE;
??bonus_missing???EXCEPTION;
BEGIN
??SELECT?sal?*?0.10,?hiredate
????INTO?bonus,?hire_date
????FROM?emp
???WHERE?empno?=?emp_id;
??IF?bonus?IS?NULL?THEN
????RAISE?bonus_missing;
??END?IF;
??IF?MONTHS_BETWEEN(SYSDATE,?hire_date)?>?60?THEN
????bonus????:=?bonus?+?500;
??END?IF;
??...
EXCEPTION
??WHEN?bonus_missing?THEN
????...
END?calc_bonus;
与OUT模式的形参对应的实参必须是变量;它不能是常量或表达式。例如,下面的调用就不合法:
calc_bonus(7499,?salary?+?commission);???--?causes?compilation?error
一个OUT实参在子程序调用之前是可以有值的。但是,在子程序调用时,这个值就会丢失,除非我们使用了NOCOPY编译器提示或是子程序因未捕获异常而终止。
与变量一样,OUT模式的形参会被初始化为NULL.所以,一个OUT模式的形参的数据类型是不能有NOT NULL约束的(包括内置类型NATURALN和POSITIVEN)。否则的话,PL/SQL就会抛出VALUE_ERROR异常,见下例:
DECLARE
??SUBTYPE?counter?IS?INTEGER?NOT?NULL;
??ROWS???counter?:=?0;
??PROCEDURE?count_emps(n?OUT?counter)?IS
??BEGIN
????SELECT?COUNT(*)
??????INTO?n
??????FROM?emp;
??END;
BEGIN
??count_emps(ROWS);???--?raises?VALUE_ERROR
在子程序退出之前,它必须要显式地为所有的OUT模式形参赋值。否则对应的实参值就为空。如果成功地退出子程序,PL/SQL就会把值赋给实参。但是,如果有未捕获异常发生,PL/SQL就不会为实参赋值。
3、使用IN OUT模式
一个IN OUT模式的参数能让我们把它的初始值传递给被调用的子程序,然后再把子程序更新后的值传递给调用者。在子程序中,一个IN OUT模式参数的作用就像一个初始化了的变量。因此,它能够被赋值,而且它的值还可以赋给其他的变量。
与IN OUT模式形参对应的实参必须是变量;它不可以是常量或表达式。如果成功地退出子程序,PL/SQL就会为实参赋值。但是,如果有未捕获异常发生,PL/SQL就不会为实参赋值。
4、子程序参数模式总结
下表总结了我们应该知道关于参数模式的所有内容:
默认 | 必须被指定 | 必须被指定 |
向子程序传值 | 向调用者返回值 | 向子程序传递初始值并向调用者返回 更新后的结果值 |
形参的作用同常量相同 | 形参的作用同变量相同 | 形参的作用同被初始化过的变量相同 |
形参不能被赋值 | 形参必须被赋值 | 形参应该被赋值 |
实参可以是常量、被初始化的变量、 文字或表达式 | 形参必须是变量 | 形参必须是变量 |
形参按引用传递 | 形参按值传递,除非使用了NOCOPY | 形参按值传递,除非使用了NOCOPY |
十、使用NOCOPY编译提示传递大型数据结构
假定子程序声明了一个IN模式参数、一个OUT模式参数和一个IN OUT模式参数。在调用子程序时,IN模式的是按引用传递的,即把指向IN模式的实参指针赋给形参。所以,两个参数引用都指向同一块内存地址,这块内存存放了实参的值。
默认情况下,OUT和IN OUT模式的参数都是按值传递的。就是把实参的值拷贝到对应的形参上。然后,如果子程序正常结束,被赋到OUT和IN OUT形参上的值就会拷贝到对应的实参上。
当参数是大型数据结构时,如集合、记录和对象实例,把它们的内容全部拷贝给形参会降低执行速度,消耗大量内存。为了防止这样的情况发生,我们可以使用NOCOPY提示来让编译器按引用传递OUT和IN OUT模式的参数。在下面的例子中,我们请求编译器按引用的方式来传递IN OUT参数my_staff:
DECLARE
??TYPE?Staff?IS?VARRAY(200)?OF?Employee;
??PROCEDURE?reorganize?(my_staff?IN?OUT?NOCOPY?Staff)?IS?...
记住,NOCOPY只是一个提示,而不是指令。所以,编译器也许仍旧会把my_staff按值传递,即使我们已经发出请求了。但是,通常情况下 NOCOPY是可以成功的。下例中,我们把一个含有25000条记录的本地嵌套表中分别传递给两个没有任何功能的过程。没有使用NOCOPY的记录花费 21秒,而使用的花费不到1秒:
SQL>?SET?SERVEROUTPUT?ON
SQL>?GET?test.sql
1?DECLARE
2?TYPE?EmpTabTyp?IS?TABLE?OF?emp%ROWTYPE;
3?emp_tab?EmpTabTyp?:=?EmpTabTyp(NULL);?--?initialize
4?t1?NUMBER(5);
5?t2?NUMBER(5);
6?t3?NUMBER(5);
7?PROCEDURE?get_time?(t?OUT?NUMBER)?IS
8?BEGIN?SELECT?TO_CHAR(SYSDATE,'SSSSS')?INTO?t?FROM?dual;?END;
9?PROCEDURE?do_nothing1?(tab?IN?OUT?EmpTabTyp)?IS
10?BEGIN?NULL;?END;
11?PROCEDURE?do_nothing2?(tab?IN?OUT?NOCOPY?EmpTabTyp)?IS
12?BEGIN?NULL;?END;
13?BEGIN
14?SELECT?*?INTO?emp_tab(1)?FROM?emp?WHERE?empno?=?7788;
15?emp_tab.EXTEND(24999,?1);?--?copy?element?1?into?2..25000
16?get_time(t1);
17?do_nothing1(emp_tab);?--?pass?IN?OUT?parameter
18?get_time(t2);
19?do_nothing2(emp_tab);?--?pass?IN?OUT?NOCOPY?parameter
20?get_time(t3);
21?dbms_output.put_line('Call?Duration?(secs)');
22?dbms_output.put_line('--------------------');
23?dbms_output.put_line('Just?IN?OUT:?'?||?TO_CHAR(t2?-?t1));
24?dbms_output.put_line('With?NOCOPY:?'?||?TO_CHAR(t3?-?t2));
25*?END;
SQL>?/
Call?Duration?(secs)
--------------------
Just?IN?OUT:?21
With?NOCOPY:?0
1、权衡NOCOPY所带来的良好性能
NOCOPY能为我们带来良好的性能,但它也能带来以下几个方面的影响:
- 因为NOCOPY只是一个提示,不是指令,所以编译器可以把NOCOPY参数按值或按引用的方式传递给子程序。所以,如果子程序因发生未捕获异常而退出时,我们就不能再信赖实参中的值了。
- 默认地,如果子程序异常退出,赋给OUT和IN OUT参数的值就不会拷贝到对应的实参上,这看起来有点像回滚操作。但是,对于按引用传递的NOCOPY参数来说,我们对形参所作的更改会立即在对应的实参上体现出来。所以,即使子程序是因异常发生而结束,它所做的变更内容也不会"回滚"。
- 目前,RPC协议允许我们只按值传递参数。例如,如果我们把一个含有NOCOPY参数的本地过程传到远程站点,这些参数就不再按引用传递了。
还有,使用NOCOPY会增加参数别名出现的可能性。
2、NOCOPY的限制
在以下几种情况中,PL/SQL编译器会忽略NOCOPY提示而直接使用按值传递参数的方法(不发生错误的情况下):
- 实参是索引表中的一个元素。这个限制并不适用于整个索引表。
- 实参是受约束的(如精度或NOT NULL等)。这个约束不会扩展到元素或属性。同样,对长度受限的字符串也不适用。
- 实参和形参都是记录,其中一个或两个使用了%ROWTYPE或%TYPE声明,且在记录中对应域的约束不同。
- 实参和形参都是记录,实参是作为游标FOR循环的索引而被声明的(隐式声明),记录之间对应域的约束不同。
- 实参传递需要进行隐式地数据类型转换。
- 子程序被外部或远程过程调用。
十一、使用子程序参数的默认值
如下例所示,我们可以为IN模式的参数初始化默认值。这样,我们就可以把不同个数的参数传给子程序,其中既可以使用参数默认值又可以使用我们传入的参数值覆盖掉默认值。并且,我们还可以在不修改每个子程序调用的情况下添加新的参数。
PROCEDURE?create_dept(
??new_dname???VARCHAR2?DEFAULT?’temp’,
??new_loc?????VARCHAR2?DEFAULT?’temp’
)?IS
BEGIN
??INSERT?INTO?dept
???????VALUES?(deptno_seq.NEXTVAL,?new_dname,?new_loc);
??...
END;
如果实参没有被传入,它所对应的形参就会使用定义时的默认值。下面是对过程create_dept的调用:
create_dept;
create_dept('MARKETING');
create_dept('MARKETING',?'NEW?YORK');
第一个调用没有传入任何实参,所以子程序会使用两个默认的参数值;而第二个调用只为第一个参数指定了实参,这样,子程序会使用第一个参数传入的值和第二个参数的默认值;最后一个调用接受两个实参,它们将对应的形参默认值覆盖,子程序就使用两个传入的值。通常我们使用位置标示法覆盖形参的默认值,但是,我们不能靠省略实参来跳过它们对应的形参。例如,像下面这样把实参值"NEW YORK"和形参new_dname关联的做法是不对的:
create_dept('NEW?YORK');???--?incorrect
create_dept(,?'NEW?YORK');???--?not?allowed
我们也不可以靠放置一个占位符来解决这个问题。例如,下面的调用就是不允许的:
create_dept(,?'NEW?YORK');???--?not?allowed
当出现这种情况,我们就需要使用名字标示法:
create_dept(new_loc?=>?'NEW?Y