В продолжение своей предыдущей статьи по архивации БД MSSQL и негативному отклику в связи с отсутствием возможности архивации логов транзакций, работаю я теперь в компании, где понадобилось автоматизировать этот момент для баз в том числе с полной моделью восстановления.

Скрип работает универсально для баз с различной моделью восстановления, в начале скрипта добавлены настройки для относительно гибкого формирования расписания. Скрипт можно поставить с SQL Agent и удобным интервалом (у меня, например, 1 раз час), первый запуск в сутках будет проверять, надо создавать или нет полную или разностную копию сегодня и далее в течении дня для БД с полной моделью восстановления будут создаваться бэкапы лога журнала транзакций.

-- пути до бэкапов 
declare @FullPath varchar(500) = 'D:\SQLBackup'
declare @DiffPath varchar(500) = 'D:\SQLBackup'
declare @LogPath varchar(500) = 'D:\SQLBackup'

-- архивируемые и исключенные из архивации БД 
declare @IncludeBase varchar(500) = 'master, msdb' -- если не пусто, то только эти минус исключенные, если пусто то все минус исключенные
declare @ExcludeBase varchar(500) = 'tempdb'


-- настроки для полного бэкапа
-- минимум хранить дней
declare @MinFullDay int = 181
-- выполнять каждые дней
declare @MinFullExecDay int = 7


-- настроки для разностного бэкапа
-- минимум хранить дней
declare @MinDiffDay int = 181
-- выполнять каждые дней
declare @MinDiffExecDay int = 1

-- настроки для бэкапа журнала
-- минимум хранить дней
declare @MinLogDay int = 35



-- как часто создавать master (дни)
declare @MinDayMaster int = 1

-- сколько хранить информацию об архивах в msdb (дни)
--declare @DayHistory int = @MinFullDay + 30


-- включим сжатие 
EXEC sp_configure 'show advanced options', 1;  EXEC sp_configure 'backup compression default', 1; RECONFIGURE WITH OVERRIDE;
-- включим xp_cmdshell (нужно для удаления старых бэкапов)
EXEC sp_configure 'show advanced options', 1;  EXEC sp_configure 'xp_cmdshell', 1;  RECONFIGURE WITH OVERRIDE;  

declare @tempcmd varchar(500) =''
declare @tempname varchar(500) =''

-- создание путей
set @tempcmd= 'md '+@FullPath
exec xp_cmdshell @tempcmd, no_output
set @tempcmd= 'md '+@DiffPath
exec xp_cmdshell @tempcmd, no_output
set @tempcmd= 'md '+@LogPath
exec xp_cmdshell @tempcmd, no_output



-- определяем список БД для архивации
declare @BaseListIncl table (name varchar(200))
declare @BaseListExcl table (name varchar(200))

if @IncludeBase='' 
		insert into @BaseListIncl select name from sys.databases where state_desc='ONLINE' 
	else
		while len(@IncludeBase)>0
		begin
			if CHARINDEX (',',@IncludeBase)>0
				begin
					insert into @BaseListIncl select name from sys.databases where state_desc='ONLINE' and name = SUBSTRING(@IncludeBase,1, CHARINDEX (',',@IncludeBase)-1)
					set @IncludeBase=LTRIM(RTRIM(SUBSTRING(@IncludeBase,CHARINDEX (',',@IncludeBase)+1, LEN(@IncludeBase))))
				end
			else
				begin
					insert into @BaseListIncl select name from sys.databases where state_desc='ONLINE' and name = @IncludeBase
					set @IncludeBase=''
				end
		 end

if @ExcludeBase<>'' 
		while len(@ExcludeBase)>0
		begin
			if CHARINDEX (',',@ExcludeBase)>0
				begin
					insert into @BaseListExcl select name from sys.databases where state_desc='ONLINE' and name = SUBSTRING(@ExcludeBase,1, CHARINDEX (',',@ExcludeBase)-1)
					set @ExcludeBase=LTRIM(RTRIM(SUBSTRING(@ExcludeBase,CHARINDEX (',',@ExcludeBase)+1, LEN(@ExcludeBase))))
				end
			else
				begin
					insert into @BaseListExcl select name from sys.databases where state_desc='ONLINE' and name = @ExcludeBase
					set @ExcludeBase=''
				end
		 end

