Transferir todos os bancos de dados do MS SQL Server para outra máquina

Recentemente, houve a necessidade de transferir todos os bancos de dados (> 50 em uma instância do SQL Server) do ambiente de desenvolvimento para outra instância do SQL Server, localizada em um hardware diferente. Eu queria minimizar o trabalho manual e fazer tudo o mais rápido possível.

aviso Legal


Os scripts são gravados para uma situação específica: este é um ambiente de desenvolvimento, todos os bancos de dados em um modelo de recuperação simples, arquivos de dados e logs de transações estão no mesmo heap.

Tudo o que está escrito abaixo se aplica apenas a essa situação, mas você pode finalizá-los facilmente (você mesmo) sem muito esforço.

Os scripts não usam o novo STRING_AGG e outras coisas interessantes; portanto, tudo deve funcionar a partir do SQL Server 2008 (ou 2008 R2, não me lembro onde a compressão de backup apareceu). Para versões mais antigas, é necessário remover WITH COMPRESSION do comando backup, mas pode não haver mais diferenças de tempo com a cópia de arquivos.

Esta não é uma instrução - "como" fazer essa transferência. Esta é uma demonstração de como os metadados podem ser usados ​​no SQL dinâmico.

Obviamente, a maneira mais rápida seria simplesmente reconectar a prateleira de disco ao novo servidor, mas essa não era a nossa opção. Desanexar - copiar - Anexar foi considerado, mas não se encaixou, pois o canal era bastante estreito e a transferência do banco de dados de forma descompactada levaria um longo período de tempo.

Como resultado, decidimos fazer um backup com compressão na bola no novo servidor e depois restaurá-lo lá. O ferro no local antigo e no novo não é ruim, o backup não é ruim, o ganho de tempo também não é ruim.

Então o "gerador de scripts" foi escrito:

DECLARE @unc_backup_path AS varchar(max) = '\\newServer\backup_share\'
	, @local_backup_path AS varchar(max) = 'E:\Backup\'
	, @new_data_path as varchar(max) = 'D:\SQLServer\data\';

SELECT name	
	, 'BACKUP DATABASE [' + name + '] TO DISK = ''' + @unc_backup_path + name + '.bak'' WITH INIT, COPY_ONLY, STATS = 5;' AS backup_command
	, 'ALTER DATABASE [' + name + '] SET OFFLINE WITH ROLLBACK IMMEDIATE;' AS offline_command
	, 'RESTORE DATABASE [' + name + '] FROM DISK = ''' + @local_backup_path + name + '.bak'' WITH ' 
		+ (
			SELECT 'MOVE ''' + mf.name + ''' TO ''' + 
				@new_data_path + REVERSE(LEFT(REVERSE(mf.physical_name), CHARINDEX('\', REVERSE(mf.physical_name))-1)) +
				''', '
			FROM sys.master_files mf
			WHERE mf.database_id = d.database_id
			FOR XML PATH('')
		) + 'REPLACE, RECOVERY, STATS = 5;' AS restore_command
FROM sys.databases d
WHERE database_id > 4 AND state_desc = N'ONLINE';

Na saída, obtemos comandos prontos para criar backups no lugar certo, transferindo o banco de dados offline, para que seus usuários não possam trabalhar com eles no servidor antigo e scripts para restaurar os backups recebidos no novo servidor (com transferência automática de todos os arquivos de dados e logs de transações para o local especificado )

O problema é que alguém precisa sentar e executar todos os scripts por vez (recuperação de backup offline) ou alguém deve primeiro iniciar todos os backups, desconectar todos os bancos de dados e restaurar tudo - há menos ações, mas é necessário sentar e acompanhar.

Eu queria automatizar todas essas operações. Por um lado, tudo é simples - já existem comandos prontos, enrole o cursor e execute. E, em princípio, fiz exatamente isso, adicionei um novo servidor como servidor vinculado ao antigo e o iniciei. No servidor local, o comando foi executado por meio de EXECUTE (@sql_text); no servidor vinculado, EXECUTE (@sql_text) AT [linkedServerName].

Assim, as operações foram realizadas seqüencialmente - backup local, traduzindo o banco de dados local offline, restaurando para o servidor vinculado. Tudo começou, parabéns, mas me pareceu que você pode acelerar um pouco o processo se os backups e restaurações forem executados independentemente um do outro.
Em seguida, o cursor inventado foi dividido em duas partes - no servidor antigo do cursor, é feito backup e transferência de cada banco de dados offline, após o que o segundo servidor deve entender que uma nova tarefa apareceu e executar uma restauração do banco de dados. Para implementar esse mecanismo, usei um registro em uma tabela em um servidor vinculado e um loop infinito (eu estava com preguiça de apresentar critérios de parada), que procura ver se há novos registros e está tentando restaurar algo, se houver.

Decisão


No servidor antigo, uma tabela temporária global ## CommandList é criada e preenchida, na qual todos os comandos são coletados e será possível rastrear o status dos backups no mesmo local. A tabela é global para que, a qualquer momento, em outra sessão, você possa ver o que está acontecendo lá agora.

DECLARE @unc_backup_path AS varchar(max) = 'D:\SQLServer\backup\' --       
	, @local_backup_path AS varchar(max) = 'D:\SQLServer\backup\'	--        
	, @new_data_path as varchar(max) = 'D:\SQLServer\data\';		--      ,    

SET NOCOUNT ON;

IF OBJECT_ID ('tempdb..##CommandList', 'U') IS NULL
	CREATE TABLE ##CommandList (
		dbName sysname unique			-- 
		, backup_command varchar(max)	--   
		, offline_command varchar(max)	--        
		, restore_command varchar(max)	--       
		, processed bit				-- : NULL -  , 0 -  , 1 - 
		, start_dt datetime			--  
		, finish_dt datetime			--  
		, error_msg varchar(max)		--  ,  
	);

INSERT INTO ##CommandList (dbname, backup_command, offline_command, restore_command)
SELECT name	
	, 'BACKUP DATABASE [' + name + '] TO DISK = ''' + @unc_backup_path + name + '.bak'' WITH INIT, COPY_ONLY, STATS = 5;' AS backup_command -- INIT -      
	, 'ALTER DATABASE [' + name + '] SET OFFLINE WITH ROLLBACK IMMEDIATE;' AS offline_command
	, 'RESTORE DATABASE [' + name + '] FROM DISK = ''' + @local_backup_path + name + '.bak'' WITH ' 
		+ (
			SELECT 'MOVE ''' + mf.name + ''' TO ''' + 
				@new_data_path + REVERSE(LEFT(REVERSE(mf.physical_name), CHARINDEX('\', REVERSE(mf.physical_name))-1)) +
				''', '	
			FROM sys.master_files mf
			WHERE mf.database_id = d.database_id
			FOR XML PATH('')
		) + 'REPLACE, RECOVERY, STATS = 5;' AS restore_command	
FROM sys.databases d
WHERE database_id > 4 
	AND state_desc = N'ONLINE'
	AND name NOT IN (SELECT dbname FROM ##CommandList)
	AND name <> 'Maintenance';	--  linked server -    ,   ,    "linked server"

Vamos ver o que aconteceu lá (SELECT * FROM ## CommandList):



Ótimo, todos os comandos são coletados lá para fazer backup / restaurar todos os bancos de dados necessários.

O banco de dados de Manutenção foi criado no novo servidor e a tabela CommandList está nele, que conterá informações sobre a restauração dos bancos de dados:

USE [Maintenance]
GO

CREATE TABLE CommandList (
	dbName sysname unique				-- 
	, restore_command varchar(max)		--  
	, processed bit						-- 
	, creation_dt datetime DEFAULT GETDATE()	--  
	, start_dt datetime					--  
	, finish_dt datetime					--  
	, error_msg varchar(max)				-- ,  
);

Um servidor vinculado foi configurado no servidor antigo, observando a nova instância do SQL Server. Os scripts que são apresentados neste post, escrevi em casa e não me incomodei com uma nova instância, usei uma e a conectei como servidor vinculado a mim mesmo. Portanto, aqui eu tenho os mesmos caminhos e não caminho local.

Agora você pode declarar um cursor para fazer backup dos bancos de dados, desconectá-los e escrever um comando para restaurar no servidor vinculado:

DECLARE @dbname AS sysname
	, @backup_cmd AS varchar(max)
	, @restore_cmd AS varchar(max)
	, @offline_cmd AS varchar(max);

DECLARE MoveDatabase CURSOR
FOR 
SELECT dbName, backup_command, offline_command, restore_command
FROM ##CommandList
WHERE processed IS NULL;

OPEN MoveDatabase;

FETCH NEXT FROM MoveDatabase INTO @dbname, @backup_cmd, @offline_cmd, @restore_cmd;

WHILE @@FETCH_STATUS = 0
	BEGIN
		--    ,  :
		--  
		--   -      
		--    ,      
		--     

		--    
		UPDATE ##CommandList
		SET start_dt = GETDATE()
		WHERE dbName = @dbname;

		BEGIN TRY
			
			RAISERROR ('  %s', 0, 1, @dbname) WITH NOWAIT; --   messages   
			
			--  
			EXEC (@backup_cmd);

			RAISERROR ('    %s', 0, 1, @dbname) WITH NOWAIT;

			--    -  linked server
			INSERT INTO [(LOCAL)].[Maintenance].[dbo].[CommandList] (dbName, restore_command)
			VALUES (@dbname, @restore_cmd);

			RAISERROR (' %s  OFFLINE', 0, 1, @dbname) WITH NOWAIT;

			--    
			EXEC (@offline_cmd);

			--  ,    
			UPDATE ##CommandList
			SET processed = 0
				, finish_dt = GETDATE()
			WHERE dbName = @dbname;

		END TRY
		BEGIN CATCH
			
			RAISERROR ('    %s.   error_msg  ##CommandList', 0, 1, @dbname) WITH NOWAIT;

			--  -   ,      
			UPDATE ##CommandList
			SET processed = 1
				, finish_dt = GETDATE()
				, error_msg = ERROR_MESSAGE();

		END CATCH

		FETCH NEXT FROM MoveDatabase INTO @dbname, @backup_cmd, @offline_cmd, @restore_cmd;
	END

CLOSE MoveDatabase;

DEALLOCATE MoveDatabase;

-- 
SELECT dbName
	, CASE processed WHEN 1 THEN '' WHEN 0 THEN '' ELSE ' ' END as Status 
	, start_dt
	, finish_dt
	, error_msg
FROM ##CommandList
ORDER BY start_dt;

DROP TABLE ##CommandList;

Cada ação é “registrada” na guia Mensagens no SSMS - lá você pode observar a ação atual. Se você usar WITH LOG no RAISERROR, em princípio, poderá colocar tudo em algum trabalho e depois examinar os logs.

Em tempo de execução, você pode acessar o ## CommandList e ver em forma de tabela o que está acontecendo e como.

No novo servidor, em paralelo, um loop infinito estava girando:


SET NOCOUNT ON;

DECLARE @dbname AS sysname
	, @restore_cmd AS varchar(max);

WHILE 1 = 1	--   ,    
BEGIN
	SELECT TOP 1 @dbname = dbName, @restore_cmd = restore_command 
	FROM CommandList
	WHERE processed IS NULL; --    ,  

	IF @dbname IS NOT NULL 
	BEGIN
		--    
		UPDATE CommandList
		SET start_dt = GETDATE()
		WHERE dbName = @dbname;

		RAISERROR('  %s', 0, 1, @dbname) WITH NOWAIT;
		
		BEGIN TRY

			--  ,  -  ,  CATCH    
			EXEC (@restore_cmd);

			--   
			UPDATE CommandList
			SET processed = 0
				, finish_dt = GETDATE()
			WHERE dbName = @dbname;

			RAISERROR(' %s  ', 0, 1, @dbname) WITH NOWAIT;

		END TRY
		BEGIN CATCH

			RAISERROR('    %s', 0, 1, @dbname) WITH NOWAIT;

			UPDATE CommandList 
			SET processed = 1
				, finish_dt = GETDATE()
				, error_msg = ERROR_MESSAGE();

		END CATCH

	END
	ELSE	--   ,    
		BEGIN

			RAISERROR('waiting', 0, 1) WITH NOWAIT;

			WAITFOR DELAY '00:00:30';

		END
		
	SET @dbname = NULL;
	SET @restore_cmd = NULL;

END

Tudo o que ele faz - olha para a tabela CommandList, se houver pelo menos um registro bruto - pega o nome do banco de dados e o comando para restaurar e tenta executá-lo usando EXEC (@sql_text); Se não houver entradas, aguarde 30 segundos e tente novamente.

O cursor e o loop processam cada registro apenas uma vez. Não funcionou? Escrevemos uma mensagem de erro na tabela e não retornamos mais aqui.

Sobre a condição de parar - na verdade eu era preguiçoso. Durante a digitação, criei pelo menos três soluções - como opção - adicionando os sinalizadores "Pronto para recuperação \ Não está pronto para recuperação \ Concluído", preenchendo a lista de bancos de dados e comandos imediatamente, ao preencher ## CommandList no servidor antigo e atualizar o sinalizador dentro do cursor. Paramos quando não há "registros prontos para recuperação", pois sabemos imediatamente a quantidade total de trabalho.

achados


Mas não há conclusões. Eu pensei que poderia ser útil / interessante para alguém ver como usar metadados para formar e executar sql dinâmico. Os scripts fornecidos na postagem, como são, não são adequados para uso no produto, no entanto, podem ser levemente finalizados por si mesmos e utilizados, por exemplo, para customização em massa de envio de logs / espelhamento de banco de dados / grupos de disponibilidade.

Ao executar um backup na bola, a conta na qual o SQL Server está executando deve ter permissões para gravar nela.

A criação do Servidor Vinculado não foi divulgada na postagem (o mouse na GUI é configurado intuitivamente em alguns minutos) e a transferência de logons para o novo servidor. Aqueles que experimentaram a migração de usuários sabem que simplesmente recriar logons sql não ajuda muito, porque eles têm sid s com os quais os usuários do banco de dados estão conectados. Scripts para gerar logons sql com senhas atuais e sid correto estão no msdn .

All Articles