对一个很大的分区表进行 UPDATE、DELETE,想要加快执行速度,可以按照分区,在不同的会话中对每个分区单独进行 UPDATE、DELETE。但是对一个很大的非分区表进行 UPDATE、DELETE,如果只在一个会话里面运行 SQL,很容易引发 UNDO 不够,如果会话连接中断,会导致大量数据从 UNDO 回滚,这将是一场灾难。

    对于非分区表,我们可以对表按照 ROWID 切片,然后开启多个窗口同时执行 SQL,这样既能加快执行速度,还能减少对 UNDO 的占用。

    Oracle 提供了一个内置函数 DBMS_ROWID.ROWID_CREATE()用于生成 ROWID。对于一个非分区表,一个表就是一个段(Segment),段是由多个区(extent)组成,每个区里面的块物理上是连续的。因此,我们可以根据数据字典 DBA_EXTENTSDBA_OBJECTS关联,然后再利用生成 ROWID 的内置函数人工生成 ROWID。

    例如,我们对 SCOTT 账户下 TEST 表按照每个 Extent 进行 ROWID 切片。

    1. select ' and rowid between ' || '''' ||
    2. dbms_rowid.rowid_create(1,
    3. b.data_object_id,
    4. a.relative_fno,
    5. a.block_id,
    6. 0) || '''' || ' and ' || '''' ||
    7. dbms_rowid.rowid_create(1,
    8. b.data_object_id,
    9. a.relative_fno,
    10. a.block_id + blocks - 1,
    11. 999) || ''';'
    12. from dba_extents a, dba_objects b
    13. where a.segment_name = b.object_name
    14. and a.owner = b.owner
    15. and b.object_name = 'TEST'
    16. and b.owner = 'SCOTT'
    17. order by a.relative_fno, a.block_id;

    切片后生成的部分数据如下所示。

    1. and rowid between 'AAASs5AAEAAB+SIAAA' and 'AAASs5AAEAAB+SPAPn';
    2. and rowid between 'AAASs5AAEAAB+SQAAA' and 'AAASs5AAEAAB+SXAPn';
    3. and rowid between 'AAASs5AAEAAB+SYAAA' and 'AAASs5AAEAAB+SfAPn';
    4. and rowid between 'AAASs5AAEAAB+SgAAA' and 'AAASs5AAEAAB+SnAPn';
    5. and rowid between 'AAASs5AAEAAB+SoAAA' and 'AAASs5AAEAAB+SvAPn';

    假如要执行delete test where object_id>50000000,test 表有 1 亿条数据,要删除其中 5 000 万行数据,我们根据上述方法对表按照 ROWID 切片。

    1. delete test
    2. where object_id > 50000000
    3. and rowid between 'AAASs5AAEAAB+SIAAA' and 'AAASs5AAEAAB+SPAPn';
    4. delete test
    5. where object_id > 50000000
    6. and rowid between 'AAASs5AAEAAB+SQAAA' and 'AAASs5AAEAAB+SXAPn';
    7. delete test
    8. where object_id > 50000000
    9. and rowid between 'AAASs5AAEAAB+SYAAA' and 'AAASs5AAEAAB+SfAPn';
    10. delete test
    11. where object_id > 50000000
    12. and rowid between 'AAASs5AAEAAB+SgAAA' and 'AAASs5AAEAAB+SnAPn';
    13. delete test
    14. where object_id > 50000000
    15. and rowid between 'AAASs5AAEAAB+SoAAA' and 'AAASs5AAEAAB+SvAPn';

    最后,我们将上述 SQL 在不同窗口中执行,这样就能加快 delete 速度,也能减少对 UNDO 的占用。

    上述方法需要手动编辑大量 SQL 脚本,如果表的 Extent 很多,这将带来大工作量。我们可以编写存储过程简化上述操作。

    因为存储过程需要访问数据字典,我们需要单独授权查询数据字典权限。

    1. grant select on dba_extents to scott;
    2. grant select on dba_objects to scott;
    3. CREATE OR REPLACE PROCEDURE P_ROWID(RANGE NUMBER, ID NUMBER) IS
    4. CURSOR CUR_ROWID IS
    5. SELECT DBMS_ROWID.ROWID_CREATE(1,
    6. B.DATA_OBJECT_ID,
    7. A.RELATIVE_FNO,
    8. A.BLOCK_ID,
    9. 0) ROWID1,
    10. DBMS_ROWID.ROWID_CREATE(1,
    11. B.DATA_OBJECT_ID,
    12. A.RELATIVE_FNO,
    13. A.BLOCK_ID + BLOCKS - 1,
    14. 999) ROWID2
    15. FROM DBA_EXTENTS A, DBA_OBJECTS B
    16. WHERE A.SEGMENT_NAME = B.OBJECT_NAME
    17. AND A.OWNER = B.OWNER
    18. AND B.OBJECT_NAME = 'TEST'
    19. AND B.OWNER = 'SCOTT'
    20. AND MOD(A.EXTENT_ID, RANGE) = ID;
    21. V_SQL VARCHAR2(4000);
    22. BEGIN
    23. FOR CUR IN CUR_ROWID LOOP
    24. V_SQL := 'delete test where object_id > 100 and rowid between :1 and :2';
    25. EXECUTE IMMEDIATE V_SQL
    26. USING CUR.ROWID1, CUR.ROWID2;
    27. COMMIT;
    28. END LOOP;
    29. END;

    如果要将表切分为 6 份,我们可以在 6 个窗口中依次执行。

    1. begin
    2. p_rowid(6, 0);
    3. end;
    4. /
    5. begin
    6. p_rowid(6, 1);
    7. end;
    8. /
    9. begin
    10. p_rowid(6, 2);
    11. end;
    12. /
    13. begin
    14. p_rowid(6, 3);
    15. end;
    16. /
    17. begin
    18. p_rowid(6, 4);
    19. end;
    20. /
    21. begin
    22. p_rowid(6, 5);
    23. end;
    24. /

    这样就达到了将表按 ROWID 切片的目的。在工作中,大家可以根据自己的具体需求对存储过程稍作修改(阴影部分)。