事情很典型。一条清理语句本该带上条件,结果条件没生效,DELETE FROM 某表 直接把两行业务数据抹了。等发现时,事务已经提交——ROLLBACK 这条路,断了。
如果这是一张没有备份的表,常规选项只剩两个:还原整个库(要停机、要丢恢复点之后的增量),或者认了。
但当时我们没急着还原。先去确认了一件事。
数据已经删了,越着急越容易做错事。我们的第一反应不是"赶紧恢复",而是问自己:
这张表的数据,现在是否还存在某个地方?
达梦的闪回机制基于 UNDO 日志——事务修改前的镜像保存在回滚段里。如果闪回是开着的,且误删时点还在保留窗口内,那数据其实还在,只是"看不见"。
所以第一步是查两个参数:
SELECT * FROM V$PARAMETER
WHERE NAME IN ('ENABLE_FLASHBACK','UNDO_RETENTION');
这里有个残酷的事实值得所有人记住:
⚠️ ENABLE_FLASHBACK 默认是关闭的,UNDO_RETENTION 默认只有 90 秒。
也就是说,在绝大多数默认配置的库里,闪回这条路其实是走不通的。窗口只有 90 秒,等你发现误删、连上库、定位时间点,早就过期了。
我们这次能救回来,是因为库里恰好开启了闪回,并且窗口被调大了。
闪回必须在一个准确的时间点上查快照。这个时间点从哪来?我们一开始想的是翻应用日志 —— 但后来发现,库里自己就记着。
达梦的闪回版本查询会返回一批"伪列",其中既有事务号、也有时间戳:
SELECT VERSIONS_STARTTIME, -- ★ 这一步的关键:删除发生的那一刻
VERSIONS_ENDTIME,
VERSIONS_STARTTRXID,
VERSIONS_ENDTRXID, -- 非空 = 这条原始记录"已经失效"(即被删)
VERSIONS_OPERATION, -- D = 这一版是删除产生的
*
FROM 表名
VERSIONS BETWEEN TIMESTAMP '<窗口起点>' AND SYSDATE;
结果很清楚:VERSIONS_OPERATION 为 D 的那两行,VERSIONS_STARTTIME 是同一个时刻 —— 那就是误删发生的瞬间。
拿到 T 之后,还有一件反直觉的事。
我们下意识觉得:既然是"删除那一刻"的数据,那就把 T 填成删除时刻呗?实测发现不行 —— 取回来是 0 行。
以删除时刻(记为 T_del)为基准扫了一圈,结果是这样:
T 取值 |
结果 |
|---|---|
T_del - 5s |
❌ -9801 闪回版本已过期(退过头,超出窗口) |
T_del - 2s |
✅ 2 行 |
T_del - 1s |
✅ 2 行 |
T_del(等于删除时刻) |
⚠️ 0 行 —— 删除已生效,取不回 |
T_del + 1s / +2s |
⚠️ 0 行 |
T_del + 10s |
❌ -9802 闪回版本不存在 |
T 必须严格早于删除时刻。 推荐就取 T_del - 1s:退 1 秒足够,退太多反而会掉出闪回窗口(报 -9801)。可用区间其实很窄。
这也解释了为什么下一步的验证动作不能省 —— 如果 T 填错,AS OF 会老老实实返回 0 行,不报错、不提示,而你以为一切正常。
很多人到这一步就急着执行恢复了。我们多做了一步 —— 先"看看"那个时间点的数据长什么样:
SELECT COUNT(*) FROM 表名 AS OF TIMESTAMP 'T';
返回了 2。数据真的还在。
这一步看着多余,其实是在兜住上一步的风险:T 填错了不会报错,只会安静地返回空。先 COUNT 一下,才知道自己拿到的到底是"误删前的数据"还是"一片空白"。
确认数据在,接下来就是插回去。我们当时的写法很"顺":
INSERT INTO 表名 (SELECT * FROM 表名 AS OF TIMESTAMP 'T');
跑一遍,好的,2 行回来了。但当我们把脚本又跑了一遍(模拟重试),行数变成了 4 行 —— 数据写重了,而且没有任何报错。
INSERT INTO t SELECT * FROM t AS OF ... 是自引用:源和目标都是同一张表。它天然没有"已经恢复过"的判断,跑几次就乘几倍,静默地把数据搞脏。
正确的做法是先落一张影子表,核对行数之后再插回:
-- 先落到影子表(不动原表)
CREATE TABLE 表名_BAK AS SELECT * FROM 表名 AS OF TIMESTAMP 'T';
-- 核对影子表行数无误后,再插回原表
INSERT INTO 表名 SELECT * FROM 表名_BAK;
COMMIT;
两行数据完整回来,MIN/MAX 与原值一致。全程没有停机,没有还原备份,分钟级完成。
过程中我们撞了一个坑,值得单独说 —— 而且一开始我们还判断错了。
按手上那份较旧的官方手册,闪回查询还有一种写法是用事务号定位:SELECT ... WHEN TRXID <事务号>。我们照着试了一次,直接报错:
-2004: 不支持的语句类型
第一反应是"手册写错了"。但多查了一步才发现真相更有意思:这条语法不是错的,是"过时了"。
我们把达梦历年手册逐版翻了一遍,又拿三个版本真机跑了一遍,结果是这样的:
| 语法 | 8.1.2.94(2021.11) | 8.1.5.8(2026.03) | 8.1.5.60(2026.06) |
|---|---|---|---|
WHEN TRXID <事务号> |
✅ 可用 | ❌ 报 -2004 | ❌ 报 -2004 |
WHEN TIMESTAMP |
✅ 可用 | ✅ 可用 | ✅ 可用 |
AS OF TIMESTAMP |
❌ 报 -2007 | ✅ 可用 | ✅ 可用 |
也就是说:在 8.1.2.94 上,WHEN TRXID 是真的能跑的,而在 8.1.2.129 之后,达梦把闪回查询子句重新设计了一遍:砍掉了 TRXID,换成了 AS OF TIMESTAMP 和 AS OF SCN|LSN。
旧语法不是"失效",是被替换了。
闪回不是万能的,把边界说清楚比夸它有用更重要。
| 场景 | 能否闪回 | 为什么 |
|---|---|---|
DELETE 已提交 |
✅ 能 | 我们这次的情况 |
DELETE 未提交 |
✅ 更简单 | 直接 ROLLBACK 就行,根本不用闪回(VERSIONS 也读不到未提交事务) |
UPDATE 改坏数据 |
✅ 能 | 同一套 UNDO 机制 |
TRUNCATE TABLE |
❌ 不能 | 它会修改对象定义,闪回窗口被打断 |
DROP TABLE |
❌ 不能 | 对象都还在,无从查起 |
| 超出保留窗口 | ❌ 不能 | UNDO 已被回收,报 -9801 闪回版本已过期 |
一句话概括:闪回救的是"行级的数据",救不了"对象的改动"。
第一,别等出事才开闪回。
这是最要命的一条。闪回开启之前产生的 UNDO,仍然按旧的 90 秒回收 —— 事后再去调大参数,补不回来。所以正确的顺序是:先调大 UNDO_RETENTION,再让业务产生数据。
对重要表,建议常态化开启闪回,并把 UNDO_RETENTION 调到至少数小时。代价是回滚段占用会增加,需要评估磁盘 —— 但比起丢数据的代价,这笔账很好算。
第二,闪回的窗口是"倒计时",发现误删后第一件事是抢救。
窗口一过,就只剩备份还原:要停机、要丢恢复点之后的增量。同样是丢数据,一个是分钟级无损找回,一个是小时级停机恢复 —— 差别全在你反应的速度。
第三,先确认删除到底提交了没有。
如果那条 DELETE 还没提交,最省事的办法就是 ROLLBACK —— 比闪回简单得多。闪回是给已经提交、回滚不了的情况准备的。
第四,时间点要"掐准",验证步骤不能省。
T 取删除时刻会取回 0 行(删除已生效),退太多又会掉出闪回窗口。取删除时刻前 1 秒是比较稳的选择。而且填错不报错、只返回空,所以恢复前的 COUNT 校验必须做。
第五,别用自引用 INSERT 直接恢复。
INSERT INTO t SELECT * FROM t AS OF ... 跑两遍就写重(实测 2 行变 4 行),且不报错。先落影子表、核对行数,再插回原表。最后说句实在的:这次能救回来,靠的不是运气,是恰好开着闪回。而"恰好"这两个字,在生产环境里从来不是好词。
如果你的库现在还是默认配置(闪回关闭、窗口 90 秒),那它和"闪回可救"之间,其实只隔了一次参数调整 —— 值得现在就去做。
文章
阅读量
获赞
