We had a requirement for around 800+ databases migration from SQL Server 2012 to SQL Server 2014. I had to plan the migration in an efficient way. I will illustrate the method used to migrate large number databases in this blog. This method is specific to our requirement and used its unique design to build the migration solution. But also I have provided generic set of script which I wrote with this that can help any SQL server DB migrations. If we have small number of databases to be migrated, then we may not even have to think much and use straight forward plan of backup and restore. But looking at number of database in this case, we may have to plan it well and how we can achieve it in much easier and faster way. I had started with some analysis on it, what is that I can do to complete this migration and that too with ease and quickly? It is always better to spend some time on outlining an efficient plan before any such large migrations. Below would illustrate the migration steps. Disk space – Make sure we analyze disk space to hold backups as well as restore. In my case we had sufficiently large SAN available to us and we were sure that space is not an issue for us. Login – Well, after restore we always face an issue with permissions. Here it is new server and had an advantage of clean system. Just migrate the logins from source to destination server. https://support.microsoft.com/en-us/kb/918992 link will be helpful and also see below for the script that is required to be executed on source master and will generate a script that can be used to migrate the Login to destination. Identify which login are required to be migrated and run those on destination server. The best part here is the SQL logins are migrated with SID so there is no issue of fixing orphan users after restore. I have migrate the logins that were required for this migration /* This code is fournished by Microsoft. Original source of this code: http://support.microsoft.com/kb/918992/ */ USE master GO IF OBJECT_ID ('sp_hexadecimal') IS NOT NULL DROP PROCEDURE sp_hexadecimal GO CREATE PROCEDURE sp_hexadecimal @binvalue varbinary(256), @hexvalue varchar (514) OUTPUT AS DECLARE @charvalue varchar (514) DECLARE @i int DECLARE @length int DECLARE @hexstring char(16) SELECT @charvalue = '0x' SELECT @i = 1 SELECT @length = DATALENGTH (@binvalue) SELECT @hexstring = '0123456789ABCDEF' WHILE (@i <= @length) BEGIN DECLARE @tempint int DECLARE @firstint int DECLARE @secondint int SELECT @tempint = CONVERT(int, SUBSTRING(@binvalue,@i,1)) SELECT @firstint = FLOOR(@tempint/16) SELECT @secondint = @tempint - (@firstint*16) SELECT @charvalue = @charvalue + SUBSTRING(@hexstring, @firstint+1, 1) + SUBSTRING(@hexstring, @secondint+1, 1) SELECT @i = @i + 1 END SELECT @hexvalue = @charvalue GO IF OBJECT_ID ('sp_help_revlogin') IS NOT NULL DROP PROCEDURE sp_help_revlogin GO CREATE PROCEDURE sp_help_revlogin @login_name sysname = NULL AS DECLARE @name sysname DECLARE @type varchar (1) DECLARE @hasaccess int DECLARE @denylogin int DECLARE @is_disabled int DECLARE @PWD_varbinary varbinary (256) DECLARE @PWD_string varchar (514) DECLARE @SID_varbinary varbinary (85) DECLARE @SID_string varchar (514) DECLARE @tmpstr varchar (1024) DECLARE @is_policy_checked varchar (3) DECLARE @is_expiration_checked varchar (3) DECLARE @defaultdb sysname IF (@login_name IS NULL) DECLARE login_curs CURSOR FOR SELECT p.sid, p.name, p.type, p.is_disabled, p.default_database_name, l.hasaccess, l.denylogin FROM sys.server_principals p LEFT JOIN sys.syslogins l ON ( l.name = p.name ) WHERE p.type IN ( 'S', 'G', 'U' ) AND p.name <> 'sa' ELSE DECLARE login_curs CURSOR FOR SELECT p.sid, p.name, p.type, p.is_disabled, p.default_database_name, l.hasaccess, l.denylogin FROM sys.server_principals p LEFT JOIN sys.syslogins l ON ( l.name = p.name ) WHERE p.type IN ( 'S', 'G', 'U' ) AND p.name = @login_name OPEN login_curs FETCH NEXT FROM login_curs INTO @SID_varbinary, @name, @type, @is_disabled, @defaultdb, @hasaccess, @denylogin IF (@@fetch_status = -1) BEGIN PRINT 'No login(s) found.' CLOSE login_curs DEALLOCATE login_curs RETURN -1 END SET @tmpstr = '/* sp_help_revlogin script ' PRINT @tmpstr SET @tmpstr = '** Generated ' + CONVERT (varchar, GETDATE()) + ' on ' + @@SERVERNAME + ' */' PRINT @tmpstr PRINT '' WHILE (@@fetch_status <> -1) BEGIN IF (@@fetch_status <> -2) BEGIN PRINT '' SET @tmpstr = '-- Login: ' + @name PRINT @tmpstr IF (@type IN ( 'G', 'U')) BEGIN -- NT authenticated account/group SET @tmpstr = 'CREATE LOGIN ' + QUOTENAME( @name ) + ' FROM WINDOWS WITH DEFAULT_DATABASE = [' + @defaultdb + ']' END ELSE BEGIN -- SQL Server authentication -- obtain password and sid SET @PWD_varbinary = CAST( LOGINPROPERTY( @name, 'PasswordHash' ) AS varbinary (256) ) EXEC sp_hexadecimal @PWD_varbinary, @PWD_string OUT EXEC sp_hexadecimal @SID_varbinary,@SID_string OUT -- obtain password policy state SELECT @is_policy_checked = CASE is_policy_checked WHEN 1 THEN 'ON' WHEN 0 THEN 'OFF' ELSE NULL END FROM sys.sql_logins WHERE name = @name SELECT @is_expiration_checked = CASE is_expiration_checked WHEN 1 THEN 'ON' WHEN 0 THEN 'OFF' ELSE NULL END FROM sys.sql_logins WHERE name = @name SET @tmpstr = 'CREATE LOGIN ' + QUOTENAME( @name ) + ' WITH PASSWORD = ' + @PWD_string + ' HASHED, SID = ' + @SID_string + ', DEFAULT_DATABASE = [' + @defaultdb + ']' IF ( @is_policy_checked IS NOT NULL ) BEGIN SET @tmpstr = @tmpstr + ', CHECK_POLICY = ' + @is_policy_checked END IF ( @is_expiration_checked IS NOT NULL ) BEGIN SET @tmpstr = @tmpstr + ', CHECK_EXPIRATION = ' + @is_expiration_checked END END IF (@denylogin = 1) BEGIN -- login is denied access SET @tmpstr = @tmpstr + '; DENY CONNECT SQL TO ' + QUOTENAME( @name ) END ELSE IF (@hasaccess = 0) BEGIN -- login exists but does not have access SET @tmpstr = @tmpstr + '; REVOKE CONNECT SQL TO ' + QUOTENAME( @name ) END IF (@is_disabled = 1) BEGIN -- login is disabled SET @tmpstr = @tmpstr + '; ALTER LOGIN ' + QUOTENAME( @name ) + ' DISABLE' END PRINT @tmpstr END FETCH NEXT FROM login_curs INTO @SID_varbinary, @name, @type, @is_disabled, @defaultdb, @hasaccess, @denylogin END CLOSE login_curs DEALLOCATE login_curs RETURN 0 GO -- Once done: EXEC master..sp_help_revlogin GO 3. Finding a pattern – (This may not be the case for everyone, but worth checking) I observed all the databases required to be migrated had a unique database naming convention. It contained constant number of character prefixed which had month and year in it and followed by Region. Addition to this the logical name and physical name were same a DB name with data (.mdf) and log extension (.ldf). This pattern help in generating my restore scripts and also plan the disk layout as per the requirement of placing the databases on specific drives region wise. 4. I wrote a script to generate a backup script that I can run them at once to take backups to required location. This script can be used in any migration irrespective of any database naming pattern. (The script below is specific to my requirement, but can be easily modified to be generic) /* Generate a backup script */ SELECT name as Databasename , SUBSTRING(name,6,3) as Month ,SUBSTRING(name,9,4) as Year , SUBSTRING(name,14,len(name)) as Region ,'BACKUP DATABASE [' + name + '] TO DISK = ''\\SDTPSQL08\M$\MP2Bak\MSSQL2014\Backups\Restore\' + SUBSTRING(name,14,len(name)) + '\' + name + '.bak'' WITH COMPRESSION' as BackupScript FROM sys.databases WHERE name not in ('master','model','msdb','tempdb','Admin') and name not like '%2016%' and name not like '%Xref%' 5. I wrote another script to provide me with restore script for the required databases to be restored at once. We plan the restore on different drive as per the region. /* Generate a Restore script */ SELECT name as Databasename , SUBSTRING(name,6,3) as Month ,SUBSTRING(name,9,4) as Year , SUBSTRING(name,14,len(name)) as Region , CASE WHEN SUBSTRING(name,14,len(name)) in ('Some Region1','Some Region2','Some Region3') THEN 'RESTORE DATABASE [' + name + '] FROM DISK = ''M:\MP2Bak\MSSQL2014\Backups\Restore\' + SUBSTRING(name,14,len(name)) + '\' + name + '.bak'' WITH MOVE ''' + name + ''' TO ''M:\MP4Data\MSSQL2014\Data\' + name + '.mdf'', MOVE ''' + name + '_log'' TO ''M:\MP1Log\MSSQL2014\Log\' + name + '.ldf''' ELSE 'RESTORE DATABASE [' + name + '] FROM DISK = ''M:\MP2Bak\MSSQL2014\Backups\Restore\' + SUBSTRING(name,14,len(name)) + '\' + name + '.bak'' WITH MOVE ''' + name + ''' TO ''M:\MP5Data\MSSQL2014\Data\' + name + '.mdf'', MOVE ''' + name + '_log'' TO ''M:\MP2Log\MSSQL2014\Log\' + name + '.ldf''' END as RestoreScript FROM sys.databases WHERE name not in ('master','model','msdb','tempdb','Admin') and name not like '%2016%' and name not like '%Xref%' I have additionally written one more script that can used on source to generate us with generic restore script. It uses split function that I used and both the scripts are provided below. Reference – http://www.sqlservercentral.com/blogs/querying-microsoft-sql-server/2013/09/19/how-to-split-a-string-by-delimited-char-in-sql-server/ Note – I have modified the script as per my requirement /* Script for creating a function to splitting a string with delimiter */ CREATE FUNCTION [dbo].[fnSplitString] ( @string NVARCHAR(MAX), @delimiter CHAR(1) ) RETURNS @output TABLE(splitdata NVARCHAR(MAX), RowNo INT ) BEGIN DECLARE @start INT, @end INT, @RwCnt INT set @RwCnt = 1 SELECT @start = 1, @end = CHARINDEX(@delimiter, @string) WHILE @start < LEN(@string) + 1 BEGIN IF @end = 0 SET @end = LEN(@string) + 1 INSERT INTO @output (splitdata, RowNo) VALUES(SUBSTRING(@string, @start, @end - @start), @RwCnt) SET @start = @end + 1 SET @end = CHARINDEX(@delimiter, @string, @start) SET @RwCnt = @RwCnt + 1 END RETURN END /* Script to generate Restore generic script */ USE MASTER GO declare @dbname varchar(100), @filename varchar(400), @Physicalfilepath varchar(400), @Physicalfilename varchar(400), @filestatus int, @backuploc varchar(400), @RestoreDataLoc varchar(400), @RestoreLogLoc varchar(400) --SET BACKUP LOCATION EXAMPLE - SET @backuploc = 'M:\MP1bak\MSSQL2014\BACKUP\RESTORE\' SET @backuploc = 'M:\MP1bak\MSSQL2014\BACKUP\RESTORE\' --SET RESTORE DATAFILE LOCATION EXAMPLE - SET @RestoreDataLoc = 'M:\MP1Data\MSSQL2014\Data\' SET @RestoreDataLoc = 'M:\MP1Data\MSSQL2014\Data\' --SET RESTORE LOGFILE LOCATION EXAMPLE - SET @RestoreLogLoc = 'M:\MP1log\MSSQL2014\Log\' SET @RestoreLogLoc = 'M:\MP1log\MSSQL2014\Log\' declare c1 cursor for SELECT name FROM sys.databases where name not in ('master','model','msdb','tempdb','Admin') open c1 fetch next from c1 into @dbname While @@fetch_status <> -1 begin print 'USE [master]' print 'GO' print 'RESTORE DATABASE [' + @dbname + '] FROM DISK = '''+ @backuploc + @dbname + '.bak'' WITH' declare c2 cursor for select b.name, b.filename, b.status from sys.databases a, sys.sysaltfiles b where a.database_id = b.dbid and a.name = @dbname open c2 fetch next from c2 into @filename, @Physicalfilepath, @filestatus while @@FETCH_STATUS <> -1 begin --select @filename, @Physicalfilename select top 1 @Physicalfilename = splitdata from dbo.fnSplitString(@Physicalfilepath,'\') order by RowNo Desc if @filestatus = 2 print 'MOVE ''' + @filename + ''' TO ''' + @RestoreDataLoc + @Physicalfilename + ''',' else print 'MOVE ''' + @filename + ''' TO ''' + @RestoreLogLoc + @Physicalfilename + ''',' fetch next from c2 into @filename, @Physicalfilepath, @filestatus end close c2 deallocate c2 print 'STATS = 10' print 'GO' fetch next from c1 into @dbname end close c1 deallocate c1 6. Completed the step 4 and 5 by executing those scripts for taking the backup and restoring the databases. 7. The next step was I wanted to change all the databases owner as “sa”, make the DB in simple recovery model, shrink the log and make them read only. There were few databases already in “Read Only” and few were not in “SIMPLE” recovery model. To do all this I wrote another script that would generate me with script that will perform all of the above in one go. The result of the below script was executed and we are all done. USE MASTER declare @dbname varchar(100) declare c1 cursor for SELECT name FROM sys.databases where name not in ('master','model','msdb','tempdb','Admin') open c1 fetch next from c1 into @dbname While @@fetch_status <> -1 begin print 'USE [master]' print 'GO' print 'ALTER DATABASE [' + @dbname + '] SET READ_WRITE WITH NO_WAIT' print 'GO' print 'ALTER DATABASE [' + @dbname + '] SET RECOVERY SIMPLE' print 'GO' print 'USE [' + @dbname + ']' print 'GO' print 'EXEC dbo.sp_changedbowner @loginame = N''sa'', @map = false' print 'GO' print 'DBCC SHRINKFILE(2,1)' print 'GO' print 'USE [master]' print 'GO' print 'ALTER DATABASE [' + @dbname + '] SET READ_ONLY WITH NO_WAIT' print 'GO' fetch next from c1 into @dbname end close c1 deallocate c1