OK, so, this is where we're at...

When a client application makes a request to SQL Server there are a few ways in which they can do it:

  1. Submit an adhoc transact-SQL statement
    • This statement can be parameterized and the parameterized statement can be deemed:
      • Safe: These are statements where the optimizer has evaluated the plan and feels that changes to values will not affect the execution plan. And, there are quite a few rules that you must meet for this to be the case. And, as a result, this is NOT something I really rely on and/or aim for (for optimization - I'd rather use stored procedures). You can monitor this through the Perfmon counter: SQLServer:SQL Statistics (Safe Auto-Params/sec).
      • Unsafe: These are statements where the optimizer feels that subsequent values will affect the execution plan. And, for adhoc statements many will be deemed unsafe. You can monitor this through the Perfmon counter: SQLServer:SQL Statistics (Unsafe Auto-Params/sec).
    • This statement might not end up being parameterized (this is not likely and the "failed parameterized statements" count should be low - you can monitor it through the Perfmon counter: SQLServer:SQL Statistics (Failed Auto-Params/sec).
  2. Submit a statement via sp_executesql that parameterizes the statement through defined parameters - this creates a plan that is neither safe nor unsafe but is ALWAYS an exact match for subsequent executions (even when the parameter values change). This is where I generally have problems with it...
  3. Execute a stored proceudre which has one or more of the above in it and/or DSE in it. This is where I think things start to get interesting and this is really the reson for why I started this series on optimizing procedural code.

First, and foremost, I believe that stored procedures are a SIGNIFICANTLY better way to develop high-end applications that absolutely must scale. Don't get me wrong, I understand the benefits of some of the tools out there that can generate your SQL and allow more rapid development of your application. And, if the application isn't trying to handle thousands of inserts/sec then this might work out really well. However, you still have to be careful. Many of these applications (that auto-generate SQL code) rely (very heavily in some cases) on sp_executesql and this can result in poor performance. Additionally, when there's A LOT of adhoc SQL then you can end up with a lot of your cache going to "single-plan use" where quite a bit of it is wasted (NOTE: there's a SQL Server 2008 configuration option [called "optimize for adhoc workloads"] that you really need to know about IF you have a lot of adhoc statements. This can significantly reduce the amount of cache that's wasted on plans that only execute once... I'll do a post on that feature NEXT). Again, while there are quite a few benefits the performance problems can become SEVERE. OK, I've definitely said this before but why am I rehashing this?

Because this is where we're at - we've looked at some of the problems with DSE in my post titled: Little Bobby Tables, SQL Injection and EXECUTE AS. Then, we started to look at EXEC and sp_executesql - how are they different? in the second post. Finally, I started to show *statement-level* recompilation using OPTION (RECOMPILE) in the post titled: Using the OPTION (RECOMPILE) option for a statement. The real reason for why I started this series is to prove: where, why and how stored procedures really shine (we're getting close!).

Reasons to use stored procedures:

  • Centralization - Sharing common code between applications asnd only need to make your changes ONCE! And, there's no possibility of different business rules being enforced in one application and not another and/or for incorrect business rules to be "out there" in your applications.
  • Simplification - Users should not need to know your schema. They shoudn't care that you've denormalized something and they should reap the benenfits through YOUR creating an access method that is simple and works even if you end up changing back and normalizing. The database schema should be isolated from the users...
  • Change control - You should be able to change your schema without breaking applications. This about this one a bit - this is EXACTLY the tactic that SQL Server takes with it's own internal tables. We access them trhough stored procedurs, views (aka catalog views) and functions. If they need to change the underlying schema then we don't need to worry - as long as they also make the sp, function or view return the same way it always has. This has complicated things over time. First, the SQL team has bascially decided that (for backward compatiblity) the sps will never handle new features (and, yes, this is HORRIBLY annoying (and, it's the reason for why I've rewriten sp_helpindex here [and, fyi, I have an even better/newer/shinier version coming soon]). And, they've had to add functions like DATABASEPROPERTYEX for the "extended" properties that were added around recovery models in SQL Server 2000. However, as long as the funtionality of the interface stays the same then our applications don't break. That's incredibly important and probably one of my favorite reasons to create some separation between the schema and the users (and/or interface).
  • Security - This is HUGE. And, it requires an understanding of object chaining and permissions. The general idea is that when objects are all within the same schema (and therefore have the same owner) that when execution rights are given on a stored procedure the caller does not need explicit permissions to the base objects (execution is essentially granted because the ownership chain is not broken). Another way to think about it is that your granting rights to execute a "process" and one that you have control (and ownershp) over all of the object involved. So, SQL Server only requires execute rights on the sp. However, this ONLY goes for explicity statements and not statements that are built dynamically. For those, the CALLER needs explicit permission OR the procedure needs to be created with EXECUTE AS. So, if you plan to use EXECUTE AS be sure to read my post titled: Little Bobby Tables, SQL Injection and EXECUTE AS.

But, more than anything else - you CAN use them for better performance. No, it's not solely because the might have an already compiled (and therefore optimized) plan that's available at runtime (yes, that CAN help) but compilation (or recompilation) can often be a small portion of runtime compared to the execution of a bad plan. And, this is where optimizing the procedural code can help. The bad news is that you have to know what you're doing. And, I could make some arguments that SQL Server could do some things better here but what I've been doing with this series is getting you familar with the options that will finally come together in the next a post titled: Stored Procedure Performance. (NOTE: I'll update this with a corrected title/link once the post is completed. I will try to get this this within the next couple of days.)

Thanks for reading!!
kt

In addition to the SQL Server 2008 Database Infrastructure and Scalability content that Paul and I delivered for SQL Server 2008, our colleague Bob Beachemin (blog) also delivered complementary content under the Developer track. This course is officially titled and available as: Clinic 10164: Essential SQL Server 2008 for Developers.

Additionally, this content has been updated for SQL Server 2008 R2. The entire "jumpstart" content has been managed by Roger Doherty (blog) and he created a very comprehensive page at Channel 9 for this which is titled and available as: Microsoft SQL Server 2008 R2 Update for Developers Training Course. He's done a few blog posts about demos and content so be sure to check out his blog as well!

And, I still have a bit more coming on resources/content!

Enjoy,
kt

When SQL Server 2008 was in beta, Paul and I worked on a project to create content that would "jumpstart" folks in learning and working with SQL Server 2008. We delivered these "new features" courses at various locations and finally, we recorded the content. All of this content is now available FOR FREE from Microsoft through their eLearning outlet. This course is officially titled and available as: Clinic 10259: SQL Server 2008: Database Infrastructure and Scalability.

There are 12 hours of lecture and demo content... lots of good stuff.

Modules & Lessons

Availability Enhancements

  • Module overview
  • Demo: Database Mirroring automatic page repair
  • Database Mirroring enhancements continued, automatic page repair, log stream
  • Backup compression and using Transparent Data Encryption (TDE) to encrypt data
  • Hot-add CPU, DBCC CHECKDB enhancements, failover clustering enhancements
  • Peer-to-peer replication enhancements
  • Demo: Peer-to-peer replication

Policy-Based Management and Multi-Server Administration

  • Module overview
  • Central management server
  • Demo: Central management server
  • Central management server metadata, multi-server script execution
  • Demo: Multi-server script execution
  • Policy-based management overview, conditions, facets and policies
  • Policy-based management demo: Facets and conditions
  • Applying facets and conditions to policies, policy enforcement
  • Demo: Applying facets and conditions to policies
  • Policy based management architecture and execution
  • Demo: Applying policies to multiple servers
  • Policy based management advanced topics

Performance Data Collection

  • Module overview, management data warehouse
  • Demo: Creating and configuring a management data warehouse
  • Data collection overview and architecture
  • Demo: Configuring system data collection on an instance
  • Demo: Transact-SQL custom data collection
  • Data collection reports and module summary

Resource Management and Troubleshooting Enhancements

  • Module Overview
  • Demo: Resource Governor
  • Extended events, predicates, actions, targets, types, maps, sessions

Security Enhancements

  • Module overview
  • Transparent data encryption
  • Demo: Transparent data encryption
  • Backups and transparent data encryption, transparent data encryption limitations
  • Integrated authentication enhancements, SQL Server 2008 auditing improvements
  • Demo: SQL Server 2008 all actions audited
  • Security Summary

Management Implications of New Features Part 1

  • Module overview, spatial indexes, integrated full-text search
  • Problems addressed by sparse columns
  • Demo: Sparse columns and column sets
  • Sparse columns and filtered indexes, best practices for sparse columns
  • Demo: Constructing tabular data sets with sparse columns
  • Limitations of sparse columns and interaction with other features
  • Overview of filtered indexes
  • Demo: Using filtered indexes with sparse columns
  • Filtered statistics, monitoring filtered indexes and statistics
  • Demo: Using filtered indexes for improved index coverage

Management Implications of New Features Part 2

  • Change tracking overview, change tracking best practices
  • Change data capture (CDC) overview
  • Demo: Change Data Capture
  • Blob storage with Filestream overview, enabling Filestream
  • Filestream security, filestream and transactions, monitoring filestream storage
  • Demo: Filestream manageability demo

Scalability Enhancements

  • Data compression overview, data compression trade-offs
  • Demo: Estimating data compression space savings
  • Estimating data compressing space savings, choosing row compression
  • Demo: Enabling compression and comparing performance
  • Enabling and disabling compression, data compression options and syntax
  • Service broker improvements, Service Broker conversation priority
  • Query optimizer enhancements, MERGE overview
  • Forcing query optimizer plans with plan guides
  • Demo: Using plan guides
  • Star join optimizer improvements
  • Partitioning enhancements overview, partition-aligned indexed views
  • Partitioning and lock escalation enhancements
  • Demo: Improving concurrency with partition lock escalation
  • Module review

There are a few more resources to mention. I've got a few more posts coming on this right away!

Enjoy!!
kt

In my last post: Little Bobby Tables, SQL Injection and EXECUTE AS, I wanted to highlight how to avoid SQL Injection when using EXEC. A few people brought up the point that I could have avoided SQL Injection by using sp_executesql. And, while sp_executesql can be used in some cases, it's not always the most ideal from a performance perspective. So, to really show this, I'm going to start with focusing on the similarities and differences of EXEC and sp_executesql. I'll start here with some performance details but you'll find that I'm going to just hit the tip of the iceberg with this one. I'll definitely need another post or two!

First, a quick overview:

sp_executesql (also known as "Forced Statement Caching")

  • Allows for statements to be parameterized
  • Only allows parameters where SQL Server would normally allow parameters; however, this string can be built using forms of dynamic constructs. I'll give more details on this in additional posts.
  • Has strongly typed variables/parameters - and this can reduce injection and offer some performance benefits!
  • Creates a plan on first execution (similar to stored procedures) and subsequent executions reuse this plan

EXEC  (also known as "Dynamic String Execution" or DSE)

  • Allows *any* construct to be built
  • Treats the statement similarly to an adhoc statement. This means that the statement goes through the same process that adHoc statements do - they are parsed, probably parameterized and possibly deemed "safe" for subsequent executions to re-use
  • Does not have strongly typed parameters in the adhoc statement and therefore can cause problems when the statements are executed (I have ways around this)
  • Does not force a plan to be cached.
    • This can be a pro in that SQL Server can create a plan for each execution
    • This can be a con in the SQL Server needs to recompile/optimize for each execution

Let's start with using sp_executesql to parameterize a query where SQL Server would also allow parameters:

DECLARE @ExecStr nvarchar(4000)
SELECT @ExecStr =
'SELECT * FROM dbo.member WHERE lastname LIKE @lastname'
EXEC sp_executesql @ExecStr, N'@lastname varchar(15)',
'Tripp'
go

Because 'Tripp' is a highly selective name, SQL Server uses an index to the lookup of the data:

So, for our next execution, I'll supply a different lastname - a lastname of Anderson. In this database, Anderson is NOT highly selective:

DECLARE @ExecStr nvarchar(4000)
SELECT @ExecStr =
'SELECT * FROM dbo.member WHERE lastname LIKE @lastname'
EXEC sp_executesql @ExecStr, N'@lastname varchar(15)',
'Anderson'
go

However, the query plan looks exactly the same:

Or does it? It turns out that this query plan does look a tiny bit different but it's not very obvious... it's in the thickness of the lines. In this second execution it clearly shows that more data is being passed between the steps. But, is this a bad thing? Maybe, maybe not. Let's drill in a bit deeper. If I hover over the Index Seek (on member.test), I can see the following tooltip:

The key point here is that it shows an "Estimated Number of Rows" of 1.96 but an "Actual Number of Rows" of 385. That's pretty far off... why? Because this statement's plan was determined by the first execution of sp_executesql. Let's try another execution:

Because the query has LIKE in it, we can use wildcards. And, let's do that! This time I'll supply a wildcard of %e%:

DECLARE @ExecStr nvarchar(4000)
SELECT @ExecStr =
'SELECT * FROM dbo.member WHERE lastname LIKE @lastname'
EXEC sp_executesql @ExecStr, N'@lastname varchar(15)',
'%e%'
go

And, again, the query plan looks the same... but with even thicker lines. Yes, I realize... this is not blindingly obvious:

And, by turning on SET STATISTICS IO ON, we can also review the [Logical] IOs performed:

For 'Tripp' 

Table 'member'. Scan count 1, logical reads 8, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

For 'Anderson'

Table 'member'. Scan count 1, logical reads 772, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

For '%e%'

Table 'member'. Scan count 1, logical reads 10019, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

This table only has 144 pages. In the cases of 'Anderson' and '%e%', SQL Server would have been better off doing a table scan. But, because I used sp_executesql I forced SQL Server to do caching. Subsequent users use the plan whether it's optimal or not.

And, there are even more complicated scenarios than this. I plan to keep tackling these issues over the next few days and I'll add quite a bit more to this. However, it's late. So, I'll dangle the carrot for now. Within the next couple of days, I'll show a series of EXEC statements that generate both SAFE and UNSAFE plans.

Thanks for reading!
kt

OK, I know many of you have seen this before (an oldie, but a goodie!):

(image from xkcd.com, with "copy and share" license described here: License)

But, what can you do to prevent this? And, when would this even be possible?

This is possible when DSE (dynamic string execution) occurs. There are still some VERY relevant and important reasons to use DSE and some are performance related (ok, this is another post for another day) but suffice it to say - I use DSE but I also know how to prevent Little Bobby Tables from running amuk within my database.

(April 5 note: after a few comments about sp_executesql, I've modified the following paragraph to explain a bit more... I'm *very* aware of sp_executesql and while it does have some benefits, it also has some negatives! I promise, this is coming soon in another post. I really want this post to focus solely on how to minimize injection when you do NEED to use a dynamically constructed string and there are times when this is the ONLY way to solve certain performance problems. sp_executesql - while often compared to EXEC as a better choice just isn't ALWAYS better and really it's not a simple comparison as they are not equivalent!! In fact, there are some cases where I would specifically avoid sp_executesql... So, I promise... this will be coming up in a blog post soon!! Great comments! Thanks!! :))

So, while the discussion on when, where and why you might use DSE is another one - especially when compared with sp_executesql (yes, I hear another blog post coming - and, well, I'm kind of on a roll right now :)... I want to give you a couple of very cool tips/tricks to reduce the possibilities of problems if/when you do use it (and, there REALLY are places where it's necessary!). It's especially important as there are a few things that many folks just don't even know exist. And, there are 3 parts to this article... I would expect that most apps need QUOTENAME() and/or REPLACE() but ALL apps should consider my recommendations/comments for EXECUTE AS - if/when you're actually using DSE (through EXEC).

  • QUOTENAME()
  • REPLACE()
  • EXECUTE AS

Part I: QUOTENAME()

In a question/thread (this is many moons ago), the following question was asked (NOTE: I have "tweaked" the code to highlight ONLY the problem here AND I've made it so that it won't execute):

I have a procedure that creates a database (below). When people use spaces in their database names it fails. How can I make it work if they supply spaces?

CREATE PROCEDURE dbo.CreateDBProc
(
     @DBName
sysname
)
AS
DECLARE
@ExecStr nvarchar(2000
)
SELECT @ExecStr = N'CREATE DATABASE ' + @DBName
-- + all of the other stuff to place the files, etc...
SELECT @ExecStr
--EXEC(@ExecStr)
go

And, there was a reply to this question where the answer was (and I am NOT recommending this answer. Please do NOT stop reading here otherwise you are asking for a world of pain - and LOTS of SQL Injection):

Just go ahead and do the following. By putting brackets around the name you will solve this problem.

CREATE PROCEDURE dbo.CreateDBProc
(
    
@DBName
sysname
)
AS
DECLARE
@ExecStr nvarchar(2000
)
SELECT @ExecStr = N'CREATE DATABASE [' + @DBName + N']'
-- + all of the other stuff to place the files, etc...
SELECT @ExecStr
--EXEC(@ExecStr)
go

OK, this is a SCARY recommendation. Not just because it won't always work (what if someone actually wants [don't get me wrong - I don't know WHY they'd want to do this but...] to put [brackets] in the name?) but because it's prone to SQL Injection.

This won't work:

EXEC dbo.CreateDBProc N'This is the most stupid :) name I can think of with [brackets] and all sorts of junk! in the name'

And, this is just scary what I can do:

EXEC dbo.CreateDBProc N'fakedbname] DROP DATABASE foo--'

EXEC dbo.CreateDBProc N'[fakedbname] EXEC(''CREATE PROC Test AS SELECT * FROM pubs.dbo.authors'') DROP DATABASE foo --'

So, what should we do?

ANSWER: If you're building strings where identifiers are parameterized (and, there are many reasons for why this many not be the best idea - but, if you absolutely must), USE QUOTENAME()!

So, here's the correct way to code this procedure so that an identifier cannot be subjected to SQL Injection:

CREATE PROCEDURE dbo.CreateDBProc
(
    
@DBName
sysname
)
AS
DECLARE
@ExecStr nvarchar(2000
)
SELECT @DBName = QUOTENAME(@DBName, N']'
)
SELECT @ExecStr = N'CREATE DATABASE ' + @DBName
-- + all of the other stuff to place the files, etc...
SELECT @ExecStr
--EXEC(@ExecStr)
go

Now, no matter what messed up name someone actually WANTS to create - this will protect it such that SQL Injection is not possible. You can try any of these combinations with this version of the proc and while creating a database actually named "[fakedbname] DROP DATABASE foo --" probably isn't desirable, dropping a database isn't either (and I would argue is MUCH worse).

All of these executions are "protected" from SQL Injection:

EXEC dbo.CreateDBProc N'this is my test database name'
EXEC dbo.CreateDBProc N
'[fakedbname] DROP DATABASE foo --'
EXEC dbo.CreateDBProc N
'[fakedbname] EXEC(''CREATE PROC Test AS SELECT * FROM pubs.dbo.authors'') DROP DATABASE foo --'
EXEC dbo.CreateDBProc N'This is the most stupid :) name I can think of with [brackets] and all sorts of junk! in the name'

Note: The first and second execute (if you actually execute instead of just SELECT @ExecStr) *and* create databases with these ugly names. The third and fourth don't execute because some of the injected characters are not supported for file/directory names. However, the most important point is that *all* are protected from SQL Injection!

Part II: REPLACE()

But, what if the string you need to work with isn't an identifier? Can you still use QUOTENAME()? And, the answer is YES, IF the string is compatible with SYSNAME which is an nvarchar(128). However, if you need longer strings or if you are protecting a string which is not an identifier - what else can you do? Well... it's definitely NOT as pretty but it works. The answer here is to use REPLACE. If I had wanted to do this with the procedure above, I would have had to do the following:

CREATE PROCEDURE dbo.CreateDBProcQuotes
(
     
@DBName sysname
)
AS
DECLARE
@ExecStr nvarchar(2000)
SELECT @DBName = REPLACE(@DBName, N'''', N'''''')
SELECT @ExecStr = N'CREATE DATABASE ' + @DBName -- + all of the other stuff to place the files, etc...
SELECT @ExecStr
--EXEC(@ExecStr)
go

However, this will NOT work here because an identifier must be properly quoted within the string and there are only 2 ways to "quote" identifiers. One is the [bracket] and the other is "double quotes" not single. Instead, this is what the procedure will look like (nope! we don't need REPLACE):

CREATE PROCEDURE dbo.CreateDBProcQuotes
(
     
@DBName sysname
)
AS
DECLARE
@ExecStr nvarchar(2000)
SELECT @ExecStr = N'CREATE DATABASE [' + @DBName + N']'-- + all of the other stuff to place the files, etc...
SELECT @ExecStr
--EXEC(@ExecStr)
go

And, SQL only supports brackets by default (without any special settings). To allow double quotes, you can turn on QUOTED_IDENTIFIER (SET QUOTED_IDENTIFIER ON) to allow double quotes instead of brackets but this has a few negative consequences (See SET Options That Affect Results) and personally, I always try to avoid making SET options changes in most of my stored procs.

CREATE PROCEDURE dbo.CreateDBProcQuotes
(
     
@DBName sysname
)
AS
SET QUOTED_IDENTIFIER ON
DECLARE
@ExecStr nvarchar(2000)
SELECT @DBName = REPLACE(@DBName, N'''', '''''')
SELECT @ExecStr = N'CREATE DATABASE "' + @DBName + N'"'-- + all of the other stuff to place the files, etc...
SELECT @ExecStr
--EXEC(@ExecStr)
go

Once again, REPLACE isn't needed. And, to be honest, this becomes a nightmare with " [double quotes] or actual [brackets] in the name. QUOTENAME() deals with those properly. So, with an identifier you really need QUOTENAME(). So, where does replace come in? With actual strings in your DSE. Imagine that you're wanting to pass in a first name but something about your application requires that this happen through DSE (and, this CAN definitely happen - mostly for performance purposes). Don't get me wrong, I just don't have the ability to do all of this within one post but simply put, because of how stored procedures create their execution plans there are OFTEN reasons for why you might want a string to be built dynamically. No, probably not one quite this simple but I'm trying to keep this particular example simple. This is probably what it would look like to start:

CREATE PROCEDURE GetMembers
(
      @FirstName nvarchar(50
)
)
AS
DECLARE
@ExecStr nvarchar(2000
)
SELECT @ExecStr = N'SELECT * FROM dbo.member WHERE FirstName = ' + @FirstName

SELECT @ExecStr
-- EXEC(@ExecStr)
go

And, on first test:

EXEC GetMembers N'Kimberly'

And, on first test it generates: SELECT * FROM dbo.member WHERE FirstName = Kimberly which gives you an error because the firstname isn't properly quoted. So, you think... oh, I need to quote these in my EXEC statement:  

ALTER PROCEDURE GetMembers
(
    
@FirstName nvarchar(50
)
)
AS
DECLARE
@ExecStr nvarchar(2000
)
SELECT @ExecStr = N'SELECT * FROM dbo.member WHERE FirstName = ''' + @FirstName +
N''''
SELECT
@ExecStr
--EXEC(@ExecStr)

go

EXEC GetMembers N'Kimberly'

Ah, that works... for awhile. Then, someone complains that it doesn't work for C'Anne (I just met someone named C'Anne - pronounced SeaAnne) this past week.

EXEC GetMembers N'C''Anne'

And, it generates:

SELECT * FROM dbo.member WHERE FirstName = 'C'Anne'

which gives you a syntax error.

Msg 102, Level 15, State 1, Line 1
Incorrect syntax near 'Anne'.
Msg 105, Level 15, State 1, Line 1
Unclosed quotation mark after the character string ''.

Darn. But, at least this is how you catch it. Instead of with Little Bobby Tables. What if this gets executed:

EXEC GetMembers N'''Robert''); DROP TABLE Students; --'''

Luckily, I got:

Msg 3701, Level 11, State 5, Line 1
Cannot drop the table 'Students', because it does not exist or you do not have permission.

But, what if there had been a table called Students AND the context of the execution was such that the could execute this? Well, that's what's proposed by the cartoon and well... I've *unfortunately* heard of that happening!

So, what do you do? First, you need to protect the string being inserted! This is what you need:

ALTER PROCEDURE GetMembers
(
     @FirstName nvarchar(50
)
)
AS
DECLARE
@ExecStr nvarchar(2000
)
SELECT @ExecStr = N'SELECT * FROM dbo.member WHERE FirstName = ''' + REPLACE(@FirstName, N'''', N'''''') +
N''''
SELECT @ExecStr
--EXEC(@ExecStr)

go

And, this requires that the string be surrounded with quotes AND the string submited have all single quotes replaced with '' [single quote followed immediately by a second single quote]). Yes, I realize that this is starting to look ugly but... what happens with Little Bobby Tables? We get his row. NOTE: We do have other potential performance problems here because the parameter (when it gets thrown back out into the string - is no longer an NVARCHAR, it's only a varchar). So, we should also consider adding an N into the string like the following:

SELECT @ExecStr = N'SELECT * FROM dbo.member WHERE FirstName = N''' + REPLACE(@FirstName, N'''', N'''''') + N''''

And, what if the string being built has numerics or other types - what should we do there? To be honest, I often EXPLICITLY type the variable in what looks like a completely unnecessary CONVERT. However, for performance purposes I *often* do this. Ugh, the future posts are REALLY starting to build from here! Again, I'm not hitting performance in this one so I'm not going to take the bait of the tangent. Another time for sure!

Finally, what if our strings are REALLY complex and we need to do some strange stuff and well... we're not sure that the string can be completely protected? What if we're passing in a much more complicated "piece" of a WHERE clause or a full and customized ORDER BY? What if it's not just a SINGLE value? Ugh... this is much harder but you definitely need EXECUTE AS.

Part III: EXECUTE AS

This is the ultimate in protection for your DSE code. This is where you decide that you're going to allow virtually anything in the WHERE clause or ORDER by but you really want to protect your database. Here, what I need is EXECUTE AS. And I'm not a big fan of this kind of code - but, again, I'm not looking at perf or anything here... I'm *just* looking at the security and potential of the injection.

SQL Server 2005 introduced EXECUTE AS and it has a lot of options - in fact, 4 options:

  • EXECUTE AS Caller
  • EXECUTE AS User
  • EXECUTE AS Self
  • EXECUTE AS Owner

EXECUTE AS Caller is the default. This means that the user that executes the procedure must have the DIRECT rights associated with the Dynamically Executed String. And, so, this is good. This should protect MOST cases altogether. However, what if the users have higher privileges? Well, I'd say that there's already something wrong with your database (because this should NEVER be the case) but... well... I've seen apps (and even RECENTLY) heard of apps that connect as SA. Yes, seriously. So... what can they do... Well, much worse that DROP TABLE. How about DROP DATABASE. Or, well, let's just say that if I found vulnerabilities in that - I could possible get outside of their SQL Server and do even more damage - to files, to the network. So... this is REALLY REALLY REALLY REALLY BAD. I would STRONGLY suggest that anyone that connects as SA - IMMEDIATELY WORK HARD TO CHANGE THIS.

EXECUTE AS Owner is almost as bad (although nothing's as bad as connecting as SA) - especially when the OWNER has elevated rights. So, I'd try to avoid EXECUTE AS Owner unless you SEVERELY restrict who has permissions to the procedure in general. But, don't EXECUTE AS Owner for a stored procedure that's granted EXEC to public. That completely defeats the purpose!

EXECUTE AS Self is a bit strange. But, when a stored procedure is created in a schema, the ownership chaining and privileges follow from the OWNER of the schema. So, what if the author of the stored procedure has rights that the owner doesn't. No, I don't really recommend this but that's where "self" comes in. This is where the procedure can execute under the context of the actual creator (not the owner). However, often these are the same and often they're DBO. This is even worse... Although still, not as bad as connecting as SA.

EXECUTE AS User (where the User is a low-privileged user with very few rights) is good. And, in fact, this will be the answer to this problem!

Yes, even if you connect as SA, I can STILL protect you. Yes, we can still protect this code even when run under the connection context of SA - with EXECUTE AS.

CREATE USER User_GetMembers
WITHOUT
LOGIN
go

GRANT SELECT ON Member TO User_GetMembers
go

ALTER PROCEDURE GetMembers
(
    
@FirstName nvarchar(50)
)
WITH EXECUTE AS N'User_GetMembers'
AS
DECLARE
@ExecStr nvarchar(2000)
SELECT @ExecStr = N'SELECT * FROM dbo.member WHERE FirstName = ''' + REPLACE(@FirstName, '''', '''''') + ''''
SELECT @ExecStr
--EXEC(@ExecStr)
go

And, even if for some reason you can't use REPLACE() this procedure will be limited in what they can do because the user (User_procedurename) has limited rights within the database. In fact, I would recommend that the user ONLY have the necessary rights for THAT stored procedure and I'd even recommend that you create one user for each stored procedure (that has DSE). Yes, that's potentially a lot of users... but, better than the alternative(s)!

Summary/Resources

Finally, please be sure that you're doing some form of auditing if you're using EXECUTE AS. And, you should also do a quick check to see if anyone is ELEVATING user's rights to a more privileged user. Check out these two blog posts for additional information:

And... ah... I'm done! There's a lot here but a lot of tips. You CAN prevent SQL Injection.

Thanks for reading!
kt

I've always been concerned with security and I've always stressed the importance of auditing the REAL user context not just the current user (see this post on EXECUTE AS and auditing). So, I generally try to avoid using dynamic string execution and if necessary create well tested/protected parameters (fyi - using QUOTENAME can be a fantasic solution to protectng identifiers as input parameters but it can't protect more complex strings).

Having said that, what if I'm looking at a database for the first time... just poking around trying to see if there's anything that needs further attention? I've come up with a quick query... And, while it's not going to "solve" your problem (as that's going to take some re-writing of code) or even truly verify if you're vulnerable, it gives you a "quick list" of where you should look first! If your code uses dynamic strings AND it's elevated - then start there! 

SELECT OBJECT_NAME(object_id) AS [Procedure Name],
  CASE
      WHEN sm.definition LIKE '%EXEC (%' OR sm.definition LIKE '%EXEC(%' THEN
'WARNING: code contains EXEC'
      WHEN sm.definition LIKE '%EXECUTE (%' OR sm.definition LIKE '%EXECUTE(%' THEN
'WARNING: code contains EXECUTE'
  END AS [Dynamic Strings]
,
  CASE
     
WHEN execute_as_principal_id IS NOT NULL THEN N'WARNING: EXECUTE AS ' + user_name(execute_as_principal_id
)
      ELSE
'Code to run as caller - check connection context'
  END AS [Execution Context Status]
FROM sys.sql_modules AS sm
ORDER BY [Procedure Name]

Is this enough? Anything else you'd check? What do you think?

THANKS!
kt

DDL Triggers were a new feature of SQL Server 2005 and while seemingly simple, they are very powerful. DDL Triggers allow you to trap an attempted DDL operation to audit it, prevent it, or do anything you want to validate/verify/”authorize”/etc – you write the code. And, since a trigger fires as part of the transaction, you can roll it back. In many conference demos/webcasts, etc., I have provided a sample script that prevents ddl within a [production] database. That script has been really helpful/useful but recently I thought about an update to it… SQL Server 2005 has another new feature "execute as". While I definitely see many benefits, I’m also a bit concerned. To a certain extent, I feel that the potential for SQL Injection is actually higher. If a developer creates a poorly written/tested stored procedure (ok, therein lies the problem, really!) that includes dynamic string execution AND then uses "execute as" to essentially elevate a user with minimal privileges to a higher level (so that they don’t need to give the base object rights to the user), a malicious user could “inject” code in and actually succeed if the “execute as” user has rights to the injected code. In prior releases, and with the default behavior (execute as caller), this is not possible (which is good for security but bad for dynamically executed strings within stored procedures as base object rights are necessary).

Having said that, and since security is always a concern, my DDL Trigger only audited for the login of the user who executed the statement, not for the actual user that’s logged in. In other words, if EXECUTE AS is used (or SETUSER is used), then the context of the user executing is actually different then the logged in user. To see this shift in context, SQL Server 2005 added a new function: ORIGINAL_LOGIN().

(reading between the lines is even more frightening in that prior to SQL Server 2005, the original user could not be tracked from SETUSER. The good news is that SETUSER is ONLY allowed to be used by DBOs so it’s not as widespread as the potential for “execute as”).OK, so how can we put all of this together? We’ll want to add the ORIGINAL_LOGIN function into our audit table in our DDL Trigger. Even if you choose NOT to rollback, at least you’ll know who performed the operation (even if from a dynamically executed string!).

USE AdventureWorks;
go

--Create a login/user - just for this exercise

CREATE LOGIN Paul WITH PASSWORD = 'PxKoJ29!07';
go

CREATE USER Paul FOR LOGIN Paul;
go

sp_addrolemember 'db_ddladmin', 'Paul'
go 

CREATE SCHEMA SecurityAdministration
go

CREATE TABLE SecurityAdministration.AuditDDLOperations
(            OpID                int               NOT NULL identity
     
                                                  CONSTRAINT AuditDDLOperationsPK
                                                           PRIMARY KEY CLUSTERED,
            OriginalLoginName    sysname           NOT NULL,
            LoginName            sysname           NOT NULL,
            UserName             sysname           NOT NULL,
            PostTime             datetime          NOT NULL,
            EventType            nvarchar(100)     NOT NULL,
            DDLOp                nvarchar(2000)    NOT NULL
);
go

GRANT INSERT ON SecurityAdministration.AuditDDLOperations TO public;
go

CREATE TRIGGER PreventAllDDL
ON DATABASE
WITH ENCRYPTION
FOR
DDL_DATABASE_LEVEL_EVENTS
AS

DECLARE
@data XML
SET @data = EVENTDATA()
RAISERROR ('DDL Operations are prohibited on this production database. Please contact ITOperations for proper policies and change control procedures.', 16, -1)
ROLLBACK
INSERT
SecurityAdministration.AuditDDLOperations
                        (OriginalLoginName,
                         LoginName,
                         UserName,
                         PostTime,
                         EventType,
                         DDLOp)
VALUES   (ORIGINAL_LOGIN(), SYSTEM_USER, CURRENT_USER, GETDATE(),
   @data.value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(2000)') )
RETURN;
go 

--Test the trigger.

CREATE TABLE TestTable (col1 int);
go

DROP TABLE SecurityAdministration.AuditDDLOperations;
go

EXECUTE AS LOGIN = 'Paul' -- note: Remember, Paul is a DDL_admin
go

DROP TABLE SecurityAdministration.AuditDDLOperations;
go

REVERT;
go 

SELECT * FROM SecurityAdministration.AuditDDLOperations;
go

DROP TRIGGER PreventAllDDL ON DATABASE;
go

DROP TABLE SecurityAdministration.AuditDDLOperations;
go

DROP SCHEMA SecurityAdministration;
go

DROP USER Paul;
go 

DROP LOGIN Paul;
go
 

So, have fun testing with this one. 

Thanks for reading!
kt

Instant Initialization is a new feature of SQL Server 2005 that is based on an NTFS feature that was added to Windows XP (and therefore is also available in Windows 2003 Server). It's a feature that's seemingly simple; it allows file allocation requests to skip zero initialization on creation. As a result, file allocation requests can occur instantly - no matter what the file size. You might wonder why this is interesting or why this make a difference? Most file allocation requests are small requests, with small incremental changes (like .doc files, .xls files, etc.) but database files can be rather large. In fact, they should be rather large as pre-allocation of a reasonable file size is a best practice to reduce file fragmentation. Additionally, autogrowth causes performance delays (more so in 2000 than 2005) but it's generally something that you want to avoid when possible. As as result, database creation times can take minutes to hours to days, depending on file allocation request. But - it's not just for database creation. ALL file requests can leverage this feature: file creation for a new database, adding a file to an existing database, manually or automatically growing a file and (IMO - the best) restoring a database where the file (or files) being restored does not already exist. The reason I think the last feature is the best is that it can reduce downtime if a database is damaged and allow you to get back up and running more quickly. This is especially important for databases that cannot leverage partial database availability, which is an Enterprise Engine feature. So, to give you some motivation, here is a test that I performed just to have some interesting and comparable numbers.

Performance Test with Zero Initialization
Hardware: Dell Precision 670 Dual Proc (x64) with Dual Core, 4 GB Memory, RAID 1+0 array w/4-142 GB, 15000rpm disks
   CREATE DATABASE with 20 GB Data file = 14:02 minutes
   ALTER DATABASE BY 10 GB = 7:01 minutes
   RESTORE 30 GB DATABASE (EMPTY Backup) = 21:07 minutes
   RESTORE 30 GB DATABASE (11GB Backup) = 38:28 minutes

Performance Test with Instant Initialization
Hardware: Dell Precision 670 Dual Proc (x64) with Dual Core, 4 GB Memory, RAID 1+0 array w/4-142 GB, 15000rpm disks
   CREATE DATABASE with 20 GB Data file = 1.3 seconds
   ALTER DATABASE BY 10 GB = .4 seconds
   RESTORE 30 GB DATABASE (EMPTY Backup) = 5 seconds
   RESTORE 30 GB DATABASE (11GB Backup) = 19:42 minutes

SQL Server can leverage this feature for DATA file requests ONLY; the transaction log must be zero initialized because of its circular nature... which brings me to why this is not "on by default" or more specifically - HOW do you get this feature. First, there's absolutely no syntax change required - SQL Server will use it if it has access to it (so what does that mean?). The SQL Server service must have been granted the Windows permission - "Perform Volume Maintenance Tasks". By default, Windows Administrators have this permission but as yet-another-best-practice, we recommend that your SQL Server run under an account that is a "lower privileged" account (i.e. NOT an administrator). Other ideal options include running as "network service" or running as a dedicated user/domain account that has very few permissions except to SQL Server and it's required resources. A lot of folks recommend using network service for its simplicity (it doesn't have a password and it has limited network/local rights) and I agree with this as long as it's truly dedicated to SQL Server. If network service is used by other services on the same machine then you could compromise security of your SQL Server (or the other services) with the elevated permissions that SQL Server grants or visa versa by the permissions that other applications may have granted to network service. Again, I'm not against network service BUT I would check your local permissions to see if there's anything that jumps out at you. If you've installed other applications/services/etc. you may have already compromised the security of the network service account. I would love to know if anyone has a quick/easy way to check windows permissions to see what may have been granted in addition to the default permissions OR even a link to where the defaults are listed online... I've had trouble doing exactly this when searching, etc... feel free to post links/comments in your comments!. Anyway, with a dedicated user account, you can make sure that it's not compromised because you only use it for SQL Server. But, even these have negative issues - like required passwords that networks invalidate after n days and that you must change on a server/service basis. From a management perspective, this can be difficult.

SIDE NOTE: Managing the service account password is a lot easier in SQL Server 2005 with the SQL Server Configuration Manager (SQL-CM). The SQL-CM allows you to change the password to a service without an active connection (meaning even if the service isn't started) and it invalidates the login token so that password changes don't require a restart of the service. SQL-CM also has a command-line interface and is scriptable with WMI. The WMI Provider allows server settings, client and server network protocols, and aliases to be scripted through the WMI Provider by means of simple VBScript code (or by using the command-line tool). What you could end up doing is creating a script that changes the password of your services on all of your servers (for example, when a password policy is enforced that requires that the passwords of service accounts be changed). I've recently completed a whitepaper that highlights the Management tools (it's really just an overview but even then it turned out to be quite large as we looked at the tools in many different ways). I'll certainly let you know when the whitepaper is published (which should be within the next couple of weeks).

Granting the permission "Perform Volume Maintenance Tasks"
To use instant initialization, your SQL Server service must be running with an account that has the required privilege. If your SQL Server service is running as a local administrator this permission already exists. For a service account which is not a local administrator (again, recommended!), the necessary privilege to grant is Perform Volume Maintenance Tasks. This permission can be granted by an administrator through the Local Security Policy tool (Start, All Programs, Administrative Tools) and once granted, SQL Server automatically uses instant initialization. IMPORTANT NOTE: If this permission is given while SQL Server is running, then SQL Server must be stopped and restarted. However, once the server is restarted, no other syntax or permissions are needed. All data file creation/extension options will use instant initialization for data files created on NTFS volumes when SQL Server 2005 is running on Windows XP or Windows 2003 Server.

Why isn't this on by default?
OK, so after all of this... the gains that you see and the lack of changes to syntax, etc. You're probably wondering why this isn't on by default? It's a security issue. The biggest vulnerability is with SQL Server Administrators who are NOT also Windows Administrators. Windows Administrators have access to local files and can easily see all files stored on the local server. For files that are not encrypted (and are not already open to another process), an Administrator can open and/or modify these files using an appropriate editor. For files that are encrypted, an Administrator can at least view the encrypted information using a hex editor. By granting “Perform Volume Maintenance Tasks” to a SQL Server instance, you are giving administrators of the instance the ability to read the encrypted contents of a recently deleted file (ONLY IF the file system decides to use this newly freed space on the creation of a new database - created with instant initialization) with the undocumented DBCC PAGE command.

SIDE NOTE: The format for DBCC PAGE is undocumented in the Books Online but you will find tips and tricks on many “official” Microsoft blogs. The SQL Server Storage Engine blog (http://blogs.msdn.com/sqlserverstorageengine/ has some very good blog posts on internals and often describes undocumented commands. Specifically, check out the blog entry titled: How to use DBCC PAGE. The first three components are fairly straightforward: database id (or name), file id, and page id. The fourth component is the tricky one: printopt (or print option). The print options for DBCC PAGE are as follows (taken almost verbatim from the SQL Server Storage Engine blog - and Paul said I could :):

0 - print just the page header
1 - page header plus per-row hex dumps and a dump of the page slot array (unless it’s a page that doesn't have one, like allocation bitmaps)
2 - page header plus whole page hex dump
3 - page header plus detailed per-row interpretation (in this case, this option is not available - even if the data you are trying to read is from a previously deleted database because the metadata is not accessible only the raw page data)

Bear in mind, even if you can access this [encrypted] information, making sense out of this data will be challenging if not incredibly difficult.

In production environments, database files should NOT be located on file server drives - especially those where restricted and/or sensitive files are stored. As a result of prudent security measures, the true impact of using instant initialization is low. However, because this vulnerability exists, this feature is off by default.

Want to try this?
I've written a lab on Instant Initialization AND it has an interesting sequence of exercises (using multiple instances):

  • You create a database on instance: SQLDev01
  • Populate a large chunk of pages with very contrived (and easy to find) "junk" data

USE TestWithZeroInitialization
go

 

CREATE TABLE JunkData
(
   
JunkDataID int identity,
   
JunkDataValue char(8000) 
      DEFAULT REPLICATE('Junk', 2000)
)
go

 

SET NOCOUNT ON
go

 

DECLARE @Counter int
SELECT
@Counter = 0
WHILE @Counter < 20000
BEGIN
   
INSERT JunkData DEFAULT VALUES
   
SELECT @Counter = @Counter + 1
END
go

Drop the database from instance: SQLDev01

  • Create a new database on a different instance: SQLDev02 (which has been configured to use Instant Initialization) and hope that it uses the freed space by having dropped the first database
  • Start walking various pages (using DBCC PAGE) to see if you can view the "junk" data from the dropped database.

DBCC PAGE ('TestSecurityExposure', 1, 200, 2)
DBCC PAGE ('TestSecurityExposure', 1, 400, 2)
DBCC PAGE ('TestSecurityExposure', 1, 600, 2)
DBCC PAGE ('TestSecurityExposure', 1, 800, 2)
DBCC PAGE ('TestSecurityExposure', 1, 1000, 2)
DBCC PAGE ('TestSecurityExposure', 1, 1500, 2)
DBCC PAGE ('TestSecurityExposure', 1, 2000, 2)
DBCC PAGE ('TestSecurityExposure', 1, 2500, 2)
DBCC PAGE ('TestSecurityExposure', 1, 3000, 2)
DBCC PAGE ('TestSecurityExposure', 1, 3500, 2)
DBCC PAGE ('TestSecurityExposure', 1, 4000, 2)
DBCC PAGE ('TestSecurityExposure', 1, 4500, 2)

 

In most cases the very first output (for page 200) will return Junk data. If this is not the case, simply drop the TestSecurityExposure database and recreate it again. Sometimes it’s a timing issue and sometimes it could be a background process (like Windows Update) that uses the expected pages. Regardless, if you do get the same pages again - our contrived data should be easy to find.

You can certainly create the environment on your own and see if you can get it to work. OR, you can get a copy of our AlwaysOn DVD that has the appropriate lab environment. I tend to give away the AlwaysOn DVD at events I speak at (on Availability/Disaster Recovery) but I'm also happy to send a few out over snail mail (it needs 2GB of memory and 10GB of disk space for the virtual environment AND you need to have Virtual PC or Virtual Server installed - which are freely downloadable from Microsoft). Paul asked for DBCC CHECKDB information here (to get a free DVD sent to you) and I'm going to ask for instant initialization numbers and how this has helped. Post a comment here and then send me an email with your snail mail address information as well. I'm willing to do this for the first 10 responses... go!

Have fun...and thanks for reading!
kt

PS - For those of you in our UK (London) Event tomorrow, we're giving away the AlwaysOn DVD (and an even cooler SQLskills pen... lol). Now there's motivation if the content doesn't (NOT!).

Today Michele and I were chatting about Security. She's focused a lot on it these days and is chatting with everyone about their best practices... I thought I'd mention a few important links/ideas here:

Login Mode:

  • Allow windows authentication only - making sure that the sa account has a strong password set (regardless of the fact that with Windows Only set it's effectively disabled)
  • When that's not possible and you need to allow SQL Authentication then make sure to use SSL so that the passwords are not transmitted in clear text.

Default Accounts Allowed Access - should you remove any?

  • Do not remove the sa account - only make sure it has a strong password.
  • Consider removing Builtin\Administrators (which is local machine\administrators group) BUT make sure that you have the following accounts setup as administrator or you could end up finding yourself locked out:
    • YOU!
    • Your SQLAgent
    • Your Cluster Admin - if you're running SQL Server on a Cluster

NOTE: if you do find yourself locked out with no way in... and have your server set to windows authentication only then as an NT Admin you can atleast set your SQL Server to Windows/SQL Auth and then login with the sa account (hopefully you know the password and hopefully you've set the password, realize that if your password is blank AND you do this there's nothing that will remind you that you have a blank SA password. If you change the authentication mode in SQLEM (but remember in this strange case you have no way to get in) then at least that prompts you for an SA password). PLEASE - get in and set the SA password ASAP. The registry key change is detailed in this KB Article in the section titled: Turn on Mixed Mode Authentication After You Install MSDE.

Interesting related link: PRB: Unsecured SQL Server with Blank (NULL) SA Password Leaves Vulnerability to a Worm

Above all - make sure that you adhere to many of the common best practices recommended:

 

Categories:
Security

After a group of RDs started chatting about stored procedures I finally felt like I could join in. Often they talk about .Net and client stuff and well, quite honestly, I don't care what you do with the data...only that I serve it up quickly. Ok, I'm really kidding here but my primary focus is backend server tuning and availability, so when the group started talking about stored procedures, I had to chime in. It all started because someone had blogged that stored procedures are not precompiled and are therefore of no use... which is COMPLETELY wrong. However, the irony is that I don't even believe that they should be compiled (and saved and therefore reused) ALL the time. In fact, a precompiled plan is also an optimized plan and that plan may or may not always be the best plan for every execution....... So, here's a bunch of stuff about sprocs. You definitely want to use them - but use them effectively!!!!
 
Benefits of stored procedures:
  • Centralized logic which can be changed with minimal client impact
  • Logic on the server so roundtrips are minimized
  • Compiled plans saved in cache. Not every plan is saved, not every plan should be saved. However, the default is that stored procedure plans are compiled and saved - on first exection. The plan is NOT created when the stored procedure is created (I've also seen this written up incorrectly before) NOR is this plan saved permanently. There are many reasons for why the procedure's plan may fall out of cache:
    • Server restart
    • Falls out of cache due to low re-use (and not enough cache to keep it around)
    • Specifically being removed from cache by:
      • Executing DBCC FREEPROCCACHE to kick ALL plans out of cache (fyi - you can see what's in cache by querying master.dbo.syscacheobects)
      • DATA on which the procedure depends changing enough to cause the statistics to be invalidated and therefore sql server proactively invalidate the plan
      • Executing an sp_recompile (on the object(s) on which the procedure depends. This last one is also nice and something I sometimes force (off hours when a lot of indexes are being rebuilt and/or added). You can execute sp_recompile tname and it will cause all plans which access this table to be invalidated.
Stored Procedures for Security:
  • Stored Procedures can be secure AND easier to manage - in terms of permissions.
  • If I am the owner of a table and I create a procedure based on my table - I can give people access to the procedure withOUT giving base table permissions. This will allow better security and minimize the accidental "oh darns" when someone forgets a where clause on a delete. Imagine a user who types in DELETE Sales WHERE invnum = 1234 BUT highlights ONLY the DELETE Sales...whose problem is that? Mine as the DBA. We can all argue that users should not have ad-hoc access to the server (and I agree) BUT stored procedures create a layer of abstraction which is ALWAYS good especially if you are using DYNAMIC SQL.
Dynamic sql has many connotations... There is "Dynamic String Execution" which can be WITHIN a stored procedure and there's Dynamic SQL which is what an application builds and sends off to the server. As for a client who sends an ad-hoc string to the server or for a dynamically built string within a stored procedure - BOTH REQUIRE that the user have the ability to execute the command directly - which means there's more room for error. However, if it doesn't need to be in a dynamic string (and many statements can be parameterized) then it could be in a stored procedure WITHOUT dynamic string exection withOUT the possible room for HUGE SQL injection problems/errors. Speaking of SQL Injection - IF you use stored procedures and mostly only allow for Identifiers as parameters (i.e. tablename, viewname column names, etc.) then you can use the QUOTENAME() function to significantly reduce SQL injection.
Caching (in General) for better Performance:
 
There are really three areas that need to be understood to really get the issues related to stored procedures:
  • Ad-hoc statement caching (that was new in SQL Server 7.0 and higher)
  • Forced statement caching (through sp_executesql)
  • Stored procedure caching (by creating stored procedures)
Ad-hoc Statement Caching
When a statement is deemed "safe" sql server will take EVEN ad-hoc statements and store a plan of execution in cache for subsequent users to use. For the plan to be re-used the statement has to be almost an identical match, the parameter has to be the EXACT same data type, the objects cannot be ambiguous and MOST statements won't really benefit from this. If you want to see an example of ad-hoc statement caching do the following:
-- 1) Clear Cache with
DBCC FREEPROCCACHE

-- 2) Look at what executable plans are in cache
SELECT sc.*
FROM master.dbo.syscacheobjects AS sc
WHERE sc.cacheobjtype = 'Executable Plan'

-- 3) execute the following statement
SELECT t.*
FROM pubs.dbo.titles AS t
WHERE t.price = 19.99

-- 4) Look again at what executable plans are in cache and you'll find that there's a
--     plan for a NUMERIC(4,2) (look at the "sql" column in output - far RIGHT)

SELECT sc.*
FROM master.dbo.syscacheobjects AS sc
WHERE sc.cacheobjtype = 'Executable Plan'
 
-- 5) If you execute the EXACT same statement with a 4,2 then you will get THAT plan
--     but if you execute with a 5,2 you'll get a new plan (the plan is not safe). Execute this:

SELECT t.*
FROM pubs.dbo.titles AS t
WHERE price = 199.99

-- 6) Look again at what executable plans are in cache...

SELECT sc.*
FROM master.dbo.syscacheobjects AS sc
WHERE sc.cacheobjtype = 'Executable Plan'
OK - so the fact that SQL Server can cache the plan is good... How often is something actually deemed safe - well, it's not that likely. A better way of getting better plan re-use IS either of the two following BUT in these two cases it's ALWAYS cached - which might not ALWAYS be good....
 
Forced Statement Caching (through sp_executesql)
This is good IF you know the plans are consistent (more on this coming up) and IF you want to type the data more strictly. Remember how SQL Server had to type the data above (numeric(4,2) or numeric(5,2), etc.). Well here you can type the parameters and force the plan to be cached. All subsequent uses will get the same plan:
DECLARE @ExecStr    nvarchar(4000)
SELECT @ExecStr = N'SELECT t.* FROM dbo.titles AS t WHERE t.price = @price'
EXEC sp_executesql @ExecStr, N'@price money', 19.99
BUT this falls into the category of being hard to know and there are better ways with stored procedures. Generally, I recommend stored procedures over statement caching...
 
Stored procedure caching (by creating stored procedures)
OK - I could go on for hours here and I'll make it sufficient to say that NOT all procedures should be cached but MANY should. The ONLY way to get the security and consistency desired from stored procedures is to know when to save them and when not to. It also requires that you typically have more smaller stored procedures rather than fewer larger procedures (i.e. modular code is a KEY component to better performance). Anyway, having said this... IF the first person who executes the procedure (and there isn't a plan for the procedure already in cache (and just to make this even more clear - stored procedure plans - when saved - are ONLY in cache they are NEVER saved to disk)) then a plan will be generated - and used for ALL subsequent users (unless a recompile is forced or occurs due to issues in third bullet in first section :). So now that there's a plan - will that plan be perfect...not necessarily. I have a great script that shows this and it's hard to explain over a short blog but the key point is this:  (I took this small section from yet another of my emails so forgive the duplication):
I would say that forced statement caching and stored procedure caching have the same problem(s) and that's that NOT all statements should be cached. There are certain strategies that can help to minimize costly overhead and there are certainly tips/tricks that we use to see if a plan should be saved or not. I think the number one thing I'd recommend is more modular procedures where the data returned SIGNIFICANTLY varies. The key reason is that smaller procedures tend to be more controllable. And - when a complex procedure is separately out then you can set that smaller more modular proc to be recompiled on each execution. That's probably the easiest way. There are certainly other things you can do to more effectively determine the cause of the problem (and that would be better) but I'd say that forcing recompilation on a smaller piece of code that generates widely varying sizes/sets of data is a good and generic solution.
So - having gotten through all of this... If you really want to play with this A LOT more below are a couple of scripts that I think are VERY useful. I use these in my lectures on Optimizing Stored Procedure performance. Regardless, this is the correct order to read/review/learn these scripts:
 
Create the Credit database (you can use CreditSampleDB.zip (55.79 KB) to create it)
Use "RecompilationIssues.sql (3.67 KB)" to get insight into bad plans being created/saved/re-used
Use "ModularProcedures.SQL (4.28 KB)" to get insight into what happens even with procedures that use conditional logic - not that there's anything wrong with that?!

Theme design by Nukeation based on Jelle Druyts