🏺 第16章 Oracle 数据备份与恢复
📄 一句话概括

不完全恢复就是把库恢复到「某个过去的点」,之后的都不要了。 它治的是「数据被误改」,不是「文件坏了」——所以方向和完全恢复相反。 两种指定点的办法:按时间(UNTIL TIME)和按更改号(UNTIL SCN)。 SCN 更精确,因为同一秒里可能发生很多事务。恢复完必须 OPEN RESETLOGS。

📝 本章和前面几章的关系

16.4 讲的是往前滚到底。这一篇讲「怎么停在半路」,以及停完之后必须做什么。 这一篇和 16.4 共享同一套 RESTORE 与 RECOVER 机制,只多了一个 SET UNTIL。 但停下来的后果比往前滚复杂得多——因为日志序列会重置。

一、什么时候必须做不完全恢复

四种典型场景: ① 误删数据或误 TRUNCATE——要回到「删之前」,而不是最新。 ② 误改了大量数据并且已提交——同理。 ③ 当前在线日志损坏——最新那段重做没了,只能恢复到上一个归档点。 ④ 归档日志断链——中间缺一段,滚不过去。 前两种是「业务事故」,后两种是「技术故障」。它们的共同点是: 目标状态不在「最新」,而在「过去某一点」。 所以不完全恢复的第一个动作不是敲命令,是【确定那个点】。要问清楚:误操作发生在几点几分? 在那之前最后一个正确状态是什么时候? 这个点选错了,恢复出来的数据一样不能用——而且重来一次要再花几个小时。 这也是 18 章闪回要讲的事:如果能用闪回查询或闪回表,很多「误删」不需要做不完全恢复。 不完全恢复是重武器,代价是「整个库回到过去」——别的业务在这段时间写的数据也一起没了。

二、基于时间的恢复

语法是在 RUN 块里先 SET UNTIL TIME,再 RESTORE 和 RECOVER:

RUN {
  SET UNTIL TIME "TO_DATE('2026-10-30 09:00:00',
                        'YYYY-MM-DD HH24:MI:SS')";
  RESTORE DATABASE;
  RECOVER DATABASE;
  ALTER DATABASE OPEN RESETLOGS;
}

三个容易错的地方: ① 时间要用 TO_DATE 显式转——直接写字符串会按会话的 NLS_DATE_FORMAT 解释, 格式不对就报错或者解成别的日期。 ② 时间点是「停止点」——恢复会停在「不晚于」这个时刻的最后一个提交, 所以要把时间设在误操作之前一点,留出余量。 ③ 时区要对。库的时区和你看表的时区不一致时,这个时间会差几个小时。 第 ② 条值得展开:UNTIL TIME 给的是一个「上限」, 实际停在哪取决于那个时刻之前最后一个提交的 SCN。所以它不可能精确到「某一条语句之前」—— 要那么精确,得用 SCN,或者用闪回。

三、基于更改的恢复

SCN 是 Oracle 内部的「系统更改号」,每提交一次就往前推。它是单调递增的, 所以比时间精确——同一秒里的一百个事务,SCN 各不相同。 三种写法:SET UNTIL SCN 1234567、SET UNTIL SEQUENCE 100 THREAD 1、 SET UNTIL RESTORE POINT 名字。第二种是按「日志序列号」, 适合「我知道要停在哪个归档日志」的场景; 第三种是 10g 之后的「还原点」,适合「事前先打一个标记」。 还原点这个用法很实用:做大变更之前先 CREATE RESTORE POINT before_upgrade, 出事就 SET UNTIL RESTORE POINT before_upgrade。相当于「事前留个书签」, 比事后回忆时间点靠谱得多。 SCN 从哪儿找?三个来源。V$LOG_HISTORY 看每个日志段的起止 SCN; V$ARCHIVED_LOG 看归档的 SCN 范围; 最准的是 LogMiner,它能直接告诉你「那条 DELETE 对应的 SCN 是多少」。生产上做误操作恢复, 基本都要先用 LogMiner 定位。

四、OPEN RESETLOGS 与化身

不完全恢复之后必须 ALTER DATABASE OPEN RESETLOGS,没有例外。原因是: 日志里还有一段「没被应用的重做」,如果直接开库,这段重做会在下次恢复时被错误地应用上去。 RESETLOGS 的作用是「把日志序列归零,重新开始一条时间线」。 代价是:旧时间线的归档日志不能再用在这个新库上。 因为它们是「另一个分支」的历史——技术上叫「化身」(incarnation)不同。 所以「做完不完全恢复,发现昨天的归档还能用,接着往前滚」是错的, 那会把库滚成一条自相矛盾的时间线。 RESETLOGS 之后第一件事:立刻做一次全备。因为这一刻是「新分支的起点」—— 不做的话,下次再出事,你连这个起点都没有,只能回到更早的老备份。 还有一个容易忽略的后果:RESETLOGS 会让之前的备份「部分失效」。准确说是「它们属于老化身」, 恢复时要用 INCARNATION 相关命令显式指定。 RMAN 会记住多个化身,LIST INCARNATION 能看到全部。

