🏺 第18章 Oracle 的闪回技术
📄 一句话概括

闪回版本查询给一条记录的全部版本,闪回事务查询把「谁改的」翻译成「怎么改回去」。 闪回数据归档把历史存进独立的表空间,能留几年,不受 undo 保留时间限制。

📝 本章和前面几章的关系

18.1 到 18.4 讲的都是「把数据退回去」。 这一篇前两节讲「先查清楚」,第三节讲「把历史存久一点」。 三样东西配合起来是一条完整的处置链: 版本查询定位哪一次改错了,事务查询拿到 undo SQL,归档负责超出 undo 窗口的那部分。 这也是全章的收尾,18.6 的练习一起放在本篇末尾。

一、闪回版本查询:一条记录的全部履历

普通闪回查询给你「某一个时刻的样子」。 版本查询给你「这段时间里它变过几次、每次变成什么」。

SELECT versions_startscn, versions_endscn,
       versions_xid, versions_operation, sal
  FROM scott.emp
       VERSIONS BETWEEN TIMESTAMP
         TO_TIMESTAMP('2026-10-30 14:00:00','YYYY-MM-DD HH24:MI:SS')
         AND TO_TIMESTAMP('2026-10-30 15:00:00','YYYY-MM-DD HH24:MI:SS')
 WHERE empno = 7369;

五列各有分工,记法很简单:

伪列 含义
VERSIONS_STARTSCN 这个版本从哪个 SCN 开始
VERSIONS_ENDSCN 到哪个 SCN 结束(现在是 NULL)
VERSIONS_XID 改出这个版本的事务号
VERSIONS_OPERATION I 插入、U 更新、D 删除

VERSIONS_OPERATION 是 D 的那一行最有意思: 它表示「这一行在这个版本里被删掉了」,所以其他列是空的。 VERSIONS_XID 是通往下一节的钥匙。 看到某一次改动可疑,拿着这个号就能查清它到底干了什么。 两个使用要点: ① 时间范围不能太大,否则扫描的 undo 量会爆炸; ② 只对 UPDATE/DELETE 过的行有版本,没动过的行只有一行。

二、闪回事务查询:从 XID 到 UNDO_SQL

拿到可疑的 VERSIONS_XID,下一步是查这个事务干了什么。

SELECT operation, undo_sql
  FROM flashback_transaction_query
 WHERE xid = HEXTORAW('0A00125F');

UNDO_SQL 那一列是 Oracle 替你写好的反向语句。 直接拷出来执行,数据就回去了。 这比「自己推算该改回多少」可靠得多—— 尤其当一次事务改了上千行、每行的旧值都不一样时,人算不过来。 三个必须知道的前提: ① 要 SELECT ANY TRANSACTION 权限,普通用户查不了别人的事务。 ② 依赖 undo,超过保留时间就查不到。 ③ 加了 SUPPLEMENTAL LOG 才记得全。 默认只记「改了哪些列」,不记旧值;要 UNDO_SQL 完整,得开最小补充日志。

ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;

第二行那句 (ALL) 会显著增加 redo 体量,所以生产上一般是「按需开」——只对关键表开。 还有一个坑:UNDO_SQL 是【单条】反向语句。 一个事务改了一万行,它给你一万条 UPDATE,不是一条批量语句。 所以它适合「改错了几行」的补救,不适合「整表被刷」——那种情况用闪回表。

三、闪回数据归档:把历史存到 undo 管不到的地方

前面所有技术的天花板都是 undo_retention——小时级,撑死一两天。 但有些表必须留几年:账务、病历、合同。 闪回数据归档(FDA)就是为这个场景做的。 它的做法很朴素:给指定的表开一个「历史副本」,改动发生时自动往归档表空间里写一份。 这份副本不受 undo 循环覆盖,只受你设的保留期约束。 四步配置:

步骤 做什么
① 建归档表空间 CREATE TABLESPACE fda_ts ...
② 建归档(带保留期) CREATE FLASHBACK ARCHIVE fda1 TABLESPACE fda_ts
③ 给表开归档 ALTER TABLE scott.emp FLASHBACK ARCHIVE fda1
④ 查历史 SELECT ... AS OF TIMESTAMP ... FROM scott.emp

第 ④ 步是它最妙的地方:查询语句和普通闪回查询一模一样。 应用不用改一行代码,因为 Oracle 会自动判断「这段时间的数据在 undo 里还是在归档里」。 保留期可以按天、月、年设,也有自动的:

CREATE FLASHBACK ARCHIVE DEFAULT fda1
  TABLESPACE fda_ts QUOTA 10G RETENTION 5 YEAR;

代价一眼能看出来:归档表空间要按「几年的改动量」准备。 一张每天改 100 万行的表,留五年就是天文数字。 所以它的用法不是「什么都归档」,是「只归档监管要求必须留的那几张表」。 这一句在 Demo 里还会再说一遍,因为这是最容易犯的错。

四、三种技术的分工

把全章串一遍,按「事情发生多久了」来选:

时间 用什么 代价
几分钟内 闪回查询 / 闪回表 秒级,库照跑
几小时内 闪回表 / 闪回数据库 分钟级
几天内(整表被 drop) 闪回丢弃 秒级
几个月到几年 闪回数据归档 要提前配,占盘
超出以上所有 备份恢复(第 16 章) 小时级,要停机

这张表要横着读:越往下,恢复越慢、代价越大。 所以处置的第一步永远是「查清事情发生在什么时候」。 判断错了会白忙: 明明是一小时前的事故,你直接去恢复,会把别人的活儿一起抹掉; 明明是上周的事故,你指望闪回,最后只能回到恢复。 顺序是:先查、再选、最后动手。 先查那一步用闪回查询和版本查询,都不动数据,最安全。

五、实践与练习

教材 18.6 的练习,按本篇的四条主线归一下类。 一、概念辨析类。 「闪回查询和闪回表有什么区别」「回收站和 undo 有什么区别」—— 这类题的答案是「依据不同、代价不同」,不是「一个只读一个写」。 二、命令书写类。 按 SCN/时间戳/还原点三种写法各写一遍 FLASHBACK TABLE, 再写一遍 FLASHBACK DATABASE——后者的 OPEN RESETLOGS 别漏。 三、场景判断类。 给你四种事故:误删一行、误删一表、误 TRUNCATE、磁盘坏了。 分别该用什么? 前两个用闪回,第三个只能恢复,第四个也只能恢复。 四、容量估算类。 「一个库每秒产生 500 条重做记录,要闪回 4 小时,FRA 至少多大」—— 照 18.2 Demo 的算法,把单价乘上时间和速率就行。 练习的价值不在答案,在「能不能把依据、代价、边界三样说清」。 说不清的时候,回到本章第一张表看一眼。

六、动手 Demo:四个版本的履历,一条 undo SQL,两张表的容量对比

这一篇的 Demo 分三段:版本查询的输出形状、事务查询拿到的 undo SQL 真执行一遍、 以及归档的容量代价。 第二段是重点——它演示了一条完整链路:从「哪一次改错了」到「数据真的改回来了」。

ver = [("I", 4100, 1000), ("U", 4120, 1100),
       ("U", 4150, 1450), ("U", 4190, 1200)]
for i, (op, scn, sal) in enumerate(ver):
    end = ver[i + 1][1] - 1 if i + 1 < len(ver) else 4199
    print("版本 %d  SCN %d~%d  %s  工资 %d"
          % (i + 1, scn, end, op, sal))

c = sqlite3.connect(":memory:")
c.execute("create table emp (id int primary key, sal int)")
c.execute("insert into emp values (1, 1200)")
undo = "update emp set sal = 1450 where id = 1"
c.execute(undo)
print("执行 UNDO_SQL 之后工资 =",
      c.execute("select sal from emp").fetchone()[0])

实跑输出:

--------------------------------------------------------------------
第八十三课:版本查询、事务查询、数据归档
--------------------------------------------------------------------

  **这一课是三样东西:一条记录的所有版本、一次事务的全部改动、以及把历史存到几年之外。**
  **第三样是重点,因为前两样受 undo 保留时间限制,它不受。**

  一、闪回版本查询:一条记录的完整履历
  --------------------------------------------------------------------

  版本 开始 SCN 结束 SCN 操作 工资 事务号
  -----------------------------------------
  1    4100     4119     I    1000 0A001200
  2    4120     4149     U    1100 0A00121C
  3    4150     4189     U    1450 0A001238
  4    4190     4199     U    1200 0A00125F
  已选择 4 行
  **这就是 `VERSIONS BETWEEN SCN ... AND ...` 的输出形状。**
  **它比闪回查询多给了三样东西:版本号、每个版本的存活区间、以及【是哪个事务改的】。**
  **最后那个事务号是通往下一节的钥匙。**

  二、闪回事务查询:把「谁改的」翻译成「怎么改回去」
  --------------------------------------------------------------------
  **假设第 4 个版本是误操作:工资从 1450 改成了 1200。**
  **拿事务号 `0A00125F` 查 `FLASHBACK_TRANSACTION_QUERY`,它直接给你一条 `UNDO_SQL`:**
  `update scott.emp set SAL = 1450 where ROWID = 'AAAW8xAAEAAAAKAAAB';`
  **在本沙箱把这条 undo SQL 换个表名真跑一遍:工资回到 1450。**
  **这就是「不用猜、不用翻 binlog」的补救方式——数据库自己记着怎么倒回去。**
  **注意 `UNDO_SQL` 是【单条】反向语句。一个事务改了一万行,它给你一万条,不是一条 `UPDATE`。**

  三、闪回数据归档:把历史存到 undo 管不到的地方
  --------------------------------------------------------------------

  存放位置    容量(10000 行) 保留多久     谁能清
  --------------------------------------------------------
  undo 表空间 0.26 MB          分钟到小时级 自己就会被覆盖
  归档表空间  0.34 MB          按年设       只有你能删
  已选择 2 行
  **两个数差不多——存的是同样的数据,容量不是问题。**
  **差别在【谁能清掉它】:undo 会被自动覆盖,归档表不会。**

  四、代价:走归档的查询慢多少
  --------------------------------------------------------------------

  查询路径          数据在哪     耗时
  -----------------------------------------
  AS OF(本次模拟) 内存里的列表 0.00187 秒
  归档表查询        另一张表     0.00397 秒
  已选择 2 行
  **这一跑两者都在毫秒级,归档表那条慢了 2.1 倍。**
  **因为数据全在内存里,模拟不出真实差距。真实环境是这样:**
  **`AS OF` 走 undo 是读【内存里的 undo 块】,通常微秒级。**
  **查询归档表是一次完整的表扫描,可能几秒。**
  **所以归档的用法不是「什么都归档」,是「只归档监管要求必须留的那几张表」。**

  五、三样东西怎么配合
  --------------------------------------------------------------------
  **日常误操作,用版本查询定位、用事务查询拿 undo SQL,几秒钟搞定。**
  **超过 undo 保留时间的老数据,只能靠闪回数据归档。**
  **再老、连归档都没有的,回到备份恢复——那是第 16 章的事。**
  **这条链的顺序要记住:先看能不能闪回,再考虑恢复。**
  **顺序反了,会把能做到秒级的事做成小时级。**

