Category Archives: Imported from BeyondRelational

These posts are imported from my old blog page: http://beyondrelational.com/modules/2/blogs/77/nakuls-blog.aspx

Filtered Indexes – Underappreciated features of Microsoft SQL Server


This is part of the series of posts that I am running under the title "Under Appreciated Features of SQL Server". The series has been inspired by Andy Warren’s editorial on SQLServerCentral.com.

(For those new to indexes, please do not proceed any further. Please go through the “Introduction to Indexes” series by Gail Shaw. You can access the series from here.)

Indexes are not new, but Microsoft SQL Server 2008 introduced a new feature called “Filtered Indexes”. A filtered index is an optimized non-clustered index (for obvious reasons, you cannot have a filtered clustered index), especially suited to cover queries that select from a well-defined subset of data. What this means is that if a particular subset of data is frequently fetched from a table (about 80% of the time), then such tables are ideal candidates to have filtered indexes on the specified range of data.

A Brief demo

It looks quite simple, but the major change is that the CREATE INDEX statement allows allows for the use of a filter predicate to index a portion of rows in the table.

If a column contains mostly NULL values, and the queries in the application only fetch the non NULL values, such columns are great candidates to have filtered indexes on. In the AdventureWorks database, bill of materials are generally important for products whose deliveries are already completed. Hence, the application queries to Production.BillOfMaterials are great candidates for use of a filtered index.

Here’s a brief demo on how filtered indexes can be created, and how they can be effective.

First up, we will see how we would conventionally address the problem of efficiently fetching ComponentID and StartDate from the Production.BillOfMaterials table. Use Ctrl+M (or Query->Include Actual Execution Plan) to view the actual Execution Plan of the query batch.

--The below example is a direct take from BOL, with a few minor changes
USE AdventureWorks2008;
GO

--Create a Regular Index on the Production.BillOfMaterials table
IF EXISTS (SELECT name FROM sys.indexes
    WHERE name = N'RIBillOfMaterialsWithEndDate' 
    AND object_id = OBJECT_ID(N'Production.BillOfMaterials'))
DROP INDEX RIBillOfMaterialsWithEndDate
    ON Production.BillOfMaterials;
GO

CREATE NONCLUSTERED INDEX "RIBillOfMaterialsWithEndDate"
    ON Production.BillOfMaterials (ComponentID, StartDate, EndDate)
GO

-- Clear the Procedure cache
DBCC FREEPROCCACHE
GO
--This select will NOT make use of the filtered index
SELECT ComponentID, StartDate FROM Production.BillOfMaterials WHERE EndDate IS NOT NULL

Looking at the execution plan, you can see that SQL Server utilizes the newly created non-clustered index.

image

Notice that we had to include the column – EndDate as part of the columns on the index. However, if the application is only interested in records whose EndDate IS NOT NULL, a better idea would be to create a Filtered non-clustered index.

--The below example is a direct take from BOL, with a few minor changes
USE AdventureWorks2008;
GO

--For a clean test, begin by dropping the "regular index"
IF EXISTS (SELECT name FROM sys.indexes
    WHERE name = N'RIBillOfMaterialsWithEndDate' 
    AND object_id = OBJECT_ID(N'Production.BillOfMaterials'))
DROP INDEX RIBillOfMaterialsWithEndDate
    ON Production.BillOfMaterials;
GO

--Create a Filtered Index on the Production.BillOfMaterials table
IF EXISTS (SELECT name FROM sys.indexes
    WHERE name = N'FIBillOfMaterialsWithEndDate' 
    AND object_id = OBJECT_ID(N'Production.BillOfMaterials'))
DROP INDEX FIBillOfMaterialsWithEndDate
    ON Production.BillOfMaterials;
GO

CREATE NONCLUSTERED INDEX "FIBillOfMaterialsWithEndDate"
    ON Production.BillOfMaterials (ComponentID, StartDate)
    WHERE EndDate IS NOT NULL;
GO

