ITPub博客

首页 > 数据库 > Oracle > Oracle如何实现批量 DML 的手动并行

Oracle如何实现批量 DML 的手动并行

原创 Oracle 作者:a960549548 时间:2026-09-29 11:29:06 0 删除 编辑

大批量 UPDATE、DELETE 的执行时间通常与行数成正比,串行跑到几个小时是常有的事。提高这类语句的速度,并行hint(`/*+ PARALLEL */``)对查询成熟,但对 DML,数据库默认只并行读、不并行写,真正要并行执行 DML 还得开启会话级并行 DML,粒度、资源都不可控。

从 11g 第 2 版起,Oracle 提供了另一个思路: DBMS_PARALLEL_EXECUTE 包。它把一张表按 ROWID(或数字列、自定义 SQL)切成若干片(chunk),每片交给一个 Scheduler 作业独立执行同一条 DML,作业数、每片的规模都由调用者指定。本质上就是把手工开四个会话、每个处理一段 ROWID 范围的老办法自动化了,而且自带断点续跑能力。

该包 EXECUTE 权限授予了 PUBLIC,但并行执行依赖 Scheduler 作业,用户需要 CREATE JOB 权限。

一、并行执行包介绍

DBMS_PARALLEL_EXECUTE
.CREATE_TASK
(

  task_name IN VARCHAR2 ,
  comment   IN VARCHAR2 DEFAULT NULL ) ;

DBMS_PARALLEL_EXECUTE .CREATE_CHUNKS_BY_ROWID (
  task_name   IN VARCHAR2 ,
  table_owner IN VARCHAR2 ,
  table_name   IN VARCHAR2 ,
  by_row       IN BOOLEAN ,     -- TRUE: chunk_size 按行数,FALSE: 按块数
  chunk_size   IN NUMBER ) ;

DBMS_PARALLEL_EXECUTE .RUN_TASK (
  task_name       IN VARCHAR2 ,
  sql_stmt       IN CLOB ,
  language_flag   IN NUMBER ,   -- 通常传 DBMS_SQL.NATIVE
  parallel_level IN NUMBER DEFAULT 0 ,
  job_class       IN VARCHAR2 DEFAULT 'DEFAULT_JOB_CLASS' ) ;

DBMS_PARALLEL_EXECUTE .DROP_TASK (
  task_name IN VARCHAR2 ) ;

除 ROWID 外,还有两个切分手段: CREATE_CHUNKS_BY_NUMBER_COL 按表的数字列区间切(适合有连续编号列的表), CREATE_CHUNKS_BY_SQL 完全用自定义 SQL 定义片边界(适合按业务维度切)。

任务与分片的状态分别记录在两张视图里:

任务状态(USER_PARALLEL_EXECUTE_TASKS) :

状态 含义
CREATED 任务已创建
CHUNKING / CHUNKING_FAILED 切分中 / 切分失败
CHUNKED / NO_CHUNKS 切分完成 / 无分片
PROCESSING 执行中
FINISHED / FINISHED_WITH_ERROR 全部成功 / 部分分片失败
CRASHED 任务崩溃

分片状态(USER_PARALLEL_EXECUTE_CHUNKS) :

状态 含义
UNASSIGNED 未分配
ASSIGNED 已分配给作业
PROCESSED / PROCESSED_WITH_ERROR 处理成功 / 处理失败

两个关键行为特征:其一, RUN_TASK 是阻塞调用,任务全部结束后才返回,执行期间可以从另一会话查分片状态观察进度;其二, 每个分片处理完即提交 ,因此中途失败的任务再次执行 RUN_TASK 时,只会处理未完成的分片,天然的断点续跑。

二、实测:把 20 万行的更新切成四片并行

1. 授权(DBA)


SQL
> 
grant 
create job 
to test
;


Grant succeeded.

2. 准备数据(TEST 用户)


SQL
> 
create 
table t_pe
(id number
, pad varchar2
(
100
)
, flag number 
default 
0
)
;


Table created.

SQL > begin
  2     for i in 1 .. 200000 loop
  3       insert into t_pe values (i , rpad ( 'x' , 100 , 'x' ) , 0 ) ;
  4     end loop ;
  5     commit ;
  6   end ;
  7  /

PL/SQL procedure successfully completed.

3. 创建任务并切分


SQL
> 
begin

  2    dbms_parallel_execute .create_task ( 'T_PE_UPDATE' , '测试任务' ) ;
  3    dbms_parallel_execute .create_chunks_by_rowid (
  4      task_name   => 'T_PE_UPDATE' ,
  5      table_owner => user ,
  6       table_name   => 'T_PE' ,
  7      by_row       => true ,
  8      chunk_size   => 50000 ) ;
  9   end ;
10  /

PL/SQL procedure successfully completed.

SQL > select chunk_id , status , start_rowid , end_rowid
  2   from user_parallel_execute_chunks
  3   where task_name = 'T_PE_UPDATE'
  4   order by chunk_id ;

 CHUNK_ID STATUS     START_ROWID        END_ROWID
---------- ---------- ------------------ ------------------
        1 UNASSIGNED AAAWiXAAEAAAAkSAAA AAAWiXAAEAAAAtPCcg
        2 UNASSIGNED AAAWiXAAEAAAAtPDAAA AAAWiXAAEAAAAyfCcg
        3 UNASSIGNED AAAWiXAAEAAAAyfDAAA AAAWiXAAEAAAA8PCcg
        4 UNASSIGNED AAAWiXAAEAAAA8PDAAA AAAWiXAAEAAABDPCcg

20 万行按每片 5 万行切成了 4 片,每片记录了 ROWID 范围边界。

4. 并行执行


SQL
> 
declare

  2    l_sql clob : =
  3       'update t_pe set flag = 1
 4       where rowid between :start_id and :end_id
 5         and flag = 0' ;
  6   begin
  7    dbms_parallel_execute .run_task (
  8      task_name       => 'T_PE_UPDATE' ,
  9      sql_stmt       => l_sql ,
10      language_flag   => dbms_sql .native ,
11      parallel_level => 4 ) ;
  12   end ;
13  /

PL/SQL procedure successfully completed.

注意 DML 中的占位符:按文档要求必须命名为 :start_id 与 :end_id ——ROWID 切分时二者的类型是 ROWID,数字列切分时是 NUMBER。 另外语句中的 flag = 0 条件让重跑时天然跳过已处理的行,是配合断点续跑的惯用写法。

5. 检查结果


SQL
> 
select 
status
, 
count
(
*
) 
from user_parallel_execute_chunks

  2   where task_name = 'T_PE_UPDATE'
  3   group by status ;

STATUS       COUNT ( * )
----------- ----------
PROCESSED           4

SQL > select task_name , status from user_parallel_execute_tasks
  2   where task_name = 'T_PE_UPDATE' ;

TASK_NAME     STATUS
------------- --------------------
T_PE_UPDATE   FINISHED

SQL > select count ( * ) from t_pe where flag = 1 ;

  COUNT ( * )
----------
    200000

四个分片全部 PROCESSED,任务状态 FINISHED,20 万行更新完毕。若有分片失败,状态为 PROCESSED_WITH_ERROR, ERROR_MESSAGE 列保留报错文本,修正后重新 RUN_TASK 即可只补跑失败的分片。

6. 清理任务


SQL
> exec dbms_parallel_execute
.drop_task
(
'T_PE_UPDATE'
)


PL/SQL procedure successfully completed.

三、注意事项

  1. 每片处理完立即提交,整个任务 不具备全局原子性 ——中断后是"做了一部分"的状态,业务上要么接受(配合幂等的 WHERE 条件重跑),要么就不适合用这个工具;

  2. 包内子程序(少数查询函数除外)调用时会提交,不要把 CREATE_TASK 等调用嵌在自己的业务事务中间;

  3. parallel_level 是同时运行的作业数,不是分片数;它决定了同时打开的并行 DML 资源消耗,按表上的锁、undo 和回滚压力确定,一般从 4、8 起步观察;

  4. ROWID 切分按行数估算边界,片与片之间行数不完全均匀(块内行数不同所致),不影响正确性,只影响负载均衡;

  5. 目标表正在被其他会话修改时,分片内的 DML 一样要等行锁,切分解决不了锁等待,只解决单进程写入速度;

  6. 任务名全局唯一,重跑前先确认旧任务状态并 DROP_TASK 清理。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013625/viewspace-3184249/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
Oracle ACE,MySQL 8.0 OCP,PMP,拥有OceanBase OBCP,TiDB PCTP,GoldenDB GDCP等多项认证,ITPUB认证专家,微信公众号:DBA之友。

注册时间:2022-02-08

  • 博文量
    132
  • 访问量
    171477