注册
误删了一张表,我们是怎么在几分钟内把它找回来的
专栏/数据追梦之路/ 文章详情 /

误删了一张表,我们是怎么在几分钟内把它找回来的

Tomliu 2026/10/01 188 0 0
摘要 一条漏写 WHERE 的 `DELETE`,两行数据瞬间消失,而且已经 COMMIT —— 回滚也回不去了。这篇记录的不是结论,而是当时那几分钟里,我们每一步是怎么想的—— 包括中间走错的那几步。

一、现场:最怕的那条 SQL

事情很典型。一条清理语句本该带上条件,结果条件没生效,DELETE FROM 某表 直接把两行业务数据抹了。等发现时,事务已经提交——ROLLBACK 这条路,断了。

如果这是一张没有备份的表,常规选项只剩两个:还原整个库(要停机、要丢恢复点之后的增量),或者认了。

但当时我们没急着还原。先去确认了一件事。

二、处置过程:先问"还能不能救",再问"怎么救"

决策点 1:先查参数,而不是先动手

数据已经删了,越着急越容易做错事。我们的第一反应不是"赶紧恢复",而是问自己:

这张表的数据,现在是否还存在某个地方?

达梦的闪回机制基于 UNDO 日志——事务修改前的镜像保存在回滚段里。如果闪回是开着的,且误删时点还在保留窗口内,那数据其实还在,只是"看不见"。

所以第一步是查两个参数:

SELECT * FROM V$PARAMETER WHERE NAME IN ('ENABLE_FLASHBACK','UNDO_RETENTION');

这里有个残酷的事实值得所有人记住:

⚠️ ENABLE_FLASHBACK 默认是关闭的,UNDO_RETENTION 默认只有 90 秒。

也就是说,在绝大多数默认配置的库里,闪回这条路其实是走不通的。窗口只有 90 秒,等你发现误删、连上库、定位时间点,早就过期了。

我们这次能救回来,是因为库里恰好开启了闪回,并且窗口被调大了。

决策点 2:从库里读出"事故时间点",而不是猜

闪回必须在一个准确的时间点上查快照。这个时间点从哪来?我们一开始想的是翻应用日志 —— 但后来发现,库里自己就记着。

达梦的闪回版本查询会返回一批"伪列",其中既有事务号、也有时间戳:

SELECT VERSIONS_STARTTIME, -- ★ 这一步的关键:删除发生的那一刻 VERSIONS_ENDTIME, VERSIONS_STARTTRXID, VERSIONS_ENDTRXID, -- 非空 = 这条原始记录"已经失效"(即被删) VERSIONS_OPERATION, -- D = 这一版是删除产生的 * FROM 表名 VERSIONS BETWEEN TIMESTAMP '<窗口起点>' AND SYSDATE;

结果很清楚:VERSIONS_OPERATION 为 D 的那两行,VERSIONS_STARTTIME 是同一个时刻 —— 那就是误删发生的瞬间。

决策点 3:T 取"删除时刻的前一秒",不是删除时刻

拿到 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 行,不报错、不提示,而你以为一切正常。

决策点 4:先验证,后恢复(这是最容易跳过的一步)

很多人到这一步就急着执行恢复了。我们多做了一步 —— 先"看看"那个时间点的数据长什么样:

SELECT COUNT(*) FROM 表名 AS OF TIMESTAMP 'T';

返回了 2。数据真的还在。

这一步看着多余,其实是在兜住上一步的风险:T 填错了不会报错,只会安静地返回空。先 COUNT 一下,才知道自己拿到的到底是"误删前的数据"还是"一片空白"。

踩到的坑:自引用 INSERT 没有幂等保护

确认数据在,接下来就是插回去。我们当时的写法很"顺":

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 秒),那它和"闪回可救"之间,其实只隔了一次参数调整 —— 值得现在就去做。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服