Transfiera todas las bases de datos de MS SQL Server a otra máquina

Recientemente, era necesario transferir todas las bases de datos (> 50 en una instancia de SQL Server) desde el entorno de desarrollo a otra instancia de SQL Server, que se encontraba en un hardware diferente. Quería minimizar el trabajo manual y hacer todo lo más rápido posible.

Descargo de responsabilidad


Los scripts se escriben para una situación específica: este es un entorno de desarrollo, todas las bases de datos en un modelo de recuperación simple, archivos de datos y registros de transacciones están en el mismo montón.

Todo lo escrito a continuación se aplica solo a esta situación, pero puede terminarlos fácilmente (sus condiciones) sin mucho esfuerzo.

Los scripts no usan el nuevo STRING_AGG y otras cosas buenas, por lo que todo debería funcionar comenzando con SQL Server 2008 (o 2008 R2, no recuerdo dónde apareció la compresión de copia de seguridad). Para versiones anteriores, debe eliminar WITH COMPRESSION del comando de copia de seguridad, pero es posible que ya no existan diferencias de tiempo con la copia de archivos.

Esta no es una instrucción: "cómo" hacer dicha transferencia. Esta es una demostración de cómo se pueden usar los metadatos en SQL dinámico.

Por supuesto, la forma más rápida sería simplemente volver a conectar el estante del disco al nuevo servidor, pero esa no era nuestra opción. Separar - copiar - Se consideró adjuntar, pero no encajaba, ya que el canal era bastante estrecho y la transferencia de la base de datos sin comprimir llevaría un período de tiempo bastante largo.

Como resultado, decidimos que haríamos una copia de seguridad con compresión en la bola en el nuevo servidor y luego la restableceríamos allí. El hierro tanto en la ubicación antigua como en la nueva no es malo, la copia de seguridad no es mala y la ganancia de tiempo tampoco es mala.

Entonces se escribió el "generador de script":

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';

En la salida, obtenemos comandos listos para crear copias de seguridad en el lugar correcto, transfiriendo la base de datos fuera de línea, para que sus usuarios no puedan trabajar con ellos en el servidor antiguo y las secuencias de comandos para restaurar las copias de seguridad recibidas en el nuevo servidor (con transferencia automática de todos los archivos de datos y registros de transacciones a la ubicación especificada )

El problema con esto es que alguien tiene que sentarse y ejecutar todos los scripts a su vez (recuperación de copia de seguridad sin conexión), o alguien primero debe iniciar todas las copias de seguridad, luego desconectar todas las bases de datos, luego restaurar todo; hay menos acciones, pero necesita sentarse y rastrear.

Quería automatizar todas estas operaciones. Por un lado, todo es simple: ya hay comandos listos para usar, ajustar el cursor y ejecutar. Y, en principio, hice exactamente eso, agregué un nuevo servidor como servidor vinculado en el anterior y lo inicié. En el servidor local, el comando se ejecutó a través de EXECUTE (@sql_text); en el servidor vinculado, EXECUTE (@sql_text) AT [LinkedServerName].

Por lo tanto, las operaciones se realizaron secuencialmente: copia de seguridad local, traducción de la base de datos local fuera de línea, restauración al servidor vinculado. Todo comenzó, saludos, pero me pareció que puedes acelerar un poco el proceso si las copias de seguridad y las restauraciones se realizan de forma independiente.
Luego, el cursor inventado se dividió en dos partes: en el antiguo servidor en el cursor, se realiza una copia de seguridad de cada base de datos y se transfiere sin conexión, después de lo cual el segundo servidor debe comprender que ha aparecido una nueva tarea y realizar una restauración de la base de datos. Para implementar este mecanismo, utilicé un registro en una tabla en un servidor vinculado y un bucle infinito (era demasiado vago para encontrar criterios de detención), que busca ver si hay nuevos registros y está tratando de restaurar algo, si lo hay.

Decisión


En el servidor anterior, se crea y completa una tabla temporal global ## CommandList, en la que se recopilan todos los comandos y será posible rastrear el estado de las copias de seguridad en el mismo lugar. La tabla es global para que en cualquier momento desde otra sesión pueda ver lo que está sucediendo allí ahora.

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"

Veamos qué sucedió allí (SELECT * FROM ## CommandList):



Genial, todos los comandos se recopilan allí para hacer una copia de seguridad / restaurar todas las bases de datos necesarias.

La base de datos de mantenimiento se creó en el nuevo servidor y la tabla CommandList está en ella, que contendrá información sobre la restauración de las bases de datos:

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)				-- ,  
);

Se configuró un servidor vinculado en el servidor anterior, mirando la nueva instancia de SQL Server. Los scripts que se dan en esta publicación, los escribí en casa y no me molesté con una nueva instancia, usé uno y lo conecté como un servidor vinculado a mí mismo. Por lo tanto, aquí tengo los mismos caminos y unc-path local.

Ahora puede declarar un cursor para hacer una copia de seguridad de las bases de datos, desconectarlas y escribir un comando para restaurar en el 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 acción se "registra" en la pestaña Mensajes en SSMS; allí puede observar la acción actual. Si usa WITH LOG en RAISERROR, en principio, puede ponerlo todo en algún trabajo y luego mirar los registros.

En tiempo de ejecución, puede acceder a la ## CommandList y ver en forma de tabla qué está sucediendo y cómo.

En el nuevo servidor, en paralelo, un ciclo sin fin giraba:


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

Todo lo que hace, mira la tabla de la Lista de comandos, si hay al menos un registro sin procesar, toma el nombre de la base de datos y el comando para restaurar e intenta ejecutarlo usando EXEC (@sql_text); Si no hay entradas, espere 30 segundos e intente nuevamente.

Tanto el cursor como el bucle procesan cada registro solo una vez. ¿No funciono? Escribimos un mensaje de error en la tabla y ya no regresamos aquí.

Sobre la condición para parar: en realidad era vago. Mientras escribía, se me ocurrieron al menos tres soluciones, como una opción, agregando las banderas "Listo para la recuperación \ No listo para la recuperación \ Terminado", completando la lista de bases de datos y comandos de inmediato, al completar ## CommandList en el servidor anterior y actualizar la bandera dentro del cursor. Nos detenemos cuando no quedan "registros listos para la recuperación", ya que sabemos de inmediato la cantidad total de trabajo.

recomendaciones


Pero no hay conclusiones. Pensé que podría ser útil / interesante para alguien ver cómo usar metadatos para formar y ejecutar sql dinámico. Las secuencias de comandos que figuran en la publicación, tal como están, no son adecuadas para su uso en el producto, sin embargo, pueden terminarse ligeramente por sí mismas y utilizarse, por ejemplo, para la personalización masiva de grupos de envío de registros / duplicación de bases de datos / disponibilidad.

Al realizar una copia de seguridad en la bola, la cuenta en la que se ejecuta SQL Server debe tener permisos para escribir allí.

La creación de Linked Server no se reveló en la publicación (el mouse en la GUI se configura intuitivamente en un par de minutos) y la transferencia de inicios de sesión al nuevo servidor. Aquellos que han experimentado la migración de usuarios saben que simplemente volver a crear inicios de sesión sql no ayuda mucho, porque tienen sid s con los que los usuarios de la base de datos están conectados. Los scripts para generar inicios de sesión sql con contraseñas actuales y sid correcto están en msdn .

All Articles