-- итоговый список БД для бэкапа
delete from @BaseListIncl 
	where name in (select name from @BaseListExcl)

declare BaseList cursor for
	select name from @BaseListIncl 

declare @BaseName varchar(500) =''
declare @BackupType varchar(10) = ''

open BaseList
fetch next from BaseList into @BaseName  

while @@FETCH_STATUS = 0  
	begin  
	    
	-- получаем тип бэкапа, который надо сделать (для мастера специальные условия, он не может ни каких кроме FULL)
	SET @BackupType =   
	    ( CASE  
			WHEN @BaseName = 'master' AND
				(EXISTS (SELECT * FROM msdb.dbo.backupset s
					INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
					WHERE s.database_name=@BaseName and s.TYPE='D' and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) < @MinDayMaster))
				THEN ''
			WHEN @BaseName = 'master' AND
				(NOT EXISTS (SELECT * FROM msdb.dbo.backupset s
					INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
					WHERE s.database_name=@BaseName and s.TYPE='D' and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) < @MinDayMaster))
				THEN 'FULL'
			WHEN (NOT EXISTS (SELECT * FROM msdb.dbo.backupset s
				INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
				WHERE s.database_name=@BaseName and s.TYPE='D' and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) < @MinFullExecDay)) 
				THEN 'FULL'
			WHEN (NOT EXISTS (SELECT * FROM msdb.dbo.backupset s
				INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
				WHERE s.database_name=@BaseName and s.TYPE IN ('I','D') and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) < @MinDiffExecDay)) 
				THEN 'DIFF'
			WHEN (EXISTS (select * from sys.databases as a where a.recovery_model_desc = 'FULL' and a.name = @BaseName)) 
				THEN 'LOG'
			ELSE ''
		END  
		) 


	IF @BackupType <> ''
	begin


	--создаем папку для БД
	SET @tempcmd = 
		(CASE
			WHEN @BackupType='FULL' OR @BaseName='master' THEN 'md '+@FullPath+'\'+@BaseName
			WHEN @BackupType='DIFF' THEN 'md '+@DiffPath+'\'+@BaseName
			ELSE 'md '+@LogPath+'\'+@BaseName
		END)
		exec xp_cmdshell @tempcmd, no_output


		
		if @BackupType='FULL'
			begin
				-- full backup
				set @tempname = @FullPath+'\'+@BaseName+'\'+@BaseName+'_'+CONVERT(varchar(8), GETDATE(), 112)+ '-' + REPLACE(CONVERT(varchar, GETDATE(),114),':','') +'_FULL.BAK'
				backup database @BaseName to disk = @tempname
			end
		else if @BackupType='DIFF'
			begin
				-- diff backup
				set @tempname = @DiffPath+'\'+@BaseName+'\'+@BaseName+'_'+CONVERT(varchar(8), GETDATE(), 112)+ '-' + REPLACE(CONVERT(varchar, GETDATE(),114),':','') +'_DIFF.BAK'
				backup database @BaseName to disk =	@tempname with differential
			end
		else 
			begin
				-- log backup
				set @tempname = @LogPath+'\'+@BaseName+'\'+@BaseName+'_'+CONVERT(varchar(8), GETDATE(), 112)+ '-' + REPLACE(CONVERT(varchar, GETDATE(),114),':','') +'_LOG.TRN'
				BACKUP LOG @BaseName to disk =	@tempname 
			end


		-- удаляем истекшие бэкапы
		declare @delpath varchar(500)=''
		declare delbackup cursor for

			SELECT mf.physical_device_name
				FROM   msdb.dbo.backupset s
				INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
			WHERE s.database_name=@BaseName and s.TYPE='D'
			and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) > @MinFullDay
			and is_copy_only = 0

		union all
			SELECT mf.physical_device_name
				FROM   msdb.dbo.backupset s
				INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
			WHERE s.database_name=@BaseName and s.TYPE='I'
			and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) > @MinDiffDay
			and is_copy_only = 0

		union all
			SELECT mf.physical_device_name
				FROM   msdb.dbo.backupset s
				INNER JOIN msdb.dbo.backupmediafamily mf ON s.media_set_id = mf.media_set_id
			WHERE s.database_name=@BaseName and s.TYPE='L'
			and DATEDIFF(day, CAST(s.backup_finish_date AS DATE), CAST(GETDATE() AS DATE)) > @MinLogDay
			and is_copy_only = 0



		 open delbackup
		 fetch next from delbackup into @delpath  
		 while @@FETCH_STATUS = 0 
			begin
				set @tempcmd= 'del /f /q '+QUOTENAME(@delpath,'"')
				exec xp_cmdshell @tempcmd, no_output
			  fetch next from delbackup into @delpath
			end
		 close delbackup
		 deallocate delbackup

		end

		fetch next from BaseList into @BaseName  
    end
  
