15 Stored Procedures and Triggers

Programming without an overall architecture or design in mind is like exploring a cave with only a flashlight: You don’t know where you’ve been, you don’t know where you’re going, and you don’t know quite where you are.Danny Thorpe

A stored procedure is a batch of SQL that’s stored permanently on the server and compiled when used. It’s not compiled in the sense of being translated to machine code or even Java byte codes—it’s pseudocompiled to speed execution. You create stored procedures using the Transact-SQL CREATE PROCEDURE command. All that really happens when you create a procedure is the insertion of its source code into the syscomments system table. The procedure isn’t compiled until it’s executed for the first time (and in certain other circumstances—see the following section, “Internals,” for more information). Despite the name, syscomments stores far more than comments—it’s the repository for the source code for stored procedures, views, triggers, rules, and defaults. If you delete the source code for an object from syscomments, the object will no longer be accessible.

You can list the source code to a procedure, view, trigger, rule, or default using the sp_helptext system procedure. If the object is not encrypted, sp_helptext will list its source, formatted similarly to the way you entered it. Here’s an example:

EXEC sp_helptext ’sp_hexstring'
Text
---------------------------------------------------------------------------------------------------------------------------
CREATE PROC dbo.sp_hexstring @int varchar(10)=NULL, @hexstring varchar(30)=NULL OUT
/*

Object: sp_hexstring
Description: Return an integer as a hexadecimal string

Usage: sp_hexstring @int=Integer to convert, @hexstring=OUTPUT parm to receive hex string

Returns: (None)

Created by: Ken Henderson. Email: [email protected]

Version: 1.0

Example: sp_hexstring 23, @myhex OUT

Created: 1999-08-02. Last changed: 1999-08-15.
*/
AS
IF (@int IS NULL) OR (@int = '/?') GOTO Help
DECLARE @i int, @vb varbinary(30)
SELECT @i=CAST(@int as int), @vb=CAST(@i as varbinary)
EXEC master..xp_varbintohexstr @vb, @hexstring OUT
RETURN 0
Help:
EXEC sp_usage @objectname=’sp_hexstring',
   @desc=’Return an integer as a hexadecimal string',
   @parameters='@int=Integer to convert, @hexstring=OUTPUT parm to receive hex string',
   @example=’sp_hexstring "23", @myhex OUT',
   @author='Ken Henderson',
   @email='[email protected]',
   @version='1', @revision='0',
   @datecreated='19990802', @datelastchanged='19990815'
RETURN -1


Stored Procedure Advantages

There are several advantages to using stored procedures; here are a few of them:

•   They allow business rules and policies to be encapsulated and changed in one place.

•   They allow sharing of application logic by different applications.

•   They can facilitate data modification, ensuring that all applications update data consistently.

•   They can simplify parameterized queries, easily facilitating running the same query repetitively with different sets of parameters.

•   Autostart procedures can automate startup routines, executing each time the server is cycled.

•   They can modularize an application, organizing it into manageable pieces.

•   They can provide security mechanisms, allowing users controlled access to database objects they could not otherwise use.

•   They can reduce network bandwidth use by greatly lessening the amount of Transact-SQL code that must traverse the network in order to accomplish tasks.

•   Since their execution plans are retained by the server for reuse, they can improve application performance considerably.

Internals

There are four major steps involved with using stored procedures:

1. Creation—where you initially create the procedure with CREATE PROC

2. User execution—where you execute it with EXEC

3. Compilation—where the server compiles and optimizes the procedure during an EXEC

4. Server execution—where the server runs its compiled execution plan during an EXEC

Creation

The creation step is where you use the CREATE PROCEDURE command to construct the procedure on the server. Each time you successfully create a new procedure, its name and other vital information are recorded in sysobjects, and its source code is stored in syscomments. Objects referenced by the procedure are not resolved until you execute it.

User Execution

The first time you execute a newly created procedure (or the server recompiles it), it’s read from syscomments, and its object references are resolved. During this process, the command processor constructs what’s known as a sequence tree or query tree that will be passed to the query optimizer for compilation and optimization.

Compilation

Once the query tree has been successfully created, the SQL Server query optimizer compiles the entire batch, optimizes it, and checks access privileges.

During the optimization phase, the optimizer scans the query tree and develops what it believes is the optimal plan for accessing the data the procedure is after. The following criteria are considered during this step:

•   The presence of the GROUP BY, ORDER BY, and UNION clauses

•   The amount of data the procedure will retrieve

•   The use of joins to link tables together

•   The characteristics of the indexes built over referenced tables

•   The degree of data distribution in each index’s key columns

•   The use of comparison operators and values in WHERE and HAVING clauses within the procedure

An execution plan is the result of this process, and it’s placed in the procedure cache when the optimizer finishes building it. This execution plan consists of the following:

•   The steps required to carry out the work of the stored procedure

•   The steps necessary to enforce constraints

•   The steps needed to branch to any triggers fired by the stored procedure

Execution plans in SQL Server 7.0 and later are reentrant and read-only. This differs from previous releases, where each connection received its own copy of the execution plan for a given procedure.

Server Execution

The execution phase is where the execution plan is processed sequentially and each step is dispatched to an appropriate internal manager process. There are a number of internal managers—the DDL and DML managers, the transaction manager, the ODSOLE manager (for processing the OLE automation procedures such as sp_OAcreate), the stored procedure manager, the utility manager, the T-SQL manager, etc. These managers are called repeatedly until all steps in the execution plan have been processed.

Execution plans are never stored on disk. The only portion of the stored procedure that’s stored permanently is its source code (in syscomments). Since they’re kept in memory, cycling the server disposes of all current execution plans (as does the undocumented DBCC FREEPROCCACHE() command).

SQL Server will automatically recreate a procedure’s execution plan when:

•   The procedure’s execution environment differs significantly from its creation environment (see the following section, “Environmental Concerns,” for more information).

•   The sysobjects schema_ver column changes for any of the objects the procedure references. The schema_ver and base_schema_ver columns are updated any time the schema information for a table changes. This includes column additions and deletions, data type changes, constraint additions and deletions, as well as rule and default bindings.

•   The statistics have changed for any of the objects the procedure references.

•   An index that was referenced by the procedure’s execution plan is dropped.

•   A copy of the procedure’s execution plan is not available in the cache. Execution plans are removed from the cache to make room for new plans using an LRU (least recently used) algorithm.

Additionally, you can force a procedure’s execution plan to be recompiled using these three methods:

1. Creating the procedure using the WITH RECOMPILE option (and then executing it)

2. Executing the procedure using the WITH RECOMPILE option

