两年前的 SQL Server 全备,竟然还能救回误删的数据?
👋 我是三笠,一名专注数据库运维与架构的 DBA,平时分享数据库实战经验与技术笔记,个人网站:ora100.com
前言
今天上班路上,突然收到业务消息,说 SQL Server 中一张表的历史数据被误删了,需要尽快恢复。
到了公司,立刻登录 SQL Server 服务器检查,本来想着找一份最近的全备,把数据恢复出来再导回生产库就行。结果翻了一圈备份目录,发现能找到的全备竟然还是两年前的,看来这次没那么简单了。
好在后面确认了可用的日志恢复条件,最终通过辅助数据库找回了历史数据,并成功回填到生产表。
数据虽然找回来了,但检查备份的过程中,又发现了不少问题。这次恢复算是完成了,后面的备份管理还得好好整理一下。
本文记录一下完整的恢复过程,分享给大家。
一张表的历史数据没了,生产库还在正常运行
接到消息后,先登录数据库确认情况。
数据库版本是 SQL Server 2016 SP2 Standard,实例运行正常,业务系统也能正常连接,没有出现数据库损坏或服务中断。
先查一下数据库状态和恢复模式:
SELECT
name,
state_desc,
recovery_model_desc
FROM sys.databases
WHERE name = N'BusinessDB';
再看看这张表还剩多少数据:
USE BusinessDB;
GO
SELECT COUNT(*) AS RowCount
FROM dbo.SalesDetail;
结果只有:
RowCount
--------
31133
业务反馈,这张销售明细表原本有不少历史数据,现在只剩下 31,133 行,明显少了很多。
由于暂时无法确定数据被删除的具体时间,还得先看看现有备份能恢复到什么程度。
考虑到生产数据库还在正常运行,不可能为了恢复一张表就把整个库回退。所以打算先恢复一套独立数据库,把历史数据找回来,核对没问题后再回填到生产表。
翻了半天备份,最近的全备竟然是两年前的
先查 SQL Server 的备份历史:
SELECT
bs.database_name,
CASE bs.type
WHEN 'D' THEN 'FULL'
WHEN 'I' THEN 'DIFF'
WHEN 'L' THEN 'LOG'
END AS BackupType,
bs.backup_start_date,
bs.backup_finish_date,
bs.is_copy_only,
bmf.physical_device_name
FROM msdb.dbo.backupset bs
JOIN msdb.dbo.backupmediafamily bmf
ON bs.media_set_id = bmf.media_set_id
WHERE bs.database_name = N'BusinessDB'
ORDER BY bs.backup_finish_date DESC;
检查了一下服务器上的备份目录,找到一份 2024 年 10 月 8 日的全备,而这次数据误删发生在 2026 年 11 月 10 日,已经过去两年多了。
这么老的备份,还能恢复出现在的数据吗?
SQL Server 在完整恢复模式下,可以通过全备和连续的事务日志备份,将数据库恢复到指定时间点。
不过,前提是日志链完整。 如果中间缺少必要的日志备份,导致日志无法衔接,即使有全备,也没办法恢复到事故发生前的状态。
所以先不急着还原,继续检查事务日志备份,看看能不能找到一条完整的恢复链。
先把当前日志备份下来
数据库还在运行,数据删除之后也已经过去了一段时间,为了尽可能保留可用于恢复的日志,先做了一份紧急日志备份:
BACKUP LOG [BusinessDB]
TO DISK = N'E:\SQLBackup\BusinessDB_emergency.trn'
WITH
COPY_ONLY,
COMPRESSION,
CHECKSUM,
STATS = 10;
GO
这里使用 COPY_ONLY,是为了尽量不影响原有日志备份安排。
不过要注意,COPY_ONLY 日志备份并不能补齐之前缺失的日志。如果中间已经存在无法衔接的日志区间,这份备份也解决不了。
备份完成后,检查备份头信息:
RESTORE HEADERONLY
FROM DISK = N'E:\SQLBackup\BusinessDB_emergency.trn';
GO
主要看 FirstLSN、LastLSN、DatabaseBackupLSN,以及恢复分支相关信息。
后面结合备份记录和实际恢复验证,确认了可用的恢复链。
这一步很重要。
遇到误删数据,尤其是还没确定删除时间的时候,最好先把当前能够保留的日志和备份文件保护好,别急着在生产库上操作。
不动生产库,先恢复一套辅助数据库
确定恢复方案后,准备了一套独立数据库:
BusinessDB_RECOVER
这样就不用直接碰生产库,可以先把历史数据恢复出来,再慢慢核对。
先查看全备中的逻辑文件名:
RESTORE FILELISTONLY
FROM DISK = N'E:\SQLBackup\BusinessDB_20241008.bak';
GO
确认文件名和存放路径后,开始恢复全备:
RESTORE DATABASE [BusinessDB_RECOVER]
FROM DISK = N'E:\SQLBackup\BusinessDB_20241008.bak'
WITH
MOVE N'BusinessDB_Data'
TO N'E:\SQLData\BusinessDB_RECOVER.mdf',
MOVE N'BusinessDB_Log'
TO N'E:\SQLData\BusinessDB_RECOVER_log.ldf',
NORECOVERY,
STATS = 10;
GO
这里使用 NORECOVERY,因为后面还要继续应用事务日志。
接着按照已经确认的日志链顺序恢复日志,最后一步再指定恢复时间点。
例如:
RESTORE LOG [BusinessDB_RECOVER]
FROM DISK = N'E:\SQLBackup\BusinessDB_emergency.trn'
WITH
STOPAT = '2026-11-10T08:00:00',
RECOVERY,
STATS = 10;
GO
这里的恢复时间只是示例,实际需要根据数据误删的时间来确定。
需要注意的是,两年前的全备并不能直接接上事故当天的日志备份。如果中间还有其他必需的日志备份,就要按照日志链顺序依次恢复,最后一步再使用 STOPAT 和 RECOVERY。
这次按照确认好的恢复顺序,成功将辅助数据库恢复到指定时间点,目标表的历史数据也找回来了。
历史数据找回来了,怎么回填生产表?
先对比一下两边的数据量:
SELECT 'Production' AS DBName, COUNT(*) AS RowCount
FROM BusinessDB.dbo.SalesDetail
UNION ALL
SELECT 'Recovered', COUNT(*)
FROM BusinessDB_RECOVER.dbo.SalesDetail;
结果如下:
| 数据来源 | 数据量 |
|---|---|
| 生产表 | 31,133 行 |
| 恢复表 | 276,304 行 |
| 相差 | 245,171 行 |
恢复库中有 276,304 行数据,而生产表只剩下 31,133 行,历史数据总算找回来了。
不过,生产表里还有现存数据,不能直接覆盖,得先确认这些数据是否需要保留。
回填前检查了一下表结构,这张表共有 16 个字段,没有主键、索引、外键和触发器,但有 27 个依赖存储过程。由于没有主键,也没办法直接通过主键判断哪些数据需要补回。
最后经过业务核对,确认可以用恢复库的数据整表替换生产表,并在操作期间暂停相关业务写入,避免回填过程中产生新的数据。
回填前,先备份生产表
确定回填方案后,先把生产表中现有的 31,133 行数据备份一份,避免后续操作出现问题时无法回退。
USE BusinessDB;
GO
SELECT *
INTO dbo.SalesDetail_BAK_20261110
FROM dbo.SalesDetail;
GO
这里直接使用 SELECT INTO 保存原表数据,操作比较简单。不过这种方式不会复制原表的索引、约束等对象,不能代替正式的数据库备份。
确认备份完成后,开始回填:
USE BusinessDB;
GO
SET XACT_ABORT ON;
GO
BEGIN TRY
BEGIN TRAN;
DELETE FROM dbo.SalesDetail;
INSERT INTO dbo.SalesDetail
SELECT *
FROM BusinessDB_RECOVER.dbo.SalesDetail;
IF (SELECT COUNT_BIG(*) FROM dbo.SalesDetail) <> 276304
THROW 50001, 'Row count validation failed', 1;
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN;
THROW;
END CATCH;
GO
这里加了事务控制,如果中途报错,就回滚本次操作,避免生产表只恢复了一部分数据。
这里的回填 SQL 只是简化示例,实际操作还需要根据表结构和业务情况调整。如果误删之后产生了需要保留的新数据,就不能直接整表替换。
276,304 行数据,恢复成功
回填完成后,再次检查生产表的数据量:
SELECT COUNT(*) AS RowCount
FROM BusinessDB.dbo.SalesDetail;
查询结果:
RowCount
--------
276304
生产表从原来的 31,133 行恢复到了 276,304 行。又抽查了部分关键业务记录,确认数据没有问题,业务验证也顺利通过。
这次恢复使用的辅助数据库占用了大约 12GB 磁盘空间,确认数据恢复完成、不再需要保留后,便将其删除。
USE master;
GO
ALTER DATABASE [BusinessDB_RECOVER]
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
GO
DROP DATABASE [BusinessDB_RECOVER];
GO
删除前再次确认数据库名称,避免误删生产库。
到这里,这次数据恢复就算完成了。
数据找回来了,但备份的问题还没解决
这次恢复折腾下来,最麻烦的其实不是执行恢复命令,而是确认手里的备份到底能不能用,以及怎么把数据安全地放回生产库。
毕竟能找到的全备已经是两年前的了,生产数据库又不能随便动。
好在最后通过辅助库找回了历史数据,生产表也恢复到了 276,304 行。
不过,恢复结束后又检查了一遍数据库备份情况,发现问题还不少。
SQL Server Agent 的运行状态、事务日志备份、历史备份清理都存在需要调整的地方。尤其是常规日志备份长期没有正常执行,日志文件空间占用也已经比较高。
这次数据虽然找回来了,但不能每次出问题都指望还能找到可用的备份。
所以后面又花了些时间,把 SQL Server 的 FULL、LOG 自动备份和历史文件清理策略重新整理了一遍。
写文章之外,我也在持续整理自己的数据库学习与运维平台 ORA100。
一些平时用到的数据库 SQL、运维脚本和排障笔记,都会慢慢整理到网站上,方便以后遇到类似问题时直接查找。
网站地址:ora100.com

- 点赞
- 收藏
- 关注作者
评论(0)