简介:ApexSql Log是一款面向SQL Server 2005/2008/2012的数据库日志恢复工具,专门解决因误删、误改或事务日志异常导致的数据丢失问题,适合数据库管理员、开发人员以及运维人员作为应急修复手段。该绿色破解版压缩包约11.13MB,包含54个文件,以dll动态库和exe可执行程序为主,同时辅以xsl样式表、css样式与config配置文件,相关运行依赖齐全,解压即可使用。包内集成了x86与x64两套核心组件,可适应不同SQL Server实例的架构环境,通过解析在线或离线事务日志还原表数据,并支持生成对应的撤销/重做脚本,方便在恢复前核对变更内容。目前已有2373人学习,能够覆盖误更新、截断表、删除记录等常见故障场景,通过界面化操作即可定位损坏操作并选择恢复粒度,帮助读者快速获得一套免安装、可移植的数据库恢复环境。
1. 误删数据别急着上备份:事务日志里可能还留着一份“后悔药”
一条没带 WHERE 的 DELETE 跑完,十几万行订单数据说没就没,全库重做备份要大半天,业务根本等不起。我遇到这种场景的第一反应从来不是翻备份,而是先看 SQL Server 的事务日志:日志里记录着每行被删之前的旧值,只要日志链还在,就有机会把删掉的数据“倒放”回去。ApexSQL Log 就是专门解析这种日志的工具,把晦涩的 LSN 记录还原成可读的 DELETE/UPDATE/INSERT 列表,再生成能把数据写回去的 undo 脚本。适合被误删支配过的 DBA、后端开发和运维。顺带提醒一句:网上标着“破解版”的包多半被改过,加载引擎缺组件、附加库闪退都是小事,怕的是后门,官方试用版足够把整条流程跑熟。
2. 事务日志恢复原理:LSN、RowLog 与备份链
2.1 被删的行没有消失:DELETE 记录里的旧值
SQL Server 的每个写操作都会先写事务日志,目的是保证事务可以回滚。DELETE 不是记一条“某表删了若干行”就完事,而是把被删行的实际内容写进日志记录,事务回滚时靠这些旧值把页面还原。也就是说,“删除”这个动作在日志里留下的是一份可以反推原数据的历史快照。
ApexSQL Log 的价值在于把这份二进制结构体解码成表格:哪张表、哪个 LSN、哪个事务、哪一行、旧值是什么。想验证这一点,可以直接用未文档化的函数 fn_dblog 查看在线日志中的删除痕迹:
USE LogRecoveryLab; GO SELECT TOP 100 [Current LSN], Operation, [Transaction ID], [Begin Time], AllocUnitName FROM sys.fn_dblog(NULL, NULL) WHERE Operation = 'LOP_DELETE_ROWS' AND AllocUnitName LIKE '%Orders%' ORDER BY [Current LSN]; GO这段查询的作用是列出 Orders 表相关的删除行日志记录。逻辑上,LOP_DELETE_ROWS 是删除行的日志操作码;AllocUnitName 显示被删除数据所属的表;[Current LSN] 是日志序列号,用于标示记录在日志中的精确位置;每个事务有独立的 [Transaction ID],后续反查事务开始时间就是拿它去匹配 LOP_BEGIN_XACT 记录。
这里的细节值得注意:事务开始记录 LOP_BEGIN_XACT 本身不带表名,所以直接在 fn_dblog 结果里按表名过滤是看不到事务头的。我一般先查 LOP_DELETE_ROWS 拿到事务 ID,再用事务 ID 反查同一事务里的所有操作,才能判断这次删除是不是和别的更新语句混在同一个事务里。
fn_dblog 只能读在线日志,而且 SQL Server 官方不支持用它做生产级恢复,性能也会随日志体积明显劣化。它的定位是快速确认“有没有删除痕迹”“大概涉及哪些表”。真正的行级数据还原,还是交给专业工具比较稳妥。
2.2 恢复模式决定你有没有“后悔药”
日志里虽然有旧值,但日志文件不是无限保留的。关键变量是数据库的恢复模式。
简单恢复模式下,每次检查点之后,非活动日志就会被截断复用。日志文件里只保留当前未提交事务的内容,历史事务的删除记录很快会被覆盖。这种模式下,ApexSQL Log 最多只能看到极短时间窗口的内容,做不了时间点恢复。完整恢复模式则会把日志持续保留到主动做日志备份为止,备份文件形成一条可回溯的日志链,这才有“找回去”的基础。
切换恢复模式也要注意一个细节:从简单切换成完整之后,必须做一次完整备份作为日志备份的基线。否则后续执行 BACKUP LOG 会直接报错,提示没有可以备份的基线。很多人在这一步漏掉,导致日志链根本没建立起来。
我遇到需要日志级恢复的场景,第一步永远是先确认恢复模式和上次日志备份时间:
SELECT name, recovery_model_desc FROM sys.databases WHERE name = 'LogRecoveryLab'; GO SELECT database_name, type, backup_finish_date FROM msdb.dbo.backupset WHERE database_name = 'LogRecoveryLab' ORDER BY backup_finish_date DESC;第一条查询确认数据库处于 FULL 恢复模式,第二条看最近一次完整备份和日志备份的时间。如果 recovery_model_desc 显示 SIMPLE,那基本可以判定日志里没有可用的历史记录,只能走备份恢复路线了。
2.3 ApexSQL Log 的三种日志来源
ApexSQL Log 读取日志的来源有三条路径,我按实用频率排个序:在线数据库(直接附加当前实例的 LDF 文件)、日志备份文件(.bak/.trn)、分离出来的 LDF 文件。在线库读取最省事,打开工具选择数据库实例和库名就能开始分析。但它的缺点是大库在线日志可能几个 GB,读取时要锁住日志分析位置,生产高峰期会有额外 I/O 开销。日志备份文件是最稳的读取方式,因为备份文件就是只读的历史快照,不干扰生产库。
选择日志备份来源时,关键是备份文件的顺序和连续性。SQL Server 的日志备份有严格的 LSN 链校验,相邻两个备份文件之间的 LSN 必须无缝衔接,中间缺任何一个备份都会导致附加失败。常见做法是,把从上次完整备份之后到误删时间点之间的所有日志备份按生成顺序一次性加入工具,工具会在解析时自动校验 LSN 连续性并标示缺口位置。
3. 实操流程:用 ApexSQL Log 做一次误删恢复
3.1 环境准备:搭一个可以反复练手的测试场景
纸上谈兵没用,恢复流程一定要有一张可以反复破坏的测试表。我先建一个实验库,打开完整恢复模式,造一张 Orders 表塞进一万行数据,再模拟误删前三千行:
IF DB_ID('LogRecoveryLab') IS NULL CREATE DATABASE LogRecoveryLab; GO ALTER DATABASE LogRecoveryLab SET RECOVERY FULL; GO BACKUP DATABASE LogRecoveryLab TO DISK = 'D:\backup\lab_full.bak' WITH INIT; GO USE LogRecoveryLab; GO CREATE TABLE dbo.Orders ( OrderID INT PRIMARY KEY, CustomerID INT NOT NULL, TotalAmount DECIMAL(10,2) NOT NULL, OrderDate DATETIME NOT NULL ); GO INSERT INTO dbo.Orders (OrderID, CustomerID, TotalAmount, OrderDate) SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), ABS(CHECKSUM(NEWID())) % 1000 + 1, CAST(ABS(CHECKSUM(NEWID())) % 100000 AS DECIMAL(10,2)) / 100.0, DATEADD(MINUTE, -ABS(CHECKSUM(NEWID())) % 100000, GETDATE()) FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b; GO DELETE FROM dbo.Orders WHERE OrderID <= 3000; GO BACKUP LOG LogRecoveryLab TO DISK = 'D:\backup\lab_log1.trn' WITH INIT; GO这段脚本的逻辑是:先建库并切到完整恢复模式,用 WITH INIT 做一次性完整备份作为基线,然后建表插数。删除之后马上做一次日志备份,把包含 DELETE 操作的日志段保存成外部文件,供工具读取。注意插数部分用了 NEWID() 生成随机排序,所以具体哪些行会被删掉无所谓,重要的是删除前后的行数差是三千。
参数说明:ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 用来生成连续的 OrderID;ABS(CHECKSUM(NEWID())) % 1000 生成 0 到 999 的随机客户编号;日志备份的 WITH INIT 表示覆盖同名文件,避免重复实验时附加到旧备份上。
3.2 打开工具并附加日志来源
实验环境就绪后,启动 ApexSQL Log,走一遍附加流程。以读日志备份为例:左侧选择 Add Database,来源类型选 Backup file;然后逐个添加日志备份文件,工具会读取备份头的信息,显示该备份覆盖的 LSN 区间和数据库名。
添加完成后,界面会列出可分析的日志段。此处有个界面选项值得留意:是否同时附加原始数据库文件。工具需要数据库的表结构来解析日志中的页 ID 和对象 ID,如果只给日志备份不给库文件,很多操作记录无法映射到具体表名。所以附加日志备份时,把实验库的 MDF 文件路径也一并指过去,解析出来的 AllocUnitName 才不会是数字 ID。
如果是直接分析在线库,选择数据库实例和库名即可,不用关心文件路径。但我的习惯是优先备份日志再分析,原因很简单:在线日志一直有写入,解析过程中新日志还在追加,结果集不稳定,排错困难。日志备份是静态快照,这次分析完,下次再分析同一个备份,结果一致。
3.3 三组核心参数:时间范围、操作类型与对象过滤
附加完成后进入解析结果页,先别急着生成脚本,把过滤条件收紧。我的经验是三个维度必调:
| 参数 | 推荐设置 | 理由 |
|---|---|---|
| Start Date / End Date | 误删前 10 分钟 到 误删后 5 分钟 | 覆盖整个事务的生命周期,避免漏掉跨时间窗的长事务 |
| Operation Filter | DELETE,若涉及清表再加 TRUNCATE | 排除 INSERT/UPDATE 噪音记录,结果集更干净 |
| Object Filter | dbo.Orders | 只保留目标表的记录,大幅降低后续脚本体积 |
时间范围是最容易踩坑的选项。很多人按“删除发生的那一刻”来设时间窗,结果生成的脚本少了一部分数据。原因是事务的执行时间和提交时间可能不同:一个事务 10:00 开始执行 DELETE,10:05 才提交,如果时间窗只截止到 10:01,事务的删除记录已经在日志里,但提交记录在窗口外,工具按事务完整性过滤时会把整个事务标记为不完整,恢复出的数据就会少。所以时间窗一定要覆盖从“最早可能开始时间”到“最晚提交时间”的完整区间,宁可宽一点再配合对象过滤,也不要窄到漏事务。
操作类型过滤的设置界面一般是复选框列表,DELETE、INSERT、UPDATE、TRUNCATE 各自独立。误删恢复只勾 DELETE,如果误操作还涉及 UPDATE 把某些值改掉了,需要一并勾选 UPDATE,否则工具不会解析改动的旧值。Object Filter 支持多选,可以针对一批业务表做批量恢复。
3.4 生成 undo 脚本并在测试库验证
解析结果列表里能看到每条操作的具体时间、登录用户、对象名和影响行数。选中目标 DELETE 记录后,最核心的动作是右键选择 Create Undo Script。生成器会让你选输出方式:保存到 SQL 文件、复制到剪贴板、直接打开到查询编辑器。我通常保存成文件,文件名带上库名和误删时间,方便留档。
生成脚本的选项中有一个经常会遇到的设置:是否把脚本包进事务。我建议选“包含 BEGIN TRANSACTION/COMMIT”,保证整批恢复要么全部成功,要么全部回滚,不会出现恢复一半的脏状态。如果表没有主键,工具还提供“用所有列的旧值构造 WHERE 条件”的选项,这个必须开启,否则定位不了要恢复的行的位置。
脚本生成之后先别在生产环境执行,先做基线对比,确认缺失行数。这一步用 SQL 就能完成:
USE LogRecoveryLab; GO SELECT COUNT(*) AS RowsBefore FROM dbo.Orders; GO -- 此处执行 ApexSQL Log 生成的 Undo 脚本 GO SELECT COUNT(*) AS RowsAfter FROM dbo.Orders; GO SELECT OrderID, COUNT(*) AS DuplicateCount FROM dbo.Orders GROUP BY OrderID HAVING COUNT(*) > 1;逻辑很简单:RowsBefore 是恢复前当前表里的行数,RowsAfter 是执行 undo 脚本后的行数,两者差值应该恰好等于被误删的行数。最后的分组查询确认主键没有因重复插入出现冲突。如果 RowsAfter 比预期少,先查是不是有主键冲突导致整批回滚;如果 DuplicateCount 大于零,说明目标表里本来就有一部分被删数据被其他事务写了回来,需要和外键、业务状态一起判断怎么处理。
4. ApexSQL Log 恢复实战中的常见坑与排查
4.1 现象:日志读取卡住,几个小时不出结果
第一次用工具读一个大库的在线日志,解析进度条走到一半就不动了,界面显示正在读取日志,但 CPU 占用不高,也没报错。我等了一个多小时,最后强行取消,重新附加才恢复正常。
原因:在线日志文件太大,工具要扫描全部活动日志才能按过滤条件筛数据,而生产库日志文件动辄几十 GB,全量扫描自然慢。更隐蔽的一个因素是,日志文件所在的磁盘 I/O 被业务写事务占满,读取进程一直在等磁盘响应。
解决:不要直接读在线日志,先把误删时间点前后的日志备份出来。就我所知,日志备份比在线读快得多,因为备份文件是顺序读,在线日志是随机读。备份命令只截取误删时间之后的日志段,体积通常只有几百 MB,解析速度能快一个数量级。另外,读取大日志前先把过滤条件里的时间窗缩到最小,工具会利用时间维度做预裁剪,不会傻傻扫全量。
4.2 现象:TRUNCATE 之后一个行都找不到
有次测试清空表,误用了 TRUNCATE TABLE,马上用工具去解析日志,结果列表里确实显示 TRUNCATE 操作,但点进去没有任何行级数据,生成 undo 脚本的按钮是灰的。
原因:TRUNCATE 的日志记录机制和 DELETE 完全不同。DELETE 逐行删除,每一行的内容都写进日志;TRUNCATE 只是释放数据页,日志里记录的是页的分配位图变化,不保存任何行内容。工具能识别 TRUNCATE 操作本身,但日志里根本没有旧值可以还原。
解决:TRUNCATE 恢复只能靠备份。如果误删前有一个完整备份或差异备份,且之后的日志链完整,可以走“备份恢复 + 日志前滚”的路线,或者只把被释放的数据页恢复出来。还有一条辅助思路:ApexSQL Log 虽不能直接恢复 TRUNCATE 的行,但能给出 TRUNCATE 发生的精确时间,这个时间点可以作为选择备份文件的定位依据,帮我把恢复目标锁定到误删前的最新备份。
4.3 现象:附加日志备份时报 LSN 链断裂
恢复一个连续误删场景,先附加了第一个日志备份,解析正常,接着附加第二个日志备份时,工具弹窗提示备份链缺失或 LSN 不连续,拒绝加载。
原因:日志备份文件不是一个一个孤立的文件,它们之间有严格的序列关系。中间某个时间段的日志备份被删掉了,或者某个备份是用 NO_TRUNCATE 选项做的,都会导致链断裂。还有人为了省磁盘空间,把老日志备份清理掉一部分,结果恢复时恰好缺了关键一环。
解决:先把所有日志备份按备份完成时间列出来,逐个确认缺失区间。如果中间确实缺了某个备份,看看磁盘上有没有对应的 .bak 残留,或者是否有人手动做过 COPY_ONLY 备份可以当作补位。最现实的兜底方案是:退回到上一个完整备份的时间点,接受部分数据丢失。这个坑的经验是,日志备份保留策略至少要覆盖业务方预期的“可恢复时间窗口”,而且每份备份的 LSN 范围要记录在案,别等出事时才逐个去试。
4.4 现象:undo 脚本执行后没恢复出任何行
在测试库执行工具生成的 undo 脚本,执行成功,没有报错,但查行数发现和恢复前一样,一行都没多。
原因:最常见的两种,一是生成脚本时在输出类型里选错了,生成了 Redo 脚本而不是 Undo 脚本。Redo 是把日志里的操作重放一遍,对 DELETE 来说等于再删一次,当然不会增加行。二是生成选项里勾选了“不包含无主键表的恢复语句”,工具对有主键的表正常生成 INSERT,对无主键的表直接跳过,但输出日志里只写了一条警告,很容易忽略。
解决:生成前确认脚本页面顶部标注的是 Undo 还是 Redo,我一般会先展开脚本开头看第一条语句,是 INSERT 就对了,是 DELETE 就回去切换模式。同时检查生成日志里的警告信息,看到“skipped”“no key”之类的关键词,返回到表结构确认是否缺少主键或唯一索引。无主键表的恢复方案是开启“使用所有列匹配”选项,让工具用全部列旧值拼 WHERE 条件来定位行,代价是脚本会明显变长,执行时间也更久。
4.5 现象:恢复的行数和误删行数对不上
按时间窗和对象过滤恢复了 DELETE,行数确实增加了,但比误删时少了八百多行,反复确认过滤条件没问题,就是缺数据。
原因:误删的 DELETE 语句可能不是一个独立事务。如果是一个存储过程里先 UPDATE 再 DELETE,或者应用层分批次循环删除,多批删除分散在不同事务里,时间跨度超过了我设的时间窗。工具按事务完整性过滤,时间窗边界上的事务会被截断,导致部分行没被纳入。
解决:把时间窗从“误删时间点”扩展成“交易开始前十分钟到交易结束后的五分钟”,并且按事务 ID 重新检查。ApexSQL Log 的解析结果里,把记录按 [Transaction ID] 分组,查看同一事务里还有没有关联的 UPDATE 或 INSERT,如果有,把这些操作一起勾选生成脚本,避免只恢复 DELETE 造成业务数据不一致。还有一种更稳妥的办法:直接把整个时间段内的所有操作全部导出,再人工筛选需要恢复的行。
5. 复杂场景:多条 DELETE、混合事务与大表回滚
5.1 同一事务里的多条语句:别只挑 DELETE 行
工具默认按操作类型过滤后,结果列表里可能只有 DELETE 记录。如果只勾选这些 DELETE 行生成脚本,遇到存储过程里“先 UPDATE 状态,再 DELETE 明细”的场景,恢复出来的数据会处于中间状态,状态字段没还原,外键关联也可能对不上。
我一般会以事务为单位选择:在解析结果里右键任意一条记录,选择查看同一事务的所有操作,把事务涉及的 INSERT、UPDATE、DELETE 一并选中,生成整组 undo 脚本。工具生成脚本时会按 LSN 的反向顺序排列,先撤销后发生的操作,再撤销先发生的操作,保证数据回到事务开始前的样子。处理混合事务时,脚本的长度会明显增加,但在测试库验证一次就能看出值得。
参数层面的一个注意点:事务过滤器的粒度会影响脚本顺序。如果工具支持按事务 ID 过滤,直接按 ID 锁定事务;如果只支持时间过滤,那就要确保时间窗把整个事务包住。另一点是生成脚本时把“包含事务控制”打开,让每组恢复操作有明确的提交边界,避免长事务把所有操作揉在一起,中途出错时难以定位。
5.2 按登录用户过滤:把日志量缩到可控范围
多人共用的业务库,一个小时内可能有几万条日志记录,其中大部分是正常业务写入。即使按表过滤了,同表的高频 UPDATE 也会混进来,人工核对成本很高。
ApexSQL Log 的过滤条件里,按登录名或主机名过滤是很好用的一个维度。误删操作通常是某个应用账号或某个开发账号跑出来的,把账号过滤到具体登录名,日志量直接少一个量级。比如某条 DELETE 是应用账号 WebAppUser 执行的,就把过滤条件设为该账号,工具只解析该账号产生的日志记录,其他账号的写入完全忽略。
这个过滤条件同时适用于读取阶段和生成脚本阶段。读取阶段过滤能加快解析速度,生成脚本阶段过滤能避免把正常业务的 UPDATE 误恢复掉。注意应用账号可能同时跑着正常的写事务,如果该账号在误删时间段内还有别的合法操作,那就要在结果列表里结合对象名和开始时间做二次筛选。
5.3 大批量恢复:防日志暴涨与执行超时
undo 脚本默认是一个大事务,几万行恢复还好,百万级的大表恢复直接把整个脚本丢进查询窗口,事务日志会在几秒内膨胀几个 GB,还可能触发长事务上的锁阻塞。
我的做法是把脚本拆成批次执行,每批一个事务,批间做短暂间隔,给日志备份留出截断窗口。给一个工程化的分批模板:
DECLARE @BatchSize INT = 5000; DECLARE @Rows INT = 1; WHILE @Rows > 0 BEGIN BEGIN TRANSACTION; -- 这里放 ApexSQL Log 生成的同一批 INSERT 语句 -- INSERT INTO dbo.Orders (OrderID, CustomerID, TotalAmount, OrderDate) -- VALUES (...); SET @Rows = @@ROWCOUNT; COMMIT TRANSACTION; -- 每批提交后做一次日志备份,控制 LDF 增长 BACKUP LOG LogRecoveryLab TO DISK = 'D:\backup\lab_log_batch.trn' WITH INIT; WAITFOR DELAY '00:00:01'; END逻辑说明:@BatchSize 是预留给批处理参数的,实际批次大小取决于工具生成脚本时的分段方式,可以把脚本按 5000 行一条 INSERT 手工拆段。每批执行后读取 @@ROWCOUNT 判断是否还有未恢复的数据。循环里做日志备份是为了让事务日志空间能被回收,不然几百 MB 的 LDF 很快会被撑满。WAITFOR DELAY 是为了避免 CPU 和磁盘 I/O 长时间满载,给业务其他请求留一点余量。
这里有个取舍:分批恢复意味着如果第五批失败了,前四批的数据已经提交,需要记录断点批次,修复后从第五批重新跑,不能简单地整体重来。所以我通常会在目标表上临时加一个标记列 IsRestored,每批恢复后打上标记,重跑时只恢复未标记的行。
6. 恢复后验证:把“日志恢复到测试库”变成固定动作
6.1 恢复前后基线对比
执行 undo 脚本前,先记录目标表的基线数据;执行后立即做一组对比,确认行数、主键、外键状态。
| 验证项 | 对比方式 | 通过标准 |
|---|---|---|
| 行数 | 恢复前后 COUNT(*) | 行数差等于被误删行数 |
| 主键冲突 | GROUP BY 主键 HAVING COUNT(*)>1 | 无重复 |
| 外键完整性 | 检查子表关联数据的 BusinessID | 子表引用都能匹配 |
| 抽样数据 | 取恢复行中 100 行查看业务字段 | 与删除前日志记录中的旧值一致 |
这一步不用写复杂 SQL,重点是恢复前后各跑一遍相同脚本,结果做差值。如果工具生成的 undo 脚本里每条 INSERT 都带了 LSN 注释,抽样时可以根据 LSN 定位到具体恢复批次,方便核对。
6.2 先测试库、再生产的闸门
无论生产环境多紧急,我的流程惯性是:当前库先做一次完整备份,再让我把 undo 脚本扔到测试库执行一遍。完整备份是负担最小的“后悔药的后悔药”,万一生产执行时出现意外逻辑错误,还能再回到执行前的状态。
测试库验证通过,真正上生产执行时,我会把整个恢复过程包在一个显式事务里,脚本执行完先不要提交,做一次行数自检,确认无误再 COMMIT;自检不过直接 ROLLBACK。用事务包住的成本很低,但能把“恢复错了”和“恢复成功”划出明确界限,不需要事后找数据。
6.3 一个习惯
早些年有一次,我嫌测试库多跑一遍浪费时间,直接把 undo 脚本怼到生产库,结果碰上无主键表开启“全列匹配”后生成的 WHERE 条件里有 NULL 值,几千行没匹配上,脚本执行了但数据没回来。从那以后,我每次做日志恢复都强制走这三步:先备份当前状态、再测试库执行一遍、最后生产库用事务包裹自检再提交。这个流程多花不到十分钟,但省掉的返工时间没法算。希望帮到你。
本文还有配套的精品资源,点击获取