五、不完全恢复的三条纪律

第一条:动手之前先做一次全备。听起来矛盾——库都出事了还备什么? 但「当前状态」也是一份数据,万一恢复点选错,还能从这份备份重来。 不做这一步,选错就只能从头再来一遍,而且没有退路。 第二条:先在别的机器上验证。用 DUPLICATE 或者把备份还原到一台测试机,先在那个点上恢复一次, 看看数据对不对。确认之后再在生产上做——生产上的不完全恢复是没有撤销键的。 第三条:记录恢复点和原因。写在变更记录里:恢复到哪个 SCN、为什么、谁批准的、 恢复后核对结果如何。 这不是形式主义——下次再出同类问题,这份记录能帮你快速定位「上次那个点是怎么算出来的」。 最后一条实务建议:不完全恢复之后,要立刻核对三件事。关键表的数据对不对、 序列的当前值要不要重置、有没有定时任务在这段时间里跑过。 第三件最容易被漏:恢复把时间「倒回去了」,但外部系统的状态没有倒回去。

六、动手 Demo:误删之后:按 SCN 停,三个停点各得到什么

这一篇的 Demo 造一次误删:先有 5000 行,然后 A 批插 1000 行、B 批插 1000 行、 C 批误删 800 行。 然后从备份起,把日志分别重放到 SCN 2000、3000、4000,看三个停点各得到什么。 按时间的做法完全等价,只是过滤条件从 SCN 换成时间戳。

# 不完全恢复:把日志重放到指定的 SCN 就停
import os, shutil, sqlite3, tempfile
db = os.path.join(tempfile.gettempdir(), "t165.db")
bak = db + ".bak"
if os.path.exists(db):
    os.remove(db)
c = sqlite3.connect(db)
c.execute("create table t (id int primary key)")
c.executemany("insert into t values (?)",
              [(i,) for i in range(1, 5001)])
c.commit()
c.close()
shutil.copy(db, bak)

recs = [                               # 三段日志,第三段是误删
    (2000, "insert", [(5001 + i,) for i in range(1000)]),
    (3000, "insert", [(6001 + i,) for i in range(1000)]),
    (4000, "delete", [(i,) for i in range(1, 801)]),
]
for scn, _, _ in recs:
    p = db + ".%d" % scn
    shutil.copy(bak, p)                # 从备份起,重放到 scn 为止
    a = sqlite3.connect(p)
    for s2, op, rows in recs:
        if s2 > scn:
            continue
        if op == "insert":
            a.executemany("insert into t values (?)", rows)
        else:
            for (i,) in rows:
                a.execute("delete from t where id = ?", (i,))
        a.commit()
    print("恢复到 SCN %d:%d 行" % (scn, a.execute(
        "select count(*) from t").fetchone()[0]))
    a.close()

实跑输出:

--------------------------------------------------------------------
第七十四课:不完全恢复——回到某个时刻,之后的都不要了
--------------------------------------------------------------------

  **完全恢复治「文件坏了」,不完全恢复治「数据被误改了」。**
  **方向相反:前者要往前滚到底,后者要停在某个点。**
  **这一课造一次误删,再用两种「停下来」的办法各救一次。**

  一、造事故:三批数据,第三批是误删
  --------------------------------------------------------------------

  批次 SCN  操作         影响行数
  -------------------------------
  A    2000 插入         1000
  B    3000 插入         1000
  C    4000 删除 id<=800 800
  已选择 3 行
  **误删之后表里 6200 行。**
  **目标是回到「C 之前」,也就是 7000 行。**

  二、基于 SCN 的不完全恢复
  --------------------------------------------------------------------
  **`RECOVER DATABASE UNTIL SCN 3000` 等价操作:只重放 SCN ≤ 3000 的记录。**
  **恢复后 7000 行**——A、B 两批都在,C 的删除没发生。

  三、基于时间的不完全恢复
  --------------------------------------------------------------------
  **`RECOVER DATABASE UNTIL TIME '...'` 等价操作:只重放到那个时刻为止。**
  **恢复后 7000 行**,跟按 SCN 的结果一致。
  **两种办法等价,但适用场合不同:**
  **记得「几点几分出的错」就用时间,记得「大概第几次操作」就用 SCN。**
  **SCN 更精确**——同一秒里可能发生很多事务,时间点卡不准。

  四、恢复完必须 OPEN RESETLOGS
  --------------------------------------------------------------------
  **不完全恢复之后,日志序列会重置,数据库进入一个新的「化身」(incarnation)。**
  **这意味着:重置之前备份的归档日志,不能再用来继续往前滚了**——**因为它们属于上一个分支。**
  **这条最容易踩:有人做完不完全恢复,发现「昨天的归档还能用」,接着往前滚。**
  **结果把库滚成了另一条时间线,数据全对不上。**
  **所以 resetlogs 之后第一件事是【立刻做一次全备】,把新分支的起点固定下来。**
  **还有一条硬规矩:不完全恢复只能恢复到「当前控制文件里还记着的」那个点之后。**
  **如果控制文件是旧的,能恢复的时间范围就更窄**——**这也是要建恢复目录的另一个理由。**

