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..
CREATE TABLE
dbName sysname unique
, backup_command varchar(max)
, offline_command varchar(max)
, restore_command varchar(max)
, processed bit
, start_dt datetime
, finish_dt datetime
, error_msg varchar(max)
);
INSERT INTO
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'
AND name NOT IN (SELECT dbname FROM
AND name <> 'Maintenance';
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
WHERE processed IS NULL;
OPEN MoveDatabase;
FETCH NEXT FROM MoveDatabase INTO @dbname, @backup_cmd, @offline_cmd, @restore_cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE
SET start_dt = GETDATE()
WHERE dbName = @dbname;
BEGIN TRY
RAISERROR (' %s', 0, 1, @dbname) WITH NOWAIT;
EXEC (@backup_cmd);
RAISERROR (' %s', 0, 1, @dbname) WITH NOWAIT;
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
SET processed = 0
, finish_dt = GETDATE()
WHERE dbName = @dbname;
END TRY
BEGIN CATCH
RAISERROR (' %s. error_msg ##CommandList', 0, 1, @dbname) WITH NOWAIT;
UPDATE
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
ORDER BY start_dt;
DROP TABLE
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
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 .