上周三傍晚,某个生产环境出了这样一件事:负责临时表清理的同事A在维护窗口执行脚本,脚本里恰好有一条DROP TABLE ... PURGE,目标是一张还在被对账任务高频使用的大表。等发现不对劲时,回收站已经是空的——PURGE 意味着对象连被闪回的机会都没留下。备份有,但策略是凌晨全备,如果直接整库恢复,至少丢大半天数据。最后是靠 Oracle LogMiner 从归档日志里挖出那条 DDL,拿到 DROP 发生的精确 SCN,再配合 RMAN 做表级时间点恢复,表和 18 万个事务数据完整回到事故前状态。
这篇文章就把这条恢复链路完整拆一遍:LogMiner 怎么挖 DDL、怎么拿 SCN、拿到 SCN 之后怎么选恢复手段,以及我在这套流程里真金白银踩过的坑。
1. 误删事故复盘:为什么最终答案是 LogMiner 而不是 Flashback
先还原一下事故本身。误 DROP 一张表的场景,我在多年运维里见过太多次,归纳起来无非三种:
- 清理脚本里混入了
DROP TABLE ... PURGE,执行时没人逐行看。 - 开发同学在测试库写好的脚本,手滑连上了生产库,表名还是那个表名。
- 应用为了重建表,先 DROP 再 CREATE,结果应用逻辑出错,把旧表唯一数据源丢了。
不管哪种,大家第一反应都是打开 SQL 文档翻恢复手段。但实际可用方案远没有想象中多,尤其是当表不是简单 DROP,而是DROP TABLE ... PURGE,或者 DROP 之后回收站被清空时,很多“常规救法”直接失效。我把当时的方案对比整理成了下表:
| 恢复手段 | 前提条件 | 典型局限 |
|---|---|---|
| Flashback Drop(回收站闪回) | 表未被 PURGE,回收站里还有 BIN$ 对象 | PURGE、回收站被清、同名表已存在都会失败 |
| Flashback Database | 启用了闪回日志 | 整库回退,影响所有应用,生产环境基本不敢用 |
| RMAN 不完全恢复(数据库/表空间级) | 有备份 + 完整归档 | 需要一个精确的恢复终点,否则要么丢数据要么重复恢复 |
| RMAN RECOVER TABLE(12c+) | 有备份 + 归档,支持表级恢复 | 需要辅助实例空间,操作偏重 |
| LogMiner 挖掘日志定位操作 | 有覆盖事故窗口的 redo/归档日志 | 本身不恢复数据,但能给其他手段提供最关键的 SCN/时间锚点 |
那次事故里,A同学执行的是 PURGE,回收站直接没有记录,FLASHBACK TABLE ... TO BEFORE DROP这条路从根上断了。Flashback Database 倒是可以,但那是核心业务库,整库回退意味着所有表都回到过去,其他正常提交的数据全要重放,业务上根本接受不了。剩下的就是 RMAN 层面做精准恢复。
问题来了:RMAN 做时间点恢复,需要提供一个 SCN 或者时间点。这个点选得太晚,表已经没了;选得太早,白白损失一大堆事后写入的数据。当时 DBA 对 DROP 发生的具体时刻只有个模糊印象——“大概是 17:20 到 17:25 之间”,这种精度根本没法直接拿去恢复。真正把时间从模糊变成精确的,就是 LogMiner。它能从归档日志里找出那条 DROP 语句,告诉我们 SCN 是多少、执行用户是谁、在哪个会话下执行的。有了这个锚点,RMAN 才能精确恢复到 DROP 前的一秒。
所以我要强调一个容易被忽略的认知:LogMiner 本身不生产数据,它只是把日志里的历史操作翻出来,但在恢复链条里,它往往才是那个决定成败的突破口。没有精确 SCN,RMAN 再强也只能盲猜。
2. 挖日志之前必须搞懂的底牌:DDL 在重做日志里的真面目
LogMiner 能挖日志,但"能挖"和"挖得到位"是两回事。很多人第一次用 LogMiner 查 DDL,会对着结果懵半天:为什么看不到一条干干净净的DROP TABLE XXX?反而是大量对obj$、seg$这类数据字典表的内部递归操作。这背后是 redo 日志的真实记录机制。
Oracle 的 redo log 本质上记录的是"数据库物理变更",不是应用层的友好操作日志。一条DROP TABLE语句在执行时,数据库做的是一连串底层动作:修改数据字典对象、释放段空间、删除相关权限记录等等。LogMiner 的作用,就是把这些物理变更重新拼装成逻辑 SQL。在启用在线字典的情况下,它能把大部分 DDL 重组成我们可以识别的文本,SQL_REDO列通常能直接看到类似drop table app.t_actual_order purge这样的内容。但某些版本、某些字典缺失场景下,你看到的会是一堆递归语句,这时候还得靠 SCN 和时间点反推。
这里有个很重要的前置条件:补充日志(Supplemental Logging)。默认情况下 redo 日志只记录变更的最小必要信息,对于 UPDATE 场景,LogMiner 经常只能告诉你"哪一行变了",但给不出完整的前后值。补充日志就是为了解决这个问题存在的。尤其是挖掘 DDL 之前,我建议至少确认数据库开启了最小补充日志,最好把主键补充日志也一并打开:
-- 检查补充日志状态 SELECT supplemental_log_data_min, supplemental_log_data_pk, supplemental_log_data_all FROM v$database; -- 若未开启,执行如下语句(会重启实例才生效,生产环境需评估窗口) ALTER DATABASE ADD SUPPLEMENTAL LOG DATA; ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (PRIMARY KEY) COLUMNS;注意一点:补充日志开启后,只影响开启之后新产生的日志,对事故发生前已经生成的 redo 无能为力。所以这个动作更多是"下一次再有事故时不吃亏",如果事故已经发生且没开补充日志,DDL 一般还能看到,但相关 DML 的SQL_REDO很可能只剩残缺的骨架。
挖日志时主要面对的是V$LOGMNR_CONTENTS这个视图,几个核心字段如下:
| 列名 | 含义 | 实用说明 |
|---|---|---|
| OPERATION | 操作类型:DDL / INSERT / DELETE / UPDATE 等 | 定位 DDL 的第一过滤条件 |
| SQL_REDO | 重做的 SQL 文本 | DDL 时通常显示原始语句,DML 时是重建后的操作 |
| SQL_UNDO | 撤销的 SQL 文本 | 可用于反推破坏前的数据内容 |
| SCN | 操作发生的系统变更号 | 这就是恢复锚点 |
| TIMESTAMP | 操作时间戳 | 与 SCN 互相印证 |
| SEG_OWNER / SEG_NAME | 对象属主和对象名 | 精确过滤目标表 |
| USERNAME | 执行用户 | 确认是谁干的 |
| SESSION_INFO | 会话信息 | 包含登录 IP、终端等信息,需要补充日志配合 |
还要提一下字典模式。LogMiner 有两种常见字典来源:在线数据字典(DICT_FROM_ONLINE_CATALOG)和字典文件(DICT_FROM_REDO_LOGS/ 外部字典文件)。实务里我几乎只用在线字典,简单、不会因为字典文件过期而解析失败。只有遇到日志时间太老、对象字典已经不存在的情况,才考虑字典文件兜底。
把上面这些理清了,你就明白:LogMiner 不是"时光机",它更像一个日志考古工具。你给它一段历史日志,它还你一批重组的 SQL 操作。能不能从中精准捞出那条 DROP,靠的是过滤条件和现场判断力。
3. 实操全链路:用 LogMiner 把 DROP 语句和 SCN 钉在墙上
这一节是整套流程的操作核心。我按自己实战时的顺序来写,照着执行可以少走弯路。
3.1 确认事故时间段,锁定日志范围
LogMiner 的第一步其实不是 Add Logfile,而是确定要挖哪一段日志。事故发生后第一步永远是看当前归档日志的时间范围,确定日志文件覆盖了事故窗口:
SELECT name, first_time, next_time FROM v$archived_log WHERE first_time >= SYSDATE - 2 ORDER BY first_time;如果事故就发生在几十分钟前,可能部分信息还在当前的 redo log 里,没有归档。当前日志要看这里:
SELECT group#, sequence#, status, first_time FROM v$log ORDER BY group#;实际操作中,我一般会把事故时间段前后各留半小时余量,比如怀疑 DROP 发生在 17:20,就选 16:50 到 18:00 之间的所有归档日志。范围太小容易漏,范围太大 LogMiner 解析时间太长,PGA 压力也大。
3.2 添加日志文件并启动 LogMiner
日志文件列表确定后,按时间顺序逐个添加,第一个用NEW,其余用ADDFILE:
EXEC DBMS_LOGMNR.ADD_LOGFILE('/u01/app/oracle/oradata/ORCL/archive/1_12345.dbf', DBMS_LOGMNR.NEW); EXEC DBMS_LOGMNR.ADD_LOGFILE('/u01/app/oracle/oradata/ORCL/archive/1_12346.dbf', DBMS_LOGMNR.ADDFILE); EXEC DBMS_LOGMNR.ADD_LOGFILE('/u01/app/oracle/oradata/ORCL/archive/1_12347.dbf', DBMS_LOGMNR.ADDFILE);然后启动 LogMiner:
EXEC DBMS_LOGMNR.START_LOGMNR( OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG + DBMS_LOGMNR.COMMITTED_DATA_ONLY + DBMS_LOGMNR.SKIP_CORRUPTION);解释一下三个选项为什么都要带:DICT_FROM_ONLINE_CATALOG用在线字典,解析出的表名和列名更准确;COMMITTED_DATA_ONLY只显示已提交事务,避免看到一堆后来回滚掉的中间操作,省去大量干扰;SKIP_CORRUPTION是保命选项,万一某条日志轻微损坏,不至于让整个解析流程中断。
3.3 定位目标 DDL 语句
启动完成后,查询V$LOGMNR_CONTENTS:
SELECT scn, timestamp, operation, seg_owner, seg_name, sql_redo, sql_undo, username, session_info FROM v$logmnr_contents WHERE operation = 'DDL' AND UPPER(seg_name) LIKE 'T_ACTUAL_ORDER%' ORDER BY scn;这里我是直接用对象名过滤的。LogMiner 解析后,SEG_NAME一般就是目标表名,省得在一堆递归字典操作里大海捞针。实际执行结果大致是这样(脱敏后的示意数据):
| SCN | TIMESTAMP | OPERATION | SEG_NAME | SQL_REDO | USERNAME |
|---|---|---|---|---|---|
| 27038489 | 2024-06-12 17:21:35 | DDL | T_ACTUAL_ORDER | drop table app.t_actual_order purge | A_USER |
| 27038490 | 2024-06-12 17:21:36 | DDL | T_ACTUAL_ORDER | create table app.t_actual_order ... | A_USER |
看到没有?同一个表名后面可能跟着两条 DDL——先 DROP,后 CREATE。这通常是因为应用或者自动脚本在重建表。如果直接拿第一条的 SCN 去恢复,没问题,那是删除点;但如果分不清哪条是 DROP 哪条是 CREATE,很容易把恢复点选到 CREATE 之后,那就什么都捞不到了。后面我会专门讲这个坑。
3.4 验证 SCN,并处理会话级结果集
定位到 DROP 的 SCN 后,不要急着收工,先做个交叉验证。用SCN_TO_TIMESTAMP反向计算一下这个 SCN 对应的时间,和 DBA 记忆中的事故时间对一下:
SELECT SCN_TO_TIMESTAMP(27038489) FROM dual;如果时间对得上,基本可以确认找对了。对不上的情况,多半是事故时间记忆有偏差,或者日志范围没覆盖全,需要扩大日志范围重新挖。
这一步有个非常容易踩的坑:**V$LOGMNR_CONTENTS是当前会话内的结果集,不是全局持久化的。** 你一旦执行END_LOGMNR,或者重新连接会话、再执行一次START_LOGMNR,前面查到的内容就没了。所以挖到目标记录后,第一时间把关键字段(SCN、TIMESTAMP、SQL_REDO)记到本地,或者直接另存查询结果,再执行清理:
EXEC DBMS_LOGMNR.END_LOGMNR;我遇到过有人查到一半,顺手执行了另一个START_LOGMNR,前面的结果全部清空,只能重新解析一遍,白白浪费了一个多小时。这种亏吃过一次就够了。
4. 拿到 SCN 之后怎么捞数据:三种恢复手段怎么选
LogMiner 把 SCN 钉死之后,恢复动作才算真正进入执行轨道。该用哪种手段,取决于误删的严重程度和手头有什么基础设施。我按优先级展开。
4.1 最轻量的分支:回收站还在,先 Flashback
如果表只是被DROP TABLE(不带 PURGE),并且回收站没有被清空,直接用 Flashback Drop 是最快的方案,完全不需要 RMAN:
-- 查看回收站中的对象 SELECT object_name, original_name, operation, droptime, dropscn FROM recyclebin WHERE original_name = 'T_ACTUAL_ORDER'; -- 闪回原表 FLASHBACK TABLE app.t_actual_order TO BEFORE DROP;要注意一个特殊情况:如果 DROP 之后,又有同名的新表被创建出来,FLASHBACK TABLE ... TO BEFORE DROP会因为名称冲突而报错。这时候要么先把新表改名,要么用回收站对象的 BIN$ 名称直接闪回:
ALTER TABLE "BIN$abcdefghijklm$0" RENAME TO t_actual_order_old;这个场景其实用不到 LogMiner,但实务中经常遇到的问题是:DBA 只记得大概几点删的,但回收站里有多个同版本对象,分不清哪个才是当时那张表。此时 LogMiner 挖出来的 DROP 时间就派上用场了,拿DROPTIME和历史 SCN 对照,就能精准确认要闪回哪一个。
4.2 最硬核的方案:RMAN RECOVER TABLE 到 DROP 前的 SCN
回到那次生产事故:PURGE 之后回收站为空,唯一可行的就是 RMAN 表级时间点恢复。Oracle 12.2 以上的 RMAN 支持RECOVER TABLE,可以直接从备份集里把一张表恢复到指定 SCN,并且自动完成辅助实例搭建、数据导出导入,整个过程不需要手工干预太多。命令很直接:
rman target / RMAN> RECOVER TABLE APP.T_ACTUAL_ORDER UNTIL SCN 27038489 AUXILIARY DESTINATION '/backup/aux_tbs';执行过程中,RMAN 会在AUXILIARY DESTINATION目录下构建一个辅助实例,从备份中恢复相关数据文件到目标 SCN,然后把表数据用 expdp 导出,最后导入到原库。整个过程视表大小而定,小时级到分钟级都有可能。
这里有几个前提条件必须满足:
- 数据库处于归档模式,目录里的归档日志能覆盖到目标 SCN。
- 目标表所在的表空间,在备份中存在且没有损坏。
AUXILIARY DESTINATION有足够的磁盘空间,至少是表大小的两倍左右,因为要容纳辅助数据文件和导出文件。- 数据库版本 12.2 以上。如果是 11g,这个命令用不了,得走 4.3 的 TSPITR 方案。
如果想顺便把表恢复到不同用户或者不同表空间,可以用REMAP参数,比如:
RMAN> RECOVER TABLE APP.T_ACTUAL_ORDER UNTIL SCN 27038489 AUXILIARY DESTINATION '/backup/aux_tbs' REMAP TABLE 'APP.T_ACTUAL_ORDER':'REP.T_ACTUAL_ORDER' REMAP TABLESPACE 'APP_DATA':'REP_DATA';我当时用的就是这个加 REMAP 的版本,因为生产业务并不希望原表立即出现在原用户下,而是要经过校验后再切换回去。这种"恢复到一个验证用户,确认无误再切换"的做法,强烈建议大家都保留,能避免二次事故。
4.3 兜底方案:TSPITR 和数据库 PITR,什么时候才用
如果生产环境是 11g,或者因为某些原因不能用RECOVER TABLE,就只能退回到表空间时间点恢复(TSPITR)或者整个数据库的不完全恢复。
TSPITR 的基本思路是把目标表所在表空间单独恢复到指定 SCN,不影响其他表空间。步骤大致是:选择要恢复的表空间,准备辅助实例,从备份恢复数据文件到目标 SCN,打开辅助库后用 expdp 把表导出,再导回原库。操作链比较长,而且如果目标表空间里还有其他表,辅助实例恢复时这些表也会一起回到过去,只是它们不会被导出,原库不会受影响。
整个数据库 PITR 就更重了,ALTER DATABASE RECOVER UNTIL SCN会把全库回退,相当于所有表都"回到过去",对生产环境来说基本等于一次计划内停机回切,通常只在事故影响面非常大、已经被迫启动容灾切换时才会考虑。
所以我的选型逻辑很简单:回收站在 → Flashback;回收站不在但 12.2+ 有备份 → RMAN RECOVER TABLE;版本旧或表空间特殊 → TSPITR;其他都不可用 → 数据库 PITR 或者接受数据丢失。LogMiner 在这个过程中,始终是那个提供精确 SCN 的引路人。
5. 恢复不是终点:校验、约束与后续跟进的完整清单
表恢复出来,不代表这事就完结了。我见过太多恢复完"看起来成功",结果第二天应用报出一堆主键冲突、外键找不到父记录的事故。恢复完成后的校验和对象重建,往往比恢复本身更考验细心程度。
5.1 数据完整性校验第一优先级
无论用哪种方式恢复的表,第一步永远是数据验证。先看行数是否合理:
SELECT COUNT(*), COUNT(DISTINCT PRIMARY_KEY_COLUMN) FROM rep.t_actual_order;光看行数不够,要结合业务特征做交叉验证。比如对账系统,就可以拿总金额、总笔数和上游系统的计数做比对:
SELECT TO_CHAR(SUM(amount), '999,999,999.99') AS total_amount, COUNT(1) AS total_cnt FROM rep.t_actual_order;如果这张表业务上存在时间连续性,还可以检查恢复后的最大事务时间是否落在 DROP 之前。这里有个小技巧:LogMiner 挖日志时,顺手查一下事务时间内最后一条对这张表的 DML,看看时间和内容,恢复后拿这个时间点去表里抽查,能很快确认"有没有恢复到足够晚的位置"。比如日志显示 17:21:30 还有一条INSERT INTO T_ACTUAL_ORDER,恢复后查一下该时间点附近的数据是否完整,基本就能验证精确度。
5.2 索引、约束、外键是重灾区
RMAN RECOVER TABLE 恢复出来的表,不同版本对索引和约束的保留程度不一样。我实测的经验是:别指望它把索引和约束完整带回来。恢复完成后,第一时间检查DBA_INDEXES、DBA_CONSTRAINTS:
SELECT index_name, uniqueness, status FROM dba_indexes WHERE table_name = 'T_ACTUAL_ORDER' AND owner = 'REP'; SELECT constraint_name, constraint_type, status FROM dba_constraints WHERE table_name = 'T_ACTUAL_ORDER' AND owner = 'REP';缺失的部分,按原表的 DDL(如果存在备份的 DDL 文件)全部重建。主键必须第一个建,然后是唯一约束、普通索引、外键。外键重建还有个顺序问题:如果这张表是别的表的父表,恢复后要先确认子表里没有孤儿数据,再重建外键约束,否则ENABLE CONSTRAINT会失败。可以跑一个简单的反连接查询:
SELECT COUNT(1) FROM rep.detail_tbl a WHERE NOT EXISTS ( SELECT 1 FROM rep.t_actual_order b WHERE b.id = a.parent_id );查出来有孤儿记录的话,先做数据核对,再决定是补数据还是删除异常记录。
5.3 触发器、权限、同义词一个都不能漏
恢复的表可能只是裸表,和它配套的还存在其他对象。检查清单里至少要包含:
- 该表上的行级触发器、语句级触发器是否恢复;
- 同义词、物化视图是否受影响;
- 表上的
SELECT、INSERT、UPDATE、DELETE授权是否还在; - 表的统计信息是否过期,如果过期直接重新收集:
BEGIN DBMS_STATS.GATHER_TABLE_STATS(ownname => 'REP', tabname => 'T_ACTUAL_ORDER', cascade => TRUE); END; /最后一步才是让应用侧验证。把恢复表切回原用户原表之前,业务同学先读写一把,确认关键功能正常后,再在维护窗口内替换正式表。整个流程做成文档,下次就不用再赌人品。
6. 实战中踩过的六个坑,每一个都让人血压升高
这一节是我最想分享的部分。LogMiner 和恢复流程本身并不复杂,复杂的是各种环境变量和操作细节。以下六个坑,每一个我都亲身踩过或者亲眼见过同事踩过。
坑一:V$LOGMNR_CONTENTS是会话级结果集,忘记录查就白挖了。这个是使用频率最高的坑。LogMiner 启动后结果集只在当前会话有效,一旦重新连接、执行END_LOGMNR或再次START_LOGMNR,之前的内容全部清空。有一次我为了追加一个日志文件,重新执行了START_LOGMNR,结果之前定位好的 DDL 记录消失,只能从头再解析一遍。所以挖到关键记录的第一动作,永远是先存到本地文件或者单独记录 SCN、时间、SQL 文本。
坑二:没开补充日志,DML 的SQL_REDO残缺,连数据校对都做不了。补充日志影响的是重做日志记录详细程度。没开最小补充日志时,DML 的SQL_REDO往往只显示部分列,UPDATE 语句甚至连前后值都看不到。这会导致你即使定位到了时间点,也没法通过 LogMiner 验证"最后一次 DML 长什么样",恢复后数据校验缺少参照。DDL 文本受影响小一些,但恢复后的校验工作会很难受。所以每次建新库,我会把补充日志这一步写进初始化检查清单。
坑三:归档日志被清理、redo 被复用,错过了时间窗口,LogMiner 也无能为力。LogMiner 只能解析磁盘上还存在的日志。归档保留期太短,或者当前 redo 因为频繁切换被循环覆盖,事故时间段的日志可能早就没了。那次事故能成功,很大程度是因为归档策略留了三天。很多环境归档保留只有一天,误删发生在午夜,早上才反应过来,日志已经没了。这个不是操作技巧能救的,是制度层面的问题,后面会讲日常准备。
坑四:DROP 之后同名表重建,LogMiner 结果里出现多条 DDL,SCN 没选对就全盘皆输。我在 3.3 节展示过,一个表名可能先后对应 DROP 和 CREATE 两条 DDL。实际处理时还有更复杂的:DROP → CREATE → 再 DROP → 再 CREATE,应用反复重启反复重建。定位时必须对 SQL_REDO 内容逐条看,确认那一条是包含了PURGE的 DROP 语句,而不是 CREATE。如果拿的是 CREATE 的 SCN 做恢复,恢复出来的可能是一张空表,因为数据早没了。
坑五:RMAN RECOVER TABLE 的辅助实例环境没准备好。AUXILIARY DESTINATION路径必须可写、空间足,而且数据库的DB_RECOVERY_FILE_DEST或者控制文件配置要能支撑辅助实例正常工作。最常见的问题是辅助实例因为监听或内存参数问题起不来,RMAN 会卡在准备阶段。建议执行前先确认辅助目录权限,查看alert.log了解辅助实例启动状态。不要等到恢复执行一半才发现空间不够,那就骑虎难下了。
坑六:CDB/PDB 环境下,LogMiner 的容器上下文搞错。多租户架构下,如果你在某个 PDB 里执行DBMS_LOGMNR.START_LOGMNR,能看到的日志范围通常是受限的。更常见的做法是在 CDB 根容器中启动 LogMiner,才能解析整个数据库的归档日志,并且在V$LOGMNR_CONTENTS中看到所有 PDB 的操作记录。挖掘结果里注意CON_ID列,别把其他 PDB 的同名表当成目标表。我在第一次处理这类场景时就被同名表干扰过,后来一律先按CON_ID过滤环境,再按表名过滤。
7. 关于日常准备,我用教训换来的五条建议
写到这,还是想多说几句日常准备。误删表的恢复,做得再好也是事后补救。真正的高手,是把突发事故变成"走流程"。这五条建议全部来自实际教训,每一条都不难,难的是坚持做到。
第一,新库上线必须开补充日志。这是成本最低的一道保险,两条ALTER DATABASE语句而已,带来的收益却是 LogMiner 可用的完整度。没有它,所有基于日志的复盘和恢复手段都打折扣。
第二,核心 DDL 操作习惯从 DROP 改成 RENAME。应用需要重建表时,先RENAME到带_OLD后缀的临时名,确认新表无误后再删除旧表。个别核心表甚至可以只 RENAME,不删除,保留一定的回溯能力。这个习惯救过我好几次,比任何恢复工具都好用。
第三,重要表独立表空间。不只是为了性能,更是为了 TSPITR 时可以把影响范围控制到单个表空间,避免为恢复一张表把同空间里十几个表全部卷入。
第四,归档日志保留期要向恢复时间点看齐。核心业务库的归档至少保留 3 天以上,如果能廉价存储,尽量留 7 天。归档是你最后的底牌,底牌被清得太早,再好的工具都是摆设。
第五,每半年做一次误删恢复演练。不需要在生产库做,用开发环境的副本模拟DROP TABLE PURGE,走一遍 LogMiner → SCN 定位 → RMAN RECOVER TABLE 的完整链路。演练过和没演练过,真正出事时的差距是小时级的。
这次事故复盘给我的总体感受是:Oracle 误删表恢复,拼的不是某个单一工具的熟练度,而是整个链路——日志、备份、精确定位、恢复手段、校验流程——是否形成闭环。LogMiner 在其中扮演的角色,是给所有恢复方案提供"精准瞄准"的能力。希望这篇复盘能帮你在真的遇到误删时,少一些慌乱,多一条清晰的路径。