Transférer toutes les bases de données MS SQL Server sur une autre machine

Récemment, il était nécessaire de transférer toutes les bases de données (> 50 sur une instance de SQL Server) de l'environnement de développement vers une autre instance de SQL Server, qui était située sur un matériel différent. Je voulais minimiser le travail manuel et tout faire le plus rapidement possible.

Avertissement


Les scripts sont écrits pour une situation spécifique: il s'agit d'un environnement de développement, toutes les bases de données dans un modèle de récupération simple, les fichiers de données et les journaux de transactions sont sur le même tas.

Tout ce qui est écrit ci-dessous ne s'applique qu'à cette situation, mais vous pouvez facilement les terminer vous-même (vos conditions) sans trop d'effort.

Les scripts n'utilisent pas le nouveau STRING_AGG et d'autres choses intéressantes, donc tout devrait fonctionner à partir de SQL Server 2008 (ou 2008 R2, je ne me souviens pas où la compression de sauvegarde est apparue). Pour les versions plus anciennes, vous devez supprimer WITH COMPRESSION de la commande de sauvegarde, mais il ne peut plus y avoir de décalage horaire avec la copie des fichiers.

Ce n'est pas une instruction - «comment» faire un tel transfert. Il s'agit d'une démonstration de la façon dont les métadonnées peuvent être utilisées dans SQL dynamique.

Bien sûr, le moyen le plus rapide serait de simplement reconnecter l'étagère de disque au nouveau serveur, mais ce n'était pas notre option. Détacher - copier - Attacher a été envisagé, mais ne convenait pas, car le canal était assez étroit et le transfert de la base de données sous une forme non compressée prendrait une période assez longue.

En conséquence, nous avons décidé de faire une sauvegarde avec compression sur la balle sur le nouveau serveur, puis de la restaurer là-bas. Le fer à repasser dans l'ancien et le nouvel emplacement n'est pas mauvais, la sauvegarde n'est pas mauvaise, le gain de temps n'est pas mauvais non plus.

Le «générateur de script» a donc été écrit:

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

À la sortie, nous obtenons des commandes toutes faites pour créer des sauvegardes au bon endroit, transférant la base de données hors ligne, afin que leurs utilisateurs ne puissent pas travailler avec eux sur l'ancien serveur et les scripts pour restaurer les sauvegardes reçues sur le nouveau serveur (avec transfert automatique de tous les fichiers de données et journaux de transactions à l'emplacement spécifié) )

Le problème est que quelqu'un doit s'asseoir et exécuter tous les scripts à tour de rôle (sauvegarde hors ligne), ou quelqu'un doit d'abord démarrer toutes les sauvegardes, puis déconnecter toutes les bases de données, puis tout restaurer - il y a moins d'actions, mais vous devez vous asseoir et suivre.

Je voulais automatiser toutes ces opérations. D'une part, tout est simple - il existe déjà des commandes toutes faites, encapsulez le curseur et exécutez. Et, en principe, c'est exactement ce que j'ai fait, j'ai ajouté un nouveau serveur en tant que serveur lié à l'ancien et l'ai démarré. Sur le serveur local, la commande a été exécutée via EXECUTE (@sql_text); sur le serveur lié, EXECUTE (@sql_text) AT [linkedServerName].

Ainsi, les opérations ont été effectuées séquentiellement - sauvegarde localement, traduction hors ligne de la base de données locale, restauration sur le serveur lié. Tout a commencé, applaudissements, mais il me semble que vous pouvez accélérer un peu le processus si les sauvegardes et la restauration sont effectuées indépendamment l'une de l'autre.
Ensuite, le curseur inventé a été divisé en deux parties - sur l'ancien serveur dans le curseur, chaque base de données est sauvegardée et transférée hors ligne, après quoi le deuxième serveur doit comprendre qu'une nouvelle tâche est apparue et effectuer une restauration de la base de données. Pour implémenter ce mécanisme, j'ai utilisé un enregistrement dans une table sur un serveur lié et une boucle infinie (j'étais trop paresseux pour trouver des critères d'arrêt), qui cherche à voir s'il y a de nouveaux enregistrements et essaie de restaurer quelque chose, le cas échéant.

