简介:一份面向数据库管理员与运维人员的 SQL Server 2008 误删除数据恢复实战文档,聚焦误删记录后如何依据备份与恢复模式快速判断可恢复性,并给出从完全备份、事务日志到第三方工具的多路径处置思路。压缩包仅含 1 个 doc 文件,约 359KB,篇幅紧凑,适合作为事故应急时的速查材料。已有 1826 人学习下载,说明该场景在真实运维中具有较高关注度。文档围绕完全恢复模式、误删前完全备份、事务日志备份与 STOPAT 时间点恢复等关键前提展开,同时梳理三种典型场景:前提齐全时走原生恢复流程,缺少完全备份时借助第三方对象,恢复模式不满足时则明确不可恢复。内容还记录了 Log Explorer、SQL Log Rescue、SysTools SQL Recovery、Recovery for SQL Server 等工具的兼容性对比与选型经验,可帮助读者少走弯路,建立从可行性判断、工具选择到恢复验证的完整思路。
1. 误删之后先别动数据库:一次没有备份的恢复现场
SQL Server 2008 数据库误删除数据的恢复,真正决定结局的往往不是你手上有几款工具,而是误删发生那一刻,数据库的恢复模式是不是 FULL、日志链有没有断。我见过一个很典型的现场:应用侧执行了一条没带 WHERE 的 DELETE,两张业务表被清空,库上没有任何备份,运维第一反应是重启实例想“看看能不能自己缓过来”,结果 checkpoint 一跑,日志里最值钱的那段被标记为可重用,救回来的成本直接翻了几倍。
这类问题适合两类人看:手上还跑着 SQL Server 2008 的老系统维护者,以及需要给“没有备份”的库做最后一搏的 DBA。后面按两条路走——日志链完整时,用三条 RESTORE 语句停到误删前一秒;日志还在但链断了时,绕过引擎直接解析 .mdf/.ldf 把记录捞出来。每条路都给出可抄的命令、参数含义和翻车点。
2. 恢复模式与日志链:为什么 FULL 是唯一的后悔药
先把原理立住,否则后面抄命令也是瞎抄。SQL Server 里一条 DELETE 并不是把数据从磁盘上抹掉,而是把“这行被删了”这件事写进事务日志,同时在数据页上做个标记。数据页上的行会不会被复用、日志里的记录会不会被截断,全看恢复模式怎么配。
2.1 三种恢复模式的代价与边界
SIMPLE 模式下,检查点一发生,日志里已经不活动的部分就被标记为可重用,日志文件循环覆盖。这种模式对性能友好、日志文件不会疯长,代价是灾难现场没有历史可回溯——你最多恢复到上一次完整备份的那个点,误删之后的那几个小时根本不存在。很多人图省事把测试库、内部系统都设成 SIMPLE,真出事的时候才发现连“后悔药”的原料都没有。
FULL 模式相反,所有事务完整写日志,并且在做过日志备份之前,日志的活跃部分不会被截断。也就是说,误删那条 DELETE 语句产生的日志记录,只要还没被日志备份“消费”掉、也没被强制截断,它就老老实实躺在 .ldf 文件里。这是时间点恢复的物理基础,没有之一。
BULK_LOGGED 是折中:普通 DML 仍然完整记录,但 BULK INSERT、SELECT INTO、索引重建这类大容量操作走最小日志。代价是这些操作进行期间,你没法把库恢复到操作中间某个时刻,只能恢复到操作开始前或结束后。做数据迁移的窗口期可以临时切过去压日志增长,迁完立刻切回 FULL,并且补一次完整备份——这一步漏掉,日志链就断了。
选型结论很简单:任何“删了要能找回来”的库,一律 FULL,并且配日志备份作业,间隔按你能承受的数据丢失量定,15 分钟到 1 小时都是常见值。恢复模式不是性能开关,是数据保险,别拿它换那点日志写入开销。
2.2 日志里到底记了什么:LSN 与日志链
事务日志是一串按 LSN(Log Sequence Number)递增的记录,每条记录带事务 ID、操作类型、被修改行的前后镜像信息。DELETE 记录里保存了被删行的完整内容,正是靠它,后续的日志重放才能把行“倒回去”。恢复过程本质上是:从完整备份的基点出发,按 LSN 顺序把日志记录一条条重放,重放到你指定的时间点就停下。
日志链就是“全备 → 日志1 → 日志2 → … → 尾日志”这条首尾相接的链条,每一环的 LSN 必须严格衔接。中间断一环,后面全部作废。常见的断链原因包括:中途把恢复模式改成 SIMPLE 又改回来、换过一次完整备份但跳过了日志备份、日志文件被手工 shrink、磁盘满导致日志备份失败而没人发现。
提示:断链不是“稍微丢一点数据”,而是“从断点之后完全无法重放”。判断方法是对比
msdb.dbo.backupset里相邻两条记录的first_lsn与last_lsn,能对上才叫链完整。
2.3 现场取证:先查恢复模式、日志备份历史和文件位置
误删之后第一件事不是还原,是把现场信息抄下来。这一步做错顺序,后面全是无用功。我一般先跑下面这组查询,把恢复模式、日志为什么没被截断、备份历史、文件路径一次性摸清楚。
-- 1) 恢复模式、日志等待原因、当前状态:判断这个库还有没有救 SELECT name, recovery_model_desc, -- SIMPLE / BULK_LOGGED / FULL log_reuse_wait_desc, -- 日志为什么还不能被重用,LOG_BACKUP 表示在等日志备份 state_desc, -- ONLINE / RESTORING / RECOVERY_PENDING log_reuse_wait FROM sys.databases WHERE name = N'ShopDB'; -- 2) 备份历史:看有没有全备、日志链是否连续 SELECT database_name, type, -- D=完整备份, L=日志备份, I=差异备份 backup_start_date, backup_finish_date, first_lsn, last_lsn, physical_device_name FROM msdb.dbo.backupset bs JOIN msdb.dbo.backupmediafamily mf ON bs.media_set_id = mf.media_set_id WHERE bs.database_name = N'ShopDB' ORDER BY bs.backup_finish_date DESC; -- 3) 数据文件与日志文件的实际落盘位置,还原时要靠它写 MOVE SELECT name, physical_name, type_desc, size * 8 / 1024 AS size_mb FROM sys.master_files WHERE database_id = DB_ID(N'ShopDB');第一段查询里,recovery_model_desc是 FULL 才有戏;log_reuse_wait_desc显示LOG_BACKUP说明日志正在等备份,这是好事,意味着历史还在;如果显示NOTHING且模式是 SIMPLE,那日志已经在循环覆盖了。第二段查询重点看type列里有没有D,以及相邻两条日志备份的first_lsn和last_lsn能不能接上。第三段查询的physical_name在还原时配合WITH MOVE使用,路径不一致时不写 MOVE 会直接报文件已存在或拒绝访问。
2.4 把恢复模式锁死在 FULL,并打一次底
确认完现场,如果库还是 SIMPLE,第一动作是切 FULL 并立刻补一次完整备份。注意顺序:先切模式,再做全备,然后做一次日志备份,这样日志链的起点才算真正建立。刚切完 FULL 到第一次日志备份之间,链是不完整的。
-- 切到 FULL 模式(对已有库是即时生效的元数据操作,不阻塞业务) ALTER DATABASE [ShopDB] SET RECOVERY FULL; -- 打底完整备份:INIT 覆盖同名备份集,CHECKSUM 校验页完整性 BACKUP DATABASE [ShopDB] TO DISK = N'D:\Backup\ShopDB_Full_Base.bak' WITH INIT, CHECKSUM, STATS = 10; -- 紧接着做一次日志备份,把链的起点固定下来 BACKUP LOG [ShopDB] TO DISK = N'D:\Backup\ShopDB_Log_Base.trn' WITH INIT, CHECKSUM;INIT表示覆盖目标文件里已有的备份集,不加则追加,追加多了还原时容易选错备份集。CHECKSUM会在备份时校验页校验和,遇到坏页直接报错而不是把坏页悄悄备进去,这个参数在老库上尤其值得加。STATS = 10只是每完成 10% 打一行进度,纯粹给人看的。
SQL Server 2008 的备份压缩只有企业版支持WITH COMPRESSION,标准版写了会直接报错,别照搬新版本的脚本。备份文件落在和数据文件不同的物理盘上,这是最基本的隔离,我见过太多备份和数据同盘、盘一坏两个一起没的现场。
日志备份作业要配起来,用维护计划或者 SQL Agent 作业都行,间隔按业务能接受的数据丢失窗口定。作业里加一步“超过 N 小时没有成功日志备份就发告警”,比事后懊悔有用得多。
3. 有全备 + FULL 模式:三条 T-SQL 走完时间点恢复
这是最理想的一种情况:误删之前有完整备份,恢复模式是 FULL,日志链没断。整个恢复不需要买任何工具,纯 T-SQL 就能停到误删前一秒。但“能恢复”和“恢复得干净”是两回事,顺序错一步就得从头再来。
3.1 三步恢复的完整脚本
核心思路是先把当前还在的日志(含误删那条 DELETE)备份出来,这叫尾日志备份;然后还原误删之前的完整备份;最后按顺序重放日志,停在误删之前的时间点。
-- 步骤 0:踢掉所有连接并切单用户,防止还原期间有人写库污染尾日志 ALTER DATABASE [ShopDB] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; -- 步骤 1:备份尾日志,NORECOVERY 让数据库进入“正在还原”状态 BACKUP LOG [ShopDB] TO DISK = N'D:\Recovery\ShopDB_Tail.trn' WITH NORECOVERY, INIT, CHECKSUM; -- 步骤 2:还原误删之前的完整备份,此时数据库仍处于还原中 RESTORE DATABASE [ShopDB] FROM DISK = N'D:\Backup\ShopDB_Full_20240115.bak' WITH NORECOVERY, REPLACE, MOVE N'ShopDB' TO N'D:\Data\ShopDB.mdf', MOVE N'ShopDB_log' TO N'D:\Data\ShopDB_log.ldf'; -- 步骤 3:按时间顺序重放中间的日志备份,全部用 NORECOVERY RESTORE LOG [ShopDB] FROM DISK = N'D:\Backup\ShopDB_Log_20240116_0800.trn' WITH NORECOVERY; RESTORE LOG [ShopDB] FROM DISK = N'D:\Backup\ShopDB_Log_20240116_1200.trn' WITH NORECOVERY; -- 步骤 4:重放尾日志,停在误删前一秒,最后用 RECOVERY 收尾 RESTORE LOG [ShopDB] FROM DISK = N'D:\Recovery\ShopDB_Tail.trn' WITH STOPAT = N'2024-01-16T14:32:06', RECOVERY;步骤 0 的SINGLE_USER WITH ROLLBACK IMMEDIATE会强制断开现有连接,生产环境执行前必须和应用侧打招呼,否则一堆连接报错。步骤 1 的NORECOVERY是关键,它让数据库保持在 RESTORING 状态而不是回滚上线,这样后续才能继续重放。步骤 2 的REPLACE在目标库已存在同名数据库时必需,MOVE的两组逻辑名要和RESTORE FILELISTONLY查出来的一致,写错了会报“逻辑文件未列出”。
步骤 3 里如果有多个日志备份,必须严格按backup_finish_date升序重放,跳过一个后面全废。步骤 4 的STOPAT是终点线,RECOVERY表示到此收工、回滚未提交事务、数据库上线。这两个参数出现在同一条语句里是标准写法,不是二选一。
还原前建议先确认备份集内容,避免拿错文件:
-- 看备份文件里有哪些备份集,重点核对 DatabaseName、BackupType、BackupFinishDate RESTORE HEADERONLY FROM DISK = N'D:\Backup\ShopDB_Full_20240115.bak'; -- 看备份里的逻辑文件名,MOVE 语句要和这里完全对应 RESTORE FILELISTONLY FROM DISK = N'D:\Backup\ShopDB_Full_20240115.bak';HEADERONLY返回的Position列在多备份集文件里用于指定WITH FILE = n,一个 .bak 里堆了十几次备份的情况在老系统上很常见,不指定就会还原到最近那次。
3.2 STOPAT 的时间点怎么取才不偏
STOPAT 是整个恢复里最玄学的一步。写早了丢数据,写晚了把误删也重放进去,前面全白干。
时间点的来源一般有三个:应用日志里那条 DELETE 请求的时间戳、业务表里的MAX(创建时间)、SQL Server 错误日志里的登录和语句记录。三者对不齐的时候,以应用侧时间戳为准,因为它最接近业务真实发生时刻。
注意 STOPAT 用的是数据库实例所在服务器的本地时间,不是 UTC,也不是你开发机的时区。跨时区部署的系统上我吃过亏:按 UTC 写进去,结果差了 8 小时,恢复出来的库比误删点还晚,数据一行没救回来。
精度上,SQL Server 的 datetime 精度是 3.33 毫秒,但 STOPAT 解析字符串时只认到毫秒,且实际停机点会落在“小于等于该时间的最后一个事务提交点”。所以实践里的做法是往前推一到两秒,宁可少恢复几条无关记录,也别把 DELETE 放进去。
还有一个技巧值得记住:尾日志可以重复重放。先用STOPAT = '较早时间点', NORECOVERY还原到一个中间状态,把库挂成只读(WITH STANDBY)核对数据;如果发现少了,再继续往后重放到更晚的时间点,全程不需要从头再来。这个“试跑”能力在数据量大、时间点不确定的场景里非常值钱。
3.3 参数逐个拆:NORECOVERY / RECOVERY / REPLACE / STANDBY / MOVE
NORECOVERY的意思是“还没完,别回滚”。数据库停在 RESTORING 状态,用户连不上,但可以继续接收后续的还原语句。整条链上除了最后一句,前面全部用它。
RECOVERY是收尾,回滚未提交事务,数据库转为 ONLINE。一旦用了它,这条链就封口了,后面再想追加重放必须从头开始。
STANDBY是中间态,数据库可读不可写,适合一边核对数据一边决定要不要继续往前推。它需要一个 undo 文件路径,WITH STANDBY = N'D:\Recovery\undo.dat',这个文件别随手删,后续还原还要用。
REPLACE只在目标库已存在时必要,它表示“我知道会覆盖,照做”。不带它而目标库存在,语句直接报错,这是引擎在保护你。
MOVE处理的是物理路径不一致的情况。还原到另一台机器、或者原路径盘已经挂了必须换盘,都靠它。逻辑名写错是最常见的翻车点,先用RESTORE FILELISTONLY查清楚再写。
3.4 还原后的校验:行数、LSN、一致性检查
库上线不等于恢复成功。我一般做三层校验。第一层是行数:对被删的两张表,对比误删前最后一次统计信息或监控采集里的行数,数量级对不上说明时间点选错了。第二层是关键业务字段,抽几条主键去对应用侧追问“这几条应该在不在”。
第三层是数据库一致性检查,这个不能省:
-- 物理与逻辑一致性检查,老库上跑一次可能要十几分钟,挑低峰期 DBCC CHECKDB (N'ShopDB') WITH NO_INFOMSGS, ALL_ERRORMSGS; -- 看当前库的 LSN 和最后一条日志记录,确认重放确实停在了预期位置 SELECT TOP 10 current_lsn, operation, context, [transaction id] FROM fn_dblog(NULL, NULL) ORDER BY [Current LSN] DESC;DBCC CHECKDB输出为空是好事,报错要区分是还原过程引入的问题还是原来就有的坏页。fn_dblog在老版本上可用,输出里的operation列能看到LOP_DELETE_ROWS之类的记录,用它反查误删发生在哪个 LSN 附近,可以反过来验证 STOPAT 选得准不准。
校验通过后别忘了把库切回多用户:ALTER DATABASE [ShopDB] SET MULTI_USER;,然后立刻做一次完整备份,把这次恢复的结果固化成一个新基点,后面的日志链从这里重新开始。
4. 只有 FULL 模式没有全备:从 .mdf/.ldf 直接捞记录
现实里更常见的是另一种情况:恢复模式确实是 FULL,日志文件几个 GB 躺在那儿,但从来没做过完整备份。这时候第 3 章那套三步走直接失效——没有基点,日志重放无从谈起。日志里的 DELETE 记录还在,可引擎没有“起点”可依。
4.1 为什么必须换工具:日志还在但链没有起点
时间点恢复要求“全备 + 日志”,本质是先有一个已知一致状态的快照,再把快照之后的操作重放上去。没有全备,就等于没有快照,日志再完整也没法重放。
但这不代表数据没了。日志文件里保存着被删行的前镜像,工具可以绕过 SQL Server 引擎,直接按页解析 .mdf 和 .ldf 的二进制结构,把 DELETE 记录里保存的行内容抽出来,生成 INSERT 语句。这条路不依赖备份,只依赖两个文件本身没被覆盖。
工具选型上踩过的坑不少,列个表更清楚:
| 工具 | 支持 SQL Server 2008 | 未备份库可恢复 | 主要限制 |
|---|---|---|---|
| Log Explorer for SQL Server | 不支持 | 不适用 | 版本停在 2005 之前 |
| SQL Log Rescue | 不支持 | 不适用 | 同上,老版本工具 |
| SysTools SQL Recovery | 支持 | 支持 | 演示版不含数据恢复功能,需授权 |
| Recovery for SQL Server | 支持 | 支持 | 演示版可导出数据,库文件不超过 24Gb |
选型逻辑很直接:先看版本支持,2008 的页结构和 2005 有差异,老工具读不了;再看演示版能不能真的导出数据,只能预览不能导出的等于没试;最后看文件大小限制是否够用。当时那个库不到 2Gb,落在演示版的能力范围内,直接用它把数据捞了出来。
这类工具都要求先停掉对数据库文件的写入。最稳妥的做法是把 .mdf 和 .ldf 复制一份到另一台机器上操作,原件保持只读,避免工具在解析过程中把文件锁死或者改坏。
4.2 操作步骤:Custom 模式与 Search for deleted records
整个流程的核心就两个开关:恢复配置选 Custom,选项里勾上搜索删除记录。这两个不选,工具只会做常规的“修复损坏库”,压根不去翻日志里的删除记录。
步骤按顺序走:
- 停掉应用对目标库的连接,把 .mdf 和 .ldf 复制到恢复机的独立目录,原件设为只读。
- 启动工具,
File > Recover,选择复制出来的 .mdf 文件,工具会自动关联同目录下的 .ldf。 - 连续 Next 进入 Recovery Configuration 界面,选择
Custom。这一步是整个流程的分水岭,默认的 Standard 模式不会去解析日志里的删除记录。 - 进入 Recovery options 窗口,勾选
Search for deleted records,并在Log file path里显式指定 .ldf 的完整路径。路径留空或者指错,工具会退化成一普通的表扫描。 - 指定
Destination folder,用来存放生成的 SQL 脚本和 bat 批处理文件。这个目录要有写权限,路径别带中文和空格。 - 点
Start,工具开始扫描页和日志,进度条走完后目标目录里会出现若干 .sql 和一个 .bat。 - 弹出
SQL Server Database Creation Utility窗口,Next 之后选择“恢复到哪个目标库”——这里务必选一个临时库名,别指向生产库同名库。 - 选择
Import available data both database and log files,让工具同时从数据文件和日志文件两个来源取数据,只选其中一个会漏记录。 - 连续 Next 执行导入,结束后核对行数。
第 8 步那个选项容易被忽略。只从 data file 导入,拿到的是“当前还活着的行”;只从 log file 导入,拿到的是“日志里记录过的变更”。两个都选,才能把误删之前的完整状态拼出来。代价是可能产生重复行,导入脚本里通常带主键冲突处理,导入后要按主键去重。
第 7 步选目标库时,如果工具默认给了生产库名,一定要手动改掉。演示版工具有时候不校验目标库是否存在,直接往同名库里灌数据,把现场彻底毁掉,这种事故我听说过不止一次。
4.3 生成的 SQL 与 bat 怎么读、怎么改
工具在目标目录里生成的东西不是黑匣子,打开看一眼能省很多事。典型的产物是一个建表脚本、一个数据插入脚本,加一个调用 sqlcmd 的批处理:
@echo off REM 工具生成的导入批处理,核心是用 sqlcmd 把 SQL 脚本灌进目标库 REM -S 指定实例,-d 指定目标库,-i 指定脚本,-b 出错即停 sqlcmd -S .\SQLEXPRESS -E -d ShopDB_Restore -i "C:\Recovery\out\data_insert.sql" -b -o "C:\Recovery\out\import.log" REM 导入完成后统计行数,和预期比对 sqlcmd -S .\SQLEXPRESS -E -d ShopDB_Restore -Q "SELECT COUNT(*) FROM dbo.Orders" -h -1-E是 Windows 身份验证,用 SQL 账号则改成-U 用户名 -P 密码。-b让 sqlcmd 在遇到第一个错误时返回非零退出码,批处理里可以据此中断,不带这个参数会一路错到底、日志里全是红字。-o把输出重定向到文件,几百 MB 的插入脚本在控制台刷屏根本看不清。
打开data_insert.sql先看三处:表名和列名是否和现网一致(有的工具会给表名加前缀)、IDENTITY列有没有被显式插入(带SET IDENTITY_INSERT ON才合法)、字符串里的单引号有没有被转义成两个单引号。这三处出错,脚本跑一半就崩。
如果脚本太大,别一次性执行,用sqlcmd的:setvar或者手工按行号切段,每段执行完提交一次。事务日志在导入期间会暴涨,临时把目标库设成 SIMPLE 模式能省不少空间,导完再切回 FULL 并补全备。
4.4 24Gb 限制与导入冲突的处理
演示版的 24Gb 上限指的是单个数据库文件的大小,不是总数据量。超了就只能在授权版上做,或者换个思路:把大库按文件组拆开,只把含目标表的那个 .ndf 拿出来单独解析,很多时候能压到限制以内。
导入冲突主要来自三类:主键重复、外键约束、触发器。处理顺序是先在目标库上禁用触发器和外键检查,导完再启用;主键重复靠NOT EXISTS过滤或者导入后ROW_NUMBER()去重。这些操作都在临时库上做,别碰生产库。
注意:从 .ldf 解析出来的记录是按 LSN 顺序排列的变更流,同一个主键可能对应“插入—更新—删除”多条记录。只取每个主键的最后一条有效状态,否则导入进去的是一堆中间态。
最后核对时不要只看总行数。按业务时间字段分组统计,比如按天统计订单数,和监控系统里的历史曲线对一下,曲线形状对得上,说明数据是连续的而不是只捞回了零散几条。
5. 避坑与排查:五条血泪记录
恢复这件事,做对的路径只有一条,做错的方式能有十几种。下面五条都是真实踩过的,按“现象 → 原因 → 解决”写,遇到类似症状可以直接对号入座。
5.1 误删后先收缩日志或重启实例
现象:尾日志备份报错,或者备份出来的文件只有几百 KB,恢复出来停在几个小时前。
原因:SIMPLE 模式下 checkpoint 会截断日志,FULL 模式下如果之前某次日志备份已经把这部分标记为可重用,重启或 shrink 会加速这个过程。日志一旦被循环覆盖,DELETE 记录就物理消失了。
解决:误删之后第一动作是停写、断连接、备份尾日志,顺序不能变。已经收缩过的,只能走第 4 章的 .ldf 解析,而且成功率取决于被覆盖的比例,越早动手越好。
5.2 恢复模式在 FULL 和 SIMPLE 之间来回切过
现象:全备和日志备份都在,但RESTORE LOG报“日志或文件与数据库不匹配”,或者提示 LSN 接不上。
原因:切到 SIMPLE 的那一刻日志链就断了,切回 FULL 需要重新做一次完整备份才能建立新链。中间那段日志即使文件还在,也不再属于当前这条链。
解决:查msdb.dbo.backupset里相邻记录的first_lsn和last_lsn,对不上的位置就是断点。断点之前的部分还能用,断点之后只能靠文件解析。
5.3 STOPAT 用了错误的时区或错误的时间源
现象:恢复顺利完成,但表里数据比误删前少一截,或者误删的记录又出现了。
原因:STOPAT 用实例本地时间;从应用日志里抄的可能是 UTC,从容器日志里抄的可能是宿主时间。差 8 小时的情况太常见了。
解决:在目标实例上执行SELECT GETDATE();取参照时间,把应用时间戳换算到同一时区再写 STOPAT。拿不准就先用WITH STANDBY试跑一个较早的时间点,核对后再往后推。
5.4 还原到不同路径时漏写 MOVE
现象:RESTORE DATABASE报“操作系统拒绝访问”或“文件已存在”,反复重试都一样。
原因:备份里记录的物理路径是原机器的路径,目标机器上那个盘符或目录不存在,引擎按原路径创建文件失败。同机还原时如果原文件还被占用,也会报同样的错。
解决:先RESTORE FILELISTONLY拿到逻辑文件名,再在RESTORE里用MOVE 逻辑名 TO 新路径,一个文件一条。日志文件也要写,只写数据文件同样报错。
5.5 用演示版工具直接恢复到生产库同名库
现象:工具跑完,生产库里的表结构变了或者数据被覆盖,现场彻底破坏。
原因:工具的“选择目标库”下拉默认带出的是原库名,下一步直接开始灌数据,没有任何二次确认。
解决:永远先恢复到临时库名,核对无误后用INSERT ... SELECT或者导入导出向导搬回生产库。恢复机上的 .mdf/.ldf 副本设只读,工具改不动原件。
5.6 误删后第一小时的检查清单
把上面这些压缩成一张表,出事时照着打勾,比翻文档快:
| 顺序 | 动作 | 目的 |
|---|---|---|
| 1 | 停止应用写入,记录误删时间点 | 防止日志继续增长和覆盖 |
| 2 | 查recovery_model_desc和log_reuse_wait_desc | 判断有没有救、走哪条路 |
| 3 | 查msdb.dbo.backupset的 LSN 链 | 确认日志链是否连续 |
| 4 | 复制 .mdf/.ldf 到独立目录并设只读 | 保住现场,供工具解析 |
| 5 | 按场景执行第 3 章或第 4 章流程 | 时间点恢复或文件解析 |
| 6 | DBCC CHECKDB+ 业务行数核对 | 确认数据完整而非只是“能打开” |
6. 把恢复变成习惯:季度演练与一个可复用的核对脚本
恢复能力不是出事那天才有的,是平时练出来的。我现在的习惯是每个季度挑一个非核心库做一次完整演练:从备份文件还原到一台临时实例,走完整个 STOPAT 流程,然后跑一遍核对脚本。演练的价值不在于“验证备份文件没坏”,而在于把命令、路径、参数顺序重新过一遍手,真出事的时候不至于边查文档边敲。
核对脚本我一般固定成下面这样,还原完直接跑,输出贴到工单里:
-- 还原后核对:库状态、文件路径、关键表行数、最近事务时间 SELECT name, state_desc, recovery_model_desc, log_reuse_wait_desc FROM sys.databases WHERE name = N'ShopDB_Restore'; SELECT name, physical_name FROM sys.master_files WHERE database_id = DB_ID(N'ShopDB_Restore'); -- 关键表行数与最后更新时间,和业务侧的历史监控曲线对比 SELECT 'Orders' AS table_name, COUNT(*) AS row_cnt, MAX(create_time) AS last_time FROM dbo.Orders UNION ALL SELECT 'OrderDetail', COUNT(*), MAX(create_time) FROM dbo.OrderDetail; -- 最近 20 条日志操作,确认没有异常的批量删除残留 SELECT TOP 20 [Current LSN], Operation, Context, [Transaction ID], [Begin Time] FROM fn_dblog(NULL, NULL) WHERE Operation IN ('LOP_DELETE_ROWS', 'LOP_INSERT_ROWS') ORDER BY [Current LSN] DESC;第一段确认库真的上线了且模式正确;第二段确认文件落在预期路径,避免还原到了错误的盘;第三段的行数和时间戳要和监控系统里的历史数据对得上,只看总数容易被“补了一些旧数据但丢了最近一小时”骗过去;第四段用fn_dblog反查最近的删除和插入操作,如果还能看到大批LOP_DELETE_ROWS,说明 STOPAT 选晚了,删除已经被重放进去,得退回去重来。
演练时还有两个细节值得固定下来。一是把还原脚本参数化,库名、备份路径、STOPAT 时间点都做成变量,出事时只改变量不改语句结构,减少手误。二是把尾日志备份放在流程最前面,而不是等确认完再备份——数据库还在线的每一秒,日志都可能被后续写入推着往前走。
从那以后我每次接手一个没有日志备份作业的库,第一件事不是优化查询也不是加索引,而是先把恢复模式改成 FULL、把日志备份作业配上、然后做一次完整备份打底。这个动作花不到十分钟,但它决定了下次有人手滑执行DELETE FROM的时候,你是能坐在工位上三条语句解决问题,还是得连夜找工具解析二进制文件。希望帮到你。
本文还有配套的精品资源,点击获取