摘要

在SQL Server备份专题分享中,前两期我们分享了三种常见的备份以及备份策略的制定,在第三期分享中,我们将要分享SQL Server的数据库备份链。完整的数据库备份链是保证数据库能够实现灾难恢复的基础,如果备份链条被打断或者备份链条上的文件损坏,势必会导致数据恢复不完整或者不能满足预期,而造成数据丢失,危害数据完整性生命线,后果非常严重。所以,理解SQL Server数据库备份链非常重要。

MSSQL· 最佳实践· SQL Server三种常见备份

MSSQL · 最佳实践 · SQL Server备份策略

场景引入

在开始今天关于SQL Server数据库备份链的分享之前,我们还是以上一期SQL Server备份策略分享的场景引入。假设我们存在如下图所示的备份文件列表,为什么我们就可以使用“全备 + 差备 + 日志备份”来实现快速的数据库还原操作呢?我们又是如何快速的查找到TestDR数据库的备份链条,并以此来还原的呢?这两个话题是我们今天要解答和解决的。 TestDR数据库备份文件列表如下图所示: 01.png

LSN介绍

在解决今天的问题之前,我需要首先介绍一个非常重要的概念:LSN。SQL Server的每一个事务日志都有自己的唯一标识号Log Sequence Number(简写为LSN)。LSN会随着时间的推移和事务日志的产生而不断增大。那么在SQL Server中如何查看以及有几种类型的LSN呢?

备份文件LSN

在SQL Server中,查看备份的LSN非常简单,我们可以使用RESTORE HEADERONLY命令跟上备份文件路径,即可查看。

  1. RESTORE HEADERONLY FROM DISK='C:\Temp\TestDR_20171217@00:00_FULL.bak'
  2. RESTORE HEADERONLY FROM DISK='C:\Temp\TestDR_20171217@01:00_LOG.trn'

执行的展示结果如下: 02.png

LSN种类

从以上截图红色方框中我们可以看出来,SQL Server的LSN分为以下四类:

  • FirstLSN:备份集中第一个事务日志的LSN;
  • LastLSN:备份集中最后一个事务日志的下一个LSN;
  • CheckpointLSN:最后一个Checkpoint发生时的LSN;
  • DatabaseBackupLSN:最后一个FULL Backup备份的LSN。

备份链原理

LSN是查找SQL Server 数据库备份链不可或缺的神一般存在的关键信息。我们可以利用前面讲到的四类LSN,使用如下五步骤来实现SQL Server备份链的查找。

  • 获取Full Backup: 查找最新一个FULL Backup文件,拿到对应CheckpointLSN;
  • 获取Diff Backup:查找Diff Backup文件,Diff Backup文件的DatabaseBackupLSN等于第一步中的CheckpointLSN,如果有多个Diff Backup文件,取FirstLSN最大者,然后拿到相应LastLSN;
  • 获取第一个Log Backup:查找第二步中的LastLSN位于Log Backup文件的FirstLSN和LastLSN之间的Log Backup,并获取相应的LastLSN;
  • 获取下一个Log Backup: 下一个Log Backup文件的FirstLSN等于第三步中获取到额LastLSN;
  • 获取第N个Log Backup:按照步骤四中的方法查找,直到所有日志文件查找完毕后,一个完整的备份链条查找完毕。

以上的理论解释稍显枯燥,我们用具体的示例来解释,就显得更为丰满和易于理解。我们把“场景引入”中的十五个备份文件,参照“备份文件LSN”中的方法来获取到LSN,如下面的截图所示: 03.png

那么,TestDR数据库的备份链可以通过如下的方法来查找:

  • 获取Full Backup: 查找最新一个FULL Backup(BackType=1)文件,拿到CheckpointLSN:24000000012800197,图中红色方框标示;
  • 获取Diff Backup:查找Diff Backup(BackType=5)文件,Diff Backup文件的DatabaseBackupLSN等于第一步中的CheckpointLSN:24000000012800197,图中黄色方框标示,取FirstLSN最大者: 24000000037600007,然后拿到LastLSN: 24000000039200001;
  • 获取第一个Log Backup:查找第二步中的LastLSN(24000000039200001)位于Log Backup(BackType=5)的FirstLSN(24000000036800001)与LastLSN(24000000040000001)之间的Log Backup,如图中绿色方框标示,并获取到LastLSN:24000000040000001;
  • 获取下一个Log Backup: 下一个Log Backup文件的FirstLSN(24000000040000001)应该等于上一步获取到额LastLSN,如图中粉红色标示;
  • 获取第N个Log Backup:按照步骤四中的方法查找,直到所有日志文件查找完毕。 所以,我们最终获取到的TestDR数据库的最简单,快速恢复的备份链是:
  1. C:\Temp\TestDR_20171217@00:00_FULL.bak => C:\Temp\TestDR_20171217@12:00_DIFF.bak => C:\Temp\TestDR_20171217@13:00_LOG.trn => C:\Temp\TestDR_20171217@14:00_LOG.trn

这个链条和我们上一期分享的数据库备份文件还原操作是完全吻合的。

具体实现

在“备份链原理”章节中,我们找出备份链是通过“用眼睛看”的查找方式来实现的。但是现实中如果数据库实例众多,数据库纷繁复杂,用眼睛看的方法就显得低效而准确率不高。那么,如何实现数据库备份链的自动查找呢?不着急,我已经为大家写好了,你只需要把相同数据库的多个备份文件路径放入输入参数中,我们就非常轻松、快捷的查找到数据库的备份链,并以此为依据来还原数据库。还是以TestDR的十五个备份文件为例来查找备份链条,代码如下:

  1. USE master
  2. GO
  3. SET NOCOUNT ON
  4. DECLARE
  5. -- all the backup files
  6. @backup_file_list NVARCHAR(MAX) = N'
  7. C:\Temp\TestDR_20171217@00:00_FULL.bak
  8. C:\Temp\TestDR_20171217@01:00_LOG.trn
  9. C:\Temp\TestDR_20171217@02:00_LOG.trn
  10. C:\Temp\TestDR_20171217@03:00_LOG.trn
  11. C:\Temp\TestDR_20171217@04:00_LOG.trn
  12. C:\Temp\TestDR_20171217@05:00_LOG.trn
  13. C:\Temp\TestDR_20171217@06:00_DIFF.bak
  14. C:\Temp\TestDR_20171217@07:00_LOG.trn
  15. C:\Temp\TestDR_20171217@08:00_LOG.trn
  16. C:\Temp\TestDR_20171217@09:00_LOG.trn
  17. C:\Temp\TestDR_20171217@10:00_LOG.trn
  18. C:\Temp\TestDR_20171217@11:00_LOG.trn
  19. C:\Temp\TestDR_20171217@12:00_DIFF.bak
  20. C:\Temp\TestDR_20171217@13:00_LOG.trn
  21. C:\Temp\TestDR_20171217@14:00_LOG.trn
  22. '
  23. ;
  24. -- Create temp table to save headeronly info
  25. IF OBJECT_ID('tempdb..#headeronly') IS NOT NULL
  26. DROP TABLE #headeronly
  27. CREATE TABLE #headeronly(
  28. RowId INT IDENTITY(1,1) NOT NULL PRIMARY KEY
  29. ,BackupName nvarchar(128) NULL
  30. ,BackupDescription nvarchar(255) NULL
  31. ,BackupType smallint NULL
  32. ,ExpirationDate datetime
  33. ,Compressed bit NULL
  34. ,Position smallint NULL
  35. ,DeviceType tinyint NULL
  36. ,UserName nvarchar(128) NULL
  37. ,ServerName nvarchar(128) NULL
  38. ,DatabaseName nvarchar(128) NULL
  39. ,DatabaseVersion int NULL
  40. ,DatabaseCreationDate datetime NULL
  41. ,BackupSize numeric(20,0) NULL
  42. ,FirstLSN numeric(25,0) NULL
  43. ,LastLSN numeric(25,0) NULL
  44. ,CheckpointLSN numeric(25,0) NULL
  45. ,DatabaseBackupLSN numeric(25,0) NULL
  46. ,BackupStartDate datetime NULL
  47. ,BackupFinishDate datetime NULL
  48. ,SortOrder smallint NULL
  49. ,CodePage smallint NULL
  50. ,UnicodeLocaleId int NULL
  51. ,UnicodeComparisonStyle int NULL
  52. ,CompatibilityLevel tinyint NULL
  53. ,SoftwareVendorId int NULL
  54. ,SoftwareVersionMajor int NULL
  55. ,SoftwareVersionMinor int NULL
  56. ,SoftwareVersionBuild int NULL
  57. ,MachineName nvarchar(128) NULL
  58. ,Flags int NULL
  59. ,BindingID uniqueidentifier NULL
  60. ,RecoveryForkID uniqueidentifier NULL
  61. ,Collation nvarchar(128) NULL
  62. ,FamilyGUID uniqueidentifier NULL
  63. ,HasBulkLoggedData bit NULL
  64. ,IsSnapshot bit NULL
  65. ,IsReadOnly bit NULL
  66. ,IsSingleUser bit NULL
  67. ,HasBackupChecksums bit NULL
  68. ,IsDamaged bit NULL
  69. ,BeginsLogChain bit NULL
  70. ,HasIncompleteMetaData bit NULL
  71. ,IsForceOffline bit NULL
  72. ,IsCopyOnly bit NULL
  73. ,FirstRecoveryForkID uniqueidentifier NULL
  74. ,ForkPointLSN numeric(25,0) NULL
  75. ,RecoveryModel nvarchar(60) NULL
  76. ,DifferentialBaseLSN numeric(25,0) NULL
  77. ,DifferentialBaseGUID uniqueidentifier NULL
  78. ,BackupTypeDescription nvarchar(60) NULL
  79. ,BackupSetGUID uniqueidentifier NULL
  80. ,CompressedBackupSize bigint NULL
  81. );
  82. -- private variables
  83. DECLARE
  84. @backup_file_list_xml xml
  85. ,@backup_file sysname
  86. ,@row_count int
  87. ,@do int = 0
  88. ,@sql NVARCHAR(MAX) = N''
  89. ,@full_backup_CheckPointLSN numeric(25,0)
  90. ,@full_backup_familyGUID uniqueidentifier
  91. ,@diff_backup_LastLSN numeric(25,0)
  92. ,@log_backup_LastLSN numeric(25,0)
  93. ,@back_chain_level_number INT = 0
  94. ,@full_backup_rowid INT
  95. ,@diff_backup_rowid INT
  96. ;
  97. SELECT
  98. @backup_file_list_xml = '<V><![CDATA[' + REPLACE(
  99. REPLACE(
  100. REPLACE(
  101. @backup_file_list,CHAR(10),']]></V><V><![CDATA['
  102. ),',',']]></V><V><![CDATA['
  103. ),CHAR(13),']]></V><V><![CDATA['
  104. ) + ']]></V>'
  105. ;
  106. DECLARE
  107. @tb_back_files TABLE(
  108. Rowid INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
  109. backup_file sysname not null
  110. );
  111. DECLARE
  112. @tb_back_file_rowid TABLE(
  113. Bak_row_id INT NOT NULL,
  114. Level_Number INT NOT NULL
  115. );
  116. -- split backup files into @table
  117. INSERT INTO @tb_back_files
  118. SELECT
  119. DISTINCT
  120. backup_file = RTRIM(LTRIM(T.C.value('(./text())[1]','sysname')))
  121. FROM @backup_file_list_xml.nodes('./V') AS T(C)
  122. WHERE T.C.value('(./text())[1]','sysname') IS NOT NULL
  123. AND LEFT(RTRIM(LTRIM(T.C.value('(./text())[1]','sysname'))), 2) <> '--'
  124. ORDER BY 1 ASC
  125. -- cursor for each backup file to get headeronly info
  126. DECLARE CUR_backup_file CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  127. FOR
  128. SELECT
  129. backup_file
  130. FROM @tb_back_files
  131. ORDER BY Rowid ASC
  132. OPEN CUR_backup_file
  133. FETCH NEXT FROM CUR_backup_file INTO @backup_file
  134. SET @row_count = @@CURSOR_ROWS
  135. WHILE @@FETCH_STATUS = 0
  136. BEGIN
  137. RAISERROR('--=== %d/%d ===Processing on %s', 10, 1, @do, @row_count, @backup_file) WITH NOWAIT
  138. SET
  139. @sql = N'RESTORE HEADERONLY FROM DISK = N' + QUOTENAME(@backup_file, '''') + N';'
  140. ;
  141. INSERT INTO #headeronly
  142. EXEC sys.sp_executesql @sql
  143. ;
  144. SET @do = @do + 1
  145. FETCH NEXT FROM CUR_backup_file INTO @backup_file
  146. END
  147. CLOSE CUR_backup_file
  148. DEALLOCATE CUR_backup_file
  149. /**
  150. * FULL backup file checking
  151. *1. make sure those backup files came from the same database.
  152. *2. there is no full backup
  153. *3. There have more than one FULL backup files.
  154. */
  155. -- make sure backup files came from the same database.
  156. IF EXISTS(
  157. SELECT TOP 1 1
  158. FROM #headeronly
  159. GROUP BY FamilyGUID
  160. HAVING COUNT(DISTINCT FamilyGUID) >= 2
  161. )
  162. BEGIN
  163. RAISERROR('Those backup fils didn''t backup from the same database, process terminated.', 16, 1) WITH NOWAIT
  164. RETURN
  165. END
  166. -- There is no full backup file
  167. IF NOT EXISTS(
  168. SELECT TOP 1 1
  169. FROM #headeronly
  170. WHERE BackupType = 1 -- FULL Backup
  171. )
  172. BEGIN
  173. RAISERROR('There is no FULL BACKUP included, process terminated.', 16, 1) WITH NOWAIT
  174. RETURN
  175. END
  176. -- Have more than one FULL Backup, we don't know use which one.
  177. IF EXISTS(
  178. SELECT TOP 1 1
  179. FROM #headeronly
  180. WHERE BackupType = 1
  181. GROUP BY BackupType
  182. HAVING COUNT(1) >= 2
  183. )
  184. BEGIN
  185. RAISERROR('Those backup fils are more than one FULL BACKUP including, we may don''t know use which one, process terminated.', 16, 1) WITH NOWAIT
  186. RETURN
  187. END
  188. -- We have only one full backup, get FULL Backup file CheckPointLSN
  189. SELECT TOP 1
  190. @full_backup_CheckPointLSN = CheckPointLSN
  191. ,@full_backup_familyGUID = FamilyGUID
  192. ,@back_chain_level_number = 0
  193. ,@full_backup_rowid = RowId
  194. FROM #headeronly
  195. WHERE BackupType = 1
  196. INSERT INTO @tb_back_file_rowid
  197. SELECT @full_backup_rowid, @back_chain_level_number;
  198. PRINT 'We got full backup check point LSN: ' + cast(@full_backup_CheckPointLSN as varchar)
  199. /**
  200. *get the very last diff backup file
  201. **/
  202. -- if exists diff backup, get the newest one whose DatabaseBackupLSN equals @full_backup_CheckPointLSN got above
  203. IF EXISTS(
  204. SELECT TOP 1 1
  205. FROM #headeronly
  206. WHERE BackupType = 5 -- DIFF Backup
  207. AND DatabaseBackupLSN = @full_backup_CheckPointLSN
  208. AND FamilyGUID = @full_backup_familyGUID
  209. )
  210. BEGIN
  211. RAISERROR('We are searching the Differential Database Backup.', 10, 1) WITH NOWAIT
  212. SELECT TOP 1
  213. @diff_backup_LastLSN = LastLSN
  214. ,@back_chain_level_number = @back_chain_level_number + 1
  215. ,@diff_backup_rowid = RowId
  216. FROM #headeronly
  217. WHERE BackupType = 5 -- DIFF Backup
  218. AND DatabaseBackupLSN = @full_backup_CheckPointLSN
  219. AND FamilyGUID = @full_backup_familyGUID
  220. ORDER BY FirstLSN DESC -- get the max firstLSN (means newest one)
  221. INSERT INTO @tb_back_file_rowid
  222. SELECT @diff_backup_rowid, @back_chain_level_number;
  223. PRINT 'We got newest Differential backup Last LSN: ' + cast(@diff_backup_LastLSN as varchar)
  224. END
  225. ELSE
  226. RAISERROR('We didn''t find any differential backups yet match the full backup.', 10, 1) WITH NOWAIT
  227. -- get the very first log backup whose FirstLSN littler and LastLSN greater than @diff_backup_LastLSN
  228. -- when exists differential backup
  229. IF @diff_backup_LastLSN IS NOT NULL
  230. BEGIN
  231. IF EXISTS(
  232. SELECT TOP 1 1
  233. FROM #headeronly
  234. WHERE BackupType = 2 -- Log backup
  235. AND FirstLSN <@diff_backup_LastLSN
  236. AND LastLSN>= @diff_backup_LastLSN
  237. AND FamilyGUID = @full_backup_familyGUID
  238. )
  239. BEGIN
  240. SELECT TOP 1
  241. @log_backup_LastLSN = LastLSN
  242. ,@back_chain_level_number = @back_chain_level_number + 1
  243. FROM #headeronly
  244. WHERE BackupType = 2
  245. AND FirstLSN <@diff_backup_LastLSN
  246. AND LastLSN>= @diff_backup_LastLSN
  247. AND FamilyGUID = @full_backup_familyGUID
  248. PRINT 'We got log backup Last LSN: ' + cast(@log_backup_LastLSN as varchar)
  249. END
  250. END
  251. ELSE
  252. -- There is no differential backup, we have to find between log backups.
  253. -- Find the very first log backup LastLSN
  254. BEGIN
  255. SELECT TOP 1
  256. @log_backup_LastLSN = LastLSN
  257. ,@back_chain_level_number = @back_chain_level_number + 1
  258. FROM #headeronly
  259. WHERE BackupType = 2 -- Log Backup
  260. AND FamilyGUID = @full_backup_familyGUID
  261. AND DatabaseBackupLSN = @full_backup_CheckPointLSN
  262. AND FirstLSN = @full_backup_CheckPointLSN
  263. END
  264. -- There is no log backup belongs to this full backup
  265. IF @log_backup_LastLSN IS NULL
  266. BEGIN
  267. RAISERROR('There is no any log backups belong to this full backup.', 10, 1) WITH NOWAIT
  268. RETURN
  269. END
  270. --SELECT
  271. -- full_backup_CheckPointLSN = @full_backup_CheckPointLSN
  272. -- ,diff_backup_LastLSN = @diff_backup_LastLSN
  273. -- ,log_backup_LastLSN = @log_backup_LastLSN
  274. -- ,back_chain_level_number = @back_chain_level_number
  275. --;
  276. -- find out all the log backup need to be restore
  277. ;
  278. WITH backup_files
  279. AS
  280. (
  281. SELECT lv = @back_chain_level_number,*
  282. FROM #headeronly
  283. WHERE LastLSN = @log_backup_LastLSN
  284. AND FamilyGUID = @full_backup_familyGUID
  285. UNION ALL
  286. SELECT bf.lv + 1,lg.*
  287. FROM #headeronly AS lg
  288. INNER JOIN backup_files as bf
  289. ON lg.FirstLSN = bf.LastLSN AND lg.FamilyGUID = bf.FamilyGUID
  290. WHERE lg.BackupType = 2
  291. AND lg.FamilyGUID = @full_backup_familyGUID
  292. )
  293. INSERT INTO @tb_back_file_rowid
  294. SELECT RowId, lv FROM backup_files
  295. SELECT
  296. bkf.Backup_file,
  297. hd.BackupType,
  298. BackupTypeDescription =
  299. CASE hd.BackupType
  300. WHEN 1 THEN 'FULL'
  301. WHEN 2 THEN 'LOG'
  302. WHEN 5 THEN 'DIFF'
  303. ELSE ''
  304. END,
  305. hd.IsDamaged,
  306. restore_order = bkr.Level_Number
  307. FROM @tb_back_file_rowid AS bkr
  308. INNER JOIN @tb_back_files AS bkf
  309. ON bkr.Bak_row_id = bkf.Rowid
  310. INNER JOIN #headeronly AS hd
  311. ON bkr.Bak_row_id = hd.RowId
  312. ORDER BY bkr.Level_Number
  313. --SELECT *
  314. --FROM #headeronly
  315. --ORDER BY rowid asc
  316. SELECT
  317. bkf.backup_file,
  318. hd.BackupType,
  319. hd.Compressed,
  320. hd.DatabaseName,
  321. hd.FirstLSN,
  322. hd.LastLSN,
  323. hd.CheckpointLSN,
  324. hd.DatabaseBackupLSN,
  325. hd.IsDamaged,
  326. hd.BackupTypeDescription
  327. FROM #headeronly AS hd
  328. INNER JOIN @tb_back_files AS bkf
  329. ON hd.RowId = bkf.Rowid
  330. ORDER BY hd.RowId

备份链结果截图展示如下: 04.png

从这个截图中,restore_order和Backup_file字段可以很清楚的看到数据库还原的顺序,以及相应的备份文件路径。所有备份文件的LSN分布如下截图: 05.png

差异备份文件损坏

从备份链条我们发现,12:00的这个差异备份C:\Temp\TestDR_20171217@12:00_DIFF.bak非常关键,假设现实中,我们发现这个文件恰恰损坏掉了,那么我们的可以实现应用的数据库还原吗?答案是肯定的,我们把刚才的脚本输入参数修改如下:

  1. DECLARE
  2. -- all the backup files
  3. @backup_file_list NVARCHAR(MAX) = N'
  4. C:\Temp\TestDR_20171217@00:00_FULL.bak
  5. C:\Temp\TestDR_20171217@01:00_LOG.trn
  6. C:\Temp\TestDR_20171217@02:00_LOG.trn
  7. C:\Temp\TestDR_20171217@03:00_LOG.trn
  8. C:\Temp\TestDR_20171217@04:00_LOG.trn
  9. C:\Temp\TestDR_20171217@05:00_LOG.trn
  10. C:\Temp\TestDR_20171217@06:00_DIFF.bak
  11. C:\Temp\TestDR_20171217@07:00_LOG.trn
  12. C:\Temp\TestDR_20171217@08:00_LOG.trn
  13. C:\Temp\TestDR_20171217@09:00_LOG.trn
  14. C:\Temp\TestDR_20171217@10:00_LOG.trn
  15. C:\Temp\TestDR_20171217@11:00_LOG.trn
  16. C:\Temp\TestDR_20171217@13:00_LOG.trn
  17. C:\Temp\TestDR_20171217@14:00_LOG.trn
  18. '
  19. ;

得到如下备份还原链条: 06.png

从这个结果,我们可以看到,系统选择了06:00这个差异备份文件,然后一步步使用接下来的事务日志备份文件进行还原。

最后总结

本期是SQL Server备份专题的第三期,在前两期基础上分享了如何通过备份文件查找SQL Server数据库备份链,并以此来还原数据库的理论方法和具体实现,可以帮助用户实现简单、快速查找数据库备份还原链条。