ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle并行DML与直接路径加载:insert parallel、truncate和delete实战解析

Oracle并行DML与直接路径加载:insert parallel、truncate和delete实战解析 很多人第一次接触 insert parallel、append 和 PDML 是在同一个需求里往一张几千万行的大表灌数据发现常规 insert 太慢于是开始加提示、开并行、试截断。结果经常是“加了 parallel 依然慢”“明明 append 了怎么 redo 还这么大”“truncate 秒完但 delete 跑了几十分钟”——这几个词看着像一家人背后却对应着完全不同的数据访问机制。我先把结论摆在这insert parallel 和“默认 append”是 Oracle 并行 DMLPDML下的行为前提是整个会话真的启用了 PDML而 truncate 与 delete 则是两种代价天差地别的清理手段。弄懂这两组差异你在大表加载和清理时被坑的概率至少能降一半。这篇文章就围绕这两个方向把我的实操经验和踩过坑的地方都说清楚。1. 先理清三个关键词insert parallel、默认append、PDML1.1 为什么会有“insert parallel默认append”的说法“insert parallel 默认 append”这句话初看容易误读成“只要 insert 加了 parallel 提示就会自动走直接路径加载”。其实严谨的说法是当会话启用 PDML 且 insert 真正以并行方式执行时Oracle 默认就会采用直接路径加载direct path insert其效果等价于给语句加上了 append 提示。这里的关键是“直接路径加载”这个动作。常规路径插入conventional insert是把数据块先弄进 buffer cache找空闲空间、写 undo、维护表块上的 ITL 信息整个过程对普通数据操作全透明。直接路径加载则完全换了一套逻辑直接在表的高水位线High Water MarkHWM之上格式化新块绕开 buffer cache 的常规缓冲路径因此生成的 undo 明显更少配合 nologging 时能极大降低事务生成量。在串行 insert 场景下你必须显式写INSERT /* APPEND */ INTO ...才能触发直接路径而一旦语句进入并行 DML 模式Oracle 默认使用 direct path这就是“默认 append”说法的来源。但这里有个非常常见的误区并行执行计划并不等于 PDML。很多人写INSERT /* PARALLEL(t 4) */ INTO t SELECT ... FROM s;以为目标表插入也被并行分成了 4 份。实际如果没有执行ALTER SESSION ENABLE PARALLEL DML;并行优化的作用域很可能只停留在 SELECT 查询部分而 INSERT 部分仍然以串行方式执行自然也不会默认 append。你打开执行计划一看走的还是LOAD TABLE CONVENTIONALredo 大小依然吓人。从机制上理解一句话就够了并行查询PQ在很多场景下可以自动启用比如全表扫描、排序、Hash Join但并行 DML 不是PDML 需要用户在会话级别明确打开开关。开这个开关意味着允许多个并行服务进程同时去修改目标表的分区或数据块这会显著影响锁行为、事务语义和资源消耗所以 Oracle 不会默认让它偷偷生效。1.2 并行DML和并行查询不是一回事并行查询开启后一条 select 的扫描、连接、排序会被拆成多个 PX slave 进程各干一段最后 coordinator 汇总。这种并行对数据只有读操作没有锁竞争也没有 undo 爆发的问题所以系统可以相对激进地自动并行。但并行 DML 是写操作多个进程同时插入、更新或删除会引入块级竞争、索引维护、外键约束检查等一系列问题。因此 Oracle 的策略是默认不启用 PDML必须显式开启会话级开关。实际使用中我建议至少从两个维度区分它们维度并行查询PQ并行DMLPDML默认状态很多时候自动启用默认关闭开启方式不用管优化器根据表并行度/参数决定ALTER SESSION ENABLE PARALLEL DML;影响操作select、create table as select 的查询段insert / update / delete / merge 的写段风险等级低主要占 CPU/IO高涉及锁、undo、redo、段空间常见误区以为加了并行提示就顺带把DML并行以为 query 并行DML 并行开启 PDML 的标准操作是-- 仅允许当前语句的DML部分走并行 ALTER SESSION ENABLE PARALLEL DML; -- 或者强制会话里的DML按指定并行度执行 ALTER SESSION FORCE PARALLEL DML PARALLEL 4;如果你用的是 19c 那样较新的版本还可以通过PDML相关初始化参数做资源管理但日常调优里最直接的方式仍然是显式开启会话开关。需要特别留意的是开了 PDML 不等于这条语句一定并行还需要目标表或者提示里给出并行度。比如INSERT /* PARALLEL(t, 4) PARALLEL(s, 4) */ INTO t SELECT /* PARALLEL(s, 4) */ * FROM s;这里PARALLEL(t, 4)是在请求目标表 t 的 DML 部分获得 4 个并行服务进程PARALLEL(s, 4)则是在告诉优化器查询源表 s 也可以使用 4 个并行度。若不启用 PDML即便你把目标表 t 的并行度写在提示里执行计划里也会出现“并行查询 串行插入”的拼接典型表现是 PX 行源下面跟着一个非并行的LOAD TABLE CONVENTIONAL操作节点。除了开关还要清楚 PDML 的几个硬限制我在生产环境里遇到的典型坑包括目标表如果有触发器PDML 会直接报错或静默降级为串行含有 LOB 列的堆表在并行 UPDATE/DELETE 时也容易踩雷会话开启 PDML 后同一对象最好不要被其他会话以串行方式同时修改否则可能出现锁等待和资源竞争。这些限制在后面的章节会展开说但它们恰恰解释了为什么“parallel”不能无脑加。2. truncate 与 delete一字之差代价天壤之别2.1 从数据文件层面看两者做了什么truncate 和 delete 虽然都是“把表里的数据清掉”但落到存储引擎层面完全是两个物种。delete 是标准的 DML 操作它逐行把数据标记为已删除并记录完整的 undo 信息以便支持 rollback。对一堆行做 delete等于告诉 Oracle“这些块里的这些行不要了”但块本身还属于这个表高水位线不会下降。你可以浅显地理解为delete 是把一抽屉文件一页一页撕掉抽屉本身还在抽屉里的空位以后还能放新文件。只是撕纸这个过程会产生大量碎纸——也就是 undo 和 redo。一张 1 亿行的表如果全量 deleteundo 表空间暴涨、归档日志飞涨是常有的事。truncate 则不同它是 DDL 操作。它不会去逐行判断“哪些行要删”而是直接把段的存储结构重置默认把高水位线降到起点并把分配出来的空间归还/重新标记。这个操作也会更新数据字典但不会像 delete 那样为每一行生成 undo因此速度通常以秒级起跳。不过要注意truncate 不会逐行触发行级触发器也不会走外键约束的逐行校验一旦执行无法通过 rollback 撤销。生产环境里这句“TRUNCATE 后不可回滚”被反复强调就是因为很多人把它当 delete 用结果手一抖就没了。从锁和并发的角度讲delete 在获得行锁的同时会在表级别加 Row Exclusive 锁其他会话可以继续查询但对该表的 DML 会被阻塞或等待truncate 则会对表加 Access Exclusive 级别的锁不同版本命名略有差异本质上是一条 DDLDDL 等待可能触发 ORA-00054。两者都会影响并发业务但影响窗口完全不同。从运维观察上看判断一条 delete 是否“跑得动”最直观的指标是 undo 增长速度和 redo 切换频率。而 truncate 的响应速度几乎不随数据量线性增长——清一张 10 万行和清一张 1 亿行在存储健康的前提下都在秒级。这也导致了一个很有意思的现象很多 DBA 一听到“清理大表”第一反应就是 truncate而开发同事一听到 “truncate 不能回滚”就开始战战兢兢。两者没有绝对的对错看场景。2.2 不同清理场景下的选型建议我在实际项目里总结过几条选型原则几乎可以套用到大多数数据清理需求里。首先看“清多少”。如果条件过滤后只删少量行比如 1%那 delete 是合理方案通过走索引定位少量数据undo 和锁都在可控范围如果条件过滤后要删掉 90% 以上的行delete 就非常不划算了——你为了移除 90% 的行反复做了 90% 行的 undo 和 redo还要忍受高水位线下那些“空洞”继续占着空间。这种场景最优解往往是“保留少数数据重建表结构”先把要保留的数据抽出来放进临时表truncate 原表再把临时表数据 append 回去。技术上可以用CREATE TABLE t_keep AS SELECT ...配合INSERT /* APPEND */完成但要注意外键和权限通常需要在一个维护窗口内一气呵成。其次看“能不能停机”。delete 适合在线小批量清理配合每 N 行 commit 一次把事务切小规避 undo 撑爆的风险truncate 适合有明确维护窗口的场景比如凌晨跑批整个表或整个分区清空。如果表被其他会话长时间持有锁truncate 就会很尴尬ORA-00054 一报任务直接失败而 delete 虽然慢至少能慢慢等行锁释放。第三个维度是“空间要不要还给表空间”。delete 之后表高水位线不动表空间文件大小大概率还是原来的样子truncate 默认会释放存储DROP STORAGE如果你只是想重置高水位线但保留已分配的空间供后续复用可以写成TRUNCATE TABLE t REUSE STORAGE;。分区表则更灵活TRUNCATE TABLE t PARTITION (p1);只清空一个分区其他分区不受影响。下面这张表我经常直接发给开发同学让他们先对号入座再决定用哪种方式对比项DELETETRUNCATE操作类型DMLDDL支持 WHERE支持不支持回滚能力未提交前可 rollback不可 rollbackundo 生成逐行产生量大几乎不逐行产生redo 生成量大索引/行变更少主要记录存储变更高水位线不下降重置空间释放不释放给表空间默认释放行级触发器可触发不触发外键引用逐行校验有外键可能报错SQL 执行耗时随行数线性增长通常秒级典型场景少量数据在线清理整表/整分区维护窗口清理3. 高性能加载与批量清理的完整实操3.1 启用PDML的配置步骤以 Oracle 数据库为例一条并行 insert 要真正“飞起来”通常需要做四件事。第一件不是加提示而是先看资源服务器 CPU 核数、当前负载、parallel_max_servers上限。并行度开太高小机直接被打满并行反而比串行还慢开太低又看不出效果。我个人习惯是从2 * CPU_COUNT往下取生产库上很少直接拉满。举个例子16 核的数据库服务器日常 OLTP 混合负载下我通常先用 DOP8 做测试观察v$px_process里实际起了几个并行服务进程再决定是否加。第二件是开启会话并行 DML 开关ALTER SESSION ENABLE PARALLEL DML;或者你想强制一批任务统一走 DOP4ALTER SESSION FORCE PARALLEL DML PARALLEL 4;第三件是确认语句执行计划确实把 insert 部分并行化了。最直接的方式EXPLAIN PLAN FOR INSERT /* PARALLEL(t, 4) PARALLEL(s, 4) */ INTO t SELECT /* PARALLEL(s, 4) */ * FROM s; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);在计划里看两个关键位置查源表 s 的TABLE ACCESS节点下面有没有PX BLOCK ITERATOR以及目标表 t 的插入节点是LOAD AS SELECT还是LOAD TABLE CONVENTIONAL。如果是前者说明插入部分已经在走直接路径如果是后者那就是 PDML 没开或者并行度失效。第四件事是监控。如果你用的是 Oracle 12c 以上版本可以直接用 SQL Monitor 看并行度实际用了几台 PX server或者实时跑SELECT sid, qcsid, server_group, server_set, status FROM v$px_session;这里能看到本会话对应的 Query Coordinator 和各 PX slave 进程。如果只看到一行 QC、没有对应 slave说明并行实际没起来。这里要提醒一个坑ALTER SESSION ENABLE PARALLEL DML只影响当前会话所有并行 DML 需求都要在每个会话里执行。很多报表系统连接池复用时你在一段脚本里开了 PDML但连接池换了一个物理 session后面的 insert 就又“退化”了。所以在自动化脚本里要把开关和 DML 语句放在同一个 session 生效的范围内测试别想当然认为一次开启全局有效。3.2 判断insert是否真正走了直接路径很多人加完 parallel 提示后心里没底不知道有没有生效。我常用的判断手段有三个按易用程度排序。第一个是看执行计划。执行计划里LOAD AS SELECT代表直接路径加载LOAD TABLE CONVENTIONAL代表常规路径加载。如果看到后者说明即便语句里写了 parallel插入部分也没有走 direct path。要注意这里必须在EXPLAIN PLAN FOR的语句里完整保留 insert 语句而不是只对 select 部分做计划。第二个是看 redo 和 undo 的增量。直接路径加载的典型特征就是事务生成的 redo 明显变少。测试方法很简单在执行前查一下当前会话的v$mystat中redo size和undo change vector size执行完再查一次相减SELECT name, value FROM v$mystat s, v$statname n WHERE s.statistic# n.statistic# AND n.name LIKE %redo size% OR n.name LIKE %undo change vector size%;如果你清空一张 1000 万行的表正常 delete 后 undo change vector 的数值一定非常夸张而走 truncate 时该指标可能只增加一点点。同样同一批数据用常规 insert 和 direct path insert 得到的 redo size 差别十分明显这往往比看执行计划更直观。第三个是查并行进程的运行痕迹。在另一个会话里执行SELECT qcsid, sid, server_group, degree FROM v$px_session;如果这个 insert 真在并行执行你会看到 QC 行下面挂着多个名为 P000、P001 这样的并行服务进程。如果只有一行 QC 或 degree1说明并行没生效先回去检查表的并行度和开关。实战里有一个细节值得记下来PDML 虽然让插入部分并行但锁和事务的复杂程度比并行查询高很多。我遇到过启用 PDML 后 insert 速度确实快了但 commit 阶段等待明显增加原因是并行服务进程把数据分块写入后协调进程要做一定程度的数据块整理和索引维护。这不是 bug而是并行 DML 的固有代价。因此但凡能在CREATE TABLE AS SELECT阶段把索引建好、减少增删改的就不要等到加完数据再去建索引否则索引上的串行合并操作可能成为新的瓶颈。3.3 一套组合拳append nologging 并行数据加载场景里最高效的组合通常是“并行 DML append 式直接路径 目标段 NOLOGGING”。先说一下 append 和 nologging 的分工。append 负责让插入走直接路径目的是绕开常规 buffer cache 路径、减少 undo 生成nologging 则是告诉数据库“这个段上的写操作不写 redo或者写最小化 redo”进一步削减日志压力。注意这两个是不同维度append 不等于 nologging即便走 direct path如果表处于 logging 模式redo 依然要写只是比常规路径可能少一些。很多教程把两者混在一起讲容易造成“我加了 append 怎么 redo 还很大”的困惑。一个典型的 ETL 加载脚本长这样ALTER SESSION ENABLE PARALLEL DML; ALTER TABLE target_tab NOLOGGING; INSERT /* APPEND PARALLEL(target_tab, 8) */ SELECT /* PARALLEL(source_tab, 8) */ col1, col2, ... FROM source_tab WHERE ... COMMIT; ALTER TABLE target_tab LOGGING;先把目标表切成 NOLOGGING加载完后再恢复 LOGGING是一种常见做法。但这里一定要想清楚生产风险的边界NOLOGGING 模式下如果数据库之后发生介质故障这部分数据可能无法通过归档日志完全恢复。所以它只适合“可从源系统重新拉取”的场景比如临时表、可重建的中间表、数据仓库里的明细贴源表对于核心交易表或者没有备份兜底的业务表慎用。我自己的底线是任何 NOLOGGING 操作之前先和备份团队确认恢复策略至少要有源文件或导出文件兜底。还有一个组合里容易被忽略的部分是目标表的“块可用状态”。直接路径加载是在高水位线之上写新块它不像常规 insert 那样先去段里找已有空闲块。如果一张表频繁 delete 后再 insert段里有很多“空洞”块直接路径加载并不会优先复用这些块反而会在高水位线之上继续分配新区最终导致表膨胀得厉害。所以不要以为“并行 append 加载 空间最优”恰恰相反如果目标表曾经被大量 delete 过先TRUNCATE ... REUSE STORAGE或者ALTER TABLE ... SHRINK SPACE才是正确的前置动作。在实际测试中我有一张 9 千万行的中间结果表原先用串行 insert 跑了 25 分钟redo 日志切换把磁盘都顶满了。改成ALTER SESSION ENABLE PARALLEL DML之后insert 部分实现并行执行时间压到 6 分钟再把临时表切成 NOLOGGING整体降到 3 分钟左右。这个效果不是固定值取决于磁盘 IO 能力、CPU 核数和目标段状态但“常规串行 - PDML 并行 - PDMLNOLOGGING”这个梯度是普遍成立的。4. 生产环境常见问题与排查实录4.1 并行insert没生效的原因这是最常被问的问题“我明明加了 parallel为什么没变快”按照经验90% 的情况是下面几个原因之一。第一PDML 开关没开。前面已经反复强调DML 部分的并行不是自动的。如果执行计划里插入节点显示的是LOAD TABLE CONVENTIONAL那基本可以断定这一点。处理方式很简单ALTER SESSION ENABLE PARALLEL DML;再来一次。第二表的并行度设置被覆盖或限制。如果你在表上定义了PARALLEL 2但提示里写PARALLEL(t, 8)通常提示优先但如果你没有写任何提示只靠表默认并行度那么表默认 degree 为 1 的时候即便开了 PDML也起不了并行。此时可以用ALTER TABLE t PARALLEL 4;或者干脆在语句里显式写提示避免依赖表属性。第三DOP 太大但资源池限制了。parallel_max_servers设得小优化器根据资源管理策略自动调低度数。对症方法是看v$px_process、v$px_session里的实际进程数别只看解释计划里的 “Degree: 4” 就高兴那可能只是优化器输出的目标 DOP实际运行时被资源管理压到了 1。第四语句形式本身不支持目标并行。比如插入值列表INSERT /* PARALLEL(t, 4) */ INTO t VALUES (1, a, SYSDATE);这种单行插入从物理上就无法并行提示会被忽略。并行 insert 主要针对INSERT INTO ... SELECT这种批量数据流。还有一个我在项目里真实踩过的坑用 PL/SQL 循环逐行 insert却在循环外面加了PARALLEL提示。逐行 insert 走的是常规路径且受限于循环的单行事务天然不可能并行。遇到这种代码正确做法是先落成临时表再一次性INSERT INTO ... SELECT或者把循环改成批量FORALL之后再用 SQL 级的并行加载。不要指望提示能救“逐行插入”。4.2 delete空间不释放truncate却被锁大表 delete 完以后看表大小还是几个 GB开发同事跑来问“删了怎么空间没变小”。这很典型delete 不会降低高水位线段的大小基本维持原样。此时如果你需要把空间还给文件系统方式包括ALTER TABLE ... ENABLE ROW MOVEMENT后SHRINK SPACE或者直接用ALTER TABLE ... MOVE重建表段再重建索引。注意这几种操作在业务高峰时都有不小的影响MOVE 期间索引可能失效SHRINK 会改变行迁移最好也放在维护窗口。反过来truncate 偶尔会“卡住”。最常见的是 ORA-00054: resource busy意思是这个表正被其他会话占用。用下面这句能看到是谁堵住了SELECT b.sid, b.serial#, b.username, b.status, o.object_name FROM v$locked_object v, dba_objects o, v$session b WHERE v.object_id o.object_id AND v.session_id b.sid;找到阻塞源之后判断是一个长事务还是连接泄漏能等就等实在不行再走 kill session 的流程。生产环境我一般不建议无脑杀尤其是有长事务在跑批时kill 会带来事务回滚成本和数据一致性问题。还有一个容易被忽略的点表上有外键约束时truncate 会报ORA-02266: unique/primary keys in table referenced by enabled foreign keys。delete 反而可以逐行处理。遇到这种情况要么先禁用外键truncate 完再启用要么改成 delete。禁用外键期间要谨慎评估数据校验窗口别把约束长期关着。4.3 PDML的限制和拆招把 PDML 当作“银弹”之前先记住几条硬限制否则容易在生产上翻车。我的经验里第一坑是“触发器”如果目标表上有 BEFORE 或 AFTER 行级触发器很多版本的 Oracle 会拒绝启用并行 DML或者静默把 DML 部分切回串行。处理思路很简单批处理阶段先禁用触发器处理完再启用前提是不影响业务审计等要求。如果有程序上的审计需求可以把记录先记到日志表再异步处理。第二坑是“LOB 列”。堆表带 LOB 列时并行 UPDATE/DELETE 会撞上限制insert 阶段也容易遇到奇怪的表现。如果非用并行不可一个拆招是把 LOB 字段拆出去基表只保留关键字段LOB 放到单独的表或者分区通过主键关联。另一个拆招是走分区表 分区交换让每个分区在结构操作层面完成数据替换不触发 PDML 的 LOB 限制。很多 DBA 到了这一步就直接改用 CTAS 重建反而更清爽。第三坑是“事务语义”。PDML 仍然属于事务的一部分可以 rollback但并行服务进程的中间结果在 commit/rollback 前是否会释放锁、是否影响查询一致性有时候会让人捉摸不透。我的建议是PDML 操作尽量在一个短事务里完成不要在一个开启超长事务的会话里混合多条并行 DML避免锁和 undo 的不可控叠加。第四坑其实不算坑是性能调优方向问题。并行 DML 把写压力分散到了多个进程但最终都要经过 I/O 通道和日志缓冲区。如果存储本身扛不住比如单磁盘写入带宽已经是瓶颈那并行只是把瓶颈暴露得更明显——redo 文件所在的磁盘、undo 表空间所在的磁盘任何一个准备不足都会把“并行加速”变成“并行排队”。所以跑大规模并行加载之前先看v$filestat里的物理写时间是不是明显偏高再决定要不要继续加压。4.4 一张速查表帮你在两种操作里选型最后把常见的现象、原因和应对方向收拢成一张速查表方便直接抄作业现象可能原因排查方法处理建议insert 加了 parallel 没提速未启用 PDML / 提示被忽略 / DOP 被限制看执行计划是否LOAD TABLE CONVENTIONAL查v$px_session先ALTER SESSION ENABLE PARALLEL DML;确认 DOP 设置redo 和 undo 增长很快走的常规路径未触发直接路径加载v$mystat对比 redo size / undo change vectorinsert 加 append或确认 PDML 生效delete 后表空间没变小delete 不降高水位线查dba_segments段大小考虑 shrink space / move / 重建表truncate 报 ORA-00054表被其他会话锁定查v$locked_object协调事务提交或等窗口执行truncate 报 ORA-02266外键约束引用查外键关系暂时禁用外键或改用 delete并行 DML 报错或降级串行表上有触发器 / LOB 列 / 资源限制看告警日志和执行计划拆表、禁用触发器、调整并行度批量加载把存储 IO 打满并行度超过存储能力v$filestat/ OS 层 iostat降 DOP或扩容/分散存储错峰执行这里我想额外强调一点任何大表操作在动手之前都应该先记一个“代价基线”。比如 delete 之前先看一下当前 undo 表空间可用量、归档日志频率、目标表的段大小、外键情况跑完之后记录实际耗时、redo 生成量和空间变化。有了这些数据下次做类似操作时就不是“拍脑袋选 delete 还是 truncate”而是真正有依据地判断。我之前接过一个案例业务方要清 3 亿行日志表里的历史分区开发坚持用 delete理由是“truncate 不敢用万一删错了回不来”。我理解这种谨慎但 3 亿行 delete 跑了四个多小时把 undo 表空间顶到 90% 报警归档目录差点写满。后来改成“先检查分区边界 - 确认要清除的分区 -TRUNCATE TABLE ... DROP PARTITION”的方式耗时从四小时变成 3 秒业务影响窗口从根本上消失了。这就是结构操作和行级操作之间的差异选对路径比单纯堆并行度重要得多。做数据处理这些年我个人的体会是不要迷信任何单一手段。insert parallel、append、PDML 这些工具是为你解决“如何更高效地写入”服务的truncate 和 delete 则是“如何更安全地清理”的选择题。两者之间真正的连接点是业务场景你要的是“把数据快速灌进去”还是“把历史包袱卸下来”决定了你该把精力花在哪套机制上。最后再分享一个我踩过几次坑后的习惯任何批量操作脚本都先在一个百行级的小表上验证执行计划和参数确认开关生效后再放大到生产表。数据量越大越是不能在“提示是否生效”这种基础问题上心存侥幸。
返回列表