本案例为好友南京越烟(QQ: 843999405)分享。

    一个存储过程从周五晚上执行了到了周一还没有执行完,存储过程代码如下。

    1. declare
    2. isMatch Boolean := false
    3. dealPnCnt number(10) := 0
    4. begin
    5. for c_no_data in select nbn.no, 69 as partition_id
    6. from TMP_NBR_NO_XXXX nbn
    7. where nbn.level_id = 1
    8. and length(nbn.no) = 8 loop
    9. dealPnCnt := dealPnCnt + 1
    10. for c_data in select nli.*, nl.nbr_level_id
    11. from tmp_xxx_item nli,
    12. a_level_item nl2i,
    13. b_level_item nl,
    14. c_level_item ns2l
    15. where nli.nbr_level_item_id = nl2i.nbr_level_item_id
    16. and nl2i.nbr_level_id = nl.nbr_level_id
    17. and nl.nbr_level_id = ns2l.nbr_level_id
    18. and ns2l.area_id = c_no_data.partition_id
    19. and ns2l.res_spec_id = 6039
    20. and ns2l.nbr_level_id between 201 and 208
    21. order by nl2i.priority loop
    22. if regexp_like(c_no_data.no, c_data.expression)) then
    23. update TMP_NBR_NO_XXXX n
    24. set n.level_id = c_data.nbr_level_id
    25. where n.no = c_no_data.no
    26. exit
    27. end if
    28. end loop
    29. if moddealPnCnt, 5000 = 0 then
    30. commit
    31. end if
    32. end loop
    33. end
    34. TMP_NBR_NO_XXXX 共有 400w 行数据,180MB
    35. select nli.*, nl.nbr_level_id
    36. from tmp_xxx_item nli
    37. a_level_item nl2i
    38. b_level_item nl
    39. c_level_item ns2l
    40. where nli.nbr_level_item_id = nl2i.nbr_level_item_id
    41. and nl2i.nbr_level_id = nl.nbr_level_id
    42. and nl.nbr_level_id = ns2l.nbr_level_id
    43. and ns2l.area_id = c_no_data.partition_id
    44. and ns2l.res_spec_id = 6039
    45. and ns2l.nbr_level_id between 201 and 208
    46. order by nl2i.priority

    上面 SQL 查询返回 43 行数据。

    在 5.1 节提到过,嵌套循环就是一个 LOOP 循环,LOOP 套 LOOP 相当于笛卡儿积。该 PLSQL 代码中有 LOOP 套 LOOP 的情况,这就导致 UPDATE TMP_NBR_NO_XXXX 要执行(400 万*43)次,TMP_NBR_NO_XXXX.no 列没有索引,TMP_NBR_NO_XXXX 每次更新都要进行全表扫描。这就是为什么存储过程从周五执行到周一还没执行完。

    大家可能会问,为什么不用 MERGE INTO 对 PLSQL 代码进行改写呢?PLSQL 代码中是用 regexp_like(c_no_data.no, c_data.expression)进行关联的,使用 like,regexp_like 关联,无法走 HASH 连接,也无法走排序合并连接,两表只能走嵌套循环并且被驱动表无法走索引。如果强行使用 MERGE INTO 进行改写,因为该 SQL 执行时间很长,会导致 UNDO 不释放,所以,我们没有采用 MERGE INTO 对代码进行改写。

    大家可能也会问,为什么不对 TMP_NBR_NO_XXXX.no 建立索引呢?这是因为关联更新可以采用 ROWID 批量更新,所以没有采用建立索引方法优化。

    下面我们采用 ROWID 批量更新方法改写上面 PLSQL,为了方便大家阅读 PLSQL 代码,先创建一个临时表用于存储 43 记录。

    1. create table TMP_DATE_TEST
    2. (
    3. expression VARCHAR2(255) not null,
    4. nbr_level_id NUMBER(9) not null,
    5. priority NUMBER(8) not null
    6. );
    7. insert into TMP_DATE_TEST
    8. select nli.expression, nl.nbr_level_id, priority from tmp_xxx_item nli,
    9. a_level_item nl2i,
    10. b_level_item nl,
    11. c_level_item ns2l
    12. where nli.nbr_level_item_id = nl2i.nbr_level_item_id
    13. and nl2i.nbr_level_id = nl.nbr_level_id
    14. and nl.nbr_level_id = ns2l.nbr_level_id
    15. and ns2l.area_id = 69
    16. and ns2l.res_spec_id = 6039
    17. and ns2l.nbr_level_id between 201 and 208;

    我们创建另外一个临时表,用于存储要被更新的表的 ROWID 以及过滤条件字段。

    1. create table TMP_NBR_NO_XXXX_TEXT
    2. (
    3. rid ROWID,
    4. no VARCHAR2(255),
    5. );
    6. insert into TMP_NBR_NO_XXXX_TEXT
    7. select rowid rid, nbn.no, from TMP_NBR_NO_XXXX nbn where nbn.level_id=1 and length(nbn.no)= 8 ;

    改写之后的 PLSQL 代码如下。

    1. declare
    2. type rowid_table_type is table of rowid index by pls_integer;
    3. updateCur sys_refcursor;
    4. v_rowid rowid_table_type;
    5. v_rowid2 rowid_table_type;
    6. begin
    7. for c_no_data in (select t.expression, t.nbr_level_id, t.priority
    8. from TMP_DATE_TEST t
    9. order by 3) loop
    10. open updateCur for
    11. select rid
    12. from TMP_NBR_NO_XXXX_TEXT nbn
    13. where regexp_like(nbn.no, c_no_data.expression);
    14. loop
    15. fetch updateCur bulk collect
    16. into v_rowid LIMIT 20000;
    17. forall i in v_rowid.FIRST .. v_rowid.LAST
    18. update TMP_NBR_NO_XXXX
    19. set level_id = c_no_data.nbr_level_id
    20. where rowid = v_rowid(i);
    21. commit;
    22. exit when updateCur%notfound;
    23. end loop;
    24. CLOSE updateCur;
    25. end loop;
    26. end;

    改写后的 PLSQL 能在 4 小时左右执行完。有没有什么办法进一步优化呢?单个进程能在 4 小时左右执行完,如果开启 8 个并行进程,那应该能在 30 分钟左右执行完。但是 PLSQL 怎么开启并行呢?正常情况下 PLSQL 是无法开启并行的,如果我们直接在多个窗口中执行同一个 PLSQL 代码,会遇到锁争用,如果能解决锁争用,在多个窗口中执行同一个 PLSQL 代码,这样就变相实现了 PLSQL 开并行功能。在第 8 章提到过,可以利用 ROWID 切片变相实现并行。

    1. select DBMS_ROWID.ROWID_CREATE(1, c.oid, e.RELATIVE_FNO, e.BLOCK_ID, 0) minrid,
    2. DBMS_ROWID.ROWID_CREATE(1,
    3. c.oid,
    4. e.RELATIVE_FNO,
    5. e.BLOCK_ID + e.BLOCKS - 1,
    6. 10000) maxrid
    7. from dba_extents e,
    8. (select max(data_object_id) oid
    9. from dba_objects
    10. where object_name = upper('TMP_NBR_NO_XXXX_TEXT')
    11. and owner = upper('RESCZ2')
    12. and data_object_id is not null) c
    13. where e.segment_name = 'TMP_NBR_NO_XXXX_TEXT'
    14. and e.owner = 'RESCZ2';

    但是这时我们发现,切割出来的数据分布严重不均衡,这是因为创建表空间的时候没有指定 uniform size 的 Extent。于是我们新建一个表空间,指定采用 uniform size 方式管理 Extent。

    1. create tablespace TBS_BSS_FIXED datafile
    2. '/oradata/osstest2/tbs_bss_fixed_500.dbf'
    3. size 500M extent management local uniform size 128k;

    我们重建一个表用来存储要被更新的 ROWID。

    1. create table RID_TABLE
    2. (
    3. rowno NUMBER,
    4. minrid VARCHAR2(18),
    5. maxrid VARCHAR2(18)
    6. ) ;

    我们将 ROWID 插入到新表中。

    1. insert into rid_table
    2. select rownum rowno,
    3. DBMS_ROWID.ROWID_CREATE(1, c.oid, e.RELATIVE_FNO, e.BLOCK_ID, 0) minrid,
    4. DBMS_ROWID.ROWID_CREATE(1,
    5. c.oid,
    6. e.RELATIVE_FNO,
    7. e.BLOCK_ID + e.BLOCKS - 1,
    8. 10000) maxrid
    9. from dba_extents e,
    10. (select max(data_object_id) oid
    11. from dba_objects
    12. where object_name = upper('TMP_NBR_NO_XXXX_TEXT')
    13. and owner = upper('RESCZ2')
    14. and data_object_id is not null) c
    15. where e.segment_name = 'TMP_NBR_NO_XXXX_TEXT'
    16. and e.owner = 'RESCZ2';

    这样 RID_TABLE 中每行指定的数据都很均衡,大概 4 035 条数据。最终更改的 PLSQL 代码如下。

    1. create or replace procedure pro_phone_grade(flag_num in number)
    2. as
    3. type rowid_table_type is table of rowid index by pls_integer;
    4. updateCur sys_refcursor;
    5. v_rowid rowid_table_type;
    6. v_rowid2 rowid_table_type;
    7. begin
    8. for rowid_cur in (select * from rid_table where mod(rowno, 8)=flag_num
    9. loop
    10. for c_no_data in (select t.expression, t.nbr_level_id, t.priority from TMP_DATE_TEST t order by 3 )
    11. loop
    12. open updateCur for select rid,rowid from TMP_NBR_NO_XXXX_TEXT nbn
    13. where rowid between rowid_cur.minrid and rowid_cur.maxrid
    14. and regexp_like(nbn.no, c_no_data.expression);
    15. loop
    16. fetch updateCur bulk collect into v_rowid, v_rowid2 LIMIT 20000;
    17. forall i in v_rowid.FIRST ..v_rowid.LAST
    18. update TMP_NBR_NO_XXXX set level_id = c_no_data.nbr_level_id where rowid = v_rowid(i);
    19. commit;
    20. exit when updateCur%notfound;
    21. end loop;
    22. CLOSE updateCur;
    23. end loop;
    24. end loop;
    25. end;

    然后我们在 8 个窗口中同时运行以上 PLSQL 代码。

    1. begin
    2. pro_phone_grade(0);
    3. end;
    4. begin
    5. pro_phone_grade(1);
    6. end;
    7. begin
    8. pro_phone_grade(2);
    9. end;
    10. .....
    11. begin
    12. pro_phone_grade(7);
    13. end;

    最终我们能在 29 分钟左右执行完所有存储过程。本案例经典之处就在于 ROWID 切片实现并行,同时考虑到了数据分布对并行的影响,其次还使用了 ROWID 关联更新技巧。