Migrating databases from development to production
       
 


Baya Pavliashvili
03 Apr 2002, Rating 4.25 (out of 5)

If you work for a small company, then your development, test and production databases might reside on the same server. However, most organizations realize the risks associated with such a setup. If your server were to become unusable due to a disk failure or power outage, you'd lose not only the business from your current customers, but also the valuable development time. If you use a production database for development and developers make a mistake, your online customers would have to wait until your production database was restored from a backup. The bottom line is that no serious development should be done on the production server. The application should be developed on one server, tested in a production-like environment on another server (sometimes referred to as Quality Assurance server), and then deployed to the production server.

The initial migration of the database is easy; you can just backup the database on the development server and restore it on the production server. But what happens if you have subsequent releases or enhancements to the already deployed application? Well, backup and restore is no longer an option--you'd lose all the data collected in production. DTS could be helpful only if your database structure is the same on all servers. In the majority of cases your database structure will change from one release to the next; therefore, you need some type of a mechanism to compare databases on two servers and come up with a plan to synchronize them. In this article, I offer you a script that compares tables, indexes and constraints on two servers and generates a comprehensive report of differences.

As you might guess, the trick is in examining the system tables and/or the information schema views. The script itself isn't terribly complicated, but if your situation calls for comparing database structures, it will give you a good head start for developing your own solution. Once you know the differences between the database structures, you can come up with scripts to make appropriate changes to the production database and transfer/transform additional data from the development environment.

The following script compares development and production databases on my test server. Replace the database names if you have to compare two databases on the same server. If you have separate servers, you'll have to set up the remote server as a linked server and modify this script to query the remote database. See inline comments for details.

SET NOCOUNT ON 

DECLARE 	@table_name VARCHAR(100), 

		@index_name VARCHAR(100), 

		@sql VARCHAR(4000)



-- temp tables to keep intermediate values:

CREATE TABLE #dev_columns (

table_name VARCHAR(100) NULL,

column_id INT NULL, 

column_name VARCHAR(100) NULL, 

data_type VARCHAR(100) NULL, 

length INT, 

nullable BIT)



CREATE TABLE #production_columns (

table_name VARCHAR(100) NULL,

column_id INT NULL, 

column_name VARCHAR(100) NULL, 

data_type VARCHAR(100) NULL, 

length INT, 

nullable BIT)



CREATE TABLE #dev_indexes (

	table_name VARCHAR(100), 

	index_name VARCHAR(100), 

	column_name VARCHAR(100), 

	ordinal_position_of_column INT, 

	isclustered BIT)



CREATE TABLE #production_indexes (

	table_name VARCHAR(100), 

	index_name VARCHAR(100), 

	column_name VARCHAR(100), 

	ordinal_position_of_column INT, 

	isclustered BIT)



CREATE TABLE #dev_constraints (

	table_name VARCHAR(100), 

	constraint_type VARCHAR(25), 

	constraint_name VARCHAR(100), 

	column_name VARCHAR(100), 

	constraint_value VARCHAR(255))



CREATE TABLE #production_constraints (

	table_name VARCHAR(100), 

	constraint_type VARCHAR(25), 

	constraint_name VARCHAR(100), 

	column_name VARCHAR(100), 

	constraint_value VARCHAR(255))



-- get all columns, indexes, and constraints from first db:

USE Development

DECLARE table_cursor CURSOR FOR 

SELECT name 

FROM Development.dbo.sysobjects 

WHERE type = 'u'

AND name <> 'dtproperties'

ORDER BY 1



OPEN table_cursor 

FETCH NEXT FROM table_cursor 

INTO @table_name

WHILE @@FETCH_STATUS = 0

