Blog Post

Follow Up to Maintaining Security and Performance Using Stored Procedures Part I – Using Execute As

,

This is a follow up to Maintaining Security and Performance using Stored Procedures Part I – Using EXECUTE AS because of a comment on that post on the SQLServerCentral syndicated feed by Ken Lee.  He commented:

Like the technique, not the example. ISNULL() is a function, it forces the where clause to look at every record in the table.

SQL is smart enough to identify constants and know the variable name is true or not and will or will not evaluate the second OR statement used below.

First SET @LastName=@LastName+N'%', if it's null it remains null. To make sure it always works declare a nvarchar(51) field and assign it instead. If the field in the table is max 50 characters then this isn't needed.

Replace the function in the first example with "(@LastName IS NULL OR LastName Like @LastName) And" logic and you should get the performance without the dynamic SQL.

Kind of curious about the stats.

Since the purpose of that post was to show how to get better performance using Dynamic SQL without compromising security, I decided I needed to do some testing on his method.  Here are the queries I ran (FREEPROCCACHE was run because the Dynamic SQL query plans were not removed from the cache when the stored procedure was altered):

DBCC FREEPROCCACHE();
GO

Exec
dbo.FindPhoneByName @FirstName = 'J', @LastName = 'A';

GO

Exec
dbo.FindPhoneByName @FirstName = 'J';

GO

Exec
dbo.FindPhoneByName @LastName = 'A';

Go

I don’t know why, but his method DOES generate a different execution plan than my method using ISNULL() when you leave the Person.Contact in, what I believe is, the original state.  In the original state the optimizer chooses to do a scan of the index, IX_Contact_MiddleName, and bookmark lookup on the clustered index.  Here’s the index definition:

CREATE NONCLUSTERED INDEX [IX_Contact_MiddleName] ON [Person].[Contact] 
(
[MiddleName] ASC
)
INCLUDE ( [FirstName],
[LastName])

While the optimizer chose a clustered index scan for Ken’s query.  This access path required more reads when both parameters were provided, but fewer when only one was provided. 

The Dynamic SQL, however, provided the best of both, using the index when both parameters were provided and using the clustered index when only one parameter was provided.

Intrigued I decided to see what happened if a covering index was made available, so I created this index:

CREATE NONCLUSTERED INDEX [IX_Contact_LastName_FirstName] ON [Person].[Contact] 
(
[LastName] ASC,
[FirstName] ASC,
[MiddleName] ASC
)
INCLUDE ( [Title],
[Suffix],
[Phone])
With a covering index in place both non-dynamic solutions produced the same execution plan, scans of the newly created covering index.  The Dynamic SQL though had seeks on the covering index for queries 1 and 3, and a scan for query 2.
Lastly I decided to see what happened when there was a non-covering index available, here’s that index:
CREATE NONCLUSTERED INDEX [IX_Contact_LastName_FirstName] ON [Person].[Contact] 
(
[LastName] ASC,
[FirstName] ASC,
[MiddleName] ASC
)

This case was similar to the first test, my solution did a scan on the index with a bookmark lookup, and Ken’s solution did a clustered index scan.  The dynamic SQL did an index seek/bookmark lookup when both parameters were provided and a clustered index scan when only one parameter was provided.

Here’s the results from statistics IO:

 IsNull() SolutionKen’s SolutionDynamic
ScansReadsScansReadsScansReads
Query 1Original1593111161593
Covered12011201113
Uncovered1457111161359
Query 2Original177831111611116
Covered120112011201
Uncovered176471111611116
Query 3Original130311111611116
Covered12011201113
Uncovered128951111611116

Conclusion

As you can see from the results above Ken’s solution does provide more consistent results than my original solution, while the Dynamic SQL still provides the best performance.  I should note that there is a tradeoff with the dynamic SQL solution, you get better plans because you get a plan for each option that is run, so depending on the number of parameter combinations you could get procedure cache bloat.  In most 64-bit implementations this shouldn’t be an issue, but you may see more compiles with a dynamic solution. 

As always test your options, monitor, and change as needed to keep your systems running as well possible.

All the files (except the AdventureWorks database, on CodePlex) can be found here.  Included are the queries, the results from statistics IO, and a trace file showing the activity and query plans.

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating