Oracle之FORALL与BULK COLLECT简介

在上篇文章Oracle大数据的导入与更新方案中介绍了导入超大数据量的操作,而要批量更新超大数据量的操作时就需要用到本文所要介绍的FORALL与BULK COLLECT语句。

1 PL/SQL块的执行过程

当PL/SQL运行时引擎处理一块代码时,它使用PL/SQL引擎来执行过程化的代码,而将SQL语句发送给SQL引擎来执行。SQL引擎执行完毕后,将结果再返回给PL/SQL引擎。这种在PL/SQL引擎和SQL引擎之间的交互,称为上下文交换(context switch)。每发生一次交换,就会带来一定的额外开销。


PL/SQL与SQL引擎之间的交换

上图摘自Oracle官网,形象的说明了上述执行过程。有兴趣的同学可以参考Oracle官网对FORALL与BULK COLLECT的详细说明。地址如下:
http://www.oracle.com/technetwork/issue-archive/2012/12-sep/o52plsql-1709862.html

2 FORALL和BULK COLLECT特点

这两个语句在PL/SQL内部进行一种数组处理,BULK COLLECT提供对数据的高速检索,FORALL可大大改进INSERT、UPDATE和DELETE操作的性能。Oracle数据库使用这些语句大大减少了PL/SQL与SQL语句执行引擎的环境切换次数,从而使其性能有了显著提高。

  1. FORALL,用于增强PL/SQL引擎到SQL引擎的交换。
  2. BULK COLLECT,用于增强SQL引擎到PL/SQL引擎的交换。
如果你要插入5000条数据,一般情况下,在pl/sql中用for循环,循环插入5000次,
而用forall一次就可以插入5000条,提高了性能和速度。

3 FORALL介绍

使用FORALL,可以将多个DML批量发送给SQL引擎来执行,最大限度地减少上下文交互所带来的开销。

3.1 FORALL语法

FORALL index_name IN
    { lower_bound .. upper_bound
     | INDICES OF collection_name [ BETWEEN lower_bound AND upper_bound ]
     | VALUES OF index_collection
    }
 [ SAVE EXCEPTIONS ] dml_statement;

说明:

  1. index_name:一个无需声明的标识符,作为集合下标使用。
  2. lower_bound .. upper_bound:数字表达式,来指定一组连续有效的索引数字下限和上限。该表达式只需解析一次。
  3. INDICES OF collection_name:用于指向稀疏数组的实际下标。跳过没有赋值的元素,例如被 DELETE 的元素,NULL 也算值。
  4. VALUES OF index_collection_name:把该集合中的值当作下标,且该集合值的类型只能是 PLS_INTEGER/BINARY_INTEGER。
  5. SAVE EXCEPTIONS:可选关键字,表示即使一些DML语句失败,直到FORALL LOOP执行完毕才抛出异常。可以使用SQL%BULK_EXCEPTIONS 查看异常信息。
  6. dml_statement:静态语句,例如:UPDATE或者DELETE;或者动态(EXECUTE IMMEDIATE)DML语句。

3.2 FORALL案例

见sqlscripts/forall-bulkcollect包下的sql脚本事例(所有事例代码见文末的github地址)。

3.3 FORALL注意事项

使用FORALL时,应该遵循如下规则:

  1. FORALL语句的执行体,必须是一个单独的DML语句,比如INSERT,UPDATE或DELETE。
  2. 不要显式定义index_row,它被PL/SQL引擎隐式定义为PLS_INTEGER类型,并且它的作用域也仅仅是FORALL。
  3. 这个DML语句必须与一个集合的元素相关,并且使用FORALL中的index_row来索引。注意不要因为index_row导致集合下标越界。
  4. lower_bound和upper_bound之间是按照步进 1 来递增的。
  5. 在sql_statement中,不能单独地引用集合中的元素,只能批量地使用集合。
  6. 在sql_statement中使用的集合,下标不能使用表达式。
a. insert into test2 values dr_table(i);dbms_output.put_line(i);
不正确,找不到i,因为forall中只能使用单条语句可以引用索引变量。
b. insert into test2 values(dr_table(i).id,dr_table(i).name);
集合的field不可以在forall中使用,必须是整体使用
c. insert into test2 values dr_table(i+1);
错误,不可以对索引变量进行运算
d. insert into test2 values(dr_table(i));
报没有足够的值错误,此处外面不可以加括号,当有多个字段的时候,单个字段可以加括号

4 BULK COLLECT的使用

4.1 在SELECT INTO中使用BULK COLLECT

DECLARE
  -- 定义记录类型
  TYPE EMP_REC_TYPE IS RECORD(
    EMPNO    EMP.EMPNO%TYPE,
    ENAME    EMP.ENAME%TYPE,
    HIREDATE EMP.HIREDATE%TYPE);
  -- 定义基于记录的嵌套表
  TYPE NESTED_EMP_TYPE IS TABLE OF EMP_REC_TYPE;
  -- 声明变量
  EMP_TAB NESTED_EMP_TYPE;
BEGIN
  -- 使用BULK COLLECT将所得的结果集一次性绑定到记录变量emp_tab中
  SELECT EMPNO, ENAME, HIREDATE BULK COLLECT INTO EMP_TAB FROM EMP;

  FOR I IN EMP_TAB.FIRST .. EMP_TAB.LAST LOOP
    DBMS_OUTPUT.PUT_LINE('当前记录: ' || EMP_TAB(I)
                         .EMPNO || CHR(9) || EMP_TAB(I)
                         .ENAME || CHR(9) || EMP_TAB(I).HIREDATE);
  END LOOP;
END;

说明:使用BULK COLLECT一次即可提取所有行并绑定到记录变量,这就是所谓的批量绑定。

4.2 在FETCH INTO中使用BULK COLLECT

在游标中可以使用BLUK COLLECT一次取出一个数据集合,比用游标单条取数据效率高,尤其是在网络不大好的情况下。

语法:

FETCH ... BULK COLLECT INTO ...[LIMIT row_number];

注意:

  1. 在使用BULK COLLECT子句时,对于集合类型会自动对其进行初始化以及扩展。因此如果使用BULK COLLECT子句操作集合,则无需对集合进行初始化以及扩展。
  2. 由于BULK COLLECT的批量特性,如果数据量较大,而集合在此时又自动扩展,为避免过大的数据集造成性能下降,因此可以使用LIMIT子句来限制一次提取的数据量。
    LIMIT子句只允许出现在FETCH操作语句的批量中.

DECLARE
  CURSOR EMP_CUR IS
    SELECT EMPNO, ENAME, HIREDATE FROM EMP;

  TYPE EMP_REC_TYPE IS RECORD(
    EMPNO    EMP.EMPNO%TYPE,
    ENAME    EMP.ENAME%TYPE,
    HIREDATE EMP.HIREDATE%TYPE);
  -- 定义基于记录的嵌套表
  TYPE NESTED_EMP_TYPE IS TABLE OF EMP_REC_TYPE;
  -- 声明集合变量
  EMP_TAB NESTED_EMP_TYPE;
  -- 定义了一个变量来作为limit的值
  V_LIMIT PLS_INTEGER := 5;
  -- 定义变量来记录FETCH次数
  V_COUNTER PLS_INTEGER := 0;
BEGIN
  OPEN EMP_CUR;

  LOOP
    -- fetch时使用了BULK COLLECT子句
    FETCH EMP_CUR BULK COLLECT
      INTO EMP_TAB LIMIT V_LIMIT; -- 使用limit子句限制提取数据量

    EXIT WHEN EMP_TAB.COUNT = 0; -- 注意此时游标退出使用了emp_tab.COUNT,而不是emp_cur%notfound
    V_COUNTER := V_COUNTER + 1; -- 记录使用LIMIT之后fetch的次数

    FOR I IN EMP_TAB.FIRST .. EMP_TAB.LAST LOOP
      DBMS_OUTPUT.PUT_LINE('当前记录: ' || EMP_TAB(I)
                           .EMPNO || CHR(9) || EMP_TAB(I)
                           .ENAME || CHR(9) || EMP_TAB(I).HIREDATE);
    END LOOP;
  END LOOP;

  CLOSE EMP_CUR;

  DBMS_OUTPUT.PUT_LINE('总共获取次数为:' || V_COUNTER);
END;

4.3 在RETURNING INTO中使用BULK COLLECT

BULK COLLECT除了与SELECT,FETCH进行批量绑定之外,还可以与INSERT,DELETE,UPDATE语句结合使用。
当与这几个DML语句结合时,需要使用RETURNING子句来实现批量绑定。

DECLARE
  TYPE EMP_REC_TYPE IS RECORD(
    EMPNO    EMP.EMPNO%TYPE,
    ENAME    EMP.ENAME%TYPE,
    HIREDATE EMP.HIREDATE%TYPE);
  TYPE NESTED_EMP_TYPE IS TABLE OF EMP_REC_TYPE;
  EMP_TAB NESTED_EMP_TYPE;
BEGIN
  DELETE FROM EMP
   WHERE DEPTNO = 20 RETURNING EMPNO, ENAME, HIREDATE -- 使用returning 返回这几个列
   BULK COLLECT INTO EMP_TAB; -- 将返回的列的数据批量插入到集合变量

  DBMS_OUTPUT.PUT_LINE('删除 ' || SQL%ROWCOUNT || ' 行记录');
  COMMIT;

  IF EMP_TAB.COUNT > 0 THEN
    -- 当集合变量不为空时,输出所有被删除的元素
    FOR I IN EMP_TAB.FIRST .. EMP_TAB.LAST LOOP
      DBMS_OUTPUT.PUT_LINE('当前记录:' || EMP_TAB(I)
                           .EMPNO || CHR(9) || EMP_TAB(I)
                           .ENAME || CHR(9) || EMP_TAB(I)
                           .HIREDATE || ' 已被删除');
    END LOOP;
  END IF;