BEGIN

     SELECT @sql = 'INSERT #dev_columns (

		table_name ,

		column_id , 

		column_name , 

		data_type , 

		length , 

		nullable )	



	SELECT  ''' + @table_name + ''' ,

		colid, 

		a.name, 

		b.name, 

		a.length, 

		a.isnullable

		FROM syscolumns a 

                INNER JOIN systypes b 

                ON a.xtype = b.xtype 

		and a.xusertype = b.xusertype

		WHERE 

            a.id = OBJECT_ID(''' + @table_name + ''')'



	EXEC(@sql)



	SELECT @sql = ' '



	SELECT @sql = 'INSERT #dev_indexes 

		SELECT DISTINCT	b.name AS table_name, 

		a.name AS index_name, 

		d.name AS column_name,

		c.keyno AS ordinal_position_of_column,

		''CLUSTERED'' = CASE 

		      		WHEN a.indid = 1 THEN 1 

		      		ELSE 0

			   	END 

		FROM sysindexes a INNER JOIN sysobjects b 

                                  ON a.id = b.id

		AND b.type = ''u'' 

                and b.name <> ''dtproperties''

		INNER JOIN sysindexkeys c 

                ON a.indid = c.indid AND c.id = b.id

		INNER JOIN syscolumns d 

                ON d.colid = c.colid 

                AND d.id = b.id

		WHERE a.indid <> 0 

                AND a.indid <> 255 

                AND a.name NOT LIKE ''_wa%''

		AND b.name = ''' + @table_name + '''

		ORDER BY 1, 2, 4'

	EXEC(@sql)



	SELECT @sql = ' '



	SELECT @sql = 'INSERT #dev_constraints

		SELECT 	b.name AS table_name, 

		constraint_type = 	

                        CASE 

			WHEN a.type = ''c'' 

                        THEN ''CHECK''

			WHEN a.type = ''d'' 

                        THEN ''DEFAULT''

			END,

		     a.name AS constraint_name, 

		     COL_NAME(b.id, a.info) 

                     AS column_name, 

		     c.text AS constraint_value

	        FROM sysobjects a 

                INNER JOIN sysobjects b 

		ON b.id = a.parent_obj 

		LEFT JOIN syscomments c 

                ON c.id = a.id

		WHERE a.type IN (''c'', ''d'')

		AND b.name = ''' + @table_name +''''



	EXEC(@sql)



	FETCH NEXT FROM table_cursor 

        INTO @table_name

END

CLOSE table_cursor

DEALLOCATE table_cursor



-- get all columns, indexes, and constraints from second db:

USE Production

/*******************************************

* if using a remote server then remove 

* "USE production" statement and use 

* four-part identifier for querying 

* the tables, as in following

* 

* SELECT name 

* FROM 

* linked_server.remote_database.dbo.sysobjects 

****************************************

*/

DECLARE table_cursor CURSOR FOR 

SELECT name FROM Production.dbo.sysobjects 

WHERE type = 'u'

AND name <> 'dtproperties'

ORDER BY 1



OPEN table_cursor 

FETCH NEXT FROM table_cursor INTO @table_name

WHILE @@FETCH_STATUS = 0

BEGIN

	SELECT @sql = 'INSERT #production_columns (

		table_name ,

		column_id , 

		column_name , 

		data_type , 

		length , 

		nullable )	



	    SELECT  ''' + @table_name + ''' ,

		colid, 

		a.name, 

		b.name, 

		a.length, 

		a.isnullable

            FROM syscolumns a INNER JOIN systypes b 

                ON a.xtype = b.xtype 

		and a.xusertype = b.xusertype

	WHERE a.id = OBJECT_ID(''' + @table_name + ''')'



	EXEC(@sql)



	SELECT @sql = ' '



	SELECT @sql = 'INSERT #production_indexes 

		SELECT 

                DISTINCT	

                 b.name AS table_name, 

		 a.name AS index_name, 

		 d.name AS column_name,

		 c.keyno AS ordinal_position_of_column,

		 'CLUSTERED' = 	

                        CASE 

		      	WHEN a.indid = 1 THEN 1 

		      	ELSE 0

			   END 

		FROM sysindexes a 

                INNER JOIN sysobjects b 

                ON a.id = b.id

		AND b.type = ''u''

		INNER JOIN sysindexkeys c 

                ON a.indid = c.indid AND c.id = b.id

		INNER JOIN syscolumns d 

                ON d.colid = c.colid AND d.id = b.id

		WHERE a.indid <> 0 

                AND a.indid <> 255 

                AND a.name NOT LIKE ''_wa%''

		AND b.name = ''' + @table_name + '''

		ORDER BY 1, 2, 4'



	EXEC(@sql)



SELECT @sql = ' '



SELECT @sql = 'INSERT #production_constraints

	SELECT 	b.name AS table_name, 

		constraint_type = 	

                        CASE 

			WHEN a.type = ''c'' 

                        THEN ''CHECK''

			WHEN a.type = ''d'' 

                        THEN ''DEFAULT''

			END,

		a.name AS constraint_name, 

		COL_NAME(b.id, a.info) 

                AS column_name, 

		c.text AS constraint_value

	FROM sysobjects a INNER JOIN sysobjects b 

	ON b.id = a.parent_obj 

	LEFT JOIN syscomments c ON c.id = a.id

	WHERE a.type IN (''c'', ''d'')

	AND 

        b.name = ''' + @table_name +''''



EXEC(@sql)



