不完全恢复就是把库恢复到「某个过去的点」,之后的都不要了。
它治的是「数据被误改」,不是「文件坏了」——所以方向和完全恢复相反。
两种指定点的办法:按时间(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 秒实验
- 把
recs里 B 批的 SCN 从 3000 改成 2500,再跑一次。 看三个停点的结果怎么变——SCN 的顺序必须和实际发生顺序一致。 - 把 C 批的删除从
delete id <= 800改成delete id <= 1800,再跑一次。 看 3000 那个停点还是不是 7000 行——这验证了「停点选对,事故就不发生」。 - 给每批记录加一个时间戳,按
ts过滤重放,和按 SCN 的结果对比。 看两者是不是一致——如果不一致,想想是为什么。
常见误区
- 「恢复到最新的前一刻就行」。 不完全恢复必须给一个明确的停点。不给停点,就是完全恢复。
- 「恢复完直接
OPEN就行」。 必须OPEN RESETLOGS, 否则那段没应用的重做下次会被错误地应用。 - 「
RESETLOGS之后旧归档还能继续用」。 不能。 它们属于另一个化身,硬要滚会把库滚成自相矛盾的时间线。 - 「误删都用不完全恢复解决」。 能用闪回就用闪回。 不完全恢复会让整个库回到过去——别的业务这段时间写的数据一起没了。
自测三问(点开对答案)
1. 不完全恢复和完全恢复,方向上的区别是什么? —— **完全恢复往前滚到失败那一刻;不完全恢复停在过去某个点,之后的都不要。**前者治文件损坏, 后者治数据被误改。 2. 为什么说 SCN 比时间精确? —— **同一秒里可能发生很多事务,时间点卡不到那么细;SCN 是每次提交唯一且递增的。** `UNTIL TIME` 实际也是换算成「那个时刻之前最后一个提交」。 3. `OPEN RESETLOGS` 之后,旧归档日志为什么不能再用? —— **因为日志序列归零、进入了新的化身(时间线)。**旧归档属于上一个分支, 硬滚会得到自相矛盾的结果。 4. 不完全恢复动手之前,为什么建议先做一次全备? —— **把「当前状态」留一份。** 万一恢复点选错,还能从这份备份重来,否则只能从头再恢复一遍, 且没有退路。第 16 章到这里结束。下一篇进第 17 章:数据导出和导入。 备份是「整库的保险」,导出导入是「数据的搬运」——前者为了灾难恢复,后者为了搬迁、升级、 交数据。
- ☐ (本篇完,下一篇 17.1 EXPDP 和 IMPDP 概述)
相关笔记
- 使用 RMAN 工具完全恢复
- 使用 RMAN 工具备份
- 备份与恢复概述
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引