close BaseList  
deallocate BaseList 


-- чистим в MSDB информацию о старых архивах
--declare @oldest DATETIME 
--SET @oldest = DATEADD(DAY, -@DayHistory, GETDATE())

--EXEC msdb.dbo.sp_delete_backuphistory @oldest_date = @oldest

Так же добавлю скрипт для создания бэкапов "длительного хранения". Создаются с опцией ONLY_COPY и не участвуют в общей цепочке восстановления, я обычно использую раз - два в месяц для снятия полной копии или по необходимости

-- ========== СОЗДАЕМ ТОЛЬКО ПОЛНЫЕ БЭКАПЫ С ОПЦИЕЙ ONLY_COPY (не учавствуют в общей цепочке)

-- пути до бэкапов (для типовых)
declare @Path varchar(500) = 'I:\SQLBackupOnlyCopy'

-- архивируемые и исключенные из архивации БД 
declare @IncludeBase varchar(500) = 'buh, zup' -- если не пусто, то только эти минус исключенные, если пусто то все минус исключенные
declare @ExcludeBase varchar(500) = 'tempdb'


-- включим сжатие 
EXEC sp_configure 'show advanced options', 1;  EXEC sp_configure 'backup compression default', 1; RECONFIGURE WITH OVERRIDE;
-- включим xp_cmdshell (нужно для удаления старых бэкапов)
EXEC sp_configure 'show advanced options', 1;  EXEC sp_configure 'xp_cmdshell', 1;  RECONFIGURE WITH OVERRIDE;  

declare @tempcmd varchar(500) =''
declare @tempname varchar(500) =''

-- создание путей
set @tempcmd= 'md '+@Path
exec xp_cmdshell @tempcmd, no_output

-- определяем список БД для архивации
declare @BaseListIncl table (name varchar(200))
declare @BaseListExcl table (name varchar(200))

if @IncludeBase='' 
		insert into @BaseListIncl select name from sys.databases where state_desc='ONLINE' 
	else
		while len(@IncludeBase)>0
		begin
			if CHARINDEX (',',@IncludeBase)>0
				begin
					insert into @BaseListIncl select name from sys.databases where state_desc='ONLINE' and name = SUBSTRING(@IncludeBase,1, CHARINDEX (',',@IncludeBase)-1)
					set @IncludeBase=LTRIM(RTRIM(SUBSTRING(@IncludeBase,CHARINDEX (',',@IncludeBase)+1, LEN(@IncludeBase))))
				end
			else
				begin
					insert into @BaseListIncl select name from sys.databases where state_desc='ONLINE' and name = @IncludeBase
					set @IncludeBase=''
				end
		 end

if @ExcludeBase<>'' 
		while len(@ExcludeBase)>0
		begin
			if CHARINDEX (',',@ExcludeBase)>0
				begin
					insert into @BaseListExcl select name from sys.databases where state_desc='ONLINE' and name = SUBSTRING(@ExcludeBase,1, CHARINDEX (',',@ExcludeBase)-1)
					set @ExcludeBase=LTRIM(RTRIM(SUBSTRING(@ExcludeBase,CHARINDEX (',',@ExcludeBase)+1, LEN(@ExcludeBase))))
				end
			else
				begin
					insert into @BaseListExcl select name from sys.databases where state_desc='ONLINE' and name = @ExcludeBase
					set @ExcludeBase=''
				end
		 end

-- итоговый список БД для бэкапа
delete from @BaseListIncl 
	where name in (select name from @BaseListExcl)

declare BaseList cursor for
	select name from @BaseListIncl 