-- Clear the Procedure cache
DBCC FREEPROCCACHE
GO
--This select will make use of the filtered index
SELECT ComponentID, StartDate FROM Production.BillOfMaterials WHERE EndDate IS NOT NULL
GO

Let’s look at the execution plan to see if SQL Server uses the filtered index or not:

image

The Advantages

While highlighting the usage of the two indexes – the regular and filtered ones, a few key differences come out:

Reduced index storage size

For performance reasons, and in order to make the SELECT an index-only operation, we had to put the EndDate on the index itself. This means that the overall row size on the index is higher in case of the regular index as compared to the filtered index. As proof of this, observe that the “Estimated Row Size” on the regular index is 27B, whereas that on the filtered index is just 19B.

While it may not look like a big difference, it is a big one when you are looking at deployment at a shared data center or the cloud – where one is charged per the amount of space used.

If you have multiple such queries, that work on pre-defined filters, it may be a better idea to replace a full-table non-clustered index with multiple filtered indexes. Of course, this depends upon the nature of the queries that the application uses, and upon the nature of the underlying data.

Reduced index maintenance costs

Coming up as a direct result of the above discussion, a filtered index is also cheaper to maintain. Indexes are maintained only when the data within the index is affected by a Data Manipulation Language (DML) operation. Because the data stored in the index is limited, it may be possible that it is not affected by many DML operations that the user might execute on the table. On the other hand, if a filtered index contains only the frequently affected data, the smaller size of the index reduces the cost of updating the index statistics.

Improved Query Performance

Enhancements are not easily accepted unless they give some immediate benefits. A well-designed filtered index improves query performance because it is smaller in size when compared to the full non-clustered index, which also contributes to filtered statistics, which are easier and much more accurate to maintain.

As proof of this, observe the following parameters from the screen-shots above:

  1. Estimated I/O cost
  2. Estimated CPU cost
  3. Estimated operator cost
  4. Estimated subtree cost

In each of these parameters, the filtered index wins hands-on. For further confirmation, we will force the query to use each of these indexes in order:

DBCC FREEPROCCACHE
GO
--This select will make use of the regular index
SELECT ComponentID, StartDate FROM Production.BillOfMaterials WITH (INDEX (RIBillOfMaterialsWithEndDate)) WHERE EndDate IS NOT NULL
GO
DBCC FREEPROCCACHE
GO
--This select will make use of the filtered index
SELECT ComponentID, StartDate FROM Production.BillOfMaterials WITH (INDEX (FIBillOfMaterialsWithEndDate)) WHERE EndDate IS NOT NULL
GO

Here’s the proof!

image

A Word of Caution

Upon looking at BOL, I encountered the following words of caution – “A filtered index can be used as table hint, but will cause the query optimizer to generate error 8622 if it does not cover all of the rows that the query selects.” This is a very important design consideration when thinking about filtered indexes.

Let’s attempt to simulate a situation wherein we attempt to fetch more rows than what are covered by the filtered index.

SELECT ComponentID, StartDate, EndDate
FROM Production.BillOfMaterials
WITH (INDEX (FIBillOfMaterialsWithEndDate))

The following is the output:

Msg 8622, Level 16, State 1, Line 1

Query processor could not produce a query plan because of the hints defined in this query. Resubmit the query without specifying any hints and without using SET FORCEPLAN.

Filtered Indexes v/s Views

Views offer a much wider range of functionality than filtered indexes. Views naturally allow for computed columns, joins, multiple tables and complex predicate logic, which filtered indexes cannot utilize.

This is exactly why we cannot create filtered indexes on views. However, this does not mean that filtered indexes cannot be used when defined on a table in a view. If the query optimizer determines that the results of the query will be correct, it will go ahead and use the filtered index.

More examples on this, and other filtered index design considerations can be found at: http://msdn.microsoft.com/en-us/library/cc280372.aspx

Some food for thought

Because we have filtered indexes, it is logical that we should have filtered statistics as well. If you have written about or are planning to write about filtered statistics, do link it up to this post. I will be sure to post my feedback 🙂