3. Flagging any of the tables the procedure references with the sp_recompile procedure (sp_recompile merely updates sysobjects' schema_ver column) and then executing it

A nifty way to load execution plans into the cache at system startup is to execute them via an autostart procedure. Rather than execute each procedure itself as an autostart routine, you should call the procedures you want to load into the cache from a single autostart procedure in order to conserve execution threads (each autostart routine gets its own thread).

Once an execution plan is in the cache, subsequent calls to the procedure can reuse the plan without rebuilding the query tree or recompiling the plan. This eliminates two of the three steps that occur when you execute a stored procedure and is the chief performance advantage stored procedures give you over plain SQL batches.

Creating Stored Procedures

You create stored procedures using the CREATE PROCEDURE command; you alter them with ALTER PROCEDURE. The advantage to using ALTER PROC rather than CREATE PROC to change a stored procedure is that it preserves access permissions, whereas CREATE PROC doesn’t. A key difference between them is that ALTER PROC requires the use of the same encryption and recompile options as the original CREATE PROC. Other than that, the semantics of using the two commands are exactly the same.

A procedure can contain any valid Transact-SQL command except these: CREATE DEFAULT, CREATE PROC, CREATE RULE, CREATE SCHEMA, CREATE TRIGGER, and CREATE VIEW. Procedures can create databases, tables, and indexes but not other procedures, defaults, rules, schemas, triggers, or views.

Image

Note

GO is not a Transact-SQL command. It’s a command batch terminator, which is to say, it tells tools like Query Analyzer and OSQL where one batch of SQL ends and another begins. As such, it’s never allowed within a stored procedure—attempting this simply terminates the procedure. One rather odd aspect of the fact that GO is not a Transact-SQL command comes into play with comments. You can’t comment out GO using the /* */ comments. If GO is the leftmost item on its line, it will terminate the command batch regardless of the comment markers. Since this will prevent the closing comment marker from being reached, you’ll get an error message about a missing end comment marker. The solution? Use the comment style, delete the GO altogether, or remove its “G.”

To execute CREATE PROC you must be a member of the sysadmin role, the db_owner role, or the db_ddladmin_role. You can also execute CREATE PROC if you’ve been explicitly granted permission by a member of either the sysadmin or db_owner role.

The maximum stored procedure size is the lesser of 65,536 * the network packet size (which defaults to 4096 bytes) and 250 megabytes. The maximum number of parameters a procedure may receive is 1024.

Creation Tips

•   Include a comment header with each procedure that identifies its author, purpose, creation date and revision history, the parameters it receives, and so on. You can place this comment block after the CREATE PROC statement itself (but before the rest of the procedure) in order to ensure that it’s stored in syscomments and is visible from tools like Enterprise Manager that can access stored procedure source code directly via syscomments. Here’s a system procedure that generates comment headers for you:

USE master
GO
IF OBJECT_ID(’dbo.sp_object_script_comments') IS NOT NULL
     DROP PROC dbo.sp_object_script_comments
GO
CREATE PROCEDURE dbo.sp_object_script_comments
    -- Required parameters
    @objectname sysname=NULL,
    @desc sysname=NULL,

    -- Optional parameters
    @parameters varchar(8000)=NULL,
    @example varchar(8000)=NULL,
    @author sysname=NULL,
    @email sysname='(none)',
    @version sysname=NULL,
    @revision sysname='0',
    @datecreated smalldatetime=NULL,
    @datelastchanged smalldatetime=NULL
/*

Object: sp_object_script_comments
Description: Generates comment headers for object-creation SQL scripts

Usage: sp_object_script_comments @objectname="ObjectName", @desc="Description
of object",@parameters="param1[,param2...]"

Created by: Ken Henderson. Email: [email protected]

Version: 3.1

Example usage: sp_object_script_comments @objectname="sp_who", @desc="Returns
a list of currently running jobs", @parameters=[@loginname]

Created: 1992-04-03. Last changed: 1999-07-01 01:13:00.

*/
AS

IF (@objectname+@desc) IS NULL GOTO Help

PRINT '/*'
PRINT CHAR(13)
EXEC sp_usage @objectname=@objectname,
  @desc=@desc,
  @parameters=@parameters,
  @example=@example,
  @author=@author,
  @email=@email,
  @version=@version, @revision=@revision,
  @datecreated=@datecreated, @datelastchanged=@datelastchanged
PRINT CHAR(13)+'*/'

RETURN 0

Help:
EXEC sp_usage @objectname=’sp_object_script_comments',
  @desc='Generates comment headers for SQL scripts',
  @parameters='@objectname="ObjectName", @desc="Description of
  object",@parameters="param1[,param2...]"',
  @example=’sp_object_script_comments @objectname="sp_who", @desc="Returns
  a list of currently running jobs", @parameters=[@loginname]',
  @author='Ken Henderson',
  @email='[email protected]',
  @version='3', @revision='1',
  @datecreated='19920403', @datelastchanged='19990701'
RETURN -1


     This procedure generates comment header information for a stored procedure by calling the sp_usage procedure detailed below. It can be executed from any database by any procedure.

•   Allow an optional single parameter to be passed into every procedure that tells the caller how to use the procedure (e.g., '/?'). You can place this usage information at the end of the procedure in order to keep it from crowding your display and to locate it consistently from procedure to procedure. The best way to do this is to set up and call a separate procedure whose whole purpose is to report usage information. Here’s a script that creates the sp_usage procedure that’s used throughout this book for that very purpose:

USE master
GO
IF OBJECT_ID(’dbo.sp_usage') IS NOT NULL
  DROP PROC dbo.sp_usage
GO
CREATE PROCEDURE dbo.sp_usage
     -- Required parameters
     @objectname sysname=NULL,
     @desc sysname=NULL,

     -- Optional parameters
     @parameters varchar(8000)=NULL,
     @returns varchar(8000)='(None)',
     @example varchar(8000)=NULL,
     @author sysname=NULL,
     @email sysname='(none)',
     @version sysname=NULL,
     @revision sysname='0',
     @datecreated smalldatetime=NULL,
     @datelastchanged smalldatetime=NULL
/*

Object: sp_usage
Description: Provides usage information for stored procedures and descriptions of
other types of objects

Usage: sp_usage @objectname="ObjectName", @desc="Description of object"
     [, @parameters="param1,param2..."]
     [, @example="Example of usage"]
     [, @author="Object author"]
     [, @email="Author email"]
     [, @version="Version number or info"]
     [, @revision="Revision number or info"]
     [, @datecreated="Date created"]
     [, @datelastchanged="Date last changed"]

Returns: (None)

Created by: Ken Henderson. Email: [email protected]

Version: 3.1

Example: sp_usage @objectname="sp_who", @desc="Returns a list of currently
running jobs", @parameters=[@loginname]

Created: 1992-04-03. Last changed: 1999-07-01.

*/
AS
SET NOCOUNT ON
IF (@objectname+@desc IS NULL) GOTO Help

PRINT 'Object: '+@objectname
PRINT ’Description: '+@desc

IF (OBJECTPROPERTY(OBJECT_ID(@objectname),'IsProcedure')=1)
OR (OBJECTPROPERTY(OBJECT_ID(@objectname),'IsExtendedProc')=1)
OR (OBJECTPROPERTY(OBJECT_ID(@objectname),'IsReplProc')=1)
OR (LOWER(LEFT(@objectname,3))=’sp_') BEGIN -- Special handling for system
procedures
  PRINT CHAR(13)+'Usage: '+@objectname+' '+@parameters
  PRINT CHAR(13)+’Returns: '+@returns
END

IF (@author IS NOT NULL)
   PRINT CHAR(13)+'Created by: '+@author+'. Email: '+@email
IF (@version IS NOT NULL)
   PRINT CHAR(13)+’Version: '+@version+'.'+@revision
IF (@example IS NOT NULL)
  PRINT CHAR(13)+'Example: '+@example
IF (@datecreated IS NOT NULL) BEGIN -- Crop time if it’s midnight
  DECLARE @datefmt varchar(8000), @dc varchar(30), @lc varchar(30)
  SET @dc=CONVERT(varchar(30), @datecreated, 120)
  SET @lc=CONVERT(varchar(30), @datelastchanged, 120)
  PRINT CHAR(13)+'Created: '+CASE
DATEDIFF(ss,CONVERT(char(8),@datecreated,108),'00:00:00') WHEN 0 THEN LEFT(@dc,10)
ELSE @dc END
+'. Last changed: '+CASE
DATEDIFF(ss,CONVERT(char(8),@datelastchanged,108),'00:00:00') WHEN 0 THEN
LEFT(@lc,10) ELSE @lc END+'.'
END

RETURN 0

Help:
EXEC sp_usage @objectname=’sp_usage',           -- Recursive call
  @desc='Provides usage information for stored procedures and descriptions of
  other types of objects',
  @parameters='@objectname="ObjectName", @desc="Description of object"
     [, @parameters="param1,param2..."]
     [, @example="Example of usage"]
     [, @author="Object author"]
     [, @email="Author email"]
     [, @version="Version number or info"]
     [, @revision="Revision number or info"]
     [, @datecreated="Date created"]
     [, @datelastchanged="Date last changed"]',
  @example=’sp_usage @objectname="sp_who", @desc="Returns a list of currently
  running jobs", @parameters=[@loginname]',
  @author='Ken Henderson',
  @email='[email protected]',
  @version='3', @revision='1',
  @datecreated='4/3/92', @datelastchanged='7/1/99'
RETURN -1


    You can call sp_usage to report usage info for any procedure. In fact, sp_usage calls itself to do just that. (That’s the source of the message “Cannot add rows to sysdepends for the current stored procedure because it depends on the missing object ’sp_usage’.” The stored procedure will still be created.) Note the use of a GOTO label to place the usage info at the end of the procedure. Since Transact-SQL doesn’t support subroutines, this is unfortunately necessary. It allows code at the start of the procedure to check for invalid parameter values and quickly jump to the usage routine if necessary.

•   Set any environment options (QUOTED_IDENTIFIER, ANSI_DEFAULTS, etc.) that materially affect the procedure early in it. It’s a good practice to set them immediately on entrance to the procedure so that their presence is obvious to other developers.

•   Avoid situations where the owner of a stored procedure and the owner of its referenced tables differ. The best way to do this is by specifying the dbo user as the owner of every object you create. Having multiple objects with the same name but different owners adds a layer of obfuscation to the database that nobody needs. While perhaps plausible during development, it’s definitely something to avoid on production servers. Allow database users besides dbo to own objects only in very special circumstances.

•   Don’t use the sp_ prefix for anything but system procedures that reside in the master database. Don’t create procedures in user databases with the sp_ prefix, and don’t create nonsystem procedures in master.

•   For procedures that must be created in a specific database (e.g., system procedures), include USE dbname, where dbname is the name of the target database, at the top of the script that creates the procedure. This ensures that the procedure winds up where you want it and alleviates having to remember to change the current database in your query tool before executing the script.

•   Keep stored procedures as simple and modular as possible. Each stored procedure should accomplish a single task or a small group of closely related tasks.

•   Use SET NOCOUNT ON to minimize network traffic from stored procedures. As a rule, it should be the first statement in every stored procedure you create. (Note that SET NOCOUNT ON can cause problems with some applications—e.g., some versions of Microsoft Access.)

•   Create a procedure using the WITH ENCRYPTION option if you want to hide its source code from users. Don’t delete it from syscomments—doing so will render the procedure unable to execute, and you’ll have to drop and recreate it.

Temporary Procedures

Temporary procedures are created the same way temporary tables are created—a prefix of one pound sign (#) creates a local temporary procedure that’s visible only to the current connection, while a prefix of two pound signs (##) creates a global temporary procedure that’s visible to all connections.

System Procedures

System procedures are procedures that reside in the master database and are prefixed with sp_. System procedures are executable from any database. When executed from a database other than master, the system procedure assumes the context of the database in which it’s running. So, for example, if it references the sysobjects table, which exists in every database, it will access the one in the database that’s current when it’s executed, not the master. Here’s an example of a simple system procedure:

Image

Image

This procedure lists the names and creation dates of the objects that match a mask.

Here’s an example that uses one of SQL Server’s own system stored procedures. Like the procedure above, it can be run from any database to retrieve info on that database:

USE pubs
EXEC sp_spaceused

Image

Sp_spaceused queries various system tables to create the report it returns. Even though it resides in the master database, it automatically reflects the context of the current database because it’s a system procedure.

Note that you can trick system procedures into running in the context of any database (regardless of the current database) by prefixing them with the target database as though they resided in that database. Here’s an example:

Image

Here, even though sp_spaceused resides in master, and despite the fact that the current database is pubs, sp_spaceused reports space utilization info for the Northwind database because we’ve prefixed its name with Northwind. Even though sp_spaceused doesn’t reside in Northwind, SQL Server correctly locates it in master and runs it within the Northwind database context.

A system procedure that’s created by a user is listed as a user object in Enterprise Manager. This is because the system bit of its status column in sysobjects (0xC0000000) isn’t set by default. You can change this by calling the undocumented procedure sp_MS_marksystemobject. The procedure takes one argument—the name of the object whose system bit you want to set. Several undocumented functions and DBCC command verbs do not work properly unless called from a system object (see Chapter 20, “Undocumented T-SQL,” for more information). You can determine whether an object’s system bit has been set via the OBJECTPROPERTY() function’s IsMSShipped property.

Extended Procedures

Extended procedures are routines that reside in DLLs (Dynamic Link Libraries) that look and work like regular stored procedures. They receive parameters and return results via the Open Data Services framework and are normally written in C or C++. They reside in the master database (you cannot create them elsewhere) and run within the SQL Server process space.

Note that there’s nothing about extended procedures that requires them to be written in C or C++, but if you intend to write them in another language, you’ll first have to complete the formidable task of translating the Microsoft-provided ODS header files into that language. I’ve personally written extended procedures using Delphi and a couple of other tools, so this can be done, but it’s not for the timid.

Another possibility for calling routines written in languages besides C/C++ is to create “wrapper” routines using a C++ compiler and the ODS headers and call your routines (which reside in some other DLL) from them. Then you get the best of both worlds—you create procedures in the language you prefer, and you’re not forced to translate a bevy of constants, function declarations, and the like to another language.

Note that, unlike SQL Server 6.5 and earlier, extended procedure names are not case sensitive. Prior to version 7.0, extended procedure calls had to match the case of the underlying routine as it existed in its DLL, regardless of the case-sensitivity setting on the server. With version 7.0 and later, the server will find the underlying routine regardless of the case used.

Calls to extended procedures do not work like system procedures. They aren’t automatically located in master when referenced from other databases, and they don’t assume the context of the current database when run. If you want to execute an extended procedure from a database other than master, you’ll have to qualify the reference (e.g., EXEC master..xp_cmdshell ’dir') fully.

A common technique of making extended procedures a bit handier is to wrap them in system stored procedures. This allows them to be called from any database without requiring the “master..” prefix. You see this in a number of SQL Server’s own routines—many undocumented extended procedures are wrapped within system stored procedures. Here’s an example of a wrapped call to an extended procedure:

USE master
IF (OBJECT_ID(’dbo.sp_hexstring') IS NOT NULL)
   DROP PROC dbo.sp_hexstring
GO
CREATE PROC dbo.sp_hexstring @int varchar(10)=NULL, @hexstring varchar(30)=NULL OUT
/*
Object: sp_hexstring
Description: Return an integer as a hexadecimal string

Usage: sp_hexstring @int=Integer to convert, @hexstring=OUTPUT parm to receive hex
string

Returns: (None)

Created by: Ken Henderson. Email: [email protected]

Version: 1.0

Example: sp_hexstring 23, @myhex OUT

Created: 1999-08-02. Last changed: 1999-08-15.
*/
AS
IF (@int IS NULL) OR (@int = '/?') GOTO Help
DECLARE @i int, @vb varbinary(30)
SELECT @i=CAST(@int as int), @vb=CAST(@i as varbinary)
EXEC master..xp_varbintohexstr @vb, @hexstring OUT
RETURN 0

Help:
EXEC sp_usage @objectname=’sp_hexstring',
  @desc=’Return an integer as a hexadecimal string',
  @parameters='@int=Integer to convert, @hexstring=OUTPUT parm to receive hex string',
  @example=’sp_hexstring "23", @myhex OUT',
  @author='Ken Henderson',
  @email='[email protected]',
  @version='1', @revision='0',
  @datecreated='19990802', @datelastchanged='19990815'
RETURN -1

GO

DECLARE @hex varchar(30)
EXEC sp_hexstring 10, @hex OUT
SELECT @hex

------------------------------
0x0000000A


All this procedure really does is clean up the parameters to be passed to the extended procedure xp_varbintohexstr before calling it. Because it’s a system procedure, it can be called from any database without referencing the extended procedure directly.

Faux Procedures

There are a number of system-supplied stored procedures that are neither true system procedures nor extended procedures—they’re implemented internally by the server itself. Examples of these include sp_executesql, sp_prepare, most of the sp_cursorXXXX routines, sp_reset_connection, etc. These routines have stubs in master..sysobjects, and are listed as extended procedures but are, in fact, implemented internally by the server, not within an external ODS-based DLL. You can’t list their source code because it’s part of the server itself, and you can’t trace into them with a T-SQL debugger because they’re not written in Transact-SQL.

Executing Stored Procedures

Executing a stored procedure can be as easy as listing it on a line by itself in a T-SQL batch, like this:

sp_who


You should make a habit of prefixing all stored procedure calls with the EXEC keyword. Stored procedures without EXEC must be the first command in a command batch. Even if that were the case initially, inserting additional lines before the procedure call at some later date would break your code.

You can specify the WITH RECOMPILE option when calling a stored procedure (with or without EXEC) in order to force the recreation of its execution plan. This is handy when you know that factors related to the execution plan creation have changed enough that performance would benefit from rebuilding the plan.

INSERT and EXEC

The INSERT command supports calling a stored procedure in order to supply rows for insertion into a table. Here’s an example:

CREATE TABLE #locks (spid int, dbid int, objid int, objectname sysname NULL,
indid int, type char(4), resource char(15), mode char(10), status char(6))
INSERT #locks (spid, dbid, objid, indid, type, resource, mode, status)
EXEC sp_lock


This is a handy way of trapping the output of a stored procedure in a table so that you can manipulate it or retain it for later use. Prior to the advent of cursor OUTPUT parameters, this was the only way to perform further work on a stored procedure’s result set within Transact-SQL.

Note that INSERT...EXEC works with extended procedures that return result sets as well. Here’s a simple example:

CREATE TABLE #cmd_result (output varchar(8000))
INSERT #cmd_result
EXEC master..xp_cmdshell 'copy errorlog.1 *.sav'


Environmental Concerns

A number of SQL Server environmental settings affect the execution of stored procedures. They’re specified via the SET command and control the way that stored procedures handle quotes, nulls, cursors, BLOB fields, etc. The status of two of these—QUOTED_IDENTIFIER and ANSI_NULLS—is actually recorded in each procedure’s status field in sysobjects. QUOTED_IDENTIFIER controls whether strings within double quotes are interpreted as object identifiers, and ANSI_NULLS controls whether non-ANSI equality comparisons with NULLs are allowed. Here’s an example that features a quoted identifier:

USE pubs
SET QUOTED_IDENTIFIER ON
GO
IF OBJECT_ID(’TABLE') IS NOT NULL
  DROP PROC "TABLE"
GO
CREATE PROC "TABLE" @tableclause varchar(8000),
@columnclause varchar(8000)='*',
@whereclause varchar(8000)=NULL,
@groupbyclause varchar(8000)=NULL,
@havingclause varchar(8000)=NULL,
@orderbyclause varchar(8000)=NULL,
@computeclause varchar(8000)=NULL
AS
DECLARE @execstr varchar(8000)
SET @execstr=’SELECT '+@columnclause+' FROM '+@tableclause
+ISNULL(' WHERE '+@whereclause,' ')
+ISNULL(' GROUP BY '+@groupbyclause,' ')
+ISNULL(' HAVING '+@havingclause,' ')
+ISNULL(' ORDER BY '+@orderbyclause,' ')
+ISNULL(' COMPUTE '+@computeclause,")

EXEC(@execstr)
GO
SET QUOTED_IDENTIFIER OFF
GO


Thanks to SET QUOTED_IDENTIFIER, we can use a reserved word, TABLE, as the name of the procedure. It allows us to build our own version of the ANSI/ISO SQL TABLE command, which Transact-SQL does not implement. Since it’s named using a reserved word, executing such a procedure requires SET QUOTED_IDENTIFIER as well:

SET QUOTED_IDENTIFIER ON
GO
"TABLE" ’sales',’title_id, SUM(qty) AS
sales',’title_id<>''PS2091''',’title_id',DEFAULT,'2 DESC'
GO
SET QUOTED_IDENTIFIER OFF
GO


(Results abridged)

Image

Note that I don’t recommend you use reserved words for object identifiers. In my opinion, this adds needless confusion to your code. It is, however, something you should be aware of because other developers sometimes do it.

Rather than allowing developers to name procedures with reserved words, the more common use of SET QUOTED_IDENTIFIER with stored procedures is to facilitate references to objects whose names contain reserved words, spaces, or other normally disallowed characters. Here’s an example:

USE Northwind
SET QUOTED_IDENTIFIER ON
GO
IF OBJECT_ID(’dbo.listorders') IS NOT NULL
   DROP PROC dbo.listorders
GO
CREATE PROC dbo.listorders
AS
SELECT * FROM "Order Details"
GO
SET QUOTED_IDENTIFIER OFF
GO

EXEC listorders


(Results abridged)

Image

The table name “Order Details” (from the Northwind sample database) contains both a reserved word and a space, so it can’t be referenced without special handling. In this case, we turned on quoted identifier support and enclosed the table name in double quotes, but a better way would be to enclose it in square brackets (e.g., [Order Details]) because this alleviates the need to change any settings. Note that square bracket delimiters are a SQL Server extension—they’re not a part of the ANSI/ISO SQL standard.

The ANSI_NULLS setting is equally important to stored procedures. It controls whether non-ANSI equality comparisons with NULLs are allowed. This is particularly important with stored procedure parameters that can allow NULLs. Here’s an example:

Image

If not for SET ANSI_NULLS, the procedure would be unable to compare a NULL @region successfully with the Region column in the Northwind Employees table. The query would never return any rows because, according to the ANSI spec, one NULL value never equals another. The handiness of this becomes more obvious when a procedure defines a number of NULLable parameters like @region. Without the ability to test NULL values for equality in a manner identical to non-NULL values, each NULLable parameter would require its own special IS NULL handling, perhaps multiplying the amount of code necessary to implement the query.

The fact that the QUOTED_IDENTIFIER and ANSI_NULLS settings are saved with each stored procedure means that you can count on them to have their original values when the procedure is executed. SQL Server restores them to the values they had when the procedure was created each time it’s executed and then resets them afterward. So, if we have this code:

SET ANSI_NULLS ON
EXEC listregionalemployees NULL


the stored procedure still executes as though ANSI_NULLS is set to OFF. Note that you can check the saved status of a procedure’s QUOTED_IDENTIFIER and ANSI_NULLS settings via the OBJECTPROPERTY() function. Here’s an example:

USE pubs
SELECT OBJECTPROPERTY(OBJECT_ID(’table'),'ExecIsQuotedIdentOn') AS 'QuotedIdent'

USE Northwind
SELECT OBJECTPROPERTY(OBJECT_ID('listregionalemployees'),'ExecIsAnsiNullsOn') AS 'AnsiNulls'

QuotedIdent
----------------
1

AnsiNulls
----------------
0


There are numerous other commands that affect how Transact-SQL code—both within and outside stored procedures—executes. Commands like SET TEXTSIZE, SET CURSOR_ CLOSE_ON_COMMIT, and SET IMPLICIT_TRANSACTIONS all affect how a stored procedure’s code carries out its duties. If you have procedure code that relies on a SET command to have a particular setting, the wise thing to do is establish that setting as early as possible in the procedure and document why it’s necessary via comments.

Parameters

Parameters can be passed to stored procedures by name or by position. Here’s an example of each method:

EXEC sp_msforeachtable @command1=’sp_help "?"', @replacechar = '?'

EXEC sp_msforeachtable ’sp_help "?"', '?'


The obvious advantage to referencing parameters by name is that they can be specified out of order.

You can force a parameter for which a default value has been defined to use that default by omitting it altogether or by passing it the DEFAULT keyword, like so:

EXEC sp_msforeachtable @command1=’sp_help "?"', @replacechar = DEFAULT


You can specify NULL to supply individual parameters with NULL values. That’s sometimes handy for procedures that expose special features when parameters are omitted or set to NULL. Here’s an example:

EXEC sp_who @loginame=NULL


(Results abridged)

Image

In this example, sp_who returns a list of all active connections because its lone parameter is NULL. When a valid login name is specified, sp_who returns just those connections established by the specified login name. When the @loginame parameter is NULL, all connections are listed. The same thing would happen if @loginame was omitted altogether. I’ve specified NULL here for illustration purposes.

Output Parameters

Output parameters allow values to be returned from stored procedures. These parameters can be integers, character strings, dates, and even cursors. Here’s an example:

USE pubs
IF OBJECT_ID(’dbo.listsales') IS NOT NULL
   DROP PROC dbo.listsales
GO
CREATE PROC dbo.listsales @bestseller tid OUT, @topsales int OUT,
       @salescursor cursor varying OUT
AS

SELECT @bestseller=bestseller, @topsales=totalsales
FROM (
     SELECT TOP 1 title_id AS bestseller, SUM(qty) AS totalsales
     FROM sales
     GROUP BY title_id
     ORDER BY 2 DESC) bestsellers

DECLARE s CURSOR
LOCAL
FOR SELECT * FROM sales

OPEN s

SET @salescursor=s
RETURN(0)

DECLARE @topsales int, @bestseller tid, @salescursor cursor
EXEC listsales @bestseller OUT, @topsales OUT, @salescursor OUT
SELECT @bestseller, @topsales

FETCH @salescursor
CLOSE @salescursor
DEALLOCATE @salescursor
------ -----------
PS2091 191

stor_id  ord_num       ord_date                             qty              payterms  title_id
---------   ---------------- --------------------------------   -------------  ------------- --------
6380       6871              1994-09-14 00:00:00.000      5                 Net 60       BU1032


Output parameters are identified with the OUTPUT keyword (which can be abbreviated as “OUT”). Notice the use of the OUT keyword in the procedure definition as well as in the EXEC parameter list. Both the procedure and its caller must specify which parameters are output parameters.

Cursor output parameters are a sensible means of returning a result set to a caller. Notice the use of the varying keyword with the cursor parameter in the procedure definition. This keyword is required with cursor parameters and indicates that the return value is nonscalar—that is, it returns more than a single value. Cursor parameters can only be output parameters, so the OUT keyword is required as well.

Result Codes

Procedures return result codes via the RETURN command. A return code of 0 indicates success, values –1 through –14 indicate different types of failures, and values –15 through –99 are reserved for future use. Table 15.1 lists the meaning of codes –1 through –14:

Table 15.1. Stock return codes and their meanings.

Image

You can access a procedure’s return code by assigning it to an integer value, like this:

DECLARE @res int
EXEC @res=sp_who


Listing Procedure Parameters

You can list a procedure’s parameters (which include its return code—considered parameter 0) using the undocumented procedure sp_procedure_params_rowset. Here’s an example:

EXEC sp_procedure_params_rowset ’sp_MSforeachtable'


(Results abridged)

Image

General Parameter Notes

•   Provide default values for parameters when it makes sense. Parameter defaults are limited to constants and the NULL value.

•   Check parameters for invalid or missing values early in your stored procedures.

•   Use human-friendly parameter names so that parameters can be passed by name easily.

•   Parameter names are local to stored procedures. You can use the same name in multiple procedures.

•   You can find stored procedure parameter information in the syscolumns system table (that’s where sp_procedure_params_rowset gets its info).

•   Stored procedures support up to 1024 parameters. The number of stored procedure local variables is limited only by available memory.

Important Automatic Variables

By their very nature, automatic variables (what the Books Online now call “functions”) are usually accessed from within stored procedures. This makes most of them relevant in some way to stored procedures. However, a few of them are more relevant to stored procedure use than the others. Table 15.2 summarizes them.

Table 15.2. Stored procedure–related automatic variables.

Image

Flow Control Language

No discussion of stored procedures would be complete without covering control-of-flow language statements. These are referred to as “flow control” or “control-of-flow” statements because they control the flow of execution through a stored procedure or batch. Transact-SQL flow control language statements include IF...ELSE, WHILE, GOTO, RETURN, WAITFOR, BREAK, CONTINUE, and BEGIN..END. Without repeating what’s already covered quite adequately by the Books Online, here’s a simple procedure that illustrates all of them:

USE pubs
IF OBJECT_ID(’dbo.listsales') IS NOT NULL
  DROP PROC dbo.listsales
GO
CREATE PROC dbo.listsales @title_id tid=NULL
AS

IF (@title_id='/?') GOTO Help     -- Here’s a basic IF

-- Here’s one with a BEGIN..END block
IF NOT EXISTS(SELECT * FROM titles WHERE title_id=@title_id) BEGIN
   PRINT 'Invalid title_id'
   WAITFOR DELAY '00:00:03' -- Delay 3 secs to view message
   RETURN -1
END

IF NOT EXISTS(SELECT * FROM sales WHERE title_id=@title_id) BEGIN
   PRINT 'No sales for this title'
   WAITFOR DELAY '00:00:03' -- Delay 3 secs to view message
   RETURN -2
END

DECLARE @qty int, @totalsales int
SET @totalsales=0

DECLARE c CURSOR
FOR SELECT qty FROM sales WHERE title_id=@title_id

OPEN c

FETCH c INTO @qty

WHILE (@@FETCH_STATUS=0) BEGIN    -- Here’s a WHILE loop
  IF (@qty<0) BEGIN
    Print 'Bad quantity encountered'
    BREAK -- Exit the loop immediately
  END ELSE IF (@qty IS NULL) BEGIN
   Print 'NULL quantity encountered -- skipping'
   FETCH c INTO @qty
   CONTINUE -- Continue with the next iteration of the loop
  END
 SET @totalsales=@totalsales+@qty
 FETCH c INTO @qty
END

CLOSE c
DEALLOCATE c

SELECT @title_id AS ’TitleID', @totalsales AS ’TotalSales'
RETURN 0      -- Return from the procedure indicating success

Help:
EXEC sp_usage @objectname='listsales',
  @desc='Lists the total sales for a title',
  @parameters='@title_id="ID of the title you want to check"',
  @example='EXEC listsales "PS2091"',
  @author='Ken Henderson',
  @email='[email protected]',
  @version='1', @revision='0',
  @datecreated='19990803', @datelastchanged='19990818'
WAITFOR DELAY '00:00:03' -- Delay 3 secs to view message
RETURN -1
GO

EXEC listsales 'PS2091'
EXEC listsales 'badone'
EXEC listsales 'PC9999'
TitleID    TotalSales
------------ -----------
PS2091    191

Invalid title_id
No sales for this title


Errors

Stored procedures report errors via return codes and the RAISERROR command. RAISERROR doesn’t change the flow of the procedure, it merely displays an error message (optionally writing it to the SQL Server error log and the NT application event log) and sets the @@ERROR automatic variable. RAISERROR can reference predefined error messages that reside in the sysmessages table (you create these with sp_addmessage), or you can supply it with a custom message string. If you supply a custom message during the call to RAISERROR, the error number is set to 50,000. RAISERROR can format messages similarly to the C printf() function, allowing you to supply your own arguments for the messages it displays.

RAISERROR allows both a severity and a state to be specified with each message. Severity values less than 16 produce informational messages in the system event log (when logged), a severity of 16 produces a warning message in the event log, and severity values greater than 16 produce error messages in the event log. Severity values up through 18 can be specified by any user; severity values 19–25 are reserved for members of the sysadmin role and require the use of the WITH LOG option. Note that severity values over 20 are considered fatal and cause the client connection to be terminated.

State is an informational value that you can use to indicate state information to your front-end application—it has no predefined meaning to SQL Server. Raising an error with a state of 127 will cause the ISQL and OSQL utilities to set the operating system ERRORLEVEL value to the error number returned by RAISERROR. Note that, unlike releases prior to 7.0, the ISQL utility no longer exits immediately when a state of 127 is used—it merely sets ERRORLEVEL; OSQL, by contrast, exits immediately. So if we have this SQL batch:

RAISERROR(’Setting the OS ERRORLEVEL variable',16,127) WITH NOWAIT
PRINT 'Prior to 7.0, execution would never make it here in ISQL'


and we execute it from this operating system command batch:

@ECHO OFF
isql -Usa -P -iraiserror01.sql
ECHO %ERRORLEVEL%
osql -Usa -P -iraiserror01.sql
ECHO %ERRORLEVEL%


here’s what happens:

D:>RAISERROR
1> 2> 3> Msg 50000, Level 16, State 127, Server CALIGULA, Line 1
Setting the OS ERRORLEVEL variable
Prior to 7.0, execution would never make it here in ISQL
50000
1> 2> 3> Msg 50000, Level 16, State 127, Server CALIGULA, Procedure , Line 1
[Microsoft][ODBC SQL Server Driver][SQL Server]Setting the OS ERRORLEVEL
variable
50000


This is handy for exiting a command batch immediately without causing undue alarm or generating unnecessary entries in the system event log. Though you could raise a message with a high severity level to terminate the connection, that creates log entries and potentially raises a red flag over something that’s completely normal—aborting a batch before processing it completely. And though you could also abort the batch with the EXIT command, the operating system ERRORLEVEL wouldn’t be set, so you’d have no way of knowing why the batch exited.

RAISERROR supports a handful of options that affect its behavior. The WITH LOG option copies the error message to the NT event log (assuming SQL Server is running on Windows NT) and the SQL Server error log regardless of whether the message was defined using the with_log option of sp_addmessage. The WITH NOWAIT option causes the message to be returned immediately to the client. The WITH SETERROR option forces the automatic @@ERROR variable to return the last error number raised, regardless of the severity of the error message.

The system procedure sp_addmessage is used to add messages to the sysmessages table that RAISERROR can then use. User messages should have error numbers of 50,000 or higher. The chief advantage of using SQL Server’s system messages facility is that it’s language independent. Because you specify a language with each message you add, you can have several messages with the same error number but with different language indicators. Then, based on the language setting the user chooses when installing SQL Server, the appropriate message will be displayed when your code calls RAISERROR.

Because RAISERROR can display a message and set the @@ERROR variable in one fell swoop, it’s sometimes used for tasks other than displaying error messages. Its printf()-like formatting ability makes it ideal for formatting strings other than error messages. Here’s an example that features RAISERROR used to list a table:

DECLARE c CURSOR
FOR SELECT title_id, SUM(qty) as sales FROM sales GROUP BY title_id

DECLARE @title_id tid, @qty int

OPEN c

RAISERROR(’Starting loop',1,1) -- Seed @@ERROR
WHILE (@@ERROR<=1) BEGIN
   FETCH c INTO @title_id, @qty
   IF (@@FETCH_STATUS=0)
     RAISERROR(’Title ID %s has sold %d units',1,1,@title_id,@qty)
   ELSE
   BREAK
END

CLOSE c

DEALLOCATE c
Msg 50000, Level 1, State 50000
Starting loop
Msg 50000, Level 1, State 50000
Title ID BU1032 has sold 45 units
Msg 50000, Level 1, State 50000
Title ID BU1111 has sold 25 units
Msg 50000, Level 1, State 50000
Title ID BU2075 has sold 35 units
Msg 50000, Level 1, State 50000
Title ID BU7832 has sold 15 units
Msg 50000, Level 1, State 50000
Title ID MC2222 has sold 10 units
Msg 50000, Level 1, State 50000
Title ID MC3021 has sold 40 units
Msg 50000, Level 1, State 50000
Title ID MC3026 has sold 30 units
Msg 50000, Level 1, State 50000
Title ID PC1035 has sold 30 units

Msg 50000, Level 1, State 50000
Title ID PC8888 has sold 50 units
Msg 50000, Level 1, State 50000
Title ID PS1372 has sold 20 units
Msg 50000, Level 1, State 50000
Title ID PS2091 has sold 191 units
Msg 50000, Level 1, State 50000
Title ID PS2106 has sold 25 units
Msg 50000, Level 1, State 50000
Title ID PS3333 has sold 15 units
Msg 50000, Level 1, State 50000
Title ID PS7777 has sold 25 units
Msg 50000, Level 1, State 50000
Title ID TC3218 has sold 40 units
Msg 50000, Level 1, State 50000
Title ID TC4203 has sold 20 units
Msg 50000, Level 1, State 50000
Title ID TC7777 has sold 20 units


Of course, the obligatory “Msg...” lines would be a bit of an annoyance, but you could strip these out in your front-end application if you decided to use this approach.

@@ERROR

Make a habit of checking @@ERROR after significant code in your procedures, especially after data modification operations. The hallmark of robust code is thorough error checking, and until Transact-SQL supports structure exception handling, @@ERROR is the best way to accomplish this.

xp_logevent

You can use the xp_logevent system procedure to add a message to the SQL Server error log or the NT event log. The main difference between this approach and calling RAISERROR is that no error message is sent to the client. The message number or string you pass to xp_logevent is silently logged without client notification.

Nesting

Stored procedures can be nested up to 32 levels deep. The @@NESTLEVEL automatic variable indicates the level of nesting at any given time. A nesting level of 0 is returned at the command batch level, 1 within each stored procedure called from level 0 (and from first-level triggers), 2 for each proc called from level 1, and so forth. Objects (including temporary tables) and cursors created within a stored procedure are visible to all objects it calls. Objects and cursors created at level 0 are visible to all objects.

Recursion

Transact-SQL supports recursion. Recursion can be defined as a method of solving a problem wherein the solution is arrived at by repetitively applying it to subsets of the problem. An obvious use of recursion is in creating parsers and performing numeric computations that lend themselves to repetitive evaluation by the same processing logic. Here’s a an example that features a stored procedure that calculates the factorial of a number:

SET NOCOUNT ON
USE master
IF OBJECT_ID(’dbo.sp_calcfactorial') IS NOT NULL
   DROP PROC dbo.sp_calcfactorial
DECLARE @typestr varchar(20)
SET @typestr=’decimal('+CAST(@@MAX_PRECISION AS varchar(2))+',0)'
IF TYPEPROPERTY('bigd','precision') IS NOT NULL
   EXEC sp_droptype 'bigd'

EXEC sp_addtype 'bigd',@typestr -- Add a custom type corresponding to the @@MAX_PRECISION variable

GO
CREATE PROC dbo.sp_calcfactorial @base_number bigd, @factorial bigd OUT
AS
SET NOCOUNT ON
DECLARE @previous_number bigd

IF ((@base_number>26) and (@@MAX_PRECISION<38)) OR (@base_number>32) BEGIN
   RAISERROR('Computing this factorial would exceed the server'’s max. numeric precision of %d or
   the max. procedure nesting level of 32',16,10,@@MAX_PRECISION)
   RETURN(-1)
END
IF (@base_number<0) BEGIN
    RAISERROR('Can'’t calculate negative factorials',16,10)
    RETURN(-1)
END

IF (@base_number<2) SET @factorial=1 -- Factorial of 0 or 1=1
ELSE BEGIN
     SET @previous_number=@base_number-1
     EXEC sp_calcfactorial @previous_number, @factorial OUT -- Recursive call
     IF (@factorial=-1) RETURN(-1) -- Got an error, return
     SET @factorial=@factorial*@base_number
     IF (@@ERROR<>0) RETURN(-1) -- Got an error, return
END
RETURN(0)
GO

DECLARE @factorial bigd
EXEC sp_calcfactorial 26, @factorial OUT
SELECT @factorial

Type added.

Cannot add rows to sysdepends for the current stored procedure because it depends on
the missing object ’sp_calcfactorial'. The stored procedure will still be created.

--------------------------------------------
403291461126605635584000000


The first thing this procedure does is create a decimal-based user-defined data type that matches the @@MAX_PRECISION automatic variable. This allows the procedure to use as large a number as the server can handle. Next, the procedure checks to make sure it has been passed a valid number for which to compute a factorial. It then recursively calls itself to perform the computation. As you can see, with the default maximum numeric precision of 28, SQL Server can handle numbers in excess of 400 septillion! 1

Autostart Procedures

Autostart procedures have lots of practical uses. You can use them to perform start-up processes and other administrative work. You can use them to load commonly used procedures into the procedure cache with each server boot. You use the sp_procoption stored procedure to flag a stored procedure as an autostart routine, like so:

EXEC sp_procoption ’sp_databases',’startup',true


Some notes about autostart procedures:

•   They must reside in the master database.

•   They must be owned by a member of the sysadmin role.

•   They cannot require any parameters.

•   They cannot return a result set.

•   You can pass trace flag 4022 (-T4022) on the SQL Server command line to prevent autostart routines from executing.

Encryption

You can encrypt the source code that’s stored in syscomments for a stored procedure, view, or trigger using the WITH ENCRYPTION option when you create the object. This prevents users from viewing your source code with tools such as Enterprise Manager, but it also thwarts stored procedure debuggers, like the one included with the Enterprise Edition of Visual Studio. Encrypted objects have the third bit of the texttype column in syscomments set.

Note that once you’ve encrypted an object, there’s no supported way of decrypting it. You can’t view it, nor can members of the sysadmin role or anyone else.

Triggers

A trigger is a special type of stored procedure that executes when a specified DML operation (an INSERT, DELETE, or UPDATE or any combination of them) occurs. Triggers are constructed via the CREATE TRIGGER command and are attached to tables. When its host table is dropped, so is the trigger.

Most of the details of stored procedure programming apply equally well to triggers. In fact, since you can call a stored procedure from a trigger, you can effectively do anything in a trigger that a stored procedure can do. One thing that triggers don’t normally do is return result sets. Most front ends have no way of handling trigger-generated result sets, so you just don’t see it in production code. Note that SQL Server doesn’t permit triggers to return result codes.

Triggers fire once per statement, not per row, regardless of the number of rows changed by a given DML statement. You can set up as many triggers as you want (well, up to 2 billion per database, anyway) for a table—triggers associated with the same DML statement will fire in succession in no particular order.

DRI (declarative referential integrity) constraints have precedence over triggers. This means that a violation of a DRI constraint by a DML command will prevent triggers from executing.

Inside triggers, you can check which columns are being updated by a DML operation via the UPDATE() and COLUMNS_UPDATE() functions. The UPDATE() function returns true or false based on whether the value of a specified column is being set (regardless of whether it’s actually changing). COLUMNS_UPDATED() returns a bitmap representing which columns are being set.

Triggers can cause other triggers to fire if the nested triggers option has been enabled with sp_configure. Triggers can fire themselves recursively if the recursive triggers database option has been enabled. The @@NESTLEVEL automatic variable returns 1 within a first-level trigger, 2 within one it causes to fire, 3 for any it causes to fire, and so forth.

When a user transaction is not active, a trigger and the DML operation that fired it are considered a single transaction. When a trigger generates a fatal error or executes a ROLLBACK TRANSACTION, the currently active transaction is rolled back and the current command batch is canceled.

SQL Server exposes special logical tables for use by triggers: the inserted and deleted tables. For INSERT operations, the inserted table lists the row(s) about to be appended to the table. For DELETE operations, the deleted table lists the row(s) about to be removed from the table. For UPDATE operations, the deleted table lists the old version of the row(s) about to be updated, and the inserted table lists the new version. You can query these tables to allow or prevent database modifications based on the columns or data the operations are attempting to modify. Rolling back the current transaction is normally the way that triggers are aborted since SQL Server’s Transact-SQL doesn’t support a ROLLBACK TRIGGER command (à la Sybase). Note that you can’t modify these logical tables—they’re for inspection only.

Nonlogged operations (operations that do not generate row modification log records) do not fire triggers. So, for example, even though TRUNCATE TABLE deletes all the rows in a table, those row deletions aren’t logged individually and therefore do not fire any delete triggers that may have been defined for the table.

You can disable a trigger via the ALTER TABLE...DISABLE TRIGGER command. Disabled triggers can be reenabled using ALTER TABLE...ENABLE TRIGGER. Here are a few examples:

ALTER TABLE sales
DISABLE TRIGGER SalesQty_INSERT_UPDATE

ALTER TABLE sales
ENABLE TRIGGER SalesQty_INSERT_UPDATE

ALTER TABLE sales
DISABLE TRIGGER ALL

ALTER TABLE sales
ENABLE TRIGGER ALL


Triggers fire just after the work has been completed by the DML statement but before it has been committed to the database. A DML statement’s execution plan branches to any triggers it fires just before returning. If the trigger permits the operation to proceed, and if no user transaction is present, any changes made by the DML statement are then committed to the database.

Here are a few trigger examples:

SET NOCOUNT ON
USE pubs
DROP TRIGGER SalesQty_INSERT_UPDATE
GO

CREATE TRIGGER SalesQty_INSERT_UPDATE ON sales FOR INSERT, UPDATE AS

IF @@ROWCOUNT=0 RETURN -- No rows affected, exit immediately

IF (UPDATE(qty)) AND (SELECT MIN(qty) FROM inserted)<10 BEGIN
    RAISERROR('Minimum order is 10 units',16,10)
    ROLLBACK TRAN
    RETURN
END
GO

-- Test a single-row INSERT
BEGIN TRAN
   INSERT sales VALUES (6380,'ORD9997',GETDATE(),5,'Net 60','BU1032')
IF @@TRANCOUNT>0 ROLLBACK TRAN
GO

-- Test a multirow INSERT
BEGIN TRAN
   INSERT sales
   SELECT stor_id, ord_num+'A', ord_date, 5, payterms, title_id FROM sales
IF @@TRANCOUNT>0 ROLLBACK TRAN
GO

DROP TRIGGER Sales_DELETE
GO
CREATE TRIGGER Sales_DELETE ON sales FOR DELETE AS

IF @@ROWCOUNT=0 RETURN -- No rows affected, exit immediately

IF (@@ROWCOUNT>1) BEGIN
  RAISERROR(’Deletions of more than one row at a time are not permitted',16,10)
  ROLLBACK TRAN
  RETURN
END
GO
BEGIN TRAN
  DELETE sales
IF @@TRANCOUNT>0 ROLLBACK TRAN
GO

DROP TRIGGER Salesord_date_qty_UPDATE
GO
CREATE TRIGGER Salesord_date_qty_UPDATE ON sales FOR INSERT, UPDATE AS

IF @@ROWCOUNT=0 RETURN -- No rows affected, exit immediately

-- Check to see whether the 3rd and 4th columns are being updated simultaneously
IF (COLUMNS_UPDATED() & (POWER(2,3-1) | POWER(2,4-1)))=12 BEGIN

UPDATE s SET payterms='Cash'
FROM sales s JOIN inserted i ON (s.stor_id=i.stor_id AND s.ord_num=i.ord_num)

IF (@@ERROR<>0) -- UPDATE generated an error, rollback transaction
   ROLLBACK TRANSACTION
RETURN

END
GO

-- Test with a single-row UPDATE
BEGIN TRAN
  UPDATE sales SET ord_date=GETDATE(), qty=15
  WHERE stor_id=7066 and ord_num='A2976'

  SELECT * FROM sales
  WHERE stor_id=7066 and ord_num='A2976'
IF @@TRANCOUNT>0 ROLLBACK TRAN
GO

-- Test with a multirow UPDATE
BEGIN TRAN
   UPDATE sales SET ord_date=GETDATE(), qty=15
   WHERE stor_id=7066

   SELECT * FROM sales
   WHERE stor_id=7066
IF @@TRANCOUNT>0 ROLLBACK TRAN

Server: Msg 50000, Level 16, State 10, Procedure CheckSalesQty, Line 3
Minimum order is 10 units
Server: Msg 50000, Level 16, State 10, Procedure CheckSalesQty, Line 3
Minimum order is 10 units
Server: Msg 50000, Level 16, State 10, Procedure CheckSalesDelete, Line 3
Deletions of more than one row at a time are not permitted

Image

Some general trigger notes:

•   Make sure your triggers allow for the possibility that more than one row could be altered at once. Triggers that work fine with single-row operations often break when multirow operations come their way. Not allowing for multirow updates is the single most common error that trigger neophytes make.

•   Begin each trigger by checking @@ROWCOUNT to see whether any rows have changed. If none have, exit immediately since there’s nothing for the trigger to do.

•   Use the UPDATE() and COLUMNS_UPDATED() functions to ensure the values you’re wanting to verify have actually changed.

•   Never wait for user input or any other user event within a trigger.

•   Check for errors after significant operations within your triggers, especially DML operations. Commands within triggers should check for errors just as stored procedures should.

•   Keep operations within a trigger to a minimum. Triggers should execute as quickly as possible to keep from adversely affecting system performance.

•   Provide descriptive error messages without being loquacious. Return user messages rather than obscure system error messages when possible.

•   Modularize your triggers by locating code that’s executed by multiple triggers or that’s lengthy or complex in separate stored procedures.

•   Check triggers that enforce referential integrity for robustness. Try every combination of columnar updates to be sure all scenarios are covered.

•   Write a test script for every trigger you build. Make sure it tests every situation the trigger is supposed to handle.

Debugging Procedures

The Enterprise Edition of Visual Studio, as well as various third-party tools, allows Transact-SQL stored procedures to be debugged. This means that you can step into stored procedures called from Visual Studio projects such as VB and VC++ applications. You can set breakpoints, establish watches, and generally do what debuggers are designed to do—debug code.

The interface by which this occurs is known as the SQL Server Debug Interface, or SDI for short. It was originally introduced with SQL Server 6.5 and has now been completely integrated with Visual Studio.

Some notes on debugging Transact-SQL with the SDI:

•   SDI is implemented via the sp_sdidebug pseudo procedure (see the section “Faux Procedures” earlier in the chapter for more information on “pseudo” procedures).

•   You should run SQL Server under a user account, not the LocalSystem account, when debugging because running under LocalSystem disables breakpoints.

•   When debugging on the same machine as your server, run the server under the same user context as the debugger.

•   Ensure that you can run SQL Server as a console app rather than a service.

•   On Windows NT, SDI messages are written to the event log under MSDEVSDI.

Summary

In this chapter, you explored many of the nuances and idiosyncrasies of building stored procedures and triggers. You learned how to construct user as well as system procedures and how to pass parameters to and from the procedures you create. You became familiar with some of the internals of the stored procedure execution process, and you learned how triggers work. You became acquainted with debugging stored procedures, and you learned about fringe elements of stored procedure creation such as encryption and execution plan recompilation.

1. This assumes the definition of septillion as used in the United States: 1 followed by 24 zeros. In Great Britain and Germany, a septillion is equal to 1 followed by 42 zeros.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset