One of the responsibilities of my current position as a SQL Developer is to prepare the scripts for deployment. For quality assurance reasons we store all of the database objects in a source control system, so each object is stored in a separate file. This can make deployments very tedious, depending on the volume of files.
Disclaimer
These processes are my processes for an internally managed application. These scripts and processes are not distributed in any way, and quite frankly are not suited for that purpose.
This does not cover any T-SQL best practices. There are far better articles written by far more knowledgeable people covering these topics. Click here for a list of best practice articles.
The Development
Here are my general guidelines for deployment, in no particular order:
1. Each script must be able to be executed multiple times without error.
There is nothing worse that running a script to test a deployment and then getting an error because an object already exists or a key violation because the data already exists. All scripts must check for the existence of the finals state before doing anything else. For stored procedures, this may me a conditional drop and then create and for tables this may mean checking for column existence before running an alter statement.
2. Object permission must be part of the object script.
Your mileage may vary with this one, but I am a fan of explicit permission granted to a database role for each database object that is being deployed. For newer architectures your roles may have blanket permissions to a schema, but I have not seen many instances of security set up this way.
Having the permission set at the end of the script (just in case you drop an re-create it :-)) will save a lot of headache if you ever need to setup the application in a new environment. NOTE: Please do not forget the batch separator (i.e. “GO”) in the script. I hate seeing permission errors where a stored procedure is trying execute permissions to itself.
3. Group the data scripts by table.
If you follow guideline #1 this shouldn’t be much of an issue, but if you have the same or worse, similar, updates in multiple places you could in a world of hurt.
4. Try to group like objects.
I try to keep like objects together. With some exceptions I will deploy objects in the following order:
- Tables – With the exception of using a function in a default value or check constraint, it is a pretty safe bet that you can deploy all table changes first without error. The most tedious part of deploying tables is their relationships. Since they are dependant on one another, order is important. I have not found a perfect way to deal with this dependency.
- Views – Again, it is possible to use functions as part of a view, but I wouldn’t recommend it. It is also possible to reference another view as part of a view definition, but I would not recommend that either. Pay attention to the order, just in case.
- Functions – In SQL Sever you cannot call a stored procedure as part of a scalar or table valued function, so deploying these before the stored procedures is advisable.
- Stored Procedures – These are next to last because they can use any of the previous three objects. They may, however, be dependant on one another, so watch the order here as well.
- Triggers – Triggers can be added to tables or view and they can use stored procedure or functions, so I save these for last.
5. Take as much care with the rollback scripts as you do with the future scripts.
What can be worse than beginning a deployment and then realizing that you have to rollback. After the rollback completes, you realize that you just dropped a primary table in your database that you just meant to alter. Oops.
For this reason I will hand write table rollback scripts. I use a script generator like ScriptDB to generate all of the other rollback scripts a week or so prior to deployment to capture any run fixes that were deployed during the development process.
I maintain a 1:1 relationship between deployment and rollback scripts. I execute the rollback scripts in reverse order to the deployments scripts. No muss, no fuss and no additional lists of scripts to maintain.
6. Don’t forget the jobs!
If you update requires job changes, add those to source control for deployment as well. All of the objects that are part of the deployment should be scripted.
7. Store the deployment order some place safe.
The largest deployment that I have worked on to date was over 600 individual scripts, with many exceptions to the steps listed above. Rebuilding that list would have taken a large amount of effort and testing, which I really did not have the time to do.
I prefer to store them in a database table table so that the list is available to other developers to make the necessary changes. I never want to be the one left holding the ball, let alone the one to drop, so let’s keep this in a safe place.
The Deployment
Take the list that you have so tediously maintained throughout the development process and script it into a file. I use sqlcmd to deploy all of the database objects, so this is a relatively simple process. Be sure that error handling is written into the script. I prefer that the batch ends when an error is encountered. If you followed #1 above, you can fix any errors and start the batch from the beginning.
The table that I use to store the scripts has the definition below. I have a template parameter for the database in the script. Use Ctrl+Shift+M or click the e “Specify Values for Template Parameters” button to populate this field.
USE <Database,,> ;
GO
IF EXISTS ( SELECT
*
FROM
sys.objects
WHERE
object_id = OBJECT_ID(N'[dbo].[SQLScripts]') AND
type IN ( N'U' ) )
DROP TABLE [dbo].[SQLScripts]
GO
SET ANSI_NULLS ON ;
GO
SET QUOTED_IDENTIFIER ON ;
GO
SET ANSI_PADDING ON ;
GO
CREATE TABLE dbo.SQLScripts
(
SQLScriptOrder SMALLINT NOT NULL,
SQLScriptDatabase VARCHAR(255) NOT NULL,
SQLScriptName VARCHAR(255) NOT NULL,
SQLScriptRelativePath VARCHAR(255) NOT NULL,
CreateTimeStamp DATETIME NOT NULL
CONSTRAINT DF_SQLScripts_CreatedTimeStamp DEFAULT ( GETDATE() ),
CreateUserID VARCHAR(50) NOT NULL
CONSTRAINT DF_SQLScripts_CreateUserID DEFAULT ( SUSER_SNAME() ),
UpdateTimeStamp DATETIME NOT NULL
CONSTRAINT DF_SQLScripts_UpdatedTimeStamp DEFAULT ( GETDATE() ),
UpdateUserID VARCHAR(50) NOT NULL
CONSTRAINT DF_SQLScripts_UpdateUserID DEFAULT ( SUSER_SNAME() ),
CONSTRAINT PK_SQLScripts PRIMARY KEY CLUSTERED ( SQLScriptOrder ASC ),
CONSTRAINT AK_SQLScripts UNIQUE ( SQLScriptDatabase ASC, SQLScriptName ASC )
) ;
GO
CREATE TRIGGER dbo.SQLScriptUpdate ON dbo.SQLScripts
AFTER UPDATE
AS
UPDATE
ss
SET
UpdateTimeStamp = GETDATE(),
UpdateUserID = SUSER_NAME()
FROM
dbo.SQLScripts ss
WHERE
EXISTS ( SELECT
1
FROM
INSERTED i
WHERE
i.SQLScriptDatabase = ss.SQLScriptDatabase AND
i.SQLScriptName = ss.SQLScriptName ) ;
To create the batch file used for deployment I use to SQL outlined below. I find this option to be very flexible. Run the result to text. I hate to use cursors like everyone else, but this ensures that I get the proper format out.
SET NOCOUNT ON ;
DECLARE
@Server VARCHAR(50),
@LocalPath VARCHAR(255),
@SQLScriptRelativePath VARCHAR(255),
@SQLScriptName VARCHAR(255),
@Command VARCHAR(1000) ;
SET @Server = '(local)' ;
SET @LocalPath = 'C:\Temp' ;
PRINT 'REM Deployment Script Generator'
PRINT 'REM Generated On: ' + CONVERT(VARCHAR(10), GETDATE(), 101)
PRINT 'REM Server Name: ' + @Server ;
PRINT '' ;
DECLARE ScriptCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
FOR SELECT
SQLScriptRelativePath,
SQLScriptName
FROM
dbo.SQLScripts
ORDER BY
SQLScriptOrder ;
OPEN ScriptCursor ;
FETCH NEXT FROM ScriptCursor INTO @SQLScriptRelativePath,@SQLScriptName ;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT 'REM ' + @SQLScriptRelativePath + '\' + @SQLScriptName ;
PRINT 'sqlcmd -S ' + @Server + ' -i "' + @LocalPath + '\' + @SQLScriptRelativePath + '\' + @SQLScriptName + '"' ;
PRINT 'if not %errorlevel%==0 goto :error' ;
FETCH NEXT FROM ScriptCursor INTO @SQLScriptRelativePath,@SQLScriptName ;
END ;
CLOSE ScriptCursor ;
DEALLOCATE ScriptCursor ;
PRINT '' ;
PRINT 'pause' ;
PRINT 'exit' ;
PRINT '' ;
PRINT ':error' ;
PRINT 'echo ERROR ENCOUNTERED' ;
PRINT 'pause' ;
Take the output of this script and save it as a batch file and VIOLA! Change the local path and order the scripts in reverse order to generate the rollback batch file.
Now you have a quick, dependable and free SQL deployment method.
Happy querying,
~Ron