declare @BaseName varchar(500) =''
declare @BackupType varchar(10) = ''

open BaseList
fetch next from BaseList into @BaseName  

while @@FETCH_STATUS = 0  
	begin  
	    
	--создаем папку для БД
	SET @tempcmd = 'md '+@Path+'\'+@BaseName
	exec xp_cmdshell @tempcmd, no_output

	--создаем архив
	set @tempname = @Path+'\'+@BaseName+'\'+@BaseName+'_'+CONVERT(varchar(8), GETDATE(), 112)+ '-' + REPLACE(CONVERT(varchar, GETDATE(),114),':','') +'_FULL_CopyOnly.BAK'
	backup database @BaseName to disk = @tempname WITH COPY_ONLY


	fetch next from BaseList into @BaseName  

    end
  
close BaseList  
deallocate BaseList 

Комментарии (15)


  1. nick-for-habr
    11.07.2022 11:28

    Здравствуйте.
    Не увидел в тексте: чем же предложенные простыни кода лучше штатных «Maintenance Plans»?
    Там всё это же накликивается в GIU за 5 минут, а ещё можно уведомления прикрутить на почту.
    Очень похоже на очередной велосипед на ещё один скрипт бэкапа базы. Без обид: по SQL куча материала, в т.ч. и на русском языке, да и принцип «бритвы Оккама» — это основа IT.


    1. Isiirk Автор
      11.07.2022 11:29

      Дискуссии на эту тему были в предыдущей статье... а в целом мне нравится именно такое решение - простое, предсказуемое, легко мониторить и настраивать


      1. nick-for-habr
        11.07.2022 12:39

        Хмм, это уже «записная книжка» получается, на статью никак не тянет. Но если спрятать код под спойлер (что хорошо бы и сделать), и добавить в заголовок в начале «Ещё один ...» — тогда и вопросов не возникнет ))
        Ещё совет, раз уж взялись собирать велосипеды: если меняете глобально параметры сервера из скрипта, делая вещи типа таких:

        -- включим xp_cmdshell (нужно для удаления старых бэкапов)
        EXEC sp_configure 'show advanced options', 1;  EXEC sp_configure 'xp_cmdshell', 1;  RECONFIGURE WITH OVERRIDE;
        то хорошим стилем будет в конце «прибраться за собой» — вернуть эти параметры в исходное состояние. Не зря же они были в таком состоянии на момент запуска скрипта, правильно?
        Показательно, что при более простой реализации через Планы обслуживания (название намекает, правда?) ничего этого ни пришлось бы делать, как и хардкодить переменные в скрипт. Это и имел в виду: ваше решение ни разу
        1. Не «более простое»: пользователю сначала нужно ручками «причесать скрипт», а для этого — минимум понять, что там накодировано, и зачем. И так — для каждой бэкапируемой базы. «Из коробки» ваш скрипт не работает;
        2. Не «более предсказуемое»: мы никогда не узнаем, что «что-то пошло не так», т.к. в скрипте нет никаких логов / алертов. Недоступно «захардкоженное» расположение бэкапа (не работает сеть)? Мимо. Не получилось удалить старую копию (нет прав)? Мимо. Целевой диск бэкапа полон (нет места)? Мимо. Никакой обработки ошибок в скрипте нет. Ну и непредсказуемое изменение глобальных параметров сервера не добавляет предсказуемости;
        3. Не «легко мониторить»: см. п. 2 выше. В плане мониторинга ваш скрипт сам по себе вообще ничего не может. Ну а внешними средствами его мониторинг ничем не отличается от мониторинга других скриптов и планов бэкапа;
        4. Не «легко настраивать»: см. п. 1 выше.
        5. Ну и от меня: не имеет никаких преимуществ перед штатным более простым решением, оправдывающих 4 пункта недостатов.

        Вы по сути воспроизвели штатных функционал SQL Server в урезанном и упрощённом виде. Это может быть полезно для личного развития, но выкладывать на всеобщее обозрение — зачем?
        А потом приходят админы и дико матерятся, продираясь через заросли подобных скриптов, в поисках причины — почему ничего не работает…


        1. Isiirk Автор
          11.07.2022 14:24

          У меня работает и много где еще с теми или иными модификациями, не вижу проблемы, если вы желаете под себя сделать, делайте как удобно, это не целая статья а всего лишь продолжение предыдущей, совпало с моими потребностями и я продолжил.


    1. uaggster
      12.07.2022 09:31
      +1

      Maintenance Plans не очень удобно поддерживать и тиражировать, когда у вас десятка два серверов и штук двести баз.
      ... а когда серверов - 200, а баз - 2000 - вообще мрак.


      1. Isiirk Автор
        12.07.2022 10:26

        Соглашусь


      1. nick-for-habr
        12.07.2022 12:08

        Ну это понятно…
        Maintenance Plans — это решение для простого и быстрого выполнения типовой задачи, которой является бэкап баз данных.
        В моём комментарии речь о том, что предлагаемые автором заметки скрипты не дают ничего сверх того, что может предложить штатный функционал. А ручной работы в итоге — больше: поправить каждый скрипт (т.к. в них всё захардкожено), создать джоб, настроить права и расписание… Через Maintenance Plans это делается быстрей и гибче. Кстати планы можно то же создавать скриптом, если мне не изменяет память…
        Для массового обслуживание конечно надо применять соответствующие средства — либо какие-то проприетарные продукты, либо самому выстраивать схему. Я например через Powershell это всё делаю, но можно и другие средства юзать.
        В целом суть моих постов — что ничего хоть сколько-нибудь нового в заметке нет: подобными скриптами забиты все форумы SQL-щиков, в том числе на русском: за 5 минут можно найти гораздо более универсальные и продвинутые варианты.


        1. Isiirk Автор
          12.07.2022 13:01

          Один раз настроил задание агента и залил скриптом на все серверы. Зачем проприетарные продукты, здесь все прозрачно и вы можете под себя подстроить любой момент. Но еще раз повторю, каждому своё. Мне удобно, делюсь со всеми, знаю много людей, кто любит простые и удобные штатные средства


        1. Isiirk Автор
          12.07.2022 13:02

          Ну поищите подобные скрипты по забитым форумам, они все либо не полноценны, либо не универсальны


  1. InChaos
    11.07.2022 13:48

    для создания бэкапов "длительного хранения". Создаются с опцией ONLY_COPY и не участвуют в общей цепочке восстановления

    Э-э-з, что простите? С какого перепугу Copy_Only предназначен для длительного хранения? Сами же пишете - "он не участвует в цепочке восстановления", т.е. фактически это "неофициальный бэкап". Обычно используется для развертывания базы где то на тестовом контуре, чтоб как раз не очищал транзакции и не висел в цепочке бэкапов. Хотя по правильному и на тестовый контур берется и накатывается обычный, нормальный Full бэкап с определенной периодичностью.


    1. Isiirk Автор
      11.07.2022 14:22

      Кавычки видим же? Что в нем не официального? Просто флаг архивации не сбрасывается со страниц. У меня есть требования хранить 5 лет, я и храню таким способом длительные периоды, основной скрип делает ретеншены


    1. uaggster
      12.07.2022 09:35

      Например я так копии "на момент окончания отчетного периода" храню.

      Они должны храниться вечно, но при этом не подразумевается, что их когда-то развернут для восстановления оперативной операционной базы. Ну, в случае атомной войны разве что.


      1. Isiirk Автор
        12.07.2022 10:31

        Они должны хранится согласно политике компании и да, они не для восстановления оперативной БД


        1. InChaos
          13.07.2022 08:17

          Про политику я в курсе, в разных компаниях она разная, но присутствует.

          Мой вопрос был конкретно, с чего взяли что именно COPY_ONLY предназначено для длительного хранения, и чем для этих целеей не подходит обычный фулл бэкап (сакжем на 31 или 01 число)? Пруф есть или просто Вы так считаете? Если последнее, то надо было так и писать что "для своего (фирмы) удобствая создаю для длительного хранения бэкапы COPY_ONLY", а не категоричное утверждение "для создания бэкапов "длительного хранения создаются с опцией ONLY_COPY"


  1. Isiirk Автор
    13.07.2022 09:32

    А где категорично то? Я с самого начал пишу, как нравится так и кроите ) only_copy просто не сбрасывает со страниц бд метку... Такая же копия как и все другие, только в цепочке восстановления не фиксируется )