本章说明 PL/SQL 编译器如何优化代码,以及如何编写高效的 PL/SQL 代码和改进现有的 PL/SQL 代码。
另请参阅
《数据库开发指南》,了解游标变量的缺点
PL/SQL 程序运行缓慢的最常见原因是 SQL 语句缓慢。要使 PL/SQL 程序中的 SQL 语句尽可能高效: - 使用适当的索引。
有关详细信息,请参阅《数据库性能调优指南》。
使用查询提示来避免不必要的全表扫描。
有关详细信息,请参阅《数据库 SQL 语言参考》。
使用以下方法分析 SQL 语句的执行计划和性能:
EXPLAIN 语句
有关详细信息,请参阅《数据库性能调优指南》。
具有调试SQL能力的工具KWR
有关详细信息,请参阅《数据库性能调优指南》。
使用批量 SQL,这是 PL/SQL 功能,可最大限度地减少 PL/SQL 和 SQL 之间通信的性能开销。
有关详细信息,请参阅“批量 SQL 和批量绑定”。
查询中调用的函数可能会运行数百万次。应该尽量避免不必要的函数,使调用尽可能高效。
在查询中的表上创建基于函数的索引。CREATE INDEX 语句可能需要一段时间,但查询可以运行得更快,因为每行的函数值都以缓存。
如果查询中将表上的一列传递给函数,则该查询不能在该列上使用用户创建的索引,因此查询可能会为表的每一行(可能非常大)调用该函数。 要最小化函数调用的数量,请使用嵌套查询。 让内部查询将结果集过滤为少量行,并让外部查询仅为这些行调用函数。
另请参阅
:ref:`数据库 SQL 语言参考`以获取有关CREATE INDEX语句语法
示例 13-1 嵌套查询提高了性能
在此示例中,两个查询生成相同的结果集,但第二个查询比第一个查询更有效。 (在示例中,时间和时间差非常小,因为 STUDENT 表非常小。对于非常大的表,它们会很重要。)
\set SQLTERM / DECLARE begin_time TIMESTAMP; end_time TIMESTAMP; BEGIN begin_time := clock_timestamp(); FOR item IN ( SELECT DISTINCT(score) FROM student ORDER BY score ) LOOP dbms_output.put_line('student score: '|| item.score); END LOOP; raise notice 'time = %', clock_timestamp() - begin_time; begin_time := clock_timestamp(); FOR item IN ( SELECT score FROM (SELECT DISTINCT score FROM student) ORDER BY score ) LOOP IF item.score IS NOT NULL THEN dbms_output.put_line('student score: '|| item.score); END IF; END LOOP; raise notice 'time = %', clock_timestamp() - begin_time; END; / \set SQLTERM ;
结果类似于:
NOTICE: time = 00:00:00.007212 NOTICE: time = 00:00:00.004924 ANONYMOUS BLOCK
因为 PL/SQL 应用程序通常是围绕循环构建的,所以优化循环本身和其中的代码非常重要。
如果您必须多次遍历结果集,或者在遍历结果集时发出其他查询,则可以更改原始查询以得到想要的结果。 可以结合多个查询的 SQL 集合运算符,如 《SQL 语言参考中》所述。
同时还可以使用子查询在多个阶段进行过滤和排序——请参阅:ref:使用子查询处理查询结果集。
另请参阅
批量 SQL 和批量绑定
13.1.4.1. 在性能关键代码中避免约束子类型
在性能关键代码中,避免使用受约束的子类型(在“受约束的子类型”中描述)。 对受约束子类型的变量或参数的每次分配都需要在运行时进行额外检查,以确保要分配的值不违反约束。
另请参阅
:ref:`PL/SQL 预定义数据类型`包括预定义的约束子类型
13.1.4.2. 最小化隐式数据类型转换
在程序运行时,如有必要,PL/SQL 会隐式(自动)在不同的数据类型之间进行转换。例如,如果您将变量分配给PLS_INTEGER变量NUMBER,那么 PL/SQL 会将PLS_INTEGER值转换为NUMBER值(因为值的内部表示不同)。
尽可能减少隐式转换。例如:
如果要将变量插入表列或从表列中分配值,则为变量赋予与表列相同的数据类型。
提示
使用 %TYPE 属性声明变量,:ref:`%TYPE`属性中所述。
使每个文本的数据类型与分配给它的变量或它出现的表达式具有相同的数据类型。
将值从 SQL 数据类型转换为 PL/SQL 数据类型,然后在表达式中使用转换后的值。
例如,将NUMBER值转换为PLS_INTEGER值,然后PLS_INTEGER在表达式中使用这些值。
在将一种 SQL 数据类型的值分配给另一种 SQL 数据类型的变量之前,使用 SQL 转换函数将源值显式转换为目标数据类型(有关 SQL 转换函数的信息,请参阅《数据库 SQL 语言参考》)。
使用接受不同数据类型参数的版本重载您的子程序,并针对其参数类型优化每个版本。有关重载子程序的信息,请参阅:ref:重载子程序。
另请参阅
《数据库 SQL 语言参考》,了解有关 SQL 数据类型(也是 PL/SQL 数据类型)的隐式转换的信息
具有相同数据类型族中基类型的子类型
SQL 有许多高度优化的字符函数,它们使用比 PL/SQL 代码更高效的低级代码。使用这些函数而不是编写 PL/SQL 代码来做同样的事情。
参考:
《数据库 SQL 语言参考》,了解有关返回字符值的 SQL 字符函数的信息
《数据库 SQL 语言参考》,了解有关返回数值的 SQL 字符函数的信息
PL/SQL 在确定结果后立即停止计算逻辑表达式。 尽可能将最简单的条件放在逻辑表达式中,从而利用这种短路求值。 例如,在测试函数返回值之前测试 PL/SQL 变量的值,这样如果变量测试失败,PL/SQL 就不需要调用函数:
IF bool_variable OR (number > 100) OR bool_function(parameter) END IF ...
另请参阅
短路评估
批量 SQL最小化了 PL/SQL 和 SQL 之间通信的性能开销。构成批量 SQL 的 PL/SQL 特性是 FORALL 语句和 BULK COLLECT 子句。为出现在 SQL 语句中的 PL/SQL 变量赋值称为绑定。
PL/SQL 和 SQL 通信如下:为了运行 SELECT INTO 或 DML 语句,PL/SQL 引擎将查询语句或 DML 语句发送到 SQL 引擎。SQL 引擎运行查询语句或 DML 语句并将结果返回给 PL/SQL 引擎。
FORALL 语句将 DML 语句从 PL/SQL 分批发送到 SQL,而不是一次一条语句。 BULK COLLECT 子句将结果从 SQL 分批返回到 PL/SQL,而不是一次返回一条结果。 如果查询语句或 DML 语句影响四条或更多结果集,则批量 SQL 可以显着提高性能。
注意
不能对远程表执行批量 SQL。
PL/SQL 绑定操作分为以下几类:
绑定类别 |
绑定条件 |
---|---|
内绑定 |
INSERT、UPDATE 或 MERGE 语句在 PL/SQL 中或在 host 变量中使用时 |
外绑定 |
INSERT、UPDATE 或 DELETE 语句的 RETURNING INTO 子句将数据库值分配给 PL/SQL 或 host 变量时 |
定义 |
当 SELECT 或 FETCH 语句将数据库值分配给 PL/SQL 或 host 变量时 |
对于内绑定和外绑定,批量 SQL 使用批量绑定;也就是说,它一次绑定整个值的集合。 对于 n 个元素的集合,批量 SQL 使用单个操作来执行相当于 n 个 SELECT INTO 或 DML 语句的操作。 使用批量 SQL 的查询可以返回任意数量的行,而无需为每一行使用 FETCH 语句。
注意
并行 DML 不能使用批量 SQL
FORALL 语句是批量 SQL 的一项功能,它将 DML 语句从 PL/SQL 批量发送到 SQL,而不是一次一条语句。
要理解 FORALL 语句,首先考虑:ref:示例 13-2 中的 FOR LOOP 语句。 它一次将这些 DML 语句从 PL/SQL 发送到 SQL:
DELETE FROM student WHERE id = 10; DELETE FROM student WHERE id = 30; DELETE FROM student WHERE id = 70;
现在考虑:ref:示例 13-3 中的 FORALL 语句。它将相同的三个 DML 语句从 PL/SQL 批量发送到 SQL。
FORALL 语句通常比等效的 FOR LOOP 语句快得多。但是,一个 FOR LOOP 语句可以包含多个 DML 语句,而一个 FORALL 语句只能包含一个。 FORALL 语句发送给 SQL 的一批 DML 语句的不同之处仅在于它们的 VALUES 和 WHERE 子句。这些子句中的值必须来自现有的填充集合。
注意
FORALL 语句中的 DML 语句可以引用多个集合,但性能优势仅适用于使用 FORALL 索引变量作为索引的集合引用。
示例 13-4 将相同的集合元素插入到两个数据库表中,对第一个表使用 FOR LOOP 语句,对第二个表使用 FORALL 语句,并显示每个语句需要多长时间。 (时间因运行情况而异。) 示例 13-5 中,FORALL语句适用于集合的子集。
另请参阅
:ref:` FORALL 语句`了解完整的语法和语义,包括限制
:ref:`隐式游标`了解有关一般隐式游标属性和可与 FORALL 语句一起使用的其他隐式游标属性的信息
示例 13-2 FOR LOOP 语句中的 DELETE 语句
DROP TABLE student_temp; CREATE TABLE student_temp AS SELECT * FROM student; \set SQLTERM / DECLARE TYPE NList IS VARRAY(30) OF PLS_INTEGER; ids NList := NList(20, 40, 60); -- id numbers BEGIN FOR i IN depts.FIRST..depts.LAST LOOP DELETE FROM student_temp WHERE id = ids(i); END LOOP; END; / \set SQLTERM ;
示例 13-3 FORALL 语句中的 DELETE 语句
DROP TABLE student_temp; CREATE TABLE student_temp AS SELECT * FROM student; \set SQLTERM / DECLARE TYPE NList IS VARRAY(30) OF PLS_INTEGER; ids NList := NList(20, 40, 60); -- id numbers BEGIN FORALL i IN depts.FIRST..depts.LAST DELETE FROM student_temp WHERE id = ids(i); END; / \set SQLTERM ;
示例 13-4 FOR LOOP 和 FORALL 语句中 INSERT 语句的时间差
DROP TABLE student_temp; CREATE TABLE student_temp ( id INTEGER, name VARCHAR2(15) ); DROP TABLE student_temp2; CREATE TABLE student_temp2 ( id INTEGER, name VARCHAR2(15) ); \set SQLTERM / DECLARE TYPE IdTab IS TABLE OF student_temp.id%TYPE INDEX BY PLS_INTEGER; TYPE NameTab IS TABLE OF student_temp.name%TYPE INDEX BY PLS_INTEGER; id IDTAB; name NAMETAB; i CONSTANT NUMBER := 50000; t1 TIMESTAMP; t2 TIMESTAMP; t3 TIMESTAMP; BEGIN FOR j IN 1..i LOOP -- name collections id(j) := j; name(j) := 'bianma. ' || TO_CHAR(j); END LOOP; t1 := clock_timestamp(); FOR i IN 1..i LOOP INSERT INTO student_temp VALUES (id(i), name(i)); END LOOP; t2 := clock_timestamp(); FORALL i IN 1..i INSERT INTO student_temp VALUES (id(i), name(i)); t3 := clock_timestamp(); raise notice 'Execution Time'; raise notice '--------------------------'; raise notice 'FOR LOOP: %', t2 - t1; raise notice 'FORALL : %', t3 - t2; COMMIT; END; / \set SQLTERM ;
结果类似于:
NOTICE: Execution Time NOTICE: -------------------------- NOTICE: FOR LOOP: 00:00:00.941640 NOTICE: FORALL : 00:00:00.000287 ANONYMOUS BLOCK
示例 13-5 集合子集的 FORALL 语句
DROP TABLE student_temp; CREATE TABLE student_temp AS SELECT * FROM student; \set SQLTERM / DECLARE TYPE NList IS VARRAY(10) OF PLS_INTEGER; ids NList := NList(5,10,20,30,50,55,57,60,70,75); BEGIN FORALL j IN 4..7 DELETE FROM student_temp WHERE id = ids(j); END; / \set SQLTERM ;
13.2.1.1. 对稀疏集合使用 FORALL 语句
如果 FORALL 语句 bounds 子句引用稀疏集合,则使用 INDICES OF 或 VALUES OF 子句仅指定现有索引值。
可以对任何集合使用 INDICES OF , 除了以字符串为索引的关联数组。只能对PLS_INTEGER 索引的 PLS_INTEGER 元素的集合使用 VALUES OF 。
由 PLS_INTEGER 索引的 PLS_INTEGER 元素的集合可以是索引集合;也就是说,指向另一个集合(索引集合)的元素的指针集合。
索引集合对于使用不同 FORALL 语句处理同一集合的不同子集很有用。与其将原始集合的元素复制到表示子集的新集合中(这可能会占用大量时间和内存),不如用索引集合表示每个子集,然后在不同 FORALL 语句的 VALUES OF 子句中使用每个索引集合。
..seealse:
:ref:`稀疏集合和 SQL%BULK_EXCEPTIONS `
示例 13-6 稀疏集合及其子集的 FORALL 语句
此示例使用带有 INDICES OF 子句的 FORALL 语句来使用稀疏集合的元素填充表。然后它使用两个带有 VALUES OF 子句的 FORALL 语句来用集合的子集填充两个表。
DROP TABLE student_tmp; CREATE TABLE student_tmp ( name VARCHAR2(32), id NUMBER(10) ); DROP TABLE teacher; CREATE TABLE teacher AS SELECT * FROM student_tmp WHERE 1 = 0; DROP TABLE administration; CREATE TABLE administration AS SELECT * FROM student_tmp WHERE 1 = 0; \set SQLTERM / DECLARE SUBTYPE stdname IS student_tmp.name%TYPE; TYPE stdname_typ IS TABLE OF stdname; std_tab stdname_typ; -- Collection of student names SUBTYPE teach_id IS teacher.id%TYPE; TYPE teachid_typ IS TABLE OF NUMBER; teach_tab teachid_typ; -- Collection of teacher id TYPE idx IS TABLE OF PLS_INTEGER; /* Collections for pointers to elements of std_tab collection (to represent two subsets of std_tab): */ teacher_tab idx := idx(); administration_tab idx := idx(); PROCEDURE student_data_collections IS BEGIN std_tab := stdname_typ('Class1','Class2','Class3','Class4','Class5'); teach_tab := teachid_typ(5000.01, 0, 150.25, 4000.00, NULL); END; BEGIN student_data_collections; raise notice '--- student number data ---'; FOR i IN 1..std_tab.LAST LOOP raise notice 'student # %, %: %', i, std_tab(i),teach_tab(i); END LOOP; -- Delete invalid orders: FOR i IN 1..std_tab.LAST LOOP IF teach_tab(i) IS NULL OR teach_tab(i) = 0 THEN std_tab.delete(i); teach_tab.delete(i); END IF; END LOOP; -- std_tab is now a sparse collection. raise notice'--- student data with invalid deleted ---'; FOR i IN 1..std_tab.LAST LOOP IF std_tab.EXISTS(i) THEN raise notice 'Customer # %, %: %', i, std_tab(i),teach_tab(i); END IF; END LOOP; -- Using sparse collection, populate student_tmp table: FORALL i IN INDICES OF std_tab INSERT INTO student_tmp VALUES (std_tab(i), teach_tab(i)); student_data_collections; -- Restore original order data -- std_tab is a dense collection again. /* Populate collections of pointers to elements of std_tab collection (which represent two subsets of std_tab): */ FOR i IN std_tab.FIRST .. std_tab.LAST LOOP IF teach_tab(i) IS NULL OR teach_tab(i) = 0 THEN administration_tab.EXTEND; administration_tab(administration_tab.LAST) := i; END IF; IF teach_tab(i) > 2000 THEN teacher_tab.EXTEND; teacher_tab(teacher_tab.LAST) := i; END IF; END LOOP; /* Using each subset in a different FORALL statement, populate rejected_orders and big_orders tables: */ FORALL i IN VALUES OF administration_tab INSERT INTO administration VALUES (std_tab(i), teach_tab(i)); FORALL i IN VALUES OF teacher_tab INSERT INTO teacher VALUES (std_tab(i), teach_tab(i)); END; / \set SQLTERM ;
结果:
NOTICE: --- student number data --- NOTICE: student # 1, Class1: 5000.01 NOTICE: student # 2, Class2: 0 NOTICE: student # 3, Class3: 150.25 NOTICE: student # 4, Class4: 4000.00 NOTICE: student # 5, Class5:NOTICE: --- student data with invalid deleted --- NOTICE: Customer # 1, Class1: 5000.01 NOTICE: Customer # 3, Class3: 150.25 NOTICE: Customer # 4, Class4: 4000.00 ANONYMOUS BLOCK
验证是否存储了正确的订单详细信息:
SELECT name "Class", id "Student Number" FROM student_tmp ORDER BY name;
结果:
Class | Number --------+-------- Class1 | 5000 Class3 | 150 Class4 | 4000 (3 rows)
查询:
SELECT name "Class", id "Teacher Number" FROM teacher ORDER BY name;
结果:
Class | Teacher Number --------+---------------- Class1 | 5000 Class4 | 4000 (2 rows)
查询:
SELECT name "Class", id "Administration Number" FROM administration ORDER BY name;
结果:
Class | Administration Number --------+----------------------- Class2 | 0 Class5 | (2 rows)
13.2.1.2. FORALL 语句中未处理的异常
在没有 SAVE EXCEPTIONS 子句的 FORALL 语句中,如果一个 DML 语句引发未处理的异常,则 PL/SQL 将停止 FORALL 语句并回滚先前 DML 语句所做的所有更改。
例如,示例 13-2 中的 FORALL 语句按此顺序执行这些 DML 语句,除非其中一个引发未处理的异常:
DELETE FROM student_temp WHERE id = ids(20); DELETE FROM student_temp WHERE id = ids(40); DELETE FROM student_temp WHERE id = ids(60);
如果第三条语句引发未处理的异常,则 PL/SQL 回滚第一条和第二条语句所做的更改。 如果第二条语句引发了未处理的异常,那么 PL/SQL 将回滚第一条语句所做的更改并且从不运行第三条语句。
您可以通过以下任一方式处理 FORALL 语句中引发的异常:
在引发每个异常时立即处理(请参阅:ref:立即处理 FORALL 异常)
在FORALL语句完成执行后,通过包含SAVE EXCEPTIONS子句处理异常(请参阅:ref:在 FORALL 语句完成后处理 FORALL 异常)
13.2.1.3. 立即处理 FORALL 异常
要立即处理 FORALL 语句中引发的异常,请省略 SAVE EXCEPTIONS 子句并编写适当的异常处理程序。
如果一个 DML 语句引发了已处理的异常,则 PL/SQL 回滚该语句所做的更改,但不会回滚先前 DML 语句所做的更改。
在:ref:示例 13-7 中,FORALL 语句旨在运行三个 UPDATE 语句。 但是,第二个引发了异常。 异常处理程序处理异常,显示错误消息并提交第一个 UPDATE 语句所做的更改。 第三个 UPDATE 语句永远不会运行。
有关异常处理程序的信息,请参阅:ref:PL/SQL 错误处理。
示例 13-7 立即处理 FORALL 异常
DROP TABLE student_temp; CREATE TABLE student_temp ( classno NUMBER(3), duty VARCHAR2(11) ); \set SQLTERM / CREATE OR REPLACE PROCEDURE p1 AS TYPE NList IS TABLE OF PLS_INTEGER; class NList := NList(100, 200, 300); errmsg VARCHAR2(100); BEGIN -- student table: INSERT INTO student_temp VALUES (100, 'Simba'); INSERT INTO student_temp VALUES (200, 'Baksha'); INSERT INTO student_temp VALUES (300, 'nano'); COMMIT; FORALL j IN class.FIRST..class.LAST UPDATE student_temp SET duty = duty || 'RDJCNB' WHERE classno = class(j); EXCEPTION WHEN OTHERS THEN errmsg := SQLERRM; raise notice '%', error_message; COMMIT; -- Commit results of successful updates RAISE; END; / \set SQLTERM ; set ora_statement_level_rollback to on;
结果:
CREATE PROCEDURE
调用过程:
Call p1();
结果:
NOTICE: value too large for column "public"."student_temp"."duty" (actual:12, maximum:11) ERROR: value too large for column "public"."student_temp"."duty" (actual:12, maximum:11) CONTEXT: SQL statement "UPDATE student_temp SET duty = duty || 'RDJCNB' WHERE classno = class[j]" PL/SQL function p1() line 16 at SQL statement
查询:
SELECT * FROM student_temp;
结果:
classno | duty ---------+------------- 200 | Baksha 300 | nano 100 | SimbaRDJCNB (3 rows)
13.2.1.4. FORALL 语句完成后处理 FORALL 异常
要允许 FORALL 语句在某些 DML 语句失败的情况下继续执行,请包含 SAVE EXCEPTIONS 子句。当 DML 语句失败时,PL/SQL 不会引发异常;相反,它会保存有关失败的信息。 FORALL 语句完成后,PL/SQL 为 FORALL 语句引发一个异常。
在异常处理程序中,您可以从隐式游标属性 SQL%BULK_EXCEPTIONS 获取有关每个单独的 DML 语句失败的信息。
SQL%BULK_EXCEPTIONS 类似于有关在最近运行的 FORALL 语句期间失败的 DML 语句的信息的关联数组。
SQL%BULK_EXCEPTIONS.COUNT 是失败的 DML 语句数。如果 SQL%BULK_EXCEPTIONS.COUNT 不为零,那么对于从 1 到 SQL%BULK_EXCEPTIONS.COUNT 的每个索引值 i: - SQL%BULK_EXCEPTIONS(i). ERROR_INDEX是失败的 DML 语句的编号。 - SQL%BULK_EXCEPTIONS(i). ERROR_CODE是失败的 KES 数据库错误代码。
例如,如果一条FORALL SAVE EXCEPTIONS语句运行了 100 条 DML 语句,而第 10 条和第 64 条失败,则: - SQL%BULK_EXCEPTIONS.COUNT = 2 - SQL%BULK_EXCEPTIONS(1).ERROR_INDEX = 10 - SQL%BULK_EXCEPTIONS(1).ERROR_CODE = 12899 - SQL%BULK_EXCEPTIONS(2).ERROR_INDEX = 64 - SQL%BULK_EXCEPTIONS(2).ERROR_CODE = 19278
注意
在没有 SAVE EXCEPTIONS 子句的 FORALL 语句引发异常后,SQL%BULK_EXCEPTIONS.COUNT = 1。
使用错误代码,您可以使用 SQLERRM 函数获取相关的错误消息(在“ SQLERRM 函数”中描述):
SQLERRM(-(SQL%BULK_EXCEPTIONS(i).ERROR_CODE))
但是,SQLERRM返回的错误消息不包括任何替换参数(比较:ref:`示例 13-7`和:ref:`示例 13-8`中的错误消息)。
示例 13-8与示例 13-7类似,不同之处在于: - FORALL语句包括SAVE EXCEPTIONS子句。 - 异常处理程序err_dml使用SQL%BULK_EXCEPTIONS和SQLERRM(和一些局部变量)来显示错误消息以及哪个语句、集合项和字符串导致了错误。
示例 13-8 在 FORALL 语句完成后处理 FORALL 异常
DROP TABLE student_temp; CREATE TABLE student_temp ( classno NUMBER(4), duty VARCHAR2(12) ); \set SQLTERM / CREATE OR REPLACE PROCEDURE p AS TYPE NList IS TABLE OF PLS_INTEGER; classes NList := NList(50, 100, 150); errmsg VARCHAR2(100); cook_no NUMBER; class_no student_temp.classno%TYPE; duty_no student_temp.duty%TYPE; err_dml EXCEPTION; PRAGMA EXCEPTION_INIT(err_dml, -10053); BEGIN -- Populate table: INSERT INTO student_temp VALUES (50 , 'simba'); INSERT INTO student_temp VALUES (100, 'baksha'); INSERT INTO student_temp VALUES (150, 'nana'); COMMIT; -- Append 9-character string to each duty: FORALL j IN classes.FIRST..classes.LAST SAVE EXCEPTIONS UPDATE student_temp SET duty = duty || ' RDJCNB' WHERE classno = classes(j); EXCEPTION WHEN err_dml THEN FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP errmsg := SQLERRM(-(SQL%BULK_EXCEPTIONS(i).ERROR_CODE)); raise notice '%', errmsg; cook_no := SQL%BULK_EXCEPTIONS(i).ERROR_INDEX; raise notice 'cook no:%', cook_no; class_no := classes(cook_no); raise notice 'class no:%', class_no; SELECT duty INTO duty_no FROM student_temp WHERE classno = class_no; raise notice 'duty no:%', duty_no; END LOOP; COMMIT; -- Commit results of successful updates WHEN OTHERS THEN raise notice 'Unrecognized error.'; raise notice '%', sqlcode; RAISE; END; / \set SQLTERM ; set ora_statement_level_rollback to on;
结果:
CREATE PROCEDURE
调用过程:
call p();
结果:
NOTICE: value too large for column "public"."student_temp"."duty" (actual:15, maximum:12) NOTICE: cook no:2 NOTICE: class no:100 NOTICE: duty no:baksha CALL
询问:
SELECT * FROM student_temp;
结果:
classno | duty ---------+-------------- 100 | baksha 50 | simba RDJCNB 150 | nana RDJCNB (3 rows)
13.2.1.4.1. 稀疏集合和 SQL%BULK_EXCEPTIONS
如果 FORALL 语句 bounds 子句引用了稀疏集合,那么要找到导致 DML 语句失败的集合元素,必须逐个遍历元素,直到找到索引为 SQL%BULK_EXCEPTIONS(i).ERROR_INDEX 的元素。然后,如果 FORALL 语句使用 VALUES OF 子句将指针集合引用到另一个集合,则必须找到索引为 SQL%BULK_EXCEPTIONS(i).ERROR_INDEX 的另一个集合的元素。
13.2.1.4.2. 获取受 FORALL 语句影响的行数
在 FORALL 语句完成后,您可以从隐式游标属性 SQL%BULK_ROWCOUNT 获取每个 DML 语句影响的行数。
要获取受 FORALL 语句影响的总行数,请使用隐式游标属性 SQL%ROWCOUNT,如“SQL%ROWCOUNT属性:受影响的行数?”。
SQL%BULK_ROWCOUNT 类似于一个关联数组,其第 i 个元素是最近完成的 FORALL 语句中受第 i 个 DML 语句影响的行数。元素的数据类型是 INTEGER。
:ref:`示例 13-9`使用 SQL%BULK_ROWCOUNT 显示 FORALL 语句中每个 DELETE 语句删除了多少行,使用 SQL%ROWCOUNT 显示删除的总行数。
:ref:`示例 13-10`使用 SQL%BULK_ROWCOUNT 显示 FORALL 语句中每个 INSERT SELECT 构造插入的行数,使用 SQL%ROWCOUNT 显示插入的总行数。
示例 13-9 显示 FORALL 中每个 DELETE 影响的行数
DROP TABLE student_temp; CREATE TABLE student_tmp(id int, classno varchar(100)); truncate student; \set SQLTERM / begin for i in 1..9 loop insert into student values(i, i ||'-simba'); end loop; end; / \set SQLTERM / DECLARE TYPE NList IS TABLE OF PLS_INTEGER; ids NList := NList(1, 3, 5); BEGIN FORALL j IN ids.FIRST..ids.LAST DELETE FROM student_tmp WHERE id = ids(j); FOR i IN ids.FIRST..ids.LAST LOOP raise notice 'deleted :% records', SQL%BULK_ROWCOUNT(i); END LOOP; raise notice 'Total rows deleted: %', SQL%ROWCOUNT; END; / \set SQLTERM ;
结果:
NOTICE: deleted :1 records NOTICE: deleted :1 records NOTICE: deleted :1 records NOTICE: Total rows deleted: 3 ANONYMOUS BLOCK
示例 13-10 显示 FORALL 中每个 INSERT SELECT 影响的行数
DROP TABLE student_temp; CREATE TABLE student_tmp(id int, classno varchar(100)); truncate student; \set SQLTERM / begin for i in 1..9 loop insert into student values(i, i ||'-simba'); end loop; end; / DECLARE TYPE class_no IS TABLE OF student_tmp.classno%TYPE; class class_no; BEGIN SELECT name BULK COLLECT INTO class FROM student; FORALL i IN 1..class.COUNT INSERT INTO student_tmp SELECT id, name FROM student WHERE name = class(i) ORDER BY name, id; FOR i IN 1..class.COUNT LOOP raise notice 'classno:%, inserted :% records', class(i), SQL%BULK_ROWCOUNT(i); END LOOP; raise notice 'Total records inserted: %', SQL%ROWCOUNT; END; / \set SQLTERM ;
结果:
NOTICE: classno:1-simba, inserted :1 records NOTICE: classno:2-simba, inserted :1 records NOTICE: classno:3-simba, inserted :1 records NOTICE: classno:4-simba, inserted :1 records NOTICE: classno:5-simba, inserted :1 records NOTICE: classno:6-simba, inserted :1 records NOTICE: classno:7-simba, inserted :1 records NOTICE: classno:8-simba, inserted :1 records NOTICE: classno:9-simba, inserted :1 records NOTICE: Total records inserted: 9 ANONYMOUS BLOCK
BULK COLLECT 子句是批量 SQL 的一项功能,它可以将结果从 SQL 批量返回到 PL/SQL,而不是一次一个。
BULK COLLECT子句可以出现在: - SELECT INTO语句 - FETCH语句 - RETURNING INTO的语句:
DELETE语句
INSERT语句
UPDATE语句
EXECUTE IMMEDIATE语句
使用 BULK COLLECT 子句,前面的每个语句都会检索整个结果集,并将其存储在单个操作中的一个或多个集合变量中(这比使用循环语句一次检索一个结果行更有效)。
注意
PL/SQL 处理 BULK COLLECT 子句的方式类似于它处理 LOOP 语句中的 FETCH 语句的方式。 当带有 BULK COLLECT 子句的语句不返回任何行时,PL/SQL 不会引发异常。 必须检查目标集合是否为空,如示例 13-17 所示。
13.2.2.1. 带有 BULK COLLECT 子句的 SELECT INTO 语句
带有 BULK COLLECT 子句的 SELECT INTO 语句(也称为 SELECT BULK COLLECT INTO 语句)将整个结果集选择到一个或多个集合变量中。
有关详细信息,请参阅:ref:SELECT INTO 语句。
警告
SELECT BULK COLLECT INTO 语句容易受到别名的影响,这可能会导致意外结果。有关详细信息,请参阅:ref:` SELECT BULK COLLECT INTO 语句和别名`。
示例 13-11 使用一条SELECT BULK COLLECT INTO语句将两个数据库列选择为两个集合(嵌套表)。
示例 13-12 使用SELECT BULK COLLECT INTO语句将结果集选择到嵌套的记录表中
示例 13-11 将两个数据库列批量选择到两个嵌套表中
\set SQLTERM / DECLARE TYPE IdTab IS TABLE OF student.id%TYPE; TYPE NameTab IS TABLE OF student.name%TYPE; ids IdTab; names NameTab; PROCEDURE print_first_n (n POSITIVE) IS BEGIN IF ids.COUNT = 0 THEN raise notice 'Collections are empty.'; ELSE raise notice 'First % students:', n; FOR i IN 1 .. n LOOP raise notice ' student # id: %, name:%;',ids(i) ,names(i); END LOOP; END IF; END; BEGIN SELECT id, name BULK COLLECT INTO ids, names FROM student ORDER BY id; print_first_n(3); print_first_n(6); END; / \set SQLTERM ;
结果:
NOTICE: First 3 students: NOTICE: student # id: 1, name:1-simba; NOTICE: student # id: 2, name:2-simba; NOTICE: student # id: 3, name:3-simba; NOTICE: First 6 students: NOTICE: student # id: 1, name:1-simba; NOTICE: student # id: 2, name:2-simba; NOTICE: student # id: 3, name:3-simba; NOTICE: student # id: 4, name:4-simba; NOTICE: student # id: 5, name:5-simba; NOTICE: student # id: 6, name:6-simba; ANONYMOUS BLOCK
示例 13-12 批量选择到嵌套记录表
truncate student; \set SQLTERM / BEGIN for i in 1..9 loop insert into student values(i, i ||'-simba', i*10); end loop; END; / create type typ1 as (name varchar(100), score numeric); / DECLARE TYPE NameSet IS TABLE OF typ1%ROWTYPE; name_students NameSet; -- nested table of records BEGIN -- Assign values to nested table of records: SELECT name, score BULK COLLECT INTO name_students FROM student WHERE id = 1 ORDER BY score; -- Print nested table of records: FOR i IN name_students.FIRST .. name_students.LAST LOOP raise notice '%, %', name_students(i).score, name_students(i).name; END LOOP; END; / \set SQLTERM ; drop type typ1;
结果:
NOTICE: 10, 1-simba ANONYMOUS BLOCK
13.2.2.1.1. SELECT BULK COLLECT INTO 语句和别名
在表格的声明中
SELECT column BULK COLLECT INTO collection FROM table ...
PL/SQL 通过引用column 和 collection 传递它们。与通过引用传递的子程序参数一样,别名可能会导致异常结果。
..seealso:
- "子程序参数别名与通过引用传递的参数"
示例 13-14 使用游标来实现示例 13-13 预期的结果。
示例 13-15 从集合 numbers1 中选择特定值,然后将它们存储在不同的集合 numbers2 中。 示例 13-15 比示例 13-14 运行得更快。
示例 13-13 SELECT BULK COLLECT INTO 具有意外结果的语句
CREATE OR REPLACE TYPE numtyp IS TABLE OF INTEGER; \set SQLTERM / CREATE OR REPLACE PROCEDURE p (i INTEGER) IS num1 numtyp := numtyp(3,4,5,6,7); BEGIN raise notice 'Before SELECT statement'; raise notice 'num1.COUNT() = %', num1.COUNT(); FOR j IN 1..num1.COUNT() LOOP raise notice 'num1(%)=%', j, num1(j); END LOOP; --Self-selecting BULK COLLECT INTO clause: SELECT a.COLUMN_VALUE BULK COLLECT INTO num1 FROM TABLE(num1) a WHERE a.COLUMN_VALUE > p.i ORDER BY a.COLUMN_VALUE; raise notice 'After SELECT statement'; raise notice 'num1.COUNT() = %', num1.COUNT(); END p; / \set SQLTERM ;
调用p:
call p(2);
结果:
NOTICE: Before SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)=3 NOTICE: num1(2)=4 NOTICE: num1(3)=5 NOTICE: num1(4)=6 NOTICE: num1(5)=7 NOTICE: After SELECT statement NOTICE: num1.COUNT() = 2 CALL
调用p:
call p(14);
结果:
NOTICE: Before SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)=3 NOTICE: num1(2)=4 NOTICE: num1(3)=5 NOTICE: num1(4)=6 NOTICE: num1(5)=7 NOTICE: After SELECT statement NOTICE: num1.COUNT() = 0 CALL
示例 13-14 示例13-13 的游标解决方法
\set SQLTERM / CREATE OR REPLACE PROCEDURE p (i INTEGER) IS num1 numtyp := numtyp(55,56,57,58,59); CURSOR c IS SELECT a.COLUMN_VALUE FROM TABLE(num1) a WHERE a.COLUMN_VALUE > p.i ORDER BY a.COLUMN_VALUE; BEGIN raise notice 'Before SELECT statement'; raise notice 'num1.COUNT() = %', num1.COUNT(); FOR j IN 1..num1.COUNT() LOOP raise notice 'num1(%)=%', j, num1(j); END LOOP; OPEN c; FETCH c BULK COLLECT INTO num1; CLOSE c; raise notice 'After SELECT statement'; raise notice 'num1.COUNT() = %', num1.COUNT(); IF num1.COUNT() > 0 THEN FOR j IN 1..num1.COUNT() LOOP raise notice 'num1(%)=%', j, num1(j); END LOOP; END IF; END p; / \set SQLTERM ;
调用p:
call p(57);
结果:
NOTICE: Before SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)=55 NOTICE: num1(2)=56 NOTICE: num1(3)=57 NOTICE: num1(4)=58 NOTICE: num1(5)=59 NOTICE: After SELECT statement NOTICE: num1.COUNT() = 2 NOTICE: num1(1)=58 NOTICE: num1(2)=59 CALL
调用p:
call p(120);
结果:
NOTICE: Before SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)=55 NOTICE: num1(2)=56 NOTICE: num1(3)=57 NOTICE: num1(4)=58 NOTICE: num1(5)=59 NOTICE: After SELECT statement NOTICE: num1.COUNT() = 0 CALL
示例 13-15 示例 13-13的第二次收集解决方法
\set SQLTERM / CREATE OR REPLACE PROCEDURE p (i IN INTEGER) AUTHID DEFINER IS num1 numtyp := numtyp(11,12,13,14,15); num2 numtyp := numtyp(0,0,0,0,0); BEGIN raise notice 'Before SELECT statement'; raise notice 'num1.COUNT() = %', num1.COUNT(); FOR j IN 1..num1.COUNT() LOOP raise notice 'num1(%)= %', j, num1(j); END LOOP; raise notice 'num2.COUNT() = %', num2.COUNT(); FOR j IN 1..num2.COUNT() LOOP raise notice 'num2(%)= %', j, num2(j); END LOOP; SELECT a.COLUMN_VALUE BULK COLLECT INTO num2 -- num2 appears here FROM TABLE(num1) a -- num1 appears here WHERE a.COLUMN_VALUE > p.i ORDER BY a.COLUMN_VALUE; raise notice 'After SELECT statement'; raise notice 'num1.COUNT() = %', num1.COUNT(); IF num1.COUNT() > 0 THEN FOR j IN 1..num1.COUNT() LOOP raise notice 'num1(%)= %', j, num1(j); END LOOP; END IF; raise notice 'num2.COUNT() = %', num2.COUNT(); IF num2.COUNT() > 0 THEN FOR j IN 1..num2.COUNT() LOOP raise notice 'num2(%)= %', j, num2(j); END LOOP; END IF; END p; / \set SQLTERM ;
调用p:
call p(12);
结果:
NOTICE: Before SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)= 11 NOTICE: num1(2)= 12 NOTICE: num1(3)= 13 NOTICE: num1(4)= 14 NOTICE: num1(5)= 15 NOTICE: num2.COUNT() = 5 NOTICE: num2(1)= 0 NOTICE: num2(2)= 0 NOTICE: num2(3)= 0 NOTICE: num2(4)= 0 NOTICE: num2(5)= 0 NOTICE: After SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)= 11 NOTICE: num1(2)= 12 NOTICE: num1(3)= 13 NOTICE: num1(4)= 14 NOTICE: num1(5)= 15 NOTICE: num2.COUNT() = 3 NOTICE: num2(1)= 13 NOTICE: num2(2)= 14 NOTICE: num2(3)= 15 CALL
调用p:
call p(30);
结果:
NOTICE: Before SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)= 11 NOTICE: num1(2)= 12 NOTICE: num1(3)= 13 NOTICE: num1(4)= 14 NOTICE: num1(5)= 15 NOTICE: num2.COUNT() = 5 NOTICE: num2(1)= 0 NOTICE: num2(2)= 0 NOTICE: num2(3)= 0 NOTICE: num2(4)= 0 NOTICE: num2(5)= 0 NOTICE: After SELECT statement NOTICE: num1.COUNT() = 5 NOTICE: num1(1)= 11 NOTICE: num1(2)= 12 NOTICE: num1(3)= 13 NOTICE: num1(4)= 14 NOTICE: num1(5)= 15 NOTICE: num2.COUNT() = 0 CALL
13.2.2.1.1.1. SELECT BULK COLLECT INTO 语句的行限制
返回大量行的 SELECT BULK COLLECT INTO 语句会生成一个大型集合。 要限制行数和集合大小,请使用以下之一: - ROWNUM伪列(在SQL 语言参考中描述) - FETCH FIRST子句(在SQL 语言参考中描述)
示例 13-16显示了几种限制 SELECT BULK COLLECT INTO 语句返回的行数的方法。
示例 13-16 使用 ROWNUM、SAMPLE 和 FETCH FIRST 限制批量选择
truncate student; \set SQLTERM / BEGIN for i in 1..100 loop insert into student values(i); end loop; END; / DECLARE TYPE IdList IS TABLE OF student.id%TYPE; ids IdList; BEGIN SELECT id BULK COLLECT INTO ids FROM student WHERE ROWNUM <= 50; SELECT id BULK COLLECT INTO ids FROM student FETCH FIRST 50 ROWS ONLY; END; /
13.2.2.1.1.2. 循环编译集合的规则
当结果集存储在集合中时,很容易遍历行并引用不同的列。这种方式可以非常快,但也非常占用内存。有如下场景可以考虑: - 只遍历结果集一次,请使用游标FOR LOOP(请参阅:ref:使用游标 FOR LOOP 语句处理查询结果集)。
这种方式避免了存储结果集副本的内存开销。
在SELECT INTO语句中进行搜索或过滤,这样可以减少结果集来搜索某些值或将结果过滤到较小的集合中。
例如,在简单查询中,使用WHERE子句;请参阅 SQL 语言参考。
在 SELECT INTO 语句的查询中使用子查询,代替循环遍历结果集并为每个结果行运行另一个查询,(请参阅:ref:使用子查询处理查询结果集)。
使用 FORALL 语句,代替循环遍历结果集并为每个结果行运行另一个 DML 语句(请参阅:ref:` FORALL 语句`)。
13.2.2.2. 带有 BULK COLLECT 子句的 FETCH 语句
带有BULK COLLECT子句的FETCH语句(也称为FETCH BULK COLLECT语句)将整个结果集提取到一个或多个集合变量中。有关详细信息,请参阅:ref:` FETCH 语句`。
示例 13-17 使用 FETCH BULK COLLECT 语句将整个结果集提取到两个集合(嵌套表)中。
示例 13-18 使用 FETCH BULK COLLECT 语句将结果集提取到记录集合(嵌套表)中。
示例 13-17 批量提取到两个嵌套表中
drop type numtyp; create type listtype as (name varchar(100), score numeric); \set SQLTERM / BEGIN for i in 1..10 loop insert into student values(i, i||'-simba', i*10); end loop; END; / DECLARE TYPE NameTpe IS TABLE OF student.name%TYPE; TYPE ScoreTyp IS TABLE OF student.score%TYPE; CURSOR c1 IS SELECT name, score FROM student WHERE score > 5 ORDER BY name; names NameTpe; scores ScoreTyp; TYPE Typ IS TABLE OF listtype; recs Typ; v_limit PLS_INTEGER := 3; PROCEDURE print_results IS BEGIN -- Check if collections are empty: IF names IS NULL OR names.COUNT = 0 THEN raise notice 'No results!'; ELSE raise notice 'Result: '; FOR i IN names.FIRST .. names.LAST LOOP raise notice' Student %:%.', names(i), scores(i); END LOOP; END IF; END; BEGIN raise notice'--- Processing all results simultaneously ---'; OPEN c1; FETCH c1 BULK COLLECT INTO names, scores; CLOSE c1; print_results(); raise notice '--- Processing % rows at a time ---', v_limit; OPEN c1; LOOP FETCH c1 BULK COLLECT INTO names, scores LIMIT v_limit; EXIT WHEN names.COUNT = 0; print_results(); END LOOP; CLOSE c1; raise notice '--- Fetching records rather than columns ---'; OPEN c1; FETCH c1 BULK COLLECT INTO recs; FOR i IN recs.FIRST .. recs.LAST LOOP -- Now all columns from result set come from one record raise notice ' Student %:%', recs(i).name, recs(i).score; END LOOP; END; / \set SQLTERM ; drop type listtype;
结果:
NOTICE: --- Processing all results simultaneously --- NOTICE: Result: NOTICE: Student 10-simba:100. NOTICE: Student 1-simba:10. NOTICE: Student 2-simba:20. NOTICE: Student 3-simba:30. NOTICE: Student 4-simba:40. NOTICE: Student 5-simba:50. NOTICE: Student 6-simba:60. NOTICE: Student 7-simba:70. NOTICE: Student 8-simba:80. NOTICE: Student 9-simba:90. NOTICE: --- Processing 3 rows at a time --- NOTICE: Result: NOTICE: Student 10-simba:100. NOTICE: Student 1-simba:10. NOTICE: Student 2-simba:20. NOTICE: Result: NOTICE: Student 3-simba:30. NOTICE: Student 4-simba:40. NOTICE: Student 5-simba:50. NOTICE: Result: NOTICE: Student 6-simba:60. NOTICE: Student 7-simba:70. NOTICE: Student 8-simba:80. NOTICE: Result: NOTICE: Student 9-simba:90. NOTICE: --- Fetching records rather than columns --- NOTICE: Student 10-simba:100 NOTICE: Student 1-simba:10 NOTICE: Student 2-simba:20 NOTICE: Student 3-simba:30 NOTICE: Student 4-simba:40 NOTICE: Student 5-simba:50 NOTICE: Student 6-simba:60 NOTICE: Student 7-simba:70 NOTICE: Student 8-simba:80 NOTICE: Student 9-simba:90 ANONYMOUS BLOCK
示例 13-18 批量提取到嵌套记录表中
create type listtype as (name varchar(100), score numeric); \set SQLTERM / DECLARE TYPE SetTyp IS TABLE OF listtype%ROWTYPE; students SetTyp; -- nested table of records cv REFCURSOR; BEGIN -- Assign values to nested table of records: OPEN cv FOR SELECT name, score FROM student WHERE id = '10' ORDER BY score; FETCH cv BULK COLLECT INTO students; CLOSE cv; -- Print nested table of records: FOR i IN students.FIRST .. students.LAST LOOP raise notice '%:%', students(i).name, students(i).score; END LOOP;END; / \set SQLTERM ; drop type listtype;
结果:
NOTICE: 10-simba:100 ANONYMOUS BLOCK
13.2.2.2.1. FETCH BULK COLLECT 语句的行限制
返回大量行的 FETCH BULK COLLECT 语句会生成一个大型集合。要限制行数和集合大小,请使用 LIMIT 子句。
在示例 13-19 中,对于 LOOP 语句的每次迭代,FETCH 语句将 10 行(或更少)提取到关联数组 ntab 中(覆盖之前的值)。 注意 LOOP 语句的退出条件。
示例 13-19 使用 LIMIT 限制批量 FETCH
\set SQLTERM / DECLARE TYPE ntab IS TABLE OF NUMBER INDEX BY PLS_INTEGER; ids ntab; CURSOR c1 IS SELECT id FROM student WHERE score = 80 ORDER BY id; BEGIN OPEN c1; LOOP -- Fetch 10 rows or fewer in each iteration FETCH c1 BULK COLLECT INTO ids LIMIT 3; raise notice '------- Results from One Bulk Fetch --------'; FOR i IN 1..ids.COUNT LOOP raise notice 'Student Id: %',ids(i); END LOOP; EXIT WHEN c1%NOTFOUND; END LOOP; CLOSE c1; END; / \set SQLTERM ;
结果:
NOTICE: ------- Results from One Bulk Fetch -------- NOTICE: Student Id: 8 ANONYMOUS BLOCK
13.2.2.3. RETURNING INTO 子句带有 BULK COLLECT 子句
带有 BULK COLLECT 子句的 RETURNING INTO 子句(也称为 RETURNING BULK COLLECT INTO 子句)可以出现在 INSERT、UPDATE、DELETE 或 EXECUTE IMMEDIATE 语句中。 使用 RETURNING BULK COLLECT INTO 子句,语句将其结果集存储在一个或多个集合中。请参阅:ref:返回条款。
示例 13-20 使用带有 RETURNING BULK COLLECT INTO 子句的 DELETE 语句从表中删除行并在两个集合(嵌套表)中返回它们。
示例 13-20 在两个嵌套表中返回已删除的行
DROP TABLE student_temp; CREATE TABLE student_temp AS SELECT * FROM student ORDER BY id; \set SQLTERM / DECLARE TYPE IdList IS TABLE OF student.id%TYPE; ids IdList; TYPE NameList IS TABLE OF student.name%TYPE; names NameList; BEGIN DELETE FROM student_temp WHERE id = 5 RETURNING id, name BULK COLLECT INTO ids, names; raise notice 'Deleted % rows:', SQL%ROWCOUNT; FOR i IN ids.FIRST .. ids.LAST LOOP raise notice 'Students # %:%', ids(i), names(i); END LOOP; END; / \set SQLTERM ;
结果:
NOTICE: Deleted 1 rows: NOTICE: Students # 5:5-simba ANONYMOUS BLOCK
在 FORALL 语句中,DML 语句可以有一个 RETURNING BULK COLLECT INTO 子句。 对于 FORALL 语句的每次迭代,DML 语句将指定的值存储在指定的集合中,而不会覆盖以前的值,就像在 FOR LOOP 语句中执行相同的 DML 语句一样。
在:ref:示例 13-21 中,FORALL 语句运行具有 RETURNING BULK COLLECT INTO 子句的 DELETE 语句。 对于 FORALL 语句的每次迭代,DELETE 语句将删除行的 id 和 score 值分别存储在集合 ids 和 scores 中。
示例 13-22 与:ref:示例 13-21 类似,只是它使用 FOR LOOP 语句而不是 FORALL 语句。
示例 13-21 FORALL 语句中使用带有 RETURN BULK COLLECT INTO 子句的 DELETE语句
DROP TABLE student_temp; CREATE TABLE student_temp AS SELECT * FROM student ORDER BY id; \set SQLTERM / DECLARE TYPE ScoreList IS TABLE OF PLS_INTEGER; tag ScoreList := ScoreList(30,60,90); TYPE IdTyp IS TABLE OF student.id%TYPE; ids IdTyp; TYPE ScoreTyp IS TABLE OF student.score%TYPE; scores ScoreTyp; BEGIN FORALL j IN tag.FIRST..tag.LAST DELETE FROM student_temp WHERE score = tag(j) RETURNING id, score BULK COLLECT INTO ids, scores; raise notice '---Deleted % rows---', SQL%ROWCOUNT; FOR i IN ids.FIRST .. ids.LAST LOOP raise notice 'Student # % from student table %', ids(i), scores(i); END LOOP; END; / \set SQLTERM ;
结果:
NOTICE: ---Deleted 3 rows--- NOTICE: Student # 3 from student table 30 NOTICE: Student # 6 from student table 60 NOTICE: Student # 9 from student table 90 ANONYMOUS BLOCK
**示例 13-22 在 FOR LOOP 语句中使用带有 RETURN BULK COLLECT INTO 子句的DELETE语句
DROP TABLE student_temp; CREATE TABLE student_temp AS SELECT * FROM student ORDER BY id; \set SQLTERM / DECLARE TYPE ScoreList IS TABLE OF PLS_INTEGER; tag ScoreList := ScoreList(30,60,90); TYPE IdTyp IS TABLE OF student.id%TYPE; ids IdTyp; TYPE ScoreTyp IS TABLE OF student.score%TYPE; scores ScoreTyp; BEGIN FOR j IN tag.FIRST..tag.LAST LOOP DELETE FROM student_temp WHERE score = tag(j) RETURNING id, score BULK COLLECT INTO ids, scores; END LOOP; raise notice '---Deleted % rows---', SQL%ROWCOUNT; FOR i IN ids.FIRST .. ids.LAST LOOP raise notice 'Student # % from student table %', ids(i), scores(i); END LOOP; END; / \set SQLTERM ;
结果:
NOTICE: ---Deleted 1 rows--- NOTICE: Student # 9 from student table 90 ANONYMOUS BLOCK
Pipelined Table函数是对数据执行多次转换的有效方式。
注意
Pipelined Table函数不能使用在database link中。原因是Pipelined Table函数的返回类型是 SQL 用户定义类型,只能在单个数据库中使用。尽管Pipelined Table函数的返回类型可能看起来是 PL/SQL 类型,但数据库实际上将该 PL/SQL 类型转换为相应的 SQL 用户定义类型。
Table函数是用户定义的 PL/SQL 函数,它返回行的集合(关联数组、嵌套表或可变数组)。
通过 SELECT 语句的 TABLE 子句中调用 Table 函数,可以从这个集合中进行选择,就像它是一个数据库表一样。 TABLE 运算符是可选的。
例如:
SELECT * FROM TABLE(TableFunctionName(ParameterList))
或者,可以在没有 TABLE 运算符的情况下编写相同的查询,如下所示:
SELECT * FROM TableFunctionName(ParameterList)
Table函数可以将行的集合作为输入(也就是说,它可以有一个输入参数,即嵌套表、可变数组或游标变量)。因此,表函数 tf1 的输出可以输入到表函数 tf2,tf2 的输出可以输入到表函数 tf3,以此类推。
Pipelined Table函数在处理完一行后立即将该行返回给它的调用者,并继续处理下一行。响应时间得到改善,因为在查询可以返回单个结果行之前,不需要构造整个集合返回到服务器。
警告
Pipelined Table函数始终引用数据的当前状态。如果在为集合打开游标后集合中的数据发生更改,则游标会反映更改。PL/SQL 变量是会话私有的,不是事务性的。因此,以适用于表数据而闻名的读取一致性不适用于 PL/SQL 集合变量。
Pipelined Table函数必须是独立函数或包函数。
Pipelined选项
对于独立函数,请在 CREATE FUNCTION 语句中指定 PIPELINED 选项(有关语法,请参阅:ref:` CREATE FUNCTION 语句`)。对于包函数,请在函数声明和函数定义中指定 PIPELINED 选项(有关语法,请参阅:ref:函数声明和定义)。
参数
通常,Pipelined Table函数具有一个或多个游标变量参数。有关作为函数参数的游标变量的信息,请参阅“作为子程序参数的游标变量”。
另请参阅
:ref:`游标变量`获取有关游标变量的一般信息
:ref:`子程序参数`获取有关子程序参数的一般信息
返回数据类型
Pipelined Table函数返回值的数据类型必须是在模式级别或包内定义的集合类型(因此,它不能是关联数组类型)。集合类型的元素必须是 SQL 数据类型,而不是只有 PL/SQL 支持的数据类型(如 PLS_INTEGER 和 BOOLEAN)。有关集合类型的信息,请参阅:ref:集合类型。有关 SQL 数据类型的信息,请参阅:ref:《 SQL 语言参考》。
PIPE ROW 语句
在Pipelined Table函数中,使用 PIPE ROW 语句将集合元素返回给调用者,而不将控制权返回给调用者。有关其语法和语义,请参见:ref:` PIPE ROW 语句`。
RETURN 语句
与每个函数一样,Pipelined Table函数中的每个执行路径都必须指向一个 RETURN 语句,该语句将控制权返回给调用者。但是,在Pipelined Table函数中,RETURN 语句不需要将值返回给调用者。有关其语法和语义,请参见:ref:` RETURN 语句`。
用例
示例 13-23 创建和调用Pipelined Table函数
此示例创建一个包,其中包含Pipelined Table函数 func1,然后从 func1 返回的行集合中进行选择。
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg1 AS TYPE NumTyp IS TABLE OF PLS_INTEGER; FUNCTION func1(i NUMBER) RETURN NumTyp PIPELINED; END pkg1; / \set SQLTERM ;
创建一个返回元素集合 (1,3,5,... x) 的Pipelined Table函数 func1。
\set SQLTERM / CREATE OR REPLACE PACKAGE BODY pkg1 AS FUNCTION func1(i NUMBER) RETURN NumTyp PIPELINED IS BEGIN FOR i IN 1..i by 2 LOOP PIPE ROW(i); END LOOP; RETURN; END; END; \set SQLTERM ; select * from TABLE(pkg1.func1(10));
结果:
column_value -------------- 1 3 5 7 9 (5 rows)
select * from pkg1.func1(10);
结果:
column_value -------------- 1 3 5 7 9 (5 rows)
带有游标变量参数的Pipelined Table函数可以用作转换函数。 该函数使用游标变量获取输入行。通过使用 PIPE ROW 语句,该函数将转换后的一行或多行通过管道传输到调用程序。 如果 FETCH 和 PIPE ROW 语句位于 LOOP 语句中,则该函数可以转换多个输入行。
在示例 13-24 中,Pipelined Table 函数将 student 表的每个选定行转换为两个嵌套表行,并通过管道传递给调用它的 SELECT 语句。
示例 13-24 Pipelined Table函数将每一行转换为两行
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg1 AUTHID DEFINER IS TYPE RecTyp IS RECORD ( score NUMBER(6), name VARCHAR2(30) ); TYPE RecSet IS TABLE OF RecTyp; FUNCTION func1 (i int) RETURN RecSet PIPELINED; END pkg1; / CREATE OR REPLACE PACKAGE BODY pkg1 IS FUNCTION func1 (i int) RETURN RecSet PIPELINED IS rec1 RecTyp; rec2 student%ROWTYPE; CURSOR cur is SELECT * FROM student WHERE id > i; BEGIN open cur; LOOP FETCH cur INTO rec2; -- input row EXIT WHEN cur%NOTFOUND; rec1.score := rec2.id; rec1.name := rec2.name; PIPE ROW(rec1); -- first transformed output row rec1.name := rec2.name; rec1.score := rec2.score; PIPE ROW(rec1); -- second transformed output row END LOOP; CLOSE cur; RETURN; END func1; END pkg1; / \set SQLTERM ; SELECT * FROM TABLE (pkg1.func1 (5));
结果:
score | name -------+---------- 6 | 6-simba 60 | 6-simba 7 | 7-simba 70 | 7-simba 8 | 8-simba 80 | 8-simba 9 | 9-simba 90 | 9-simba 10 | 10-simba 100 | 10-simba (10 rows)
为了帮助您隔离大型 PL/SQL 程序中的性能问题,PL/SQL 提供了这些工具,以 PL/SQL 包的形式实现。
工具 |
包 |
描述 |
---|---|---|
探查器界面 |
PLSQL-PLPROFILER |
计算您的 PL/SQL 程序在每一行和每个子程序中花费的时间。 |