简介:面向 SQL Server 2008 数据库管理员与运维人员的误删除数据恢复专题文档,聚焦无备份或备份不完整时的应急处理场景,帮助读者理清恢复模式、事务日志与完全备份之间的依赖关系,并了解不同条件下的可行恢复路径与工具选择思路。包内共 1 个 doc 文件,压缩包约 359KB,篇幅紧凑,适合作为故障排查时的案头参考。已有 1826 人学习,说明该主题在数据库运维实务中关注度较高。文档围绕完全恢复模式前提、三种典型场景的判定方式,以及 Recovery for SQL Server 等第三方工具的适用性展开整理,可帮助读者建立从条件核验到方案取舍的恢复框架,减少误删后盲目操作带来的二次风险。
1. DELETE 已经执行了:为什么「有日志就能恢复」只对了一半
DELETE FROM 订单明细敲下去,回车,(41382 行受影响)。真正要命的不是这条语句本身,而是三分钟后你才想起去问:这个库上一次完整备份是什么时候?我见过太多人在这个瞬间才去翻维护计划,然后发现那个每天凌晨跑的备份作业,半年前就因为磁盘满悄无声息地失败了。SQL Server 2008 误删数据的恢复,流传最广的说法是「从事务日志恢复即可,不是什么难事」——这话只对了一半。剩下那一半,取决于两个在你按下回车之前就已经定好的配置:恢复模式是不是 Full,以及有没有一条没断的备份链。这篇笔记按「先判能不能救、再选怎么救、最后记住怎么不翻车」的顺序走一遍,适合已经删了数据正在找后悔药的人,也适合还没出事、想提前把退路铺好的人。
2. 恢复前先判生死:恢复模式与备份链的三条判定命令
误删之后最浪费时间的动作,是还没确认前提就开始敲RESTORE。还原语句本身只有三行,但它能不能成功,在你执行之前就已经被数据库的配置决定了。这一章先把「能不能救」这件事判定清楚,再谈具体动作。
2.1 为什么 Full 恢复模式是整条路的地基
事务日志不是「操作流水账」这么简单,它是一份物理级的重做与撤销记录。一条DELETE语句在执行时,SQL Server 会为被删的每一行写一条LOP_DELETE_ROWS日志记录,里面带着分配单元 ID、页号、槽位号,以及这一行的完整旧值。恢复的本质就是把这串物理记录重放一遍,或者反过来读,把行重新拼出来。所以日志里到底还留着多少东西,直接决定了你有多少种救法。
关键在于日志空间会被重用。SIMPLE 恢复模式下, checkpoint 一过,日志的虚拟日志文件(VLF)就可以被新记录覆盖,你昨天删的数据,今天日志里早就没影了。FULL 模式下日志只有在做过日志备份之后才允许截断,理论上只要没备份过,从建库到现在的每一条记录都还躺在 .ldf 里——这也是为什么「没备份但模式是 Full」还有救,而「模式是 Simple」基本只能靠更早的备份文件。
这里有个非常容易被忽略的坑:很多人以为执行一句ALTER DATABASE [库名] SET RECOVERY FULL就等于上了保险。并不是。切换到 FULL 之后、做第一次完整备份之前,数据库处于一种被称为「伪简单恢复」的状态,日志的行为仍然按 SIMPLE 走,checkpoint 照样截断。真正的日志链起点,是切换模式之后的那一次完整备份。常见做法是切完模式立刻补一次全备加一次日志备份,把链头钉死,否则你以为的后悔药其实是空的。
判断当前状态,看sys.databases里的log_reuse_wait_desc比看恢复模式更有信息量。它是LOG_BACKUP,说明日志正在等一次日志备份,记录都还在,这是好消息;它是NOTHING而恢复模式又是 SIMPLE,那日志大概率已经被重用过一轮了。
2.2 三条 T-SQL 查清现场:模式、备份历史、日志链
先把现场信息固定下来,再动手。第一条查恢复模式与日志状态,注意把库名换成你自己的,别在生产库上跑更新类语句,这三条都是只读的。
-- 1) 看恢复模式、日志重用等待原因、当前状态 SELECT name, recovery_model_desc, -- SIMPLE / FULL / BULK_LOGGED log_reuse_wait_desc, -- 日志在等什么:LOG_BACKUP / CHECKPOINT / NOTHING state_desc -- ONLINE / RECOVERY_PENDING / SUSPECT FROM sys.databases WHERE name = N'YourDb';recovery_model_desc是FULL且state_desc是ONLINE,才有资格谈时间点恢复。如果已经是SUSPECT,先别急着还原,把 .mdf 和 .ldf 各复制一份出去,任何后续操作都在副本上做,原始文件是最后的证据。
第二条查备份历史。msdb里的backupset记录了每一次备份的类型、起止 LSN 和完成时间,type字段里D是完整备份、I是差异备份、L是日志备份。
-- 2) 列出这个库最近的备份记录,重点看 type 和 LSN 是否首尾相接 SELECT TOP 30 bs.database_name, bs.type, -- D=完整 I=差异 L=日志 bs.backup_start_date, bs.backup_finish_date, bs.first_lsn, bs.last_lsn, mf.physical_device_name FROM msdb.dbo.backupset AS bs JOIN msdb.dbo.backupmediafamily AS mf ON bs.media_set_id = mf.media_set_id WHERE bs.database_name = N'YourDb' ORDER BY bs.backup_finish_date DESC;看两个东西:一是有没有一条backup_finish_date早于误删时间的D类型记录;二是这些日志备份的first_lsn能不能和上一条的last_lsn接上。断链的典型表现是某条日志的first_lsn比前一条的last_lsn大出一截,中间那段被别的操作吃掉了,后面还原必然报错。
第三条是直接问备份文件本身,不依赖 msdb——因为 msdb 也可能被还原过、被清理过,跟实际文件对不上。
-- 3) 不信任历史表时,直接读备份文件的头信息 RESTORE HEADERONLY FROM DISK = N'D:\Backup\YourDb_full.bak'; RESTORE VERIFYONLY FROM DISK = N'D:\Backup\YourDb_full.bak' WITH CHECKSUM;HEADERONLY会列出这个文件里包含的所有备份集,包括BackupType、BackupStartDate、FirstLSN、LastLSN。VERIFYONLY只做完整性校验,不还原,几十 G 的库也就几分钟,但能提前告诉你这个备份文件是不是早就损坏了——我一般会先跑它,免得还原到一半才发现介质读不出来。
2.3 三种场景分流:纯脚本能救、只能上工具、根本救不回
把上面的信息凑齐,结局只有三种,先对号入座再选工具,能省掉大量无用功。
| 场景 | 恢复模式 | 有无误删前的完整备份 | 可用手段 | 成功率 |
|---|---|---|---|---|
| 一 | Full | 有,且日志链完整 | 纯 T-SQL,三步还原到时间点 | 高,但受限于备份时间点 |
| 二 | Full | 没有 | 第三方日志解析工具读 .ldf | 中,取决于日志是否被截断 |
| 三 | Simple 或日志已被截断 | 没有 | 无可靠手段 | 极低 |
场景一是最舒服的,全程不用装任何东西,靠BACKUP LOG+RESTORE DATABASE+RESTORE LOG ... STOPAT三句就能把库拉回误删前一秒,具体写法放在第 3 章。场景二是大多数人真正遇到的局面:模式确实是 Full,日志也一直在长,但从来没做过完整备份,所以没有链头可以接,只能绕过备份、直接从 .ldf 里把LOP_DELETE_ROWS反解成 INSERT,第 4 章讲这条路。场景三没什么可说的,只能从更早的备份或者其他系统里找数据,硬来的结果通常是浪费几个小时还耽误了止损时间。
注意:判定阶段就已经该做一件事——立刻把 .mdf 和 .ldf 各复制一份到另一块盘。后续无论走哪条路,都在副本上折腾,原库保持只读或者干脆停掉写入,别让日志继续滚动把证据盖掉。
3. 前提都满足时:用 STOPAT 把库拉回误删前一秒
这是最省事的一条路,但省事不等于随便敲。三步的顺序不能乱,而且每一步的WITH选项都有它必须存在的理由,改错一个字母,轻则报错重来,重则把最后一次恢复的机会用掉。
3.1 第一件事不是还原,是把当前日志尾备份出来
误删之后,日志里最新的那部分记录还停在日志文件末尾,没有进过任何备份文件。如果不先把它导出来,后面执行RESTORE DATABASE ... WITH REPLACE的时候,这个库的日志文件会被直接覆盖,误删那一刻的记录就永久消失了。这一步是整个流程里唯一不可逆的操作。
-- 尾日志备份:把还没备份过的日志(含误删动作)导出来 -- NORECOVERY 在 BACKUP LOG 里等价于 NO_TRUNCATE:不截断日志,即使数据文件有问题也能备 BACKUP LOG [YourDb] TO DISK = N'D:\Recovery\tail_log_20240522.trn' WITH NORECOVERY, INIT, CHECKSUM, STATS = 10;WITH NORECOVERY这个词在备份语句里看起来有点反直觉,因为它在还原语句里是「保持正在还原状态」的意思。但在BACKUP LOG里它等价于NO_TRUNCATE,作用是两件事:不截断日志,以及在数据文件已经损坏的情况下依然能把日志读出来。INIT表示覆盖目标文件里已有的备份集,CHECKSUM让备份带上校验和,方便后面验证。备份路径要和数据库文件放在不同的物理磁盘上,日志文件本来就大,写满同一块盘会更麻烦。
备份完立刻验证一下,别等还原到一半才发现这个 .trn 是坏的:
RESTORE VERIFYONLY FROM DISK = N'D:\Recovery\tail_log_20240522.trn' WITH CHECKSUM; RESTORE HEADERONLY FROM DISK = N'D:\Recovery\tail_log_20240522.trn';HEADERONLY的输出里记下LastLSN,如果它和 msdb 里最后一条日志备份的LastLSN差得离谱,说明中间还有别的日志备份没找到,得先补齐再往下走。
3.2 还原完整备份:NORECOVERY 与 REPLACE 到底在做什么
第二步是把误删之前的完整备份还原回去。这里有个决定性的选择:直接在原库上覆盖,还是还原成一个影子库。我的建议一律是影子库,哪怕磁盘紧张也想办法腾出空间。原因很简单,WITH REPLACE一旦执行,原库就没了,如果 STOPAT 的时间点取错、或者日志链中间缺一段,你已经没有第二次机会。
-- 先看清楚备份里有哪些逻辑文件名,MOVE 的目标必须照抄这里的 LogicalName RESTORE FILELISTONLY FROM DISK = N'D:\Backup\YourDb_full.bak'; -- 还原成影子库,保持「正在还原」状态,等待日志接上 RESTORE DATABASE [YourDb_Restore] FROM DISK = N'D:\Backup\YourDb_full.bak' WITH NORECOVERY, REPLACE, MOVE N'YourDb_Data' TO N'D:\Restore\YourDb_Restore.mdf', MOVE N'YourDb_Log' TO N'D:\Restore\YourDb_Restore_log.ldf', STATS = 10;NORECOVERY是这三步能串起来的关键。它告诉 SQL Server:这个库还没还原完,先别做恢复(undo 未提交事务)那一步,保持「正在还原」状态,这样后续的日志才能继续往上叠。如果这里手滑写成RECOVERY,数据库会上线,日志链就断了,只能从头再来。
REPLACE的作用是允许覆盖同名数据库,并跳过对现有备份链的检查。还原到影子库时其实不加也行,但只要目标名和源库同名就必须加。MOVE的两个逻辑名必须来自上一条FILELISTONLY的输出,照抄文件路径是新手最容易犯的错——逻辑名是建库时指定的,跟现在的物理文件名经常对不上。
如果误删和上一次完整备份之间还有若干个日志备份,要按时间顺序一个个RESTORE LOG ... WITH NORECOVERY追平,中间不能跳。跳一个,后面就会报 LSN 不衔接。
3.3 STOPAT 的时间点怎么取才不白跑一趟
这是整个流程里最容易出岔子的地方。「大概下午两点左右删的」这种精度,会让你恢复出来的库要么还没删,要么已经删完了。时间点必须精确到秒,而且必须早于DELETE语句开始执行的那一刻。
最可靠的办法是直接问日志。sys.fn_dblog是个未文档化的表值函数,但一直能用,它能读出当前数据库日志里的每一条记录:
-- 在误删的生产库上执行(只读,不改数据),找最近的大批量删除发生在哪一秒 SELECT TOP 20 [Current LSN], Operation, -- LOP_DELETE_ROWS 就是行删除 Context, [Transaction ID], [Begin Time], -- 事务开始时间,比提交时间更贴近误删那一刻 AllocUnitName -- 涉及的表或索引 FROM sys.fn_dblog(NULL, NULL) WHERE Operation IN ('LOP_DELETE_ROWS', 'LOP_INSERT_ROWS') ORDER BY [Current LSN] DESC;AllocUnitName能直接告诉你哪张表在被删,[Begin Time]是事务开始时间。取这个时间再往前推几秒作为STOPAT的值,宁可早一点——早了只是少恢复几条正常数据,晚了就是数据已经被删掉了,白跑。
接下来是关键技巧:把最后一次RECOVERY往后拖。先用NORECOVERY把日志应用到目标时间点,然后连上去看看数据对不对,确认无误再让库上线。
-- 应用尾日志到指定时间点,但先不上线 RESTORE LOG [YourDb_Restore] FROM DISK = N'D:\Recovery\tail_log_20240522.trn' WITH STOPAT = N'2012-05-22T13:40:00', NORECOVERY, STATS = 10; -- 此时库处于「正在还原」,可以只读查询验证 SELECT COUNT(*) AS cnt FROM [YourDb_Restore].dbo.Orders; -- 确认无误后,让库正式上线(这一步之后就不能再应用日志了) RESTORE DATABASE [YourDb_Restore] WITH RECOVERY;这个写法救过我一次:第一次STOPAT取早了,恢复出来的表只有误删前的部分数据,因为还停在NORECOVERY状态,我直接把时间点改成13:42:30重新应用了一遍日志,没有从头再来。如果当初直接RECOVERY,就只能再复制一遍几十 G 的备份文件。
3.4 恢复完怎么验证:别只看行数对不对
数据「看起来回来了」和「能直接切回生产」之间还隔着好几件事。行数对得上只是最低标准,真正的验证要覆盖自增列、触发器、外键和索引。
USE [YourDb_Restore]; GO -- 1) 数量与业务主键抽查 SELECT COUNT(*) FROM dbo.Orders; SELECT TOP 10 * FROM dbo.Orders ORDER BY OrderID DESC; -- 2) 自增列当前种子,避免切回生产后新插入撞主键 DBCC CHECKIDENT ('dbo.Orders', NORESEED); -- 3) 外键与约束是否可信 DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS;DBCC CHECKIDENT带NORESEED只显示当前种子,不改。如果恢复出来表里最大 ID 是 41382、而种子还停在 40000,切回生产后第一批新订单就会主键冲突,这个坑很隐蔽,报错要等到业务写入时才炸出来。
还有一个常被忽略的点:恢复出来的库上如果带着触发器,某些审计触发器会在你插数据的时候再写一遍日志表,导致数据重复。切回生产前把非必要触发器禁用掉,比对完再启用。
4. 没有完整备份:从 .ldf 里把 DELETE 反向解析出来的操作路径
现在说真正麻烦的那一类:恢复模式是 Full,日志文件也一直在长,但从来没有做过完整备份,所以没有任何链头可以接。这种情况下RESTORE系列语句一条都用不上,只能绕过备份机制,直接去读日志文件里的物理记录。原文里那位同行的库就属于这一类,最后是靠商业工具救回来的。
4.1 为什么这种情况下日志里还留着记录
先解释清楚可行性,你才知道什么时候该放弃。FULL 模式下日志只在日志备份之后才被标记为可重用,如果这个库从来没做过日志备份,那么从建库那天起的所有日志记录理论上都还在 .ldf 里,只是文件可能已经涨到几十 G。误删的那些LOP_DELETE_ROWS记录夹在中间,工具要做的就是把它们挑出来、按页和槽位反推出原始行、再拼成 INSERT 语句。
但这套逻辑对操作类型很敏感。DELETE是逐行记录的,每一行都带有完整旧值,反解成功率最高;UPDATE记录的是前后两个版本,也能还原;TRUNCATE TABLE就麻烦得多,它只记录页释放(deallocation),不记录每一行的内容,很多工具遇到它只能恢复出表结构,数据救不回来;DROP TABLE同理,得先把对象 ID 和列定义从系统表里找回来,再重建。所以看到「误删」两个字,第一件事是问清楚到底是 DELETE、TRUNCATE 还是 DROP,这直接决定了要不要抱希望。
日志被截断的判断也有迹可循。如果这个库被 shrink 过、被 detach 再 attach 过、或者中途从 Full 切回 Simple 再切回来,日志链就断了,那些记录是真的没了。工具不是万能的,它只能读还在文件里的东西。
4.2 工具选型的坑:版本兼容和 Demo 限制
这类工具市面上就那么几款,但踩坑率极高,因为 SQL Server 2008 的日志格式对很多工具来说太老了。原文里试了三款:前两款直接不支持 2008,第三款支持但要付费且 Demo 没有恢复功能,最后一款商业软件 Demo 版能恢复、但对数据库文件大小有上限(当时是 24Gb 以内)。这个经历基本能代表大多数人的遭遇,我在选型时会按下面几项逐个排除,省得装完才发现不行。
| 检查项 | 为什么关键 | 怎么快速确认 |
|---|---|---|
| 是否明确标注支持 SQL Server 2008 | 老版本日志格式与 2012+ 差异大,很多工具只做新版本 | 看官方支持矩阵,别信安装向导的自动检测 |
| Demo 版是否允许导出数据 | 有些只让你「预览」记录,导出要授权 | 装完先用一个小库跑通完整导出流程 |
| 文件大小上限 | Demo 常按 .mdf 大小卡,生产库轻易超限 | 看授权说明里的容量限制,提前估算 |
| 能否只读打开 .ldf | 有些工具会以独占方式挂载,把生产库锁住 | 在副本上试,绝不在原库上试 |
| 输出格式 | 生成 SQL 脚本比直接写库安全得多 | 看输出目录里是不是有 .sql 和 .bat |
最后一条特别重要。我坚持要「生成脚本」而不是「直接写回数据库」的工具,原因在 4.4 会讲。如果在副本上试的时候发现它是直接往目标库写数据的,那就要格外小心目标库选错。
4.3 向导里必须改的那几个选项
工具界面各有不同,但流程骨架是一致的:选数据文件 → 选解析模式 → 指定日志文件 → 指定输出目录 → 生成脚本 → 导回目标库。其中有三个选项是决定成败的,默认值基本都是错的。
第一步,File > Recover之后选择 .mdf 文件。这里要选副本,不要选生产库路径。有些工具会顺手把同目录下的 .ldf 也带上,如果没带,下一步要手动指定。
第二步,进入Recovery Configuration界面,模式一定要选Custom(自定义)。默认的快速模式只做「数据库打不开时把数据读出来」这件事,它走的是页级扫描,根本不去解析日志记录,自然也找不到被删的行。只有选了 Custom,后面才会出现日志相关的选项。
第三步,在Recovery options里勾上Search for deleted records(搜被删记录),并指定日志文件(.ldf)的完整路径。这一步是整个流程的核心,勾选框的名字在不同工具里可能叫Find deleted records或者Recover deleted rows,认准「deleted」这个词。
第四步,选目标文件夹(Destination folder)。这个目录用来存放恢复过程中生成的两类东西:一堆 .sql 脚本(里面是反解出来的 INSERT 语句)和一个 .bat 批处理(负责按顺序执行这些脚本)。目录要选在空间充足的盘上,脚本体积可能比你想的大很多。
第五步,点Start,等它跑完。跑完之后会弹出SQL Server Database Creation Utility窗口,这时候别急着点 Next 一路到底——它会问你恢复到哪个目标数据库、导入哪些内容。目标数据库选一个空库或者影子库,导入选项选Import available data both database and log files,意思是把数据文件里的现有数据和日志里反解出来的被删数据一起导进去。如果只选日志,恢复出来的可能只有被删的那部分,上下文全丢。
4.4 生成的 SQL 和 bat 不要直接双击
向导最后一步会问你要不要立刻执行,很多人顺手就点了确定,然后数据被写进了生产库——那是最坏的结果,因为反解出来的行有可能重复、有可能顺序错乱,直接写进去等于制造二次污染。
正确的做法是先看脚本。生成目录里的 .sql 文件,开头通常会有USE [库名]以及SET IDENTITY_INSERT的开关,先确认它指向的是影子库而不是生产库。
# 生成的目录里通常有成百上千个 sql 片段,先扫一遍它们往哪儿写 grep -n -i -E "^\s*(USE|INSERT\s+INTO|SET\s+IDENTITY_INSERT|BEGIN\s+TRAN)" ./output/*.sql | head -50 # 统计一下涉及哪些表,心里有数 grep -h -i -oE "INSERT\s+INTO\s+\[?[A-Za-z0-9_\.]+\]?" ./output/*.sql \ | sort | uniq -c | sort -rn | head -20第一条命令的关键是看USE后面跟的库名。如果它指向生产库,直接把 .bat 里的连接串改掉再执行,或者干脆把脚本里的USE语句批量替换成影子库。第二条帮你确认影响范围:如果某个表出现在几万条 INSERT 里,说明这张表就是重灾区,导回去之后要重点核对。
确认无误再执行 .bat,执行完立刻做一次主键去重检查。反解出来的数据是按页和槽位排的,顺序和原始插入顺序不一定一致,涉及自增列的表尤其容易出问题:
-- 导入后先查重复,再决定要不要补唯一索引 SELECT OrderID, COUNT(*) AS dup FROM dbo.Orders GROUP BY OrderID HAVING COUNT(*) > 1; -- 确认无重复后,把自增种子校准到当前最大值 DBCC CHECKIDENT ('dbo.Orders', RESEED);5. 排查与避坑:误删恢复最容易翻车的五个现场
下面这五条都是我在真实场景里撞过的,每条按「现象 → 原因 → 解决」写,撞上了直接对照着查。
5.1 报 3101:还原卡在第一步,提示无法获得排他访问权
现象:执行RESTORE DATABASE立刻失败,报错 3101,大意是「因为数据库正在使用,无法获得对数据库的排他访问权」,并列出几个占用连接的 SPID。
原因:还原(覆盖式)需要独占整个数据库,只要有一个连接挂着——应用连接池、SSMS 里忘了关的查询窗口、复制代理、甚至某个定时作业——就会失败。
解决:把库切成单用户模式,强制踢掉所有连接,再执行还原。这一步不要在生产高峰做。
-- 强制断开所有连接并切单用户,随后立刻执行还原 ALTER DATABASE [YourDb] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; GO RESTORE DATABASE [YourDb] FROM DISK = N'D:\Backup\YourDb_full.bak' WITH NORECOVERY, REPLACE; GO -- 还原全部结束后务必改回来,否则应用连不上 ALTER DATABASE [YourDb] SET MULTI_USER; GOWITH ROLLBACK IMMEDIATE会回滚正在跑的事务,如果是长事务,这一步可能卡几秒到几分钟。改回MULTI_USER这一步经常被忘,第二天早上应用集体连不上,原因就在这里。
5.2 报 4305 或 3118:日志链断了,STOPAT 白设
现象:RESTORE LOG报 4305,提示「此备份集中的日志开始于 LSN xxx,该 LSN 太新,无法应用到数据库」;或者报 3118,「没有文件准备好进行前滚」。
原因:日志链中间缺了一段。常见触发方式是中间做过一次完整备份或差异备份(差异备份会打断日志链)、某个日志备份文件被误删、或者STOPAT的时间点早于这条日志备份的起点。
解决:用RESTORE HEADERONLY把手上所有 .trn 文件的FirstLSN和LastLSN列出来,按 LSN 排序,看断在哪一段。断了就只能找到缺失的那个文件,找不到就得退回场景二,用日志解析工具处理。
-- 把每个日志备份文件的起止 LSN 摊开对比,断点一眼可见 RESTORE HEADERONLY FROM DISK = N'D:\Backup\YourDb_log_01.trn'; RESTORE HEADERONLY FROM DISK = N'D:\Backup\YourDb_log_02.trn';5.3 工具报「找不到可恢复的记录」:日志早就被截断了
现象:日志解析工具扫描完 .ldf,被删记录数是 0,或者只有寥寥几条,跟实际删除的几万行完全对不上。
原因:日志被重用过。shrink 日志文件、把恢复模式从 Full 改成 Simple 再改回来、把数据库 detach 后 attach、甚至长时间只做完整备份不做日志备份,都会让旧记录被覆盖。
解决:这种情况下时间点恢复没有意义,只能去工具里翻更早的日志备份文件(如果做过),或者从业务侧的其他数据源补。预防手段很明确:Full 模式下必须配日志备份作业,并且监控作业是否真的成功,别只看作业存在。
5.4 数据回来了但自增 ID 错位,新插入就撞主键
现象:恢复出来的表数据齐全,业务一写就报「违反 PRIMARY KEY 约束,不能在对象中插入重复键」。
原因:反解出来的 INSERT 里如果带了显式的 ID 值,而表的IDENTITY种子没有跟着推上去,下一条新记录会从旧种子开始分配。手工导入时如果没加SET IDENTITY_INSERT ON,要么插入失败,要么 ID 被重新分配导致业务关联错乱。
解决:导入前打开IDENTITY_INSERT,导入后用DBCC CHECKIDENT校准种子。
SET IDENTITY_INSERT dbo.Orders ON; -- 此处执行反解出来的 INSERT 脚本 SET IDENTITY_INSERT dbo.Orders OFF; GO -- 第二参数留空表示重置为表内当前最大值 DBCC CHECKIDENT ('dbo.Orders', RESEED);5.5 图省事直接在生产实例上 REPLACE
现象:还原完成,数据确实回到了误删前,但误删之后那几个小时正常产生的业务数据全没了,而且原库的日志文件已被覆盖,再想找回那部分数据没有任何可能。
原因:RESTORE DATABASE ... WITH REPLACE是覆盖式的,它不管你现在库里有什么。误删到发现之间往往还有新的业务写入,这些数据在覆盖的一瞬间一起消失。
解决:一律还原到影子库(第 3.2 节的MOVE写法),比对确认后再决定是整库切换还是只把缺失的行导回生产。导回时用INSERT ... SELECT加NOT EXISTS做增量,别整表覆盖。
6. 误删后的前十分钟:几条命令判断能不能救、救得对不对
真出事的时候,最值钱的不是恢复技术,是前十分钟的判断力。我的习惯是把这个顺序固定下来,避免慌乱中先做了不可逆的动作。
| 顺序 | 动作 | 目的 | 判断依据 |
|---|---|---|---|
| 1 | 复制 .mdf 与 .ldf 到另一块盘 | 保住证据,后续全在副本上操作 | 复制完成前不执行任何 RESTORE |
| 2 | 查sys.databases的恢复模式与log_reuse_wait_desc | 判断属于三种场景中的哪一种 | Full + LOG_BACKUP 是好消息 |
| 3 | 查msdb.dbo.backupset的备份历史与 LSN | 确认有没有链头、链是否完整 | first_lsn与上一条last_lsn是否衔接 |
| 4 | 在生产库跑sys.fn_dblog找LOP_DELETE_ROWS | 拿到精确到秒的误删时间 | 看[Begin Time]与AllocUnitName |
| 5 | 做尾日志备份BACKUP LOG ... WITH NORECOVERY | 把还没备份的记录固化下来 | 立即VERIFYONLY校验 |
走完这五步,你手上应该有三个确定的信息:库属于哪个场景、误删发生在哪一秒、尾日志备份文件在哪。这三个信息齐了,后面的路就只有一条,不会再犹豫。
验证恢复结果时,我一般会做「三查一比」:查行数是否与误删前一致、查最大主键与自增种子是否匹配、查外键约束是否可信(DBCC CHECKCONSTRAINTS),最后拿业务侧的一张汇总表跟恢复库里的汇总数比对。行数对不代表内容对,但汇总数对不上就一定有问题。这一步花十分钟,能避免切回生产后半夜被叫起来。
还有个习惯是从那以后养成的:任何要动生产库的DELETE或UPDATE,先BEGIN TRAN跑一遍看@@ROWCOUNT,确认影响范围再决定提交还是回滚;批量删除前一定先SELECT INTO备份一份受影响的行。这些动作花不了两分钟,但比事后找工具、找备份、算时间点省下的是几个小时和一身冷汗。希望帮到你。
本文还有配套的精品资源,点击获取