Oracle DDL与特殊事务:当CREATE TABLE按下回车后,内核到底在忙什么?
2026/8/25 21:35:17 网站建设 项目流程

前言

干了这么多年 DBA,我发现一个挺有意思的现象:绝大多数人对 Oracle 事务的理解,都停留在 insert / update / delete 加 commit / rollback 这个层面。顶多再知道个 savepoint,能说出"DDL 会自动提交"就已经算不错的了。

但如果你真的去 dump 过块、看过 undo 链、追过递归 SQL,你会发现:用户层面那条简简单单的 CREATE TABLE,在内核眼里根本不是"一条语句",而是一场涉及数据字典、空间管理、段头格式化的"多兵种联合作战"。而所谓的"自动提交",背后藏着一整套递归事务、控制文件事务、自治事务的机制。

这篇文章我就从一个最平常的动作聊起——你在 SQL*Plus 里敲下 CREATE TABLE t (id NUMBER);,按下回车。光标闪了那么零点几秒,返回了 Table created.。

这零点几秒里,内核到底在忙什么?

一、CREATE TABLE 不是一条语句,是一堆 DML

先从最重要的认知颠覆开始:DDL 在用户层面是单条语句,在内核层面是一堆递归 DML。

你以为你执行的是一条 CREATE TABLE,实际上 Oracle 替你悄悄地往数据字典基表里插了一大堆行。具体都干了啥?我给你列个清单:

  • insert/update OBJ$

    :对象登记表,你的表在这里有了个户口(obj#)

  • delete from FET$ / insert into UET$

    :字典管理表空间下,空闲区(Free Extent)转成已用区(Used Extent)

  • insert into SEG$

    :段信息登记

  • update TSQ$

    :表空间配额扣减

  • insert into TAB$

    :表定义

  • insert into COL$

    :列定义,一列一行

  • 维护I_OBJ1I_OBJ2等字典索引

  • 在目标数据文件里格式化段头块(segment header)

注意最后一条——这不只是写字典,还涉及物理块的格式化。这也是为什么 CREATE TABLE 会产生 redo:所有这些变化都要记 redo log,段头块格式化的 redo opcode 是13.1(Layer 13 是块格式化层)。11.2 之后有了 deferred segment creation(延迟段创建),空表不建段,这个动作会推迟到第一行数据插入时才发生——但那是后话。

这些对字典基表的 insert/update/delete,Oracle 内部叫递归 SQL(recursive SQL)。名字挺形象:你的 SQL 触发了 Oracle 自己的 SQL,一层套一层。

二、隐式提交:DDL 最坑人的特性

接着说一个坑,很多老手都踩过。

DDL 有一个特性:执行成功会自动提交。但这句话只说了一半。完整的真相是——DDL 开始执行之前,会先把你当前会话里所有未提交的 DML 隐式提交掉。

也就是说:

UPDATE emp SET sal = sal * 1.1 WHERE deptno = 10; -- 没提交CREATE TABLE tmp_t (id NUMBER); -- 这一敲,UPDATE 被提交了

你本来以为那个 UPDATE 还能 ROLLBACK 反悔,结果 CREATE TABLE 一执行,反悔的机会没了。DDL 前后的两个隐式提交(执行前提交一次,成功后再提交一次)是 Oracle 的经典行为,目的之一是让字典修改和你的事务彻底解耦。

所以我的习惯是:DDL 永远单独开一个会话执行,别和业务 DML 混在一起。这不是洁癖,是血泪教训。

三、DDL 失败了,会全部回滚吗?不会

这里有个反直觉的点,值得好好说说。

既然 DDL 内部是一堆递归 DML,那它中途失败了(比如磁盘满、配额不够),是不是把所有递归操作全回滚?

不是。Oracle 只回滚"让数据库恢复一致所必需的部分",刻意不回滚空间管理操作。

最经典的例子:你执行一个大 insert,过程中表空间给你分配了新的 extent,然后语句失败了。回滚这个语句时,extent 的分配不会被回滚——新分配的 extent 就留在那里了。

为什么?因为失败的语句经常会被重试。如果每次失败都把 extent 还回去,下次重试又得重新分配,空间管理事务来来回回折腾,纯属浪费。Oracle 的设计哲学是:空间管理事务次数降到最少,一致性恢复只做到"够用"就行。

这个设计思路其实贯穿了 Oracle 内核很多地方:不是所有东西都要"干净利落",性能优先的前提下,够用就好。

四、递归事务与 SYSTEM undo 段的"特权"

下面聊一个大部分人不知道的机制:递归事务(Recursive-Level Transaction)。

递归 SQL 执行时也是要改数据的(改字典基表),改数据就要有事务保护,就可能失败、可能崩溃。所以 Oracle 为递归操作和空间管理事务生成 undo 和 redo——即使它们最终会被隐式提交,undo 也不能少,这是崩溃恢复的底线。

递归事务有几个特点,我觉得挺优雅:

第一,递归事务总是和它的顶层事务使用同一个 undo 段。不另起炉灶,undo 都记在一个地方,恢复的时候顺着一条链走就行。

第二,递归事务可以使用 SYSTEM undo 段——这是特权。

大家知道,数据字典基表(OBJ$、TAB$、COL$ 这些)都放在 SYSTEM 表空间里。Oracle 有一条铁律:SYSTEM 回滚段只给 SYSTEM 表空间上的事务用。普通用户事务永远轮不到 SYSTEM undo 段——为什么?因为 Oracle 要保证 SYSTEM undo 段里始终有槽位留给递归事务。DDL 随时可能来,字典随时可能要改,SYSTEM undo 段的槽位是给它们预留的,不能让用户事务挤占了。

这个设计在 RBU(手工回滚段)和 AUM(自动 undo 管理)模式下都成立。你可以理解为:SYSTEM undo 段是急诊通道,平时空着也不许私家车走,因为救护车随时要来。

第三,递归事务的提交是立即生效的。

经典例子是 sequence。对一个没 cache 的 sequence 取 nextval:

SELECT seq_t.nextval FROM dual;

这个取值动作是一个递归事务,它立即提交——不管你外层事务最后是 commit 还是 rollback,sequence 的值已经涨上去了。所以 sequence 有缺口(gap)是正常的,第二个会话永远能拿到递增后的值。想让 sequence 连续无缺口?别想了,那是拿并发性能换来的,Oracle 不干这事。

顺带一提,Resource Manager 的 UNDO_POOL 指令下,递归事务消耗的 undo 是计入顶层事务的,顶层事务结束前额度不还回消费组。细节虽小,做资源管控排障时可能会遇到。

五、ORA-604:递归事务的报错哲学

递归事务失败时,Oracle 报的错误是:

ORA-00604: error occurred at recursive SQL level 1

这句话翻译过来就是:"你那条 SQL 本身没问题,但它触发的内部 SQL 挂了。"

ORA-604 最让人头疼的地方是:它常常把真正的错误藏起来。你只能看到一个"递归层级"的数字,不知道底下到底发生了什么。多数时候 ORA-604 后面会跟着真正的错误(比如 ORA-01653 表空间无法扩展),但偶尔就是没有,只给你一行干巴巴的 604。

这时候别慌,抓 errorstack:

ALTER SESSION SET events = '604 trace name errorstack';

然后再执行一遍出问题的操作,到 trace 文件里看完整的错误栈,真错误就藏在里面。

我排障这些年,ORA-604 见得多了。经验就一条:永远别只盯着 604 本身,它只是个报信的,真凶在栈里。

六、控制文件事务:双缓冲的艺术

说完了字典,再看看另一个特殊的"事务"——控制文件事务。

加数据文件、改文件名、检查点更新、表空间进热备模式、介质恢复……这些操作都要修改控制文件。但控制文件不是普通数据块,它不在 buffer cache 里走常规的 redo/undo 流程,它有自己的玩法:双镜像翻转。

控制文件内部维护着两份镜像(image)。进程要修改控制文件时,流程是:

  1. 在 CF Enqueue 的保护下,修改非活跃的那份镜像(shadow block)

  2. 改完后,翻转一个 bit,切换活跃/非活跃的角色

  3. 释放 enqueue

熟不熟悉?这就是**双缓冲(double buffering)**的思路——图形学里防画面撕裂用的就是这招。改的是影子,翻的是开关,读者永远看到的是完整的一致版本。

redo 层面也有对应的设计:redo 第六层(Layer 6)的 opcode 保留给控制文件相关的 undo 操作。注意措辞:控制文件事务本身不产生 redo,但表空间/数据文件这类操作需要 undo 记录(防止中途失败),这些 undo 记录修改的是回滚段的块,而对 undo 块的修改会以 opcode5.1记入 redo。

举几个具体的 opcode,体会一下这套设计的对称美:

操作

undo opcode

含义(失败时怎么补救)

创建表空间

6.7

remove tablespace(把刚建的表空间抹掉)

创建表空间

6.1

remove data file(把刚加的文件抹掉)

offline 表空间

6.4

online(失败了就改回 online)

drop 表空间

6.2

add data file(失败就把文件加回来)

drop 表空间

6.8

add tablespace(失败就把表空间加回来)

看出来了吗?每条 undo 记录都是正操作的逆操作。创建表空间要记两条 undo(6.7 + 6.1),drop 也要记两条(6.2 + 6.8)——因为这些操作涉及"表空间"和"数据文件"两个实体,回滚得分别撤销。这种一一对应的设计,让崩溃恢复的逻辑变得极其清晰。

七、Savepoint:不是"存盘",是一个地址

接下来聊几个大家或多或少听过、但未必理解本质的"特殊事务家族"。先说 savepoint。

SAVEPOINT s1; 这条命令干了什么?很多人望文生义,以为 Oracle 把当前状态"存了一份"。不是的——savepoint 没有任何物理落盘动作,它只是内存里的一个关联记录:savepoint 序列号 + UBA(Undo Block Address)。

UBA 是什么?undo 块地址。说白了,savepoint 就是在 undo 链上插了一面小旗子:"记住这个位置。"内存里有个 ktcsp 结构维护 savepoint 表和对应的 UBA。

当你执行 ROLLBACK TO s1 时,Oracle 做的事是:从事务表里的 undo 链尾端开始,逐条应用 undo 记录,直到目标 UBA 为止。回滚到那面小旗子的位置,停。

由此带出 savepoint 的几条行为规则,其实都是这个实现的必然结果:

  • 只回滚 savepoint 之后的语句(undo 链回滚到 UBA 为止)

  • 指定的 savepoint 保留,它之后建立的 savepoint 全部丢失(旗子在链上,链回滚了,后面的旗子自然没了)

  • 释放 savepoint 之后获得的表锁/行锁,之前的保留

  • 事务保持活跃,可以继续

最后一条值得展开——这里有个著名的"savepoint 不公平性"现象,做个实验就能看到:

-- 会话 T1UPDATE t SET val = 1 WHERE id = 1; -- 改 row1SAVEPOINT s1;UPDATE t SET val = 2 WHERE id = 2; -- 改 row2-- 会话 T2UPDATE t SET val = 99 WHERE id = 2; -- 等待(row2 被 T1 锁着)-- 回到 T1ROLLBACK TO s1; -- 回滚 row2 的修改

按上面的规则,rollback to s1 会释放 savepoint 之后获得的锁——row2 的行锁应该释放了。但你会发现T2 还在等!锁没释放干净?

更诡异的还在后面:这时开个会话 T3 来 update row2,T3 能成功,而先来的 T2 反而继续等着。

这就是 savepoint 锁释放机制的历史遗留行为——rollback to savepoint 在锁释放的排队处理上并不严格公平,后来的事务可能插队。知道这个现象的存在,将来遇到"明明回滚了锁怎么还不放"的场景,你就不会怀疑人生了。

八、自治事务:栈里的独立王国

savepoint 有个天然缺陷:没有 COMMIT TO SAVEPOINT——你不能只提交事务的一部分。但现实中确实有这种需求,最典型的就是错误日志表:无论主事务成功还是失败,错误日志都必须留下来。

Oracle 给出的答案是自治事务(Autonomous Transaction)。

思路很直白:临时把当前事务挂起,开一个完全独立的子事务,子事务 commit/rollback 后,父事务接着走,两边互不影响。

CREATE OR REPLACE PROCEDURE log_error (p_msg VARCHAR2) ASPRAGMA AUTONOMOUS_TRANSACTION;BEGININSERT INTO error_log (msg, ts) VALUES (p_msg, SYSDATE);COMMIT; -- 必须显式提交END;/

几个关键规则:

  • 块结束返回前必须显式COMMITROLLBACK,否则报 ORA-06519

  • 自治事务提交后立即对所有会话可见

  • 自治事务看不到父事务未提交的修改(它是个独立事务,读一致基准不同)

  • 父事务回滚,不影响已提交的自治事务

Oracle 内部把事务组织成一个栈结构:任何时刻只有栈顶事务可访问,挂起的事务压在下面。挂起事务的数量上限由 TRANSACTIONS 参数控制。前面说的递归事务,用的也是类似机制——你看,内核的设计语言是统一的。

ATM 取钱是个经典的教学例子:无论你取款成功还是失败(余额不足),用卡记录(usage 表)都必须提交——银行可不能因为交易失败就丢掉"你用过这张卡"这个事实。这种"无论成败都要留痕"的需求,就是自治事务的主场。

最后提醒一个坑:自治事务和父事务可能互相死锁——父事务持有某行的锁,自治事务去改同一行,就会等自己的"爹",而爹在等儿子返回。Oracle 不预防这种死锁,只会报个专门的错误。所以用自治事务的函数里,千万别碰父事务可能修改的表,这是开发者自己的责任。

九、串行化事务:ORA-8177 与 ITL 槽的故事

再往上一个隔离级别:Serializable(串行化)。​​​​​​​

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;-- 或者ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;

效果等同于所有事务一个接一个串行执行:你只能看到自己事务的修改 + 事务开始前已提交的数据。不可重复读、幻读,统统防住。(初始化参数 SERIALIZABLE 早已废弃,别去翻旧文档了。)

代价呢?除了并发度下降,最出名的就是ORA-08177: can't serialize access for this transaction。

8177 的根源要挖到块结构里。串行化事务构建读一致视图时,依赖块里可用的 ITL(Interested Transaction List)槽。ITL 数量由 INITRANS/MAXTRANS/PCTFREE 决定:块内空间够(PCTFREE 预留的那部分)时 ITL 可以扩展,也会复用。索引块还要额外考虑串行化期间可能发生的块分裂。

实战经验就三条:

  1. 如果串行化事务生命周期内可能有 N 个独立事务更新同一个块,就给它分配 N+1 个 ITL(建表时把 INITRANS 调大)

  2. 多个事务可能改同一行,先SELECT ... FOR UPDATE把行锁住

  3. ORA-8177 要在应用设计阶段就考虑怎么处理(重试逻辑),别等上线了被它打个措手不及

十、PDML:并行事务的两阶段提交

最后一个家族成员:并行 DML(PDML)。

UPDATE /*+ parallel(sales,4) */ sales SET amount = amount * 1.1;

一条 PDML 实际是"一个协调者 + 一堆 slave 事务"。内部用**两阶段提交(2PC)**协议保证原子性——但注意,失败的 PDML不会在 dba_2pc_pending 里留记录,也不需要 distributed option,这个 2PC 是实例内部用的,跟分布式数据库没关系。

PDML 的行为规则有点特殊,容易踩坑:

  • PDML 之后必须显式 commit/rollback,才能发后续 DML

  • 如果 PDML 后面紧跟 DDL,DDL 会隐式强制提交这个 PDML。比如:

UPDATE /*+ parallel(sales,4) */ sales SET amount = amount * 1.1;ALTER TABLE sales DROP PARTITION p2023; -- 这个 drop 会强制提交上面的 update
  • PDML 里不允许SET TRANSACTION

还有个性能细节:并行 slave 事务如果挤在同一个 undo 段,会争用 undo 段头。为减少争用,slave 事务应该分散到尽可能多的回滚段上——AUM 模式下 Oracle 通常自己就分好了,手工 RBU 时代这是 DBA 要操心的事。

想观察并行事务的父子关系?查 v$transaction,里面有 PTX 标识列,ptx_xidusn / ptx_xidslt / ptx_xidsqn 三列指向父事务的 XID,一目了然。

十一、动手环节:亲眼看看内核在干什么

纸上得来终觉浅。上面说的这些东西,全都可以亲手验证。我给你三个实验。

实验 1:SQL_TRACE 看 CREATE TABLE 的递归 SQL

​​​​​​​

ALTER SESSION SET sql_trace = TRUE;CREATE TABLE trace_t (id NUMBER, name VARCHAR2(20));ALTER SESSION SET sql_trace = FALSE;

找到 trace 文件(select value from v$diag_info where name='Default Trace File'),跑 tkprof:

tkprof orcl_ora_12345.trc out.prf sys=no

打开 out.prf,你能清清楚楚看到对 OBJ$、TAB$、COL$、SEG$ 的递归 insert/update——一条 CREATE TABLE 背后站着多少条 SQL,数字会震撼你。

实验 2:dump 块,看段头和 ITL​​​​​​​

-- 查表在哪个文件哪个块(段头)SELECT header_file, header_block FROM dba_segmentsWHERE segment_name = 'TRACE_T' AND owner = '你的用户名';-- dump 段头块ALTER SYSTEM DUMP DATAFILE BLOCK ;

到 trace 文件里找,你能看到 ITL 条目、Xid(格式是 undo段号.槽位.序列号)、Uba 字段——这些就是前面讲的理论的"肉身"。

实验 3:追踪 undo 链

拿到 Xid 的第一段(undo 段号)后:​​​​​​​

SELECT file#, block# FROM undo$ WHERE us# = ;

定位到 undo 段头块,再 dump 段头和 UBA 指向的 undo 块,顺着链条一路看下去——一条 DDL 的 undo 足迹就完整地呈现在你眼前了。

这三个实验做下来,"DDL 是一堆递归 DML + 空间管理事务 + 块格式化"这句话,对你来说就不再是概念,而是你亲眼见过的东西。

总结​​​​​​​

回到开头那个问题:CREATE TABLE 按下回车后,内核在忙什么?

现在你可以回答了:它在开隐式提交,在往 OBJ$/TAB$/COL$/SEG$ 里插行,在做 FET$/UET$ 的空间流转,在用 SYSTEM undo 段的专属槽位记录递归事务的 undo,在以 opcode 13.1 把段头格式化写进 redo,在控制文件里翻转双镜像。

而这一切的设计语言是统一的:undo 记录逆操作、递归事务复用顶层 undo 段、SYSTEM undo 段预留特权槽位、空间管理事务能省则省。

Oracle 内核没有魔法,只有无数个这样精心设计、互相咬合的小机制。DBA 这行干得越久,我越觉得:所谓功力,就是当别人只看到 Table created. 的时候,你脑子里能自动放映出这一整串画面。

下次敲 CREATE TABLE 的时候,不妨停一秒——你知道内核正在为你忙什么了。

今天话题就聊到这,欢迎留言交流。觉得内容有用,别忘了点赞转发给有需要的朋友,回见!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询