FETCH NEXT FROM table_cursor INTO @table_name



END

CLOSE table_cursor

DEALLOCATE table_cursor



-- now compare the values:

SELECT 'the following tables are in development, but not in production database:'

SELECT DISTINCT table_name FROM #dev_columns 

    WHERE table_name NOT IN 

    (SELECT table_name FROM #production_columns)



SELECT 'the following tables are in production, but not in development database:'

SELECT DISTINCT table_name 

FROM #production_columns 

WHERE table_name 

NOT IN (SELECT table_name FROM #dev_columns)



SELECT 'the following columns are in development, but not in production database:'

SELECT a.* FROM #dev_columns a 

WHERE a.column_name 

NOT IN (SELECT column_name 

     FROM #production_columns b 

     WHERE a.table_name = b.table_name)



SELECT 'the following columns are in production, but not in development database:'

SELECT a.* FROM #production_columns a 

WHERE a.column_name NOT IN 

 (SELECT column_name FROM #dev_columns b 

  WHERE a.table_name = b.table_name)



SELECT 'the following columns have varying data types, length or nullability:'

SELECT  

DISTINCT 

a.table_name,

	a.column_name,

	a.data_type AS dev_dt, 

	a.length AS dev_length, 

	a.nullable AS dev_nullability,

	b.data_type AS production_dt, 

	b.length AS production_length, 

	b.nullable AS production_nullability

FROM #dev_columns a 

INNER JOIN #production_columns b ON 

a.column_name = b.column_name

AND

a.table_name = b.table_name

AND

(a.data_type <> b.data_type

OR

a.length <> b.length

OR 

a.nullable <> b.nullable)

ORDER BY 1



DROP TABLE #dev_columns, #production_columns



SELECT 'the following tables have indexes in dev, but not 

production:'

SELECT DISTINCT table_name 

FROM #dev_indexes 

WHERE table_name 

NOT IN (SELECT table_name 

        FROM #production_indexes)



SELECT 'the following tables have indexes in production, but 

not dev:'

SELECT DISTINCT table_name 

FROM #production_indexes 

WHERE table_name 

NOT IN (SELECT table_name FROM #dev_indexes)



SELECT 'the following indexes appear in dev, but not in 

production:'

SELECT * FROM #dev_indexes a 

WHERE index_name NOT IN 

	(SELECT index_name 

          FROM #production_indexes b 

           WHERE a.table_name = b.table_name)



SELECT 'the following indexes appear in production, but not in development:'

SELECT * FROM #production_indexes a 

WHERE index_name NOT IN 

	(SELECT index_name FROM #dev_indexes b 

         WHERE a.table_name =    

         b.table_name)



SELECT 'the ordinal position of columns is different in 

following indexes:'

SELECT 	

a.table_name,

a.index_name, 

a.ordinal_position_of_column as 'dev_position', 

b.ordinal_position_of_column as 'production_position'

FROM #dev_indexes a 

INNER JOIN #production_indexes b ON 

a.table_name = b.table_name 

AND 

a.index_name = b.index_name

AND 

a.column_name = b.column_name 

AND 

a.ordinal_position_of_column <> 

b.ordinal_position_of_column



SELECT 'the following indexes are clustered in development 

but not in production, or vice versa:'

SELECT 	

a.table_name, 

 	a.index_name, 

	a.isclustered AS isclustered_in_dev, 	

	b.isclustered AS isclustered_in_production

FROM #dev_indexes a 

INNER JOIN #production_indexes b ON 

a.table_name = b.table_name 

AND 

a.index_name = b.index_name

AND 

a.isclustered <> b.isclustered



DROP TABLE #dev_indexes, #production_indexes



SELECT 'the following constraints appear in development, 

but not in production:'

SELECT * FROM #dev_constraints WHERE constraint_name 

         NOT IN 

	(SELECT DISTINCT constraint_name 

         FROM #production_constraints)



SELECT 'the following constraints appear in production but not in development:'

SELECT * FROM #production_constraints 

         WHERE constraint_name NOT IN 

	(SELECT DISTINCT constraint_name 

         FROM #dev_constraints)



DROP TABLE #dev_constraints, #production_constraints 

Reader Feedback

Mike C. writes: I have found that calling the OBJECT_ID function across linked servers does not work however you attempt to prefix it. But you can amend the two lines of the format:

WHERE a.id = OBJECT_ID(''' + @table_name + ''')'

...to be like:

WHERE a.id = (

    SELECT id

    FROM server2.db_name.dbo.sysobjects

    WHERE name = ''' + @table_name + '''

)'