END;

4.4 BULK COLLECT的注意事项

  1. BULK COLLECT INTO 的目标对象必须是集合类型。
  2. 只能在服务器端的程序中使用BULK COLLECT,如果在客户端使用,就会产生一个不支持这个特性的错误。
  3. 不能对使用字符串类型作键的关联数组使用BULK COLLECT子句。
  4. 复合目标(如对象类型)不能在RETURNING INTO子句中使用。
  5. 如果有多个隐式的数据类型转换的情况存在,多重复合目标就不能在BULK COLLECT INTO子句中使用。
  6. 如果有一个隐式的数据类型转换,复合目标的集合(如对象类型集合)就不能用于BULK COLLECTINTO子句中

5 FORALL与BULK COLLECT综合运用

FORALL与BULK COLLECT是实现批量SQL的两个重要方式,我们可以将其结合使用以提高性能.

-- create tb_emp_test
 CREATE TABLE tb_emp_test AS
    SELECT empno, ename, hiredate
   FROM   EMP_TEST
   WHERE  1 = 0;

 DECLARE
   -- declare cursor
   CURSOR EMP_CUR IS
     SELECT EMPNO, ENAME, HIREDATE FROM EMP_TEST;
   -- 基于游标的嵌套表类型
   TYPE NESTED_EMP_TYPE IS TABLE OF EMP_CUR%ROWTYPE;
   -- 声明变量
   EMP_TAB NESTED_EMP_TYPE;
 BEGIN
   SELECT EMPNO, ENAME, HIREDATE BULK COLLECT
     INTO EMP_TAB
     FROM EMP_TEST
    WHERE SAL > 1000;

   -- 使用FORALL语句将变量中的数据插入到表tb_emp
   FORALL I IN 1 .. EMP_TAB.COUNT
     INSERT INTO
       (SELECT EMPNO, ENAME, HIREDATE FROM TB_EMP_TEST)
     VALUES EMP_TAB
       (I);

   COMMIT;
   DBMS_OUTPUT.PUT_LINE('总共向 tb_emp 表中插入记录数: ' || EMP_TAB.COUNT);
 END;

6 总结

  1. limit减少内存占用,如果数据量较大一次性全部加载到内存中,对PGA来说压力太大,可采用limit的方法一次加载一定数量的数据,建议值通常为1000。使用limit时注意,循环的时候如果用while cursor_name%found loop,对于最后一次fetch的数据量不足设定值1000,%found条件就会不成立。示例使用v_oid_lst.count > 0 作为判断条件。
  2. 在写plsql代码块,定义数值变量时,建议采用pls_integer类型,或者simple_integer类型。两者的区别:
Oracle9i之前有binary_integer类型,和11g中引入的pls_integer数值范围相同:-2147483647~+2147483647,但pls_integer有更高的性能。两者性能均优于number类型。
Oracle中也引入了simple_integer类型,不过不能包含null值,范围:-2147483648~2147483647,性能优于pls_integer。
  1. 使用ref cursor。
  2. 使用绑定变量。
  3. 自定义table类型。
  4. Bulk collect into加载到内存中,处理完业务逻辑后forall批量插入到数据表中。
  5. Forall可以使用returning bulk collect into,且可使用sql%rowcount返回其更新行数。
  6. type numbers is table of number index by binary_integer/pls_integer/simple_integer; 其作用是:
a. 加了"index by binary_integer "后,numbers类型的下标就是自增长,numbers类型在插入元素时,不需要初始化,不需要每次extend增加一个空间。
b. 如果没有这句话"index by binary_integer",那就得要显示对初始化,且每插入一个元素到numbers类型的table中时,都需要先extend。

事例代码地址:
https://github.com/landy8530/database-example/tree/master/oracle/sqlscripts

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 转 # https://www.cnblogs.com/easypass/archive/2010/12/ 08/...
    吕品㗊阅读 9,617评论 0 44
  • 你发上有蓝色的蝴蝶 哑口盘旋 双眼枯毁 你花格子的衣裳 风没有眼泪 来自格子里远方的人 手里带着玫瑰 天空上有猩红...
    树来阅读 358评论 0 2
  • 酒店外的活动,我们主要集中在东滩湿地周边,所有好玩的居然都是免费的,比如抓🦀️,看蜻蜓,赏荷花,揪狗尾巴草… 萱萱...
    f204a0de9504阅读 328评论 0 2
  • 思乡曲 夏老秋还月夜凉,苍梧疏影照红裳。 几多惶恐痴儿念,望断天涯思故乡。
    一叶纤尘阅读 66评论 0 1
  • 在利用动态规划进行全局比对(二)中我们探讨了两条序列全局比对的算法原理。 这一算法是由芝加哥的Needleman和...
    井底蛙蛙呱呱呱阅读 4,071评论 0 4