Until we meet next time,

Be courteous. Drive responsibly.

Compound Operators – the most common mistake


I will be taking a break from my series on “Underappreciated features of Microsoft SQL Server” to address a problem that I see frequently during the code reviews that I conduct in my capacity as a technical lead in the project I work for.

SQL Server 2008 introduced a great new feature – support for compound operators. This feature makes T-SQL programming a breeze for anyone who is a typical “hybrid” database developer – the kind that switches often between let’s say C# programming with a little bit of T-SQL here and there.

Compound Operators

As a quick introduction, compound operators execute some operation and set an original value to the result of the operation. This is what we commonly know see as “i++” to run a loop in code – the “++” operator takes the original value of the variable “i” and increments it by 1. The examples for compound operators provided on Books-On-Line provide great learning material – you can get these from here.

The most common error

I have often seen a very common mistake with respect to compound operators during code reviews – the sequence! Sequence is important here as well like most things in life, and the slightest typo error will cause SQL Server to behave differently. Let’s see this a practical example.

DECLARE @iTest INT
SET @iTest = 1

--Wrong! This works like an assignment.
--Effectively, we are assigning a value of +2 to @iTest
SELECT @iTest =+ 2
SELECT @iTest

--Right! This will use Compound Operators
SELECT @iTest += 2
SELECT @iTest

--For those who are curious, this works! (performs simple addition)
SELECT @iTest ++ 2

-- But, none of the following works
SELECT @iTest ++= 2
SELECT @iTest =++ 2

Simply run the above code in SSMS against your instance of SQL Server 2008 to see the difference. The first set of statements will cause a normal garden-variety assignment operation – the second will perform the compound operation, which we originally intended it to do. Do note that SQL Server will NOT throw an error message, because there is nothing wrong with the statement! It’s a simple, logical error, which might take days to troubleshoot if not looked at carefully.

As you would probably have guessed by now, this is only applicable to the compound operators += and –= (all others will cause SQL Server to error out). These are some of the most common operations that we use in code, and our habit of typing fast causes this error to occur very frequently. So the next time you use compound operators, be extra careful. For code reviewers, update your checklists to explicitly check for these errors.

Until we meet next time,

Be courteous. Drive responsibly.

“Zoom” feature in SSMS – SQL11 (“Denali”)


Recently, I got a chance to install my own copy of SQL 11 (“Denali”) – CTP 1 and I must say, I was highly impressed that we finally have full integration with the Visual Studio shell, which means that the usability features available in the Visual Studio 2010 shell are also available to SQL Server Management Studio as well. The best example for this is clipboard ring, which has been documented by Pinal Dave (blog) and Chintak Chhapia (blog) recently.

If you want to know more about SQL 11 (“Denali”)

If you want to know more about SQL 11 (“Denali”), a good place to start will be the “What’s new” listing available on this very web-site! You can reach this list right here – http://beyondrelational.com/whatisnew/sqlserver/denali.aspx. Simply click on the feature you want to explore and if one of us from the community has written about it, chances are, it is linked here.

“Zoom” feature in SSMS

One things you would immediately notice is that the SSMS now has the provision of zooming both the content in both the query editor and the “Messages” tab from the Results window. We don’t have this on the “Results” tab, but I think that it will follow soon. Isn’t this feature a boon for anyone who needs to present on the big screen often?

I am eagerly awaiting for the release of CTP 2, which would contain Project Juneau – the full-blown UI enhancements for Denali. I am sure you are as well. Have fun exploring SQL 11 (“Denali”), and until we meet next time,

Be courteous. Drive responsibly.

image

Sunset for RAISERROR and sunrise for THROW – SQL 11 (“Denali”)


Recently, I wrote (a rather long) post on Exception handling in T-SQL/TRY…CATCH – Underappreciated features of Microsoft SQL Server. As part of my research, I encountered a few interesting points about the current (and future) exception handling mechanisms in Microsoft SQL Server. I thought of sharing them with you via this post.