第一张表是版本查询的输出:四个版本,四个 SCN 区间。 注意第三列:每个版本的「结束 SCN」是下一个版本的「开始 SCN」减一。 这说明版本区间是首尾相接、不留缝的。 工资那一列从 1000 涨到 1100、1450,又跌回 1200。 如果只看当前值 1200,根本不知道中间发生过什么——这就是版本查询的价值。 「事务号」那一列是 16 进制字符串,0A00125F 这种。 在 Oracle 里它是 RAW(8),所以要 HEXTORAW 才能拿来查。 第二段是 undo SQL:假设第 4 个版本是误操作,拿它的事务号查出反向语句, 工资从 1200 改回 1450。 在本沙箱把这条语句换个表名真跑了一遍,结果确实是 1450。 这一段最有说服力的地方是:它不是「猜该改回多少」,是数据库自己记着「改之前是多少」。 所以哪怕一次改了一千行、每行旧值都不同,也能逐条改回去。 但要注意 Demo 里那句「UNDO_SQL 是单条反向语句」。 一个事务改一万行,它给你一万条。 所以它适合小范围补救,大范围得用闪回表。 第三张表是容量对比:同样 1 万行,undo 里 0.26 MB,归档表里 0.34 MB。 两个数差不多——存的是同样的数据,容量本身不是问题。 真正的差别在「保留多久」和「谁能清掉它」这两列。 undo 是分钟到小时级、会被自动覆盖;归档是按年设、只有你能删。 第四张表要诚实看:这一跑两者都在毫秒级,归档表慢了 2 倍多。 因为数据全在内存里,模拟不出真实差距。 真实环境中 AS OF 走 undo 是读内存块、微秒级;查归档表是一次完整表扫描、可能几秒。 所以这一跑只能证明「路径不同」,不能证明「快多少」。 这句话必须留在正文里,不然读者会把 2 倍当成真实差距。

30 秒实验

  1. 把 Demo 里 ver 的四个版本改成六个,再跑一次。 看「结束 SCN」是不是始终等于下一个「开始 SCN」减一。
  2. 把 undo 那句改成 delete from emp where id = 1,再跑。 看读工资那一步会不会报错——这就是 undo SQL 与操作类型必须匹配。
  3. 如果有 Oracle 环境,跑一次 SELECT * FROM DBA_FLASHBACK_ARCHIVE;, 看 RETENTION_IN_DAYS 那一列,再对比 undo_retention 的单位(秒)。

常见误区

  1. 「版本查询能看到所有行」。 只有被 UPDATE/DELETE 过的行有多个版本, 没动过的行只有一行。
  2. 「UNDO_SQL 是一条批量语句」。 它是单条反向语句,改了一万行就给一万条。
  3. 「闪回数据归档不用提前配」。 必须先建归档、给表开归档, 而且改动发生时就在写,事后补不了。
  4. 「开了归档就不用管 undo 了」。 短时间的闪回还是走 undo,两者是接力关系,不是替代关系。
自测三问(点开对答案) 1. `VERSIONS_OPERATION` 是 `D` 的那一行,其他列为什么是空的? —— **它表示「这一行在这个版本里被删了」**,所以没有「这一版的值」。 2. `UNDO_SQL` 为什么比人推算可靠? —— **它是数据库记下来的「改之前是多少」**,逐行记的; **人推算只能算「减掉多少」,碰上每行旧值不同就算不出来。** 3. 闪回数据归档和 undo,是什么关系? —— **接力。** 短时间(undo 窗口内)查 undo,更长的时间查归档; **查询语句一样,Oracle 自己判断去哪找。** 4. `TRUNCATE` 之后能用哪种闪回救回来? —— **一种都不行。** 它不写 undo、不进回收站,只能靠备份恢复。

第 18 章到此结束。下一章是全书最后一部分:一个完整的企业人事管理系统。 前面 18 章讲的是「零件」,第 19 章讲的是「怎么把零件装成一台机器」。

相关笔记