Décision


Sur l'ancien serveur, une table temporaire globale ## CommandList est créée et remplie, dans laquelle toutes les commandes sont collectées et il sera possible de suivre l'état des sauvegardes au même endroit. Le tableau est global de sorte qu'à tout moment d'une autre session, vous pouvez voir ce qui s'y passe actuellement.

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"

Voyons ce qui s'est passé là-bas (SELECT * FROM ## CommandList):



Super, toutes les commandes y sont collectées pour sauvegarder / restaurer toutes les bases de données nécessaires.

La base de données de maintenance a été créée sur le nouveau serveur et la table CommandList s'y trouve, qui contiendra des informations sur la restauration des bases de données:

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

Un serveur lié a été configuré sur l'ancien serveur, en examinant la nouvelle instance de SQL Server. Les scripts qui sont donnés dans ce post, j'ai écrit à la maison et je n'ai pas pris la peine d'une nouvelle instance, j'en ai utilisé un et je l'ai connecté en tant que serveur lié à moi-même. Par conséquent, ici, j'ai les mêmes chemins et local non-chemin.

Vous pouvez maintenant déclarer un curseur dans lequel sauvegarder les bases de données, les déconnecter et écrire une commande à restaurer sur le serveur lié:

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;

Chaque action est «enregistrée» dans l'onglet Messages de SSMS - vous pouvez y observer l'action en cours. Si vous utilisez WITH LOG dans RAISERROR, en principe, vous pouvez tout mettre dans une tâche, puis consulter les journaux.

Au moment de l'exécution, vous pouvez accéder à la ## CommandList et voir sous forme de tableau ce qui se passe et comment.

Sur le nouveau serveur, en parallèle, une boucle sans fin tournait:


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

Tout ce qu'il fait - regarde dans la table CommandList, s'il y a au moins un enregistrement brut - prend le nom de la base de données et la commande à restaurer et essaie de l'exécuter en utilisant EXEC (@sql_text);. S'il n'y a pas d'entrées, attendez 30 secondes et réessayez.

Le curseur et la boucle ne traitent chaque enregistrement qu'une seule fois. N'a pas fonctionné? Nous écrivons un message d'erreur dans la table et ne revenons plus ici.

À propos de la condition d'arrêt - j'étais en fait paresseux. Lors de la frappe, j'ai trouvé au moins trois solutions - en option - en ajoutant les drapeaux «Prêt pour la récupération \ Pas prêt pour la récupération \ Terminé», en remplissant immédiatement la liste des bases de données et des commandes, en remplissant ## CommandList sur l'ancien serveur et en mettant à jour le drapeau à l'intérieur du curseur. Nous nous arrêtons lorsqu'il n'y a plus de «documents prêts à être récupérés», car nous connaissons immédiatement la quantité totale de travail.

résultats


Mais il n'y a pas de conclusions. J'ai pensé qu'il pourrait être utile / intéressant pour quelqu'un de voir comment utiliser les métadonnées pour former et exécuter SQL dynamique. Les scripts donnés dans la publication, tels quels, ne conviennent pas pour une utilisation sur le prod, cependant, ils peuvent être légèrement finis pour eux-mêmes et utilisés, par exemple, pour la personnalisation de masse des groupes d'envoi de journaux / de mise en miroir de bases de données / disponibilité.

Lorsque vous effectuez une sauvegarde sur une boule, le compte sous lequel SQL Server s'exécute doit avoir les autorisations pour y écrire.

La création de Linked Server n'a pas été révélée dans la publication (la souris dans l'interface graphique est configurée intuitivement en quelques minutes) et le transfert des connexions au nouveau serveur. Ceux qui ont expérimenté la migration des utilisateurs savent que la simple recréation des connexions SQL n'aide pas beaucoup, car ils ont des sids avec lesquels les utilisateurs de la base de données sont connectés. Les scripts de génération des connexions SQL avec les mots de passe actuels et le sid correct sont sur msdn .

All Articles