The days of RAISERROR are numbered

Almost for time immemorial now, RAISERROR has been a handy command for database developers and administrators alike. However, RAISERROR has a very major drawback in the fact that by default, it was unable to re-raise the original error and also unable to maintain the originating line number of an error. (You can re-raise error numbers from 13000 through 2147483647, but not 50000.)

However, despite of this, RAISERROR is a very useful, and widely used command – and I would go out on a limb here and say that no production application exists that does not use RAISERROR. Unfortunately, if we look at the preview documentation of SQL 11 (“Denali”), we are in for a surprise:

“This feature will be removed in a future version of Microsoft SQL Server. Avoid using this feature in new development work, and plan to modify applications that currently use this feature. New applications should use THROW instead.” (Reference: http://msdn.microsoft.com/en-us/library/ms178592%28SQL.110%29.aspx)

The days of RAISERROR are numbered.

THROW – A comparison with RAISERROR

THROW is a command introduced in SQL 11 (“Denali”) to overcome the limitations of RAISERROR. You can read more about this from Books On Line at: http://msdn.microsoft.com/en-us/library/ee677615(SQL.110).aspx. The THROW command allows us to re-raise an error message, thus allowing us to raise the exception throw a series of nested programming objects. However, today, let us explore briefly a comparison between the RAISERROR and THROW commands, as mentioned in the “Denali” preview documentation.

Raising system and custom messages

RAISERROR had the ability to raise system messages, and also custom messages. Let us see if THROW can do the same.

--Select a system message which is less than 50000
SELECT TOP 1 *
FROM sys.sysmessages 
WHERE msglangid = 1033 and error < 50000
ORDER BY error DESC

--Attempt to RAISERROR and THROW this system message
RAISERROR (49903, 16, 1);
THROW 49903, 'RAM related message.', 1

Following is the output indicating that THROW cannot throw a system error message. When explicitly specified, the value of the error_number parameter in THROW must be greater than or equal to 50000 and less than or equal to 2147483647.

Msg 49903, Level 16, State 1, Line 8

Detected (null) MB of RAM. This is an informational message; no user action is required.

Msg 35100, Level 16, State 10, Line 9

Error number 49903 in the THROW statement is outside the valid range. Specify an error number in the valid range of 50000 to 2147483647.

Now, let us try to raise a non-system message, that is not defined in sys.sysmessages

--Attempt to RAISERROR and THROW a non-system message, not defined in sys.sysmessages
RAISERROR (51000, 16, 1);
THROW 51000, 'This is a custom error!', 1

Following is the output indicating that RAISERROR failed because it failed to find the corresponding error message in the sys.sysmessages table. THROW however, succeeded in raising the error.

Msg 18054, Level 16, State 1, Line 2

Error 51000, severity 16, state 1 was raised, but no message with that error number was found in sys.messages. If error is larger than 50000, make sure the user-defined message is added using sp_addmessage.

Msg 51000, Level 16, State 1, Line 3

This is a custom error!

Now, let us define a custom error message is sys.sysmessages and attempt to raise a non-system message:

--Add a new custom error message
EXEC sys.sp_addmessage @msgnum = 51000, @severity = 16, @msgtext  = N'This is a custom error! Error parameters are: %d.';

--Attempt to RAISERROR and THROW a non-system message
RAISERROR (51000, 16, 1);
THROW 51000, 'This is a custom error!', 1

Here’s the output. Both RAISERROR and THROW worked fine.

Msg 51000, Level 16, State 1, Line 5

This is a custom error! Error parameters are: (null).

Msg 51000, Level 16, State 1, Line 6

This is a custom error!

Handling message formatting

RAISERROR allows for substitution of parameters in the error message.

--Attempt to RAISERROR the non-system message created earlier
RAISERROR (51000, 16, 1, 999);

Here’s the output:

Msg 51000, Level 16, State 1, Line 2

This is a custom error! Error parameters are: 999.

Let’s see if THROW allows us to do this:

--Attempt to THROW the non-system message created earlier
THROW 51000, 'This is a custom error! Error parameter: %d', 1, 999

Here’s the output

Msg 102, Level 15, State 1, Line 2

Incorrect syntax near ‘,’.

THROW can therefore, not handle parameterization of message in the way that RAISERROR does. There is however, one other way. Here’s how you can do this:

--Attempt to THROW the non-system message created earlier
DECLARE @ErrorMsg VARCHAR(100) = FORMATMESSAGE(51000,999);
THROW 51000, @ErrorMsg, 1

Here’s the output:

Msg 51000, Level 16, State 1, Line 3

This is a custom error! Error parameters are: 999.

In this aspect, THROW and RAISERROR provide similar functionality, and RAISERROR can be replaced with minimal changes to the code. Luckily, FORMATMESSAGE is not being deprecated.

Changing Severity levels

RAISERROR allows us to specify severity levels greater than 18 if we use the WITH LOG option:

--Attempt to RAISERROR the non-system message created earlier - this time with a severity of 24
RAISERROR (51000, 24, 1, 999) WITH LOG;

Here’s the output:

Msg 2745, Level 16, State 2, Line 2

Process ID 55 has raised user error 51000, severity 24. SQL Server is terminating this process.

Msg 2745, Level 16, State 2, Line 2

Process ID 51 has raised user error 51000, severity 24. SQL Server is terminating this process.

Msg 51000, Level 24, State 1, Line 2

This is a custom error! Error parameters are: 999.

Msg 0, Level 20, State 0, Line 0

A severe error occurred on the current command.  The results, if any, should be discarded.

Unfortunately, THROW does not come with any way to set the severity level, when used outside of a TRY…CATCH construct. Within a TRY…CATCH construct, THROW obeys the severity level of the exception being raised. 

Please NOTE:

When used outside of a TRY…CATCH construct, THROW results in termination of the session.

Some other differences:

The following are some differences that I noticed:

  1. Because we cannot change the severity level, there is no way to THROW a non-severe error
  2. It is required that the statement before a THROW must end in a semi-colon (;)

Conclusion

Whether THROW is good or better than RAISERROR, I will leave the it up to the reader to decide. According to me, THROW has it’s advantages over RAISERROR, but needs to mature a little more. Any new code that I write for SQL 11 (“Denali”) will have THROW in them – it is better to start the migration from RAISERROR now itself rather than wait for the release of SQL Server after “Denali” when RAISERROR will simply fail to work.

Do you know of any other differences between RAISERROR and THROW? If yes, share them here, or write a blog post, and link back to this post.

Until we meet next time,

Be courteous. Drive responsibly.

Exception handling in T-SQL/TRY…CATCH – Underappreciated features of Microsoft SQL Server


As we continue our journey through the “Underappreciated features of SQL Server”, this week are are looking at a few of the T-SQL enhancements that the community felt did not get the deserved attention. This was in response to Andy Warren’s editorial of the same name on SQLServerCentral.com.

Today, we will look at exception handling in T-SQL using the TRY…CATCH statements introduced since SQL Server 2005. Because this is now almost 6 years old, I was a bit surprised to see this in the list of underappreciated features. Hence, while I will touch upon the essential points, I will provide pointers that would help you to get started on the use of TRY…CATCH and then explore a new related feature introduced in SQL 11 (“Denali”). I am posting this on a Thursday, because I will be covering the exception handling options from SQL 7/2000 onwards and also so that it the reader gets some time to experiment with the various options presented here over the week-end.

Exception handling in T-SQL – before SQL Server 2005

Before SQL Server 2005, exception handling was very primitive, with a handful of limitations. Let’s see how exception handling was done in the days of SQL Server 7/2000:

BEGIN TRANSACTION ExceptionHandling
   DECLARE @ErrorNum INT
   DECLARE @ErrorMsg VARCHAR(8000)

   --Divide by 0 to generate the error
   SELECT 1/0

   -- Error handling in SQL 7/2000 
   -- Drawback #1: Checking for @@Error must be done immediately after execution fo a statement
   SET @ErrorNum = @@ERROR
   IF @ErrorNum <> 0 
   BEGIN
      -- Error handling in SQL 7/2000 
      -- Drawback #2: When returning an error message to the calling program, the Message number,
      --              and location/line# are no longer same as the original error.
      --              Explicit care must be taken to ensure that the error number & message
      --              is returned to the user.
      -- Drawback #3: Error message is not controlled by the application, but by the T-SQL code!
      -- (Please note that there is only one 'E' in RAISERROR)
      SELECT @ErrorMsg = description FROM sysmessages WHERE error = @ErrorNum
      RAISERROR ('An error occured within a user transaction. Error number is: %d, and message is: %s',16, 1, @ErrorNum, @ErrorMsg) WITH LOG
      ROLLBACK TRANSACTION ExceptionHandling
   END

IF (@ErrorNum = 0)
   COMMIT TRANSACTION ExceptionHandling

image

While a revolutionary method in those days, to our “developed” eyes, a couple of issues that jump right as us:

  1. You cannot monitor a batch for Exceptions/error, i.e. you cannot be sure that a particular block of code will be able to “catch” all errors that might happen in another block of code somewhere else
  2. The checking for the return value of the @@ERROR system function is where most mistakes happen. This has to be checked immediately after the execution of the T-SQL statement that you want to monitor
  3. You can inform the calling program/application about the error, however the error number, error description and originating line number change – the error number is now that of RAISERROR() and the error message is custom. The originating line number is that of the line from where the call to RAISERROR is made. When used with the “WITH LOG” option, this is big drawback when doing a post-error analysis
  4. Because the error message is custom, the calling application has no control over the nature of the message (e.g. sometimes, we might want to customize the message text that is displayed to the users) – this cannot be done

Clearly, with the advent of nested operations and multi-tier architecture and patterns, this had a lot of drawbacks, and the world was in need of something new.

Exception handling in T-SQL – SQL Server 2005/2008

SQL Server 2005 saw a major change in the history and architecture of Microsoft SQL Server. SQL Server now started using the Visual Studio platform and was now Microsoft .NET framework based. One of the things that this move gave to SQL Server was the fact that it could now leverage the built-in TRY…CATCH functionality provided by the framework. To know more about this functionality, please refer an article from our esteemed MVP, Mr. Pinal Dave (blog).

Let’s see how our code would look and work when we implement TRY…CATCH blocks:

BEGIN TRANSACTION ExceptionHandling
   -- Error handling in SQL 2005 & up
   -- Monitor an entire batch of queries for errors
   BEGIN TRY
      --Some valid operations here
      SELECT 1/1

      --Divide by 0 to generate the error
      SELECT 1/0

      --Some valid operations here
      SELECT 1/2

      COMMIT TRANSACTION ExceptionHandling
   END TRY
   BEGIN CATCH
      -- Error handling in SQL 2005 & up
      -- Error information can be returned to the calling code as a result set
      SELECT ERROR_NUMBER() AS ErrorNumber
            ,ERROR_SEVERITY() AS ErrorSeverity
            ,ERROR_STATE() AS ErrorState
            ,ERROR_PROCEDURE() AS ErrorProcedure
            ,ERROR_LINE() AS ErrorLine
            ,ERROR_MESSAGE() AS ErrorMessage;

      -- Or via RAISERROR
      DECLARE @ErrorNumber    INT            = ERROR_NUMBER()
      DECLARE @ErrorMessage   NVARCHAR(4000) = ERROR_MESSAGE()
      DECLARE @ErrorProcedure NVARCHAR(4000) = ERROR_PROCEDURE()
      DECLARE @ErrorLine      INT            = ERROR_LINE()

      RAISERROR ('An error occurred within a user transaction. 
                  Error Number        : %d
                  Error Message       : %s  
                  Affected Procedure  : %s
                  Affected Line Number: %d'
                  , 16, 1
                  , @ErrorNumber, @ErrorMessage, @ErrorProcedure,@ErrorLine)
       
      IF @@TRANCOUNT > 0
         ROLLBACK TRANSACTION ExceptionHandling
   END CATCH

As you can see, most of the problems are now resolved. Microsoft SQL Server provides specific functions that would return the correct information about an error occurring from anywhere within a block of monitored code (i.e. the code in “TRY”). One problem remains however – if your code uses RAISERROR, you would still have the problem of an incorrect error number (if not embedded within the message).

image

The basic structure and use of TRY…CATCH is quite simple as demonstrated above. There is much more to TRY…CATCH, which I will leave it upon the reader to explore.

Exception handling in T-SQL – SQL 11 (Code named: “Denali”)

The remaining drawback of RAISERROR mentioned above and the solution provided by Microsoft in it’s upcoming SQL Server release (SQL 11 – Code named: “Denali”) is handled very well by the renowned MVP, Mr. Jacob Sebastian (blog) here.

SQL 11/”Denali” comes with a new THROW command that helps the T-SQL code to re-raise the same error that has been encountered in a monitored block of code. While I was reading Jacob’s article, a question cropped up in my mind as to whether we could re-raise errors in a nested hierarchy. The concept of “inner exceptions” is still foreign to SQL Server, but THROW does allow for nesting. Please find below the script I developed to experiment with nesting exception-handling:

IMPORTANT NOTE: The THROW command must be preceded with a statement ending in a semi-colon (;)

IF EXISTS (SELECT * FROM sys.objects WHERE name = 'InnerProc' AND type = 'P')
    DROP PROCEDURE InnerProc
GO

CREATE PROCEDURE InnerProc
AS
BEGIN
    BEGIN TRANSACTION ExceptionHandling
       BEGIN TRY
          PRINT 'In the TRY block of the Inner Procedure...'
          SELECT 1/1

          RAISERROR('An error occured in the Inner procedure.',17,1)  --Line #10 considering CREATE PROC...as Line #1

          COMMIT TRANSACTION ExceptionHandling
       END TRY
       BEGIN CATCH
          SELECT ERROR_NUMBER() AS ErrorNumber
                ,ERROR_SEVERITY() AS ErrorSeverity
                ,ERROR_STATE() AS ErrorState
                ,ERROR_PROCEDURE() AS ErrorProcedure
                ,ERROR_LINE() AS ErrorLine
                ,ERROR_MESSAGE() AS ErrorMessage;

          IF @@TRANCOUNT > 0
             ROLLBACK TRANSACTION ExceptionHandling

          PRINT 'Throwing error from the CATCH block of the INNER Procedure...';   
          --Preceding statement MUST be a semi-colon ';'
          THROW
       END CATCH
END
GO

IF EXISTS (SELECT * FROM sys.objects WHERE name = 'OuterProc' AND type = 'P')
    DROP PROCEDURE OuterProc
GO
CREATE PROCEDURE OuterProc
AS
BEGIN
    BEGIN TRY
        PRINT 'In the TRY block of the Outer Procedure...'
        EXEC InnerProc
    END TRY
    BEGIN CATCH
        PRINT 'In the CATCH block of the Outer Procedure...';
        --Preceding statement MUST be a semi-colon ';'
        THROW
    END CATCH
END
GO

--Executing the outer procedure
EXEC OuterProc

image

Edit (October 15, 2011): You can explore the differences between THROW and RAISEERROR in my post here – Sunset for RAISERROR and sunrise for THROW – SQL 11 (“Denali”) (End Edit)

Phew!…That was a long one. I hope that you now have a good overview of the powerful exception handling that Microsoft SQL Server provides. In the next release of your product, ensure that all our T-SQL code is protected from unhandled exceptions.

If you want to read more on exception handling, you can refer Erland Sommarskog’s (blog) post here.

If you have any good exception handling tips to share, do let us know by leaving your feedback.

Until we meet next time,

Be courteous. Drive responsibly.