周五下午 4 点 32 分,手机响了。开发组组长打来的,声音发抖:"我把 orders 表删了。"那一瞬间,我感觉心脏停跳了一拍。orders 表,核心业务表,500 多万条订单数据,每天的流水全靠它。但 30 分钟后,数据完好无损地回来了。这篇文章,把从接到电话到恢复完成的每一秒、每一条 SQL 都写清楚。希望你永远用不上,但一定要会。

目录
- 事故发生:那一通电话
- 冷静分析:先搞清楚删了什么
- 恢复方案选择:四条路走哪条
- [实战恢复:30 分钟全记录](#实战恢复:30 分钟全记录)
- 恢复后的验证
- 复盘:这次事故暴露了什么问题
- 预防:怎么保证下次不再翻车
- [SQL Server 恢复技术速查表](#SQL Server 恢复技术速查表)
第一章:事故发生
1.1 时间线
16:32 接到电话:"我把 orders 表删了"
16:33 确认情况:DROP TABLE 还是 DELETE?
16:35 连接数据库,确认表确实不存在了
16:37 开始评估恢复方案
16:40 确定方案:日志时间点恢复
16:42 开始执行恢复
16:58 恢复完成,开始验证
17:02 验证通过,数据完整
17:05 通知开发组,事故结束
总耗时:33 分钟。其中 20 分钟在等备份文件传输。
1.2 事故现场
开发组组长在测试环境执行一段 SQL,本想清理测试数据。但他忘了切连接,直接在生产库上执行了:
sql
-- 他本意是清理测试数据
-- 但连接的是生产库!
-- 第一句:删除了历史归档数据(影响不大)
DELETE FROM order_archive WHERE create_date < '2024-01-01';
-- 第二句:他以为 orders 是临时表
-- 实际上 orders 是核心业务表
DROP TABLE orders; -- 💀
1.3 当时的数据库状态
sql
-- 确认表不存在
SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'orders';
-- 结果:0 行。表确实没了。
-- 确认数据库还在
SELECT name, state_desc, recovery_model_desc
FROM sys.databases WHERE name = 'ShopDB';
-- 结果:
-- name | state_desc | recovery_model_desc
-- ShopDB | ONLINE | FULL ← 谢天谢地,是完整恢复模式!
⚠️ 关键信息 :数据库是 FULL(完整)恢复模式。这意味着我们有完整的事务日志备份链,可以做时间点恢复(Point-in-Time Recovery)。如果是 SIMPLE(简单)恢复模式,那就只能恢复到上一次完整备份的时间点,中间的数据全丢。
那一刻我心里默念了三遍:感谢前任 DBA 没有把恢复模式改成 SIMPLE。
第二章:冷静分析
DBA 遇到事故的第一件事不是动手,是冷静。然后搞清楚四个问题:
2.1 问题一:删了什么?
sql
-- 查看最近的操作记录(通过默认 Trace)
SELECT
te.name AS EventName,
t.StartTime,
t.LoginName,
t.ApplicationName,
t.TextData
FROM sys.fn_trace_gettable(
(SELECT path FROM sys.traces WHERE is_default = 1), DEFAULT
) t
JOIN sys.trace_events te ON t.EventClass = te.trace_event_id
WHERE t.DatabaseName = 'ShopDB'
AND te.name LIKE '%Object%Deleted%'
AND t.StartTime >= DATEADD(HOUR, -1, GETDATE())
ORDER BY t.StartTime DESC;
结果:
| EventName | StartTime | LoginName | TextData |
|---|---|---|---|
| Object:Deleted | 16:31:45 | dev_zhangsan | DROP TABLE orders |
| Object:Deleted | 16:31:30 | dev_zhangsan | DELETE order_archive |
确认了 :16:31:45 执行了 DROP TABLE orders。
2.2 问题二:数据量有多大?
sql
-- 查看最后一次统计的 orders 表行数
-- (表已经没了,但我们可以从备份信息或监控数据中获取)
-- 从监控表查看最近一次行数记录
SELECT TOP 1
table_name, row_count, check_time
FROM dba_monitor.table_stats
WHERE table_name = 'orders'
ORDER BY check_time DESC;
-- 结果:
-- table_name | row_count | check_time
-- orders | 5,327,891 | 2026-07-27 16:00:00
532 万条数据。 这不是一个小表,但也不算特别大。恢复时间取决于备份文件大小和网络传输速度。
2.3 问题三:有没有外键依赖?
sql
-- orders 表被哪些表引用?
-- (这个信息需要从备份中恢复,或者查历史文档)
-- 从我们的数据字典文档中查到:
-- orders 表被以下表引用:
-- order_items.order_id → orders.id (外键)
-- payments.order_id → orders.id (外键)
-- shipments.order_id → orders.id (外键)
-- order_logs.order_id → orders.id (外键)
orders 表是核心中的核心。 但它被 DROP 的时候,外键约束已经被级联删除了。这些子表的 order_id 字段还在,但外键约束没了。
2.4 问题四:现在的业务影响是什么?
当前影响:
✅ 数据库还能连(只是 orders 表没了)
✅ 其他表正常
⚠️ 新订单无法插入(orders 表不存在)
⚠️ 订单查询全部报错
⚠️ 支付回调写入失败
❌ 每分钟损失约 200 笔订单
紧急程度:🔴 严重。需要立即恢复。
第三章:恢复方案选择
面对误删表,SQL Server 有四条恢复路线:
3.1 方案对比
| 方案 | 操作 | 数据丢失 | 停机时间 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| A. 完整恢复 | 恢复整个数据库到最后一次完整备份 | 丢失备份后的所有数据 | 长(30-120 分钟) | 低 | 小库、无日志备份 |
| B. 时间点恢复 | 恢复到误删前 1 秒 | 不丢失任何数据 | 中(15-30 分钟) | 中 | 有日志备份链 ✅ |
| C. 页面级恢复 | 只恢复损坏的数据页 | 不丢失 | 短 | 高 | 数据页损坏 |
| D. 第三方工具 | 用工具从日志中提取被删数据 | 不丢失 | 短-中 | 高 | 无备份的情况 |
3.2 我选择了方案 B:时间点恢复
选择理由:
✅ 数据库是 FULL 恢复模式
✅ 有完整的事务日志备份链(每 15 分钟一次)
✅ 知道误删的精确时间(16:31:45)
✅ 可以恢复到 16:31:44(误删前 1 秒)
恢复策略:
1. 先备份当前的事务日志(尾部日志备份)
2. 恢复到最近的完整备份
3. 依次恢复差异备份和日志备份
4. 最后一个日志备份用 STOPAT 恢复到 16:31:44
5. 把恢复出来的 orders 表数据导回生产库
为什么不直接恢复整个数据库?
因为直接恢复整个数据库会覆盖当前数据库,丢失误删之后到恢复完成之间的所有数据(虽然 orders 表没了,但其他表可能有新数据写入)。
正确的做法是:恢复到一个临时数据库,然后从临时库里把 orders 表的数据导回来。
第四章:实战恢复------30 分钟全记录
4.1 第一步:备份当前尾部日志(16:35-16:37)
sql
-- ⭐ 这一步极其重要!不备份尾部日志就会丢失最新数据
BACKUP LOG [ShopDB]
TO DISK = 'C:\Backups\ShopDB_TailLog_20260727_163500.trn'
WITH NO_TRUNCATE, -- 即使数据库有问题也要备份
COMPRESSION,
STATS = 10;
-- 结果:尾部日志备份成功,大小 234 MB
-- 这个备份包含了从最后一次日志备份(16:15)到现在的所有事务
⚠️ 永远先备份尾部日志! 这是 DBA 的肌肉记忆。不管什么恢复场景,先备份当前日志。
4.2 第二步:确认备份链完整性(16:37-16:38)
sql
-- 查看可用的备份文件
SELECT
database_name,
type AS BackupType, -- D=完整, I=差异, L=日志
backup_start_date,
backup_finish_date,
first_lsn,
last_lsn,
backup_size / 1024 / 1024 AS Size_MB,
physical_device_name
FROM msdb.dbo.backupset bs
JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id
WHERE database_name = 'ShopDB'
AND backup_start_date >= DATEADD(DAY, -1, GETDATE())
ORDER BY backup_start_date;
结果:
| BackupType | Start | Finish | First_LSN | Last_LSN | Size_MB | File |
|---|---|---|---|---|---|---|
| D(完整) | 02:00 | 02:35 | 45000000001 | 45000000100 | 45,000 | ShopDB_Full_20260727.bak |
| I(差异) | 14:00 | 14:12 | 45000000101 | 45000000200 | 8,500 | ShopDB_Diff_20260727.bak |
| L(日志) | 14:15 | 14:16 | 45000000201 | 45000000250 | 120 | ShopDB_Log_1415.trn |
| L(日志) | 14:30 | 14:31 | 45000000251 | 45000000300 | 135 | ShopDB_Log_1430.trn |
| ... | ... | ... | ... | ... | ... | ... |
| L(日志) | 16:15 | 16:16 | 45000000801 | 45000000850 | 180 | ShopDB_Log_1615.trn |
| L(尾部日志) | 16:35 | 16:36 | 45000000851 | 45000000900 | 234 | ShopDB_TailLog_163500.trn |
LSN 链完整! 从完整备份到尾部日志,每一个备份的 first_lsn 都等于上一个的 last_lsn + 1。
4.3 第三步:恢复到临时数据库(16:38-16:55)
sql
-- ⭐ 恢复到临时数据库 ShopDB_Restore,不影响生产库
-- 第一步:恢复完整备份(NORECOVERY,因为还要继续恢复)
RESTORE DATABASE [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_Full_20260727.bak'
WITH
MOVE 'ShopDB' TO 'D:\Data\ShopDB_Restore.mdf',
MOVE 'ShopDB_log' TO 'D:\Data\ShopDB_Restore_log.ldf',
NORECOVERY, -- 关键!保持恢复中状态
REPLACE, -- 如果存在同名数据库则覆盖
STATS = 5; -- 每 5% 显示进度
-- 结果:(约 5 分钟)
-- 已处理 45000 页... 100%
-- RESTORE DATABASE 成功处理了 45000 页
-- 第二步:恢复差异备份
RESTORE DATABASE [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_Diff_20260727.bak'
WITH NORECOVERY, STATS = 10;
-- 结果:(约 2 分钟)
-- 第三步:依次恢复事务日志备份
-- 恢复 14:15 的日志
RESTORE LOG [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_Log_1415.trn'
WITH NORECOVERY;
-- 恢复 14:30 的日志
RESTORE LOG [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_Log_1430.trn'
WITH NORECOVERY;
-- ... 依次恢复每个 15 分钟的日志备份 ...
-- 恢复 16:00 的日志
RESTORE LOG [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_Log_1600.trn'
WITH NORECOVERY;
-- 第四步:⭐ 关键步骤!恢复最后一个日志备份,精确到误删前 1 秒
RESTORE LOG [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_Log_1615.trn'
WITH NORECOVERY;
-- 第五步:恢复尾部日志,STOPAT 到 16:31:44(误删前 1 秒)
RESTORE LOG [ShopDB_Restore]
FROM DISK = 'C:\Backups\ShopDB_TailLog_20260727_163500.trn'
WITH
RECOVERY, -- 最后一步,完成恢复
STOPAT = '2026-07-27 16:31:44'; -- ⭐ 精确到误删前 1 秒!
-- 结果:
-- RESTORE LOG 成功处理了 12000 页,用时 4.2 秒
-- 数据库 ShopDB_Restore 已恢复并可正常使用
STOPAT 是 SQL Server 时间点恢复的精髓。 它会把数据库恢复到指定的精确时间点------不早一秒,不晚一秒。
4.4 第四步:把 orders 表导回生产库(16:55-17:00)
sql
-- 确认临时库里的 orders 表存在且数据完整
SELECT COUNT(*) FROM ShopDB_Restore.dbo.orders;
-- 结果:5,327,891 行。和监控记录一致!
-- 查看 orders 表的结构(确认列、索引、约束)
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM ShopDB_Restore.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'orders'
ORDER BY ORDINAL_POSITION;
-- 在生产库中重建 orders 表
-- 方法一:用 SSMS 生成建表脚本(推荐,最准确)
-- 右键 ShopDB_Restore → 任务 → 生成脚本 → 选择 orders 表 → 生成
-- 方法二:手动建表(如果你记得表结构)
USE ShopDB;
GO
CREATE TABLE orders (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
order_no VARCHAR(32) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
create_date DATETIME2 NOT NULL DEFAULT GETDATE(),
update_date DATETIME2 NOT NULL DEFAULT GETDATE(),
-- ... 其他列
);
CREATE NONCLUSTERED INDEX IX_orders_user_id ON orders(user_id);
CREATE NONCLUSTERED INDEX IX_orders_status ON orders(status);
CREATE NONCLUSTERED INDEX IX_orders_create_date ON orders(create_date);
GO
-- ⭐ 导入数据(530 万行,约 3 分钟)
SET IDENTITY_INSERT orders ON; -- 允许插入自增值
INSERT INTO orders (id, order_no, user_id, total_amount, status, create_date, update_date)
SELECT id, order_no, user_id, total_amount, status, create_date, update_date
FROM ShopDB_Restore.dbo.orders;
SET IDENTITY_INSERT orders OFF;
-- 重置自增种子
DECLARE @MaxId BIGINT;
SELECT @MaxId = MAX(id) FROM orders;
DBCC CHECKIDENT ('orders', RESEED, @MaxId);
4.5 第五步:重建外键约束(17:00-17:02)
sql
-- 重建被 DROP TABLE 级联删除的外键约束
USE ShopDB;
GO
ALTER TABLE order_items
ADD CONSTRAINT FK_order_items_orders
FOREIGN KEY (order_id) REFERENCES orders(id);
ALTER TABLE payments
ADD CONSTRAINT FK_payments_orders
FOREIGN KEY (order_id) REFERENCES orders(id);
ALTER TABLE shipments
ADD CONSTRAINT FK_shipments_orders
FOREIGN KEY (order_id) REFERENCES orders(id);
ALTER TABLE order_logs
ADD CONSTRAINT FK_order_logs_orders
FOREIGN KEY (order_id) REFERENCES orders(id);
-- 验证外键重建成功
SELECT
fk.name AS FK_Name,
OBJECT_NAME(fk.parent_object_id) AS ChildTable,
OBJECT_NAME(fk.referenced_object_id) AS ParentTable
FROM sys.foreign_keys fk
WHERE OBJECT_NAME(fk.referenced_object_id) = 'orders';
第五章:恢复后的验证
5.1 数据完整性验证
sql
-- 1. 行数对比
SELECT '生产库' AS Source, COUNT(*) AS RowCount FROM ShopDB.dbo.orders
UNION ALL
SELECT '恢复库' AS Source, COUNT(*) AS RowCount FROM ShopDB_Restore.dbo.orders;
-- 预期结果:两个行数完全一致
-- 2. 抽样验证(随机抽 100 条订单对比)
SELECT TOP 100
p.id, p.order_no, p.total_amount, p.status, p.create_date
FROM ShopDB.dbo orders p
JOIN ShopDB_Restore.dbo.orders r ON p.id = r.id
WHERE p.order_no != r.order_no
OR p.total_amount != r.total_amount
OR p.status != r.status;
-- 预期结果:0 行(没有差异)
-- 3. 关键业务指标验证
SELECT
CONVERT(DATE, create_date) AS OrderDate,
COUNT(*) AS OrderCount,
SUM(total_amount) AS TotalRevenue
FROM orders
WHERE create_date >= '2026-07-27'
GROUP BY CONVERT(DATE, create_date);
-- 对比业务部门的日报数据,确认一致
5.2 业务验证
sql
-- 让开发组确认:
-- 1. 订单查询接口正常 ✅
-- 2. 新订单可以正常创建 ✅
-- 3. 支付回调可以正常写入 ✅
-- 4. 历史订单数据完整 ✅
-- 5. 报表数据与昨天对比正常 ✅
5.3 清理恢复环境
sql
-- 确认一切正常后,清理临时数据库
DROP DATABASE ShopDB_Restore;
-- 删除临时备份文件(可选,建议保留一周)
-- del C:\Backups\ShopDB_TailLog_20260727_163500.trn
第六章:复盘
6.1 这次为什么能快速恢复?
| 因素 | 说明 | 重要程度 |
|---|---|---|
| FULL 恢复模式 | 支持时间点恢复 | ⭐⭐⭐⭐⭐ |
| 日志备份每 15 分钟 | 最多丢失 15 分钟数据 | ⭐⭐⭐⭐⭐ |
| 备份链完整 | LSN 链没有断裂 | ⭐⭐⭐⭐⭐ |
| 知道误删精确时间 | 可以从 Trace 中查到 | ⭐⭐⭐⭐ |
| 有数据字典文档 | 快速确认外键依赖 | ⭐⭐⭐⭐ |
| 备份文件在本地 | 传输速度快 | ⭐⭐⭐ |
6.2 这次事故暴露了什么问题?
问题一:开发环境没有和生产环境隔离
根本原因:开发人员能在生产库上执行 DDL 操作
解决方案:
1. 收回开发人员对生产库的写权限
2. 所有生产环境的 DDL 变更必须通过审批流程
3. 使用专用的数据库变更管理工具(如 Bytebase、Flyway)
问题二:DROP TABLE 没有保护机制
根本原因:SQL Server 允许直接 DROP 任何表
解决方案:
1. 创建 DDL 触发器,禁止直接 DROP 核心表
2. 核心表加 SCHEMA_BINDING 保护
3. 使用软删除代替物理删除
问题三:没有 DROP 操作的实时告警
根本原因:DROP TABLE 发生后,DBA 是被动得知的
解决方案:
1. 配置 DDL 变更的实时告警(邮件 + 企微/钉钉)
2. 使用 Extended Events 监控所有 DDL 操作
6.3 事故报告模板
【事故报告】
事故时间:2026-07-27 16:31:45 - 17:05:00(共 33 分钟)
事故等级:P1(核心业务中断)
影响范围:订单模块完全不可用,约 200 笔订单受影响
根本原因:开发人员在生产库上误执行 DROP TABLE
恢复方式:事务日志时间点恢复(STOPAT)
数据丢失:0 行(完全恢复)
责任人:张三(开发人员)
改进措施:
1. 收回开发人员生产库写权限(负责人:DBA,期限:1 周内)
2. 添加 DDL 触发器保护核心表(负责人:DBA,期限:3 天内)
3. 配置 DDL 操作实时告警(负责人:DBA,期限:1 周内)
4. 建立生产环境变更审批流程(负责人:技术总监,期限:2 周内)
第七章:预防------怎么保证下次不再翻车
7.1 DDL 触发器:禁止 DROP 核心表
sql
-- 创建一个 DDL 触发器,保护核心表不被 DROP
CREATE TRIGGER trg_ProtectCoreTables
ON DATABASE
FOR DROP_TABLE
AS
BEGIN
DECLARE @EventData XML = EVENTDATA();
DECLARE @TableName NVARCHAR(256) = @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)');
DECLARE @LoginName NVARCHAR(256) = @EventData.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(256)');
-- 核心表清单
DECLARE @ProtectedTables TABLE (TableName NVARCHAR(256));
INSERT INTO @ProtectedTables VALUES
('orders'), ('users'), ('payments'), ('products'),
('order_items'), ('inventory');
IF EXISTS (SELECT 1 FROM @ProtectedTables WHERE TableName = @TableName)
BEGIN
-- 记录告警日志
RAISERROR('⛔ 核心表 [%s] 不允许直接 DROP!操作已被阻止。操作人: %s', 16, 1, @TableName, @LoginName);
-- 发送告警邮件
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'DBA_Alert',
@recipients = 'dba@company.com',
@subject = '🚨 有人试图 DROP 核心表!',
@body = N'表名: ' + @TableName + N'\n操作人: ' + @LoginName;
-- 阻止操作
ROLLBACK;
END
END
GO
7.2 Extended Events:监控所有 DDL 操作
sql
-- 创建 Extended Events 会话,监控所有 DDL 操作
CREATE EVENT SESSION [DDL_Monitor] ON SERVER
ADD EVENT sqlserver.object_altered(
ACTION(sqlserver.client_hostname, sqlserver.username, sqlserver.sql_text)
WHERE database_name = 'ShopDB'
),
ADD EVENT sqlserver.object_created(
ACTION(sqlserver.client_hostname, sqlserver.username, sqlserver.sql_text)
WHERE database_name = 'ShopDB'
),
ADD EVENT sqlserver.object_deleted(
ACTION(sqlserver.client_hostname, sqlserver.username, sqlserver.sql_text)
WHERE database_name = 'ShopDB'
)
ADD TARGET package0.event_file(
SET filename = N'C:\XE\DDL_Monitor.xel',
max_file_size = 100,
max_rollover_files = 5
);
ALTER EVENT SESSION [DDL_Monitor] ON SERVER STATE = START;
7.3 备份策略加固
sql
-- 确认恢复模式是 FULL
SELECT name, recovery_model_desc
FROM sys.databases
WHERE name = 'ShopDB';
-- 如果是 SIMPLE,立即改为 FULL
ALTER DATABASE ShopDB SET RECOVERY FULL;
-- 备份策略(已经配好的,确认一下):
-- 完整备份:每天凌晨 02:00
-- 差异备份:每天 14:00
-- 日志备份:每 15 分钟
-- 建议:核心库改为每 5 分钟日志备份
-- 查看当前日志备份间隔
SELECT
j.name AS JobName,
s.freq_subday_interval AS IntervalMinutes
FROM msdb.dbo.sysjobs j
JOIN msdb.dbo.sysjobschedules js ON j.job_id = js.job_id
JOIN msdb.dbo.sysschedules s ON js.schedule_id = s.schedule_id
WHERE j.name LIKE '%Log Backup%';
7.4 权限收紧
sql
-- 查看谁有 DROP TABLE 权限
SELECT
dp.name AS UserName,
dp.type_desc AS UserType,
p.permission_name,
p.state_desc
FROM sys.database_permissions p
JOIN sys.database_principals dp ON p.grantee_principal_id = dp.principal_id
WHERE p.permission_name IN ('ALTER', 'CONTROL', 'DROP')
AND p.state_desc = 'GRANT'
ORDER BY dp.name;
-- 收回不必要的权限
-- 开发人员只给 db_datareader + 特定表的 SELECT
-- 生产环境的变更通过专用账号 + 审批流程执行
附录:SQL Server 恢复技术速查表
恢复模式对比
| 特性 | FULL(完整) | SIMPLE(简单) | BULK_LOGGED(大容量) |
|---|---|---|---|
| 事务日志保留 | 完整保留 | 自动截断 | 大部分保留 |
| 时间点恢复 | ✅ 支持 | ❌ 不支持 | ⚠️ 部分支持 |
| 日志备份 | 需要定期备份 | 不需要 | 需要定期备份 |
| 日志文件增长 | 可能很大 | 自动回收 | 中等 |
| 推荐场景 | 生产环境 | 开发/测试 | 大批量导入时 |
RESTORE 命令速查
sql
-- 1. 完整恢复(覆盖)
RESTORE DATABASE [TargetDB]
FROM DISK = 'backup.bak'
WITH REPLACE, RECOVERY;
-- 2. 完整恢复(继续恢复日志)
RESTORE DATABASE [TargetDB]
FROM DISK = 'backup.bak'
WITH NORECOVERY;
-- 3. 恢复差异备份
RESTORE DATABASE [TargetDB]
FROM DISK = 'diff.bak'
WITH NORECOVERY;
-- 4. 恢复日志备份
RESTORE LOG [TargetDB]
FROM DISK = 'log.trn'
WITH NORECOVERY;
-- 5. 时间点恢复(精确到秒)
RESTORE LOG [TargetDB]
FROM DISK = 'log.trn'
WITH RECOVERY, STOPAT = '2026-07-27 16:31:44';
-- 6. 恢复到新位置(MOVE 文件)
RESTORE DATABASE [TargetDB]
FROM DISK = 'backup.bak'
WITH MOVE 'DB' TO 'D:\Data\TargetDB.mdf',
MOVE 'DB_log' TO 'D:\Data\TargetDB_log.ldf',
NORECOVERY;
-- 7. 查看备份内容(不恢复)
RESTORE HEADERONLY FROM DISK = 'backup.bak';
RESTORE FILELISTONLY FROM DISK = 'backup.bak';
恢复决策流程图
数据丢失/损坏
│
├─ 表被 DROP/DELETE?
│ ├─ FULL 恢复模式 + 日志备份链完整
│ │ └─ → 时间点恢复(STOPAT)到临时库 → 导回数据
│ ├─ SIMPLE 恢复模式
│ │ └─ → 恢复到上次完整备份(可能丢失中间数据)
│ └─ 无任何备份
│ └─ → 第三方工具(ApexSQL、SQL Log Rescue)
│
├─ 数据库文件损坏?
│ ├─ 数据页损坏
│ │ └─ → 页面级恢复(PAGE 选项)
│ └─ 整个文件损坏
│ └─ → 完整恢复 + 差异恢复 + 日志恢复
│
└─ 服务器宕机?
├─ 有 Always On
│ └─ → 自动故障转移到备节点
└─ 无高可用
└─ → 在新服务器上恢复最新备份
总结
这次恢复成功的关键
1. ✅ 恢复模式是 FULL ------ 支持时间点恢复
2. ✅ 日志备份每 15 分钟 ------ 备份链完整
3. ✅ 知道误删精确时间 ------ STOPAT 精确到秒
4. ✅ 先备份尾部日志 ------ 没有丢失最新数据
5. ✅ 恢复到临时库 ------ 没有影响其他表的新数据
DBA 的三条铁律
铁律一:生产库永远是 FULL 恢复模式
SIMPLE 模式的数据库,就是在裸奔
铁律二:日志备份不能断
日志备份断了 = 时间点恢复能力没了 = 数据可能丢
铁律三:备份必须定期验证恢复
没有验证过的备份,等于没有备份
最后说一句掏心窝的话:DBA 最重要的能力不是优化 SQL,不是搭集群,而是在事故发生时能冷静、快速、准确地恢复数据。 备份恢复是 DBA 的看家本领,没有之一。希望这篇文章你收藏了但永远用不上,但用到的那一天,它能帮你省下一份辞职信。
📎 相关阅读
📌 关于作者
一名经历过多次数据恢复实战的 DBA。最大的感悟是:平时花 10 分钟维护备份策略,关键时刻能省 10 个小时的紧急抢救------还有可能保住你的饭碗。欢迎关注我获取更多 DBA 实战经验分享。