В серии статей, посвященных группам доступности AlwaysOn и заданиям агентов SQL Server, мне уже доводилось излагать принципы, лежащие в основе настоящей статьи. Так что отчасти мне придется повторяться. Однако я пришел к выводу, что тема эта весьма важна и ей стоит посвятить отдельный материал.
Как я отмечал в статьях упомянутого выше цикла, многие администраторы баз данных на дух не переносят планы обслуживания SQL Server. И, конечно, чем больше опыта у меня за плечами, тем труднее мне отказаться от мысли, что практически все задания, включенные в планы обслуживания, подготовлены стажерами (или людьми, не читавшими рекомендаций по работе с SQL Server). Рассмотрим для примера задание Rebuild Index Task. При его выполнении невозможно указать, какой уровень фрагментации влечет за собой необходимость выполнения процедуры Rebuild. Иными словами, если вы настроите задание для выполнения в определенной базе данных, оно перестроит все индексы вне зависимости от того, есть в том необходимость или нет. Если вы случайно выполните задание два раза подряд, все ваши индексы будут перестроены, и опять же дважды.
Соответственно, если задания, предназначенные для включения в планы обслуживания, в большинстве своем не выдерживают никакой критики, то базовый процессор, используемый для обработки планов обслуживания, всегда производил на меня благоприятное впечатление, поскольку его отличают мощность и богатые функциональные возможности. А это, разумеется, всякому очевидно, ибо здесь мы говорим о SSIS. К тому же я всегда с симпатией относился к логике и механизму реализации, воплощенным в заданиях Back Up Database Tasks. Пусть они явно не дотягивают до уровня некоторых продуктов сторонних производителей, представленных на рынке, пусть сценарии Олла Халленгрена, как утверждается, превосходят их во многих отношениях. Однако резервные копии планов обслуживания характеризуются, помимо прочего, простотой и логической стройностью, которая меня всегда подкупала. К примеру, способность размещать резервные копии той или иной базы данных в особую папку представляет собой, с моей точки зрения, большое достижение. Предположим, в моей организации произошел аварийный сбой, при этом у меня нет желания задействовать графический интерфейс и Backup History для выяснения, какие именно файлы мне потребуются. Так вот, можете мне поверить: то обстоятельство, что все эти файлы свалены в гигантскую «кучу-малу», не будет вызывать в моей душе восторга. А вот возможность безо всяких затруднений выполнять операции по удалению просроченных резервных копий (через задание Maintenance Cleanup Task), на мой взгляд, очень ценна. Есть что-то простое и изящное в решении, в соответствии с которым вы просто указываете определенное число дней (или часов) в качестве срока хранения резервных копий базы данных.
Замена резервных копий плана обслуживания сценарием
С другой стороны, планы обслуживания действительно имеют свои недостатки. Так, трудно (хотя и возможно) назначить владельцем плана обслуживания учетную запись SysAdmin. К тому же пусть возможности службы SSIS и весьма широки, сама идея «переноса» планов обслуживания с одного сервера на другой (или постоянной синхронизации их на нескольких серверах) сопряжена с такими серьезными затруднениями, что некоторое время назад я взялся за составление ряда сценариев. Эти сценарии в функциональном отношении в большей или меньшей степени обеспечивают те же ключевые преимущества, которые предоставляют планы обслуживания, позволяя при этом обходиться без таких планов.
Приведу один из этих сценариев, используемый мной для экземпляров, не относящихся к категории SQL Server Express (см. листинг 1).
Как видите, все довольно просто: вы указываете, какие базы данных или типы баз данных хотите зарезервировать и какие типы резервных копий собираетесь запускать, а также вводите информацию о пути и детали «сохранения». В противном случае система выдаст резервные копии, практически неотличимые от тех, что генерируются с помощью функции Maintenance Plan Backup (вплоть до имен файлов). Хотя если присмотреться внимательнее, вы заметите, что некоторые средства и возможности в вашей копии не представлены. Если без них вам трудно обойтись, реализуйте их по мере необходимости.
Я использовал аналогичный подход при выполнении резервных копий в копиях SQL Server Express, но фактически удалял возможность WITH COMPRESS и изменял сигнатуру хранимой процедуры, чтобы несколько облегчить доступ к ней через файлы. bat или. ps1 (см. листинг 2).
Используя параметр @CleanupTime (в часах), я избавляю себя от необходимости дополнительно запускать запрос или операцию с целью указания параметра @olderThan, как в сценарии листинга 1. А это в свою очередь означает, что я могу удерживать все необходимые данные для вызова данной версии SQL Express из одной строки. Например, следующую вставку я вношу в файл. bat, чтобы обеспечить получение полных резервных копий пользовательских баз данных на экземпляре SQL Server Express:
REM FULL Backups of User DBs: osql -S. -E -Q "EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = ' [USER_DBS]', @BackupDirectory = 'D:\SQLBackups\User', @CleanupTime = 72;"
Можно использовать и резервные копии T-Log, но с @BackupType со значением ‘LOG’. В таком случае останется только настроить задание с помощью планировщика Windows Task Scheduler, чтобы обеспечить выполнение.
/* -- Эта хранимая процедура замещает резервные копии SQL Server Maintenance Task. -- В нее достаточно вставить путь, список dbs, который нужно зарезервировать, и указать категорию резервной копии... -- а также проставить временную метку для удаления данных, введенных ранее времени X. -- ВНИМАНИЕ: на Express и Web ... сжатие не поддерживается -- Резервные копии системных баз данных: DECLARE @olderThan datetime; SET @olderThan = DATEADD(dd, -3, GETDATE()); EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = '[SYSTEM_DBS]', @BackupDirectory = 'D:\SQLBackups\System', @OlderBackupDeletionTime = @olderThan; GO -- Полные резервные копии всех пользовательских баз данных: DECLARE @olderThan datetime; SET @olderThan = DATEADD(hh, -48, GETDATE()); EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = '[USER_DBS]', @BackupDirectory = 'D:\SQLBackups\User', @OlderBackupDeletionTime = @olderThan; GO -- Полные резервные копии указанных пользовательских баз данных: DECLARE @olderThan datetime; SET @olderThan = DATEADD(hh, 25, GETDATE()); EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = 'meddling,ssv2', @BackupDirectory = 'D:\SQLBackups\User', @OlderBackupDeletionTime = @olderThan; GO -- Разностные резервные копии указанных баз данных: DECLARE @olderThan datetime; SET @olderThan = DATEADD(hh, -48, GETDATE()); EXEC dbo.dba_DatabaseBackups @BackupType = 'DIFF', @DatabasesToBackup = 'meddling,ssv2', @BackupDirectory = 'D:\SQLBackups\User', @OlderBackupDeletionTime = @olderThan; GO -- Резервные копии T-Log всех пользовательских баз данных: DECLARE @olderThan datetime; SET @olderThan = DATEADD(hh, -36, GETDATE()); EXEC dbo.dba_DatabaseBackups @BackupType = 'LOG', @DatabasesToBackup = '[USER_DBS]', @BackupDirectory = 'D:\SQLBackups\User', @OlderBackupDeletionTime = @olderThan; GO */ USE master; GO IF OBJECT_ID('dbo.dba_DatabaseBackups','P') IS NOT NULL DROP PROC dbo.dba_DatabaseBackups; GO CREATE PROC dbo.dba_DatabaseBackups @BackupType sysname, @DatabasesToBackup nvarchar(1000), @BackupDirectory sysname, @OlderBackupDeletionTime datetime, @PrintOnly bit = 0 AS SET NOCOUNT ON; DECLARE @jobStart datetime; SET @jobStart = GETDATE(); -- проверка IF UPPER(@BackupType) NOT IN ('FULL', 'DIFF','LOG') BEGIN PRINT 'Usage: @BackupType = FULL|DIFF|LOG'; RAISERROR('Invalid @BackupType Specified.', 16, 1); END IF @OlderBackupDeletionTime >= GETDATE() BEGIN RAISERROR('Invalid @OlderBackupDeletionTime - greater than or equal to NOW.', 16, 1); END -- определяем базы данных: DECLARE @targetDatabases TABLE ( database_name sysname NOT NULL ); IF UPPER(@DatabasesToBackup) = '[SYSTEM_DBS]' BEGIN INSERT INTO @targetDatabases (database_name) SELECT 'master' UNION SELECT 'msdb' UNION SELECT 'model'; END IF UPPER(@DatabasesToBackup) = '[USER_DBS]' BEGIN IF @BackupType = 'LOG' INSERT INTO @targetDatabases (database_name) SELECT name FROM sys.databases WHERE recovery_model_desc = 'FULL' AND name NOT IN ('master', 'model', 'msdb', 'tempdb') ORDER BY name; ELSE INSERT INTO @targetDatabases (database_name) SELECT name FROM sys.databases WHERE name NOT IN ('master', 'model', 'msdb','tempdb') ORDER BY name; END IF (SELECT COUNT(*) FROM @targetDatabases) <= 0 BEGIN -- десериализуем список баз данных, подлежащих резервированию: SELECT TOP 400 IDENTITY(int, 1, 1) as N INTO #Tally FROM sys.columns; DECLARE @SerializedDbs nvarchar(1200); SET @SerializedDbs = ',' + REPLACE(@DatabasesToBackup, ' ', '') + ','; INSERT INTO @targetDatabases (database_name) SELECT SUBSTRING(@SerializedDbs, N + 1, CHARINDEX(',', @SerializedDbs, N + 1) - N - 1) FROM #Tally WHERE N < LEN(@SerializedDbs) AND SUBSTRING(@SerializedDbs, N, 1) = ','; IF @BackupType = 'LOG' BEGIN DELETE FROM @targetDatabases WHERE database_name NOT IN ( SELECT name FROM sys.databases WHERE recovery_model_desc = 'FULL' ); END ELSE DELETE FROM @targetDatabases WHERE database_name NOT IN (SELECT name FROM sys.databases); END -- удостоверяемся в том, что мы что-то получили: IF (SELECT COUNT(*) FROM @targetDatabases) <= 0 BEGIN PRINT 'Usage: @DatabasesToBackup = [SYSTEM_DBS]|[USER_DBS]|dbname1,dbname2,dbname3,etc'; RAISERROR('No databases for backup.', 16, 1); END -- нормализуем путь: IF(RIGHT(@BackupDirectory, 1) = ‘\’) SET @BackupDirectory = LEFT(@BackupDirectory, LEN(@BackupDirectory) - 1); -- Начинаем резервные копии (???): DECLARE backups FAST_FORWARD FOR SELECT database_name FROM @targetDatabases ORDER BY database_name; DECLARE @currentDB sysname; DECLARE @backupPath sysname; DECLARE @backupStatement nvarchar(2000); DECLARE @backupName sysname; DECLARE @now datetime; DECLARE @timestamp sysname; DECLARE @extension sysname; DECLARE @offset sysname; DECLARE @verifyStatement nvarchar(2000); DECLARE @Errors TABLE ( ErrorID int IDENTITY(1,1) NOT NULL, [Database] sysname NOT NULL, ErrorMessage nvarchar(2000) ); DECLARE @ErrorMessage sysname; OPEN backups; FETCH NEXT FROM backups INTO @currentDB; WHILE @@FETCH_STATUS = 0 BEGIN SET @backupPath = @BackupDirectory + N'\' + @currentDB; -- удостоверяемся в том, что подкаталог существует: IF @PrintOnly = 1 BEGIN PRINT 'Verify/Create Directory: ' + @backupPath; END ELSE EXECUTE master.dbo.xp_create_subdir @backupPath; -- создаем имя резервной копии: SET @extension = ‘.bak’; IF @BackupType = 'LOG' SET @extension = '.trn'; SET @now = GETDATE(); SET @timestamp = REPLACE(REPLACE(REPLACE(CONVERT (sysname, @now, 120), '-','_'), ':',''), ' ', '_'); SET @offset = RIGHT(CAST(CAST(RAND() AS decimal(12,11)) AS varchar(20)),7); SET @backupName = @currentDB + '_backup_' + @timestamp + '_' + @offset + @extension; -- главное отличие данной резервной копии от резервной копии плана обслуживания: CHECKSUM... SET @backupStatement = 'BACKUP ' + QUOTENAME (@currentDB, '[]') + ' TO DISK = N''' + @backupPath + '\' + @backupName + ''' WITH COMPRESSION, NOFORMAT, NOINIT, NAME = N''' + @backupName + ''', SKIP, REWIND, NOUNLOAD, CHECKSUM, STATS = 25;' IF @BackupType IN ('FULL', 'DIFF') BEGIN SET @backupStatement = REPLACE(@backupStatement, '', 'DATABASE'); IF @BackupType = 'DIFF' SET @backupStatement = REPLACE (@backupStatement, '', 'DIFFERENTIAL,'); ELSE SET @backupStatement = REPLACE (@backupStatement, '{1}', ''); END ELSE BEGIN -- log file backup SET @backupStatement = REPLACE (@backupStatement, '', 'LOG'); SET @backupStatement = REPLACE(@backupStatement, '', ''); END SET @verifyStatement = 'RESTORE VERIFYONLY FROM DISK = N''' + @backupPath + '\' + @backupName + ''' WITH NOUNLOAD, NOREWIND;'; BEGIN TRY IF @PrintOnly = 1 BEGIN PRINT @backupStatement; PRINT @verifyStatement; END ELSE BEGIN EXEC sp_executesql @backupStatement; EXEC sp_executesql @verifyStatement; END END TRY BEGIN CATCH SELECT @ErrorMessage = ERROR_MESSAGE(); INSERT INTO @Errors ([Database], ErrorMessage) VALUES (@currentDB, @ErrorMessage); END CATCH FETCH NEXT FROM backups INTO @currentDB; END; CLOSE backups; DEALLOCATE backups; -- Осуществляем удаление любых/всех файлов по мере необходимости: DECLARE @deleteStatement nvarchar(2000); SET @deleteStatement = 'EXECUTE master.dbo.xp_delete_file 0, N''' + @BackupDirectory + ''', N''' + REPLACE(@extension, '.','') + ''', N''' + REPLACE(CONVERT(nvarchar(20), @OlderBackupDeletionTime, 120), ' ', 'T') + ''', 1;'; BEGIN TRY IF @PrintOnly = 1 PRINT @deleteStatement ELSE EXEC sp_executesql @deleteStatement; END TRY BEGIN CATCH SELECT @ErrorMessage = ERROR_MESSAGE(); INSERT INTO @Errors ([Database], ErrorMessage) VALUES ('File Deletion', @ErrorMessage); END CATCH IF (SELECT COUNT(*) FROM @Errors) > 0 BEGIN PRINT 'The Following Errors were Detectected: '; DECLARE errors FAST_FORWARD FOR SELECT [Database],[ErrorMessage] FROM @Errors ORDER BY ErrorID; OPEN errors; FETCH NEXT FROM errors INTO @currentDB, @ErrorMessage; WHILE @@FETCH_STATUS = 0 BEGIN PRINT 'DATABASE/OPERATION: ' + @currentDB + ' -> ' + @ErrorMessage; FETCH NEXT FROM errors INTO @currentDB, @ErrorMessage; END CLOSE errors; DEALLOCATE errors; -- Инициируем ошибку, чтобы знать, что возникли проблемы: RAISERROR('Unexpected errors executing backups - see output.', 16, 1); END RETURN 0; GO
/* -- Эта хранимая процедура замещает резервные копии заданий SQL Server Maintenance Task. -- В нее достаточно вставить путь, список dbs, который нужно зарезервировать, и указать категорию резервной копии... -- а также проставить временную метку для удаления данных, введенных ранее времени X. -- Резервные копии системных баз данных: EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = '[SYSTEM_DBS]', @BackupDirectory = 'D:\SQLBackups\System', @CleanupTime = 72; GO -- Полные копии ВСЕХ пользовательских баз данных: EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = '[USER_DBS]', @BackupDirectory = 'D:\SQLBackups\User', @CleanupTime = 48; GO -- Полные резервные копии указанных пользовательских баз данных: EXEC dbo.dba_DatabaseBackups @BackupType = 'FULL', @DatabasesToBackup = 'meddling,ssv2', @BackupDirectory = 'D:\SQLBackups\User', @CleanupTime = 25; GO -- разностные резервные копии указанных баз данных: EXEC dbo.dba_DatabaseBackups @BackupType = 'DIFF', @DatabasesToBackup = 'meddling,ssv2', @BackupDirectory = 'D:\SQLBackups\User', @CleanupTime = 48; GO -- Резервные копии T-Log всех пользовательских баз данных: EXEC dbo.dba_DatabaseBackups @BackupType = 'LOG', @DatabasesToBackup = '[USER_DBS]', @BackupDirectory = 'D:\SQLBackups\User', @CleanupTime = 36; GO */ USE master; GO IF OBJECT_ID('dbo.dba_DatabaseBackups','P') IS NOT NULL DROP PROC dbo.dba_DatabaseBackups; GO CREATE PROC dbo.dba_DatabaseBackups @BackupType sysname, @DatabasesToBackup nvarchar(1000), @BackupDirectory sysname, @CleanupTime int = 72, -- в часах... @PrintOnly bit = 0 AS SET NOCOUNT ON; DECLARE @jobStart datetime; SET @jobStart = GETDATE(); -- проверяем IF UPPER(@BackupType) NOT IN ('FULL', 'DIFF','LOG') BEGIN PRINT 'Usage: @BackupType = FULL|DIFF|LOG'; RAISERROR('Invalid @BackupType Specified.', 16, 1); END -- переводим часовые настройки: DECLARE @OlderBackupDeletionTime datetime; SET @OlderBackupDeletionTime = DATEADD(hh, 0 - @CleanupTime, GETDATE()); IF @OlderBackupDeletionTime >= GETDATE() BEGIN RAISERROR(‘Invalid @OlderBackupDeletionTime - greater than or equal to NOW.’, 16, 1); END -- определяем базы данных: DECLARE @targetDatabases TABLE ( database_name sysname NOT NULL ); IF UPPER(@DatabasesToBackup) = '[SYSTEM_DBS]' BEGIN INSERT INTO @targetDatabases (database_name) SELECT 'master' UNION SELECT 'msdb' UNION SELECT 'model'; END IF UPPER(@DatabasesToBackup) = '[USER_DBS]' BEGIN IF @BackupType = 'LOG' INSERT INTO @targetDatabases (database_name) SELECT name FROM sys.databases WHERE recovery_model_desc = 'FULL' AND name NOT IN ('master', 'model', 'msdb', 'tempdb') ORDER BY name; ELSE INSERT INTO @targetDatabases (database_name) SELECT name FROM sys.databases WHERE name NOT IN ('master', 'model', 'msdb','tempdb') ORDER BY name; END IF (SELECT COUNT(*) FROM @targetDatabases) <= 0 BEGIN -- десериализуем список баз данных, подлежащих резервному копированию: SELECT TOP 400 IDENTITY(int, 1, 1) as N INTO #Tally FROM sys.columns; DECLARE @SerializedDbs nvarchar(1200); SET @SerializedDbs = ',' + REPLACE(@DatabasesToBackup, ' ', '') + ','; INSERT INTO @targetDatabases (database_name) SELECT SUBSTRING(@SerializedDbs, N + 1, CHARINDEX(',', @SerializedDbs, N + 1) - N - 1) FROM #Tally WHERE N < LEN(@SerializedDbs) AND SUBSTRING(@SerializedDbs, N, 1) = ','; IF @BackupType = 'LOG' BEGIN DELETE FROM @targetDatabases WHERE database_name NOT IN ( SELECT name FROM sys.databases WHERE recovery_model_desc = 'FULL' ); END ELSE DELETE FROM @targetDatabases WHERE database_name NOT IN (SELECT name FROM sys.databases); END -- удостоверяемся в том, что мы что-то получили: IF (SELECT COUNT(*) FROM @targetDatabases) <= 0 BEGIN PRINT 'Usage: @DatabasesToBackup = [SYSTEM_DBS]|[USER_DBS]|dbname1,dbname2,dbname3,etc'; RAISERROR('No databases for backup.', 16, 1); END -- нормализуем путь: IF(RIGHT(@BackupDirectory, 1) = ‘\’) SET @BackupDirectory = LEFT(@BackupDirectory, LEN(@BackupDirectory) - 1); -- Начинаем резервные копии (???): DECLARE backups FAST_FORWARD FOR SELECT database_name FROM @targetDatabases ORDER BY database_name; DECLARE @currentDB sysname; DECLARE @backupPath sysname; DECLARE @backupStatement nvarchar(2000); DECLARE @backupName sysname; DECLARE @now datetime; DECLARE @timestamp sysname; DECLARE @extension sysname; DECLARE @offset sysname; DECLARE @verifyStatement nvarchar(2000); DECLARE @Errors TABLE ( ErrorID int IDENTITY(1,1) NOT NULL, [Database] sysname NOT NULL, ErrorMessage nvarchar(2000) ); DECLARE @ErrorMessage sysname; OPEN backups; FETCH NEXT FROM backups INTO @currentDB; WHILE @@FETCH_STATUS = 0 BEGIN SET @backupPath = @BackupDirectory + N'\' + @currentDB; -- удостоверяемся в том, что подкаталог существует: IF @PrintOnly = 1 BEGIN PRINT 'Verify/Create Directory: ' + @backupPath; END ELSE EXECUTE master.dbo.xp_create_subdir @backupPath; -- создаем имя резервной копии: SET @extension = ‘.bak’; IF @BackupType = 'LOG' SET @extension = '.trn'; SET @now = GETDATE(); SET @timestamp = REPLACE(REPLACE(REPLACE(CONVERT (sysname, @now, 120), '-','_'), ':',''), ' ', '_'); SET @offset = RIGHT(CAST(CAST(RAND() AS decimal(12,11)) AS varchar(20)),7); SET @backupName = @currentDB + '_backup_' + @timestamp + '_' + @offset + @extension; -- главное отличие данной резервной копии от резервной копии плана обслуживания: CHECKSUM... SET @backupStatement = 'BACKUP ' + QUOTENAME (@currentDB, '[]') + ' TO DISK = N''' + @backupPath + '\' + @backupName + ''' WITH {1} NOFORMAT, NOINIT, NAME = N''' + @backupName + ''', SKIP, REWIND, NOUNLOAD, CHECKSUM;' IF @BackupType IN ('FULL', 'DIFF') BEGIN SET @backupStatement = REPLACE(@backupStatement, '', 'DATABASE'); IF @BackupType = 'DIFF' SET @backupStatement = REPLACE (@backupStatement, '', 'DIFFERENTIAL,'); ELSE SET @backupStatement = REPLACE (@backupStatement, '', ''); END ELSE BEGIN -- log file backup SET @backupStatement = REPLACE (@backupStatement, '{0}', 'LOG'); SET @backupStatement = REPLACE (@backupStatement, '', ''); END SET @verifyStatement = 'RESTORE VERIFYONLY FROM DISK = N''' + @backupPath + '\' + @backupName + ''' WITH NOUNLOAD, NOREWIND;'; BEGIN TRY IF @PrintOnly = 1 BEGIN PRINT @backupStatement; PRINT @verifyStatement; END ELSE BEGIN EXEC sp_executesql @backupStatement; EXEC sp_executesql @verifyStatement; END END TRY BEGIN CATCH SELECT @ErrorMessage = ERROR_MESSAGE(); INSERT INTO @Errors ([Database], ErrorMessage) VALUES (@currentDB, @ErrorMessage); END CATCH FETCH NEXT FROM backups INTO @currentDB; END; CLOSE backups; DEALLOCATE backups; -- Теперь удаляем любые/все файлы по мере необходимости: DECLARE @deleteStatement nvarchar(2000); SET @deleteStatement = 'EXECUTE master.dbo.xp_delete_file 0, N''' + @BackupDirectory + ''', N''' + REPLACE(@extension, '.','') + ''', N''' + REPLACE(CONVERT(nvarchar(20), @OlderBackupDeletionTime, 120), ' ', 'T') + ''', 1;'; BEGIN TRY IF @PrintOnly = 1 PRINT @deleteStatement ELSE EXEC sp_executesql @deleteStatement; END TRY BEGIN CATCH SELECT @ErrorMessage = ERROR_MESSAGE(); INSERT INTO @Errors ([Database], ErrorMessage) VALUES ('File Deletion', @ErrorMessage); END CATCH IF (SELECT COUNT(*) FROM @Errors) > 0 BEGIN PRINT 'The Following Errors were Detectected: '; DECLARE errors FAST_FORWARD FOR SELECT [Database],[ErrorMessage] FROM @Errors ORDER BY ErrorID; OPEN errors; FETCH NEXT FROM errors INTO @currentDB, @ErrorMessage; WHILE @@FETCH_STATUS = 0 BEGIN PRINT 'DATABASE/OPERATION: ' + @currentDB + ' -> ' + @ErrorMessage; FETCH NEXT FROM errors INTO @currentDB, @ErrorMessage; END CLOSE errors; DEALLOCATE errors; -- Инициируем ошибку, чтобы знать, что возникли проблемы: RAISERROR('Unexpected errors executing backups - see output.', 16, 1); END RETURN 0; GO