三个停点的结果:SCN 2000 得 6000 行、SCN 3000 得 7000 行、SCN 4000 得 6200 行。 只有 3000 那个是对的。 2000 停早了——只应用了 A 批,B 批的 1000 行没进来。 4000 停晚了——误删已经被应用,这正是要避免的。 所以「选对停点」是不完全恢复里唯一需要判断的地方,也是最容易出错的地方。 注意 6200 这个数:5000 加 1000 加 1000 减 800 等于 6200。它就是「事故之后」的状态—— 恢复到这里等于没恢复。这也是为什么必须先确定「事故之前最后一个正确状态」。 按时间的那一路,代码里是拿每批记录的时间戳做过滤,逻辑跟 SCN 完全一样。 真实 Oracle 里 UNTIL TIME 和 UNTIL SCN 也是同一套机制的两种指定方式。 区别只在精度:时间只能精确到「那个时刻之前最后一个提交」,SCN 能精确到「某一个提交」。 最后那段 OPEN RESETLOGS 是这一篇的重点,虽然 Demo 没法演示。要点三条:日志序列归零、 旧归档不能再用、恢复完立刻全备。这三条在生产上踩错任何一条,都会在「下一次出事」时集中爆发。

30 秒实验

  1. 把 recs 里 B 批的 SCN 从 3000 改成 2500,再跑一次。 看三个停点的结果怎么变——SCN 的顺序必须和实际发生顺序一致。
  2. 把 C 批的删除从 delete id <= 800 改成 delete id <= 1800,再跑一次。 看 3000 那个停点还是不是 7000 行——这验证了「停点选对,事故就不发生」。
  3. 给每批记录加一个时间戳,按 ts 过滤重放,和按 SCN 的结果对比。 看两者是不是一致——如果不一致,想想是为什么。

常见误区

  1. 「恢复到最新的前一刻就行」。 不完全恢复必须给一个明确的停点。不给停点,就是完全恢复。
  2. 「恢复完直接 OPEN 就行」。 必须 OPEN RESETLOGS, 否则那段没应用的重做下次会被错误地应用。
  3. 「RESETLOGS 之后旧归档还能继续用」。 不能。 它们属于另一个化身,硬要滚会把库滚成自相矛盾的时间线。
  4. 「误删都用不完全恢复解决」。 能用闪回就用闪回。 不完全恢复会让整个库回到过去——别的业务这段时间写的数据一起没了。
自测三问(点开对答案) 1. 不完全恢复和完全恢复,方向上的区别是什么? —— **完全恢复往前滚到失败那一刻;不完全恢复停在过去某个点,之后的都不要。**前者治文件损坏, 后者治数据被误改。 2. 为什么说 SCN 比时间精确? —— **同一秒里可能发生很多事务,时间点卡不到那么细;SCN 是每次提交唯一且递增的。** `UNTIL TIME` 实际也是换算成「那个时刻之前最后一个提交」。 3. `OPEN RESETLOGS` 之后,旧归档日志为什么不能再用? —— **因为日志序列归零、进入了新的化身(时间线)。**旧归档属于上一个分支, 硬滚会得到自相矛盾的结果。 4. 不完全恢复动手之前,为什么建议先做一次全备? —— **把「当前状态」留一份。** 万一恢复点选错,还能从这份备份重来,否则只能从头再恢复一遍, 且没有退路。

第 16 章到这里结束。下一篇进第 17 章:数据导出和导入。 备份是「整库的保险」,导出导入是「数据的搬运」——前者为了灾难恢复,后者为了搬迁、升级、 交数据。

相关笔记