一.本文所涉及的内容(Contents)
- 本文所涉及的内容(Contents)
- 背景(Contexts)
- 案例分析(Case)
- 实现代码(SQL Codes)
- 主分区完整、差异还原(Primary Backup And Restore)
- 参考文献(References)
二.背景(Contexts)
在我的数据库实例中,有很多类似下图所示的数据库,这些数据库的名称是有规律的,每个数据库包含的表都是相同的,其中2个表是类似流水记录的表,表的数据量会比较大,占用的空间有几十G到上百G不等,这2个表相对于其它的配置表来说是比较不重要的。
现在有一个需求就是对数据库进行备份,允许丢失这两个表的数据,保留重要的配置表数据,你是否遇到过同样的问题呢?这个时候你会怎么做呢?你有什么方案呢?有什么方法可以快速备份这些数据库呢?
(Figure1:数据库列表)
阅读本文之前你可以先参考:SQL Server 批量完整备份
三.案例分析(Case)
通过上面的描述,其中很重要的一点就是每个数据库中有2个大表,而且这些数据是不重要的,那么我们对这2个大表做表分区,把大数据放到其它文件组中,只留重要的配置表在主文件组(PRIMARY)中,接着就可以对主文件组进行备份,这样既满足了备份重要表数据,而且不会造成备份文件过大、占用磁盘空间、备份时间过长等问题。
使用维护计划;请参考:SQL Server 维护计划备份主分区,只要在【执行T-SQL语句】的任务中使用循环递归所有数据库进行备份,在【清除历史记录】任务和【清除维护】任务中选择父目录就可以了;
方案一中通过维护计划生成的作业是看不到具体的备份代码的,所以方案二就是为了补充方案一的。如果你没有作业执行时间的特殊要求,你可以创建一个作业,循环数据库名称进行主分区的备份,只是把维护计划【执行T-SQL语句】的内容放到了作业中;
这个方案有以下几个缺点:
a) 没有办法单独控制某个数据库备份的时间;
b) 数据库一多的话,在Job的Message里面没有办法显示那么多的信息;
c) 备份串行执行的,没有办法进行并行备份;
d) 个个数据库的备份不是单独的,如果作业出错,会造成后面的数据库无法备份;
e) 貌似是先写入内存,等作业完成后才一次性写入硬盘的?当数据库备份文件比较大的时候会莫名只备份几个数据库作业就退出了,没有查明是什么原因;
具体的操作步骤可以参考:SQL Server 批量主分区备份(One Job),这个方案最终的作业形式为:
(Figure2:作业列表)
方案三是为了控制备份的时间而准备的,就是每个数据库都创建一个作业,这样的好处是可以充分的控制到每个数据库,因为我们可以需要对某个数据库进行完整备份,而且备份的粒度也有可能不同(重要客户的备份粒度会更小)
(Figure3:作业列表)
既然我们选择了方案三,那我们如何快速(批量)创建这些作业呢?我们先以数据库Barefoot.Opinion.9197为例,创建出一个作业的模板后,通过修改、替换这个作业的代码来实现批量生成新的可执行的作业脚本;
通过下面的几个步骤你就可以批量的生成备份作业:
1) 备份主分区完整备份的SQL代码(备份配置表);备份主分区差异备份的SQL代码(减轻备份文件空间压力)
2) 自动删除备份文件的SQL代码;(保证磁盘有足够空间)
3) 批量创建文件夹;(每个数据库单独一个文件夹)
4) 批量创建作业方案;(每个作业的名称、目录都不相同)
5) 批量修改作业计划的时间;(均衡分配作业的执行时间)
6) 批量删除作业;(方便维护)
7) 查看作业的执行情况;(防止作业时间过长,过长可以考虑预警)
四.实现代码(SQL Codes)
(一) 下面的代码实现了主分区完整备份和主分区差异备份,当是星期一的深夜的时候,我们做完整备份,如果是其它时候我们就做差异备份,具体是什么时候,这个就通过计划里面的时候来控制了(计划的执行时间为星期一、星期三、星期五,这就代表星期一深夜做了完整备份、星期三和星期五分别做了差异备份)。
生成的备份文件名为:(这样的文件名方便阅读;而且能精确到秒,重复的几率不大)
DBName _Primary_Full_2013_01_14_002007.bak
DBName_Primary_Diff_2013_01_16_002034.bak
--1设置完整模式 USE [master] GO ALTER DATABASE [DBName.9197] SET RECOVERY FULL WITH NO_WAIT GO --2备份主分区 DECLARE @CurrentTime VARCHAR(50), @FileName VARCHAR(200) SET @CurrentTime = REPLACE(REPLACE(REPLACE(CONVERT(VARCHAR, GETDATE(), 120 ),'-','_'),' ','_'),':','') IF(DATEPART(DW, GETDATE()) = 2)--星期一 BEGIN --完整备份 SET @FileName = 'E:DBBackupDBName.9197DBName.9197_Primary_Full_' + @CurrentTime+'.bak' BACKUP DATABASE [DBName.9197] FILEGROUP='PRIMARY' TO DISK=@FileName WITH FORMAT END ELSE BEGIN --差异备份 SET @FileName = 'E:DBBackupDBName.9197DBName.9197_Primary_Diff_' + @CurrentTime+'.bak' BACKUP DATABASE [DBName.9197] FILEGROUP='PRIMARY' TO DISK=@FileName WITH DIFFERENTIAL,FORMAT END GO --3设置简单模式 USE [master] GO ALTER DATABASE [DBName.9197] SET RECOVERY SIMPLE WITH NO_WAIT GO
(二) 下面的代码实现了删除备份文件,从下面的代码中可以看出删除了14天之前的备份文件;
--删除之前的备份文件 DECLARE @DeleteDate DATETIME SET @DeleteDate = DATEADD(DAY, -14, GETDATE()) EXECUTE MASTER.SYS.XP_DELETE_FILE 0, N'E:DBBackupDBName.9197', N'bak', @DeleteDate
(三) 下面的代码实现了批量创建文件夹,需要开启xp_cmdshell开关,使用了游标循环数据库名进行创建文件夹;
--批量创建文件夹 EXEC sp_configure 'show advanced options', 1 RECONFIGURE EXEC sp_configure 'xp_cmdshell', 1 RECONFIGURE DECLARE @DBName VARCHAR(100) DECLARE @SQL VARCHAR(1000) DECLARE CurDBName CURSOR FOR SELECT name FROM sys.databases WHERE name LIKE '%Opinion%' AND STATE =0 OPEN CurDBName FETCH NEXT FROM CurDBName INTO @DBName WHILE @@FETCH_STATUS = 0 BEGIN SET @SQL = 'mkdir E:DBBackup' + @DBName EXEC xp_cmdshell @SQL FETCH NEXT FROM CurDBName INTO @DBName END CLOSE CurDBName DEALLOCATE CurDBName EXEC sp_configure 'show advanced options', 0 RECONFIGURE EXEC sp_configure 'xp_cmdshell', 0 RECONFIGURE
(四) 下面的代码实现了批量创建作业,这里有一个创建作业的模板:JobTemplet.sql,我写了一个Replaced.bat的批处理文件,这个批处理文件替换模板文件中数据库的字符串,并生成创建新数据库作业的SQL脚本,创建之后就会执行这个SQL脚本来创建作业;
1. 使用上面提供的SQL代码,创建好作业的步骤和计划,再使用SSMS生成创建作业的脚本,这个就是JobTemplet.sql;
USE [msdb] GO /****** 对象: Job [Barefoot_Opinion_9565] 脚本日期: 01/06/2013 14:07:27 ******/ BEGIN TRANSACTION DECLARE @ReturnCode INT SELECT @ReturnCode = 0 /****** 对象: JobCategory [Database Maintenance] 脚本日期: 01/06/2013 14:07:27 ******/ IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'Database Maintenance' AND category_class=1) BEGIN EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'Database Maintenance' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback END DECLARE @jobId BINARY(16) EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name=N'DBName_9565', @enabled=1, @notify_level_eventlog=0, @notify_level_email=0, @notify_level_netsend=0, @notify_level_page=0, @delete_level=0, @description=N'备份主分区', @category_name=N'Database Maintenance', @owner_login_name=N'oofraBnimdA_gz', @job_id = @jobId OUTPUT IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback /****** 对象: Step [Bakcup] 脚本日期: 01/06/2013 14:07:27 ******/ EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Bakcup', @step_id=1, @cmdexec_success_code=0, @on_success_action=3, @on_success_step_id=0, @on_fail_action=2, @on_fail_step_id=0, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command=N'--1设置完整模式 USE [master] GO ALTER DATABASE [DBName.9565] SET RECOVERY FULL WITH NO_WAIT GO --2备份主分区(完整备份) DECLARE @CurrentTime VARCHAR(50), @FileName VARCHAR(200) SET @CurrentTime = REPLACE(REPLACE(REPLACE(CONVERT(VARCHAR, GETDATE(), 120 ),''-'',''_''),'' '',''_''),'':'','''') IF(DATEPART(DW, GETDATE()) = 2)--星期一 BEGIN SET @FileName = ''E:DBBackupDBName.9565DBName.9565_Primary_Full_'' + @CurrentTime+''.bak'' BACKUP DATABASE [DBName.9565] FILEGROUP=''PRIMARY'' TO DISK=@FileName WITH FORMAT END ELSE BEGIN SET @FileName = ''E:DBBackupDBName.9565DBName.9565_Primary_Diff_'' + @CurrentTime+''.bak'' BACKUP DATABASE [DBName.9565] FILEGROUP=''PRIMARY'' TO DISK=@FileName WITH DIFFERENTIAL,FORMAT END GO --3设置简单模式 USE [master] GO ALTER DATABASE [DBName.9565] SET RECOVERY SIMPLE WITH NO_WAIT GO ', @database_name=N'DBName.9565', @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback /****** 对象: Step [Delete] 脚本日期: 01/06/2013 14:07:27 ******/ EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Delete', @step_id=2, @cmdexec_success_code=0, @on_success_action=1, @on_success_step_id=0, @on_fail_action=2, @on_fail_step_id=0, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command=N'--删除之前的备份文件 DECLARE @DeleteDate DATETIME SET @DeleteDate = DATEADD(DAY, -14, GETDATE()) EXECUTE MASTER.SYS.XP_DELETE_FILE 0, N''E:DBBackupDBName.9565'', N''bak'', @DeleteDate', @database_name=N'DBName.9565', @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId, @name=N'Plan', @enabled=1, @freq_type=8, @freq_interval=34, @freq_subday_type=1, @freq_subday_interval=0, @freq_relative_interval=0, @freq_recurrence_factor=1, @active_start_date=20130105, @active_end_date=99991231, @active_start_time=10000, @active_end_time=235959 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback COMMIT TRANSACTION GOTO EndSave QuitWithRollback: IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION EndSave:
2. 把下面的代码保存为Replaced.bat文件,这个批处理文件接受两个参数,一个是@OldStr一个是@NewStr,即旧的数据库名和新的数据库名;
@if "%1"=="" goto error_parm @if "%2"=="" goto error_parm @echo off setlocal enabledelayedexpansion set file=E:DBBackupJobTemplet.sql set "file=%file:"=%" for %%i in ("%file%") do set file=%%~fi echo. set replaced=%1 echo. set all=%2 for /f "delims=" %%i in ('type "%file%"') do ( set str=%%i set "str=!str:%replaced%=%all%!" echo !str!>>"%file%"_%2.sql )
3. 到这里我们已经有JobTemplet.sql和Replaced.bat文件了,我们再创建一个Replaced_Test.bat批处理进行测试,这个批处理设置两个变量,一个是@OldStr一个是@NewStr,再调用Replaced.bat来替换字符串;
@set temp1=9565
@set temp2=9001
call Replaced_2.bat %temp1%,%temp2%
通过上面3个步骤,创建了3个文件,如下图所示:
(Figure4:创建的模板和批处理文件)
4. 下面的SQL代码就是使用游标的形式,循环符合条件的数据库,使用批处理替换模板文件,生成新的T-SQL文件,再执行新生成的T-SQL文件,这样就创建了游标当前数据库的备份作业了:
-- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2013/12/03> -- Description: <批量创建作业的T-SQL文件(替换字符串),并执行这个T-SQL文件> -- ============================================= --开启高级功能 EXEC sp_configure 'show advanced options', 1 RECONFIGURE EXEC sp_configure 'xp_cmdshell', 1 RECONFIGURE DECLARE @DBName VARCHAR(100)--数据库名称 DECLARE @CmdFile VARCHAR(1000)--创建File的Cmd命令字符串 DECLARE @CmdJob VARCHAR(1000)--执行Job的Cmd命令字符串 DECLARE @OldStr varchar(100)--需要替换的字符串 DECLARE @NewStr varchar(100)--替换成字符串 SET @OldStr = '9565' --查找符合条件的数据库 DECLARE CurDBName CURSOR FOR SELECT name FROM sys.databases WHERE name LIKE '%Opinion%' AND STATE =0 OPEN CurDBName FETCH NEXT FROM CurDBName INTO @DBName WHILE @@FETCH_STATUS = 0 BEGIN --使用批处理替换模板文件,生成新的T-SQL文件 SET @NewStr = SUBSTRING(@DBName,LEN('DBName.')+1,LEN(@DBName)) SET @CmdFile = 'E:DBBackupReplaced.bat ' + @OldStr + ' ' + @NewStr PRINT @CmdFile EXEC xp_cmdshell @CmdFile --执行新生成的T-SQL文件 SET @CmdJob = 'sqlcmd -i"E:DBBackupJobTemplet.sql _'+@NewStr+'.sql"' PRINT @CmdJob EXEC xp_cmdshell @CmdJob --循环下一个数据库 FETCH NEXT FROM CurDBName INTO @DBName END CLOSE CurDBName DEALLOCATE CurDBName --关闭高级功能 EXEC sp_configure 'xp_cmdshell', 0 RECONFIGURE EXEC sp_configure 'show advanced options', 0 RECONFIGURE
上面的脚本中你需要注意两点,如下图所示:“bat”字符串后面是有一个空格的,如果没有这个空格就会报错的;另外一点就是“sql”字符串与“_”之间也是有个空格的,这个空格是因为在使用Replaced.bat批处理创建文件的文件名的时候多一个空格(暂时还没找出原因),所以这里的文件名称需要加一个空格;
执行完上面的脚本之后,在E:DBBackup的目录下会生成下图所示的SQL脚本文件:
(五) 到这里,SSMS中已经创建了10个作业,就如下图所显示:
(Figure7:创建的作业列表)
但是为了能分散作业的执行时间,我们有两种方式做到这点:
1) 在JobTemplet.sql中有一个参数@active_start_time=10000,这个参数的意思是在深夜1点钟的时候执行作业,所以可以在Replaced.bat这个批处理再加一个参数,并且在JobTemplet.sql中替换成我们想要的值,在外层T-SQL调用Replaced.bat的时候记录这个值,并传入到Replaced.bat中(这就留给读者自己实现吧,我只实现了下面的第二种方法);
2) 修改已经创建作业的执行时间,下面的代码实现了批量修改作业的执行时间,使用游标的形式,循环调用msdb.dbo.sp_update_schedule修改作业的执行时间;
USE [msdb] GO -- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2013/12/03> -- Description: <批量修改Job备份时间> -- ============================================= DECLARE @DBName VARCHAR(100) DECLARE @ScheduleId INT DECLARE @Date DATETIME DECLARE @Time VARCHAR(50) DECLARE @SQL VARCHAR(1000) SET @Date = '2013-01-08 00:20:00.000' DECLARE CurDBName CURSOR FOR SELECT name,schedule_id FROM SYSJOBS AS J LEFT JOIN [SYSJOBSCHEDULES] AS S ON J.job_id= S.job_id WHERE NAME LIKE 'DBName_%' ORDER BY NAME OPEN CurDBName FETCH NEXT FROM CurDBName INTO @DBName,@ScheduleId WHILE @@FETCH_STATUS = 0 BEGIN --修改作业的执行时间 SET @Time = REPLACE(CONVERT(VARCHAR, @Date, 8 ),':','') SET @SQL = 'EXEC msdb.dbo.sp_update_schedule @schedule_id = ''' + CONVERT(VARCHAR(50),@ScheduleId) + ''', @active_start_time=' + @Time PRINT(@DBName +':'+ @SQL) EXEC(@SQL) --递增分钟 SET @Date = DATEADD(mi,20,@Date) --Get Next DataBase FETCH NEXT FROM CurDBName INTO @DBName,@ScheduleId END CLOSE CurDBName DEALLOCATE CurDBName
(六) 下面的代码实现了批量删除作业,使用游标的形式,循环调用sp_delete_job;
USE [msdb] GO -- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2013/12/03> -- Description: <批量删除Job> -- ============================================= DECLARE @DBName VARCHAR(100) DECLARE CurDBName CURSOR FOR SELECT name FROM msdb.dbo.sysjobs OPEN CurDBName FETCH NEXT FROM CurDBName INTO @DBName WHILE @@FETCH_STATUS = 0 BEGIN --删除Job exec sp_delete_job @job_name = @DBName --Get Next DataBase FETCH NEXT FROM CurDBName INTO @DBName END CLOSE CurDBName DEALLOCATE CurDBName
(七) 查看作业的运行情况;
--查询作业的执行情况 SELECT b.name,b.enabled,b.description,b.date_created,b.date_modified, a.step_id,a.step_name,message,run_date,run_time,run_duration FROM [msdb].dbo.[sysjobhistory] AS a LEFT JOIN [msdb].[dbo].[sysjobs] AS b ON a.job_id = b.job_id ORDER BY name
(Figure8:作业执行情况)
--查询作业与计划的对应关系 SELECT J.name,schedule_id FROM [msdb].[dbo].[sysjobs] AS J LEFT JOIN [msdb].[dbo].[sysjobschedules] AS S on J.job_id= S.job_id WHERE J.name LIKE '%' ORDER BY J.name
(Figure9:作业与计划的对应关系)
在表[msdb].[dbo].[sysschedules]中也同样包含作业计划信息;
(Figure10:sysschedules信息)
五.主分区完整、差异还原(Primary Backup And Restore)
既然做了上面主文件组的备份,当然我们需要去测试这个主文件组的还原了,这样才可以当遇到问题可以快速还原备份文件,达到还原数据的目的;
接下来会在另外一篇文章里面专门讲解;
六.参考文献(References)
sp_update_schedule (Transact-SQL)