Changing exec to sp_executesql doesn’t provide any benefit if you are not using parameters correctly

I was looking through some code recently and noticed all these sp_executesql calls which did not use parameters correctly.
A typical SQL statement would look like this

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
declare @Col2 smallint
declare @Col1 int
 
select @Col2 = 4,@Col1 = 5
 
declare @SQL nvarchar(1000)
select @SQL = 'select * from test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)
 
 
 
exec sp_executesql @SQL
declare @Col2 smallint
declare @Col1 int

select @Col2 = 4,@Col1 = 5

declare @SQL nvarchar(1000)
select @SQL = 'select * from test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)



exec sp_executesql @SQL

What that code does is it builds a SQL statement and executes it. The problem is that when you do something like that the query plan will not be reused when you change the values of @Col2 and @Col1. When a new plan is generated everytime your values change you will bloat SQL Server’s procedure cache and less memory will be available for data.
Below is some code to demonstrate what I mean, I have tested this code on SQL Server 2008 only!!

First create this table

T-SQL
1
2
3
4
5
create table dbo.test (Col1 int primary key,
Col2 smallint not null,
SomeDate datetime default getdate(),
SomeValue char(10) default 'ABCDEFG')
GO
create table dbo.test (Col1 int primary key,
Col2 smallint not null,
SomeDate datetime default getdate(),
SomeValue char(10) default 'ABCDEFG')
GO

Insert a bunch of rows

T-SQL
1
2
3
4
insert dbo.test(Col1,Col2)
select number+ 1,number from master..spt_values
where type = 'P'
order by number
insert dbo.test(Col1,Col2)
select number+ 1,number from master..spt_values
where type = 'P'
order by number

Now let’s see what we inserted

T-SQL
1
select * from dbo.test
select * from dbo.test

(results abridged)
Col1 Col2 SomeDate SomeValue
1 0 2009-06-09 11:50:04.327 ABCDEFG
2 1 2009-06-09 11:50:04.327 ABCDEFG
3 2 2009-06-09 11:50:04.327 ABCDEFG
4 3 2009-06-09 11:50:04.327 ABCDEFG
5 4 2009-06-09 11:50:04.327 ABCDEFG
6 5 2009-06-09 11:50:04.327 ABCDEFG
7 6 2009-06-09 11:50:04.327 ABCDEFG
…..
…..
…..
2047 2046 2009-06-09 11:50:04.327 ABCDEFG
2048 2047 2009-06-09 11:50:04.327 ABCDEFG

First let’s clear our procedure cache

T-SQL
1
dbcc freeproccache
dbcc freeproccache

run these 2 queries 5 times

T-SQL
1
2
3
4
5
6
7
8
9
select * from dbo.test
where Col2 = 3
and Col1 = 4
go
 
select * from dbo.test
where Col2 = 4
and Col1 = 5
go
select * from dbo.test
where Col2 = 3
and Col1 = 4
go

select * from dbo.test
where Col2 = 4
and Col1 = 5
go

Now run the following query to see how many plans we have.

T-SQL
1
2
3
4
5
6
7
8
9
10
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'

As you can see we have 2 plans and each was used 5 times. So for each change in the value a new plan gets generated

Let’s clear the cache again

T-SQL
1
dbcc freeproccache
dbcc freeproccache

Using dynamic SQL with changing parameters also creates a new plan every time you change the values of the parameters.
Run the following block of code 5 times

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
declare @Col2 smallint
declare @Col1 int
 
select @Col2 = 11,@Col1 = 12
 
declare @SQL varchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)
 
 
exec (@SQL)
 
go
 
declare @Col2 smallint
declare @Col1 int
 
select @Col2 = 12,@Col1 = 13
 
declare @SQL varchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)
 
 
exec (@SQL)
go
declare @Col2 smallint
declare @Col1 int

select @Col2 = 11,@Col1 = 12

declare @SQL varchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)


exec (@SQL)

go

declare @Col2 smallint
declare @Col1 int

select @Col2 = 12,@Col1 = 13

declare @SQL varchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)


exec (@SQL)
go

Now let’s see how many plans we have

T-SQL
1
2
3
4
5
6
7
8
9
10
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'

As you can see we have 2 plans with a count of 5 for each.

Now let’s convert that query to use sp_executesql instead of exec

Run the query below

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
declare @Col2 smallint
declare @Col1 int
 
select @Col2 = 3,@Col1 = 4
 
declare @SQL varchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)
 
 
 
exec sp_executesql @SQL
declare @Col2 smallint
declare @Col1 int

select @Col2 = 3,@Col1 = 4

declare @SQL varchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)



exec sp_executesql @SQL

And you get the following message
Server: Msg 214, Level 16, State 2, Procedure sp_executesql, Line 1
Procedure expects parameter ‘@statement’ of type ‘ntext/nchar/nvarchar’.

This is because sp_executesql expects nvarchar and not varchar

Below is the correct query(but it is not correctly parameterized). First clear the cache again

T-SQL
1
dbcc freeproccache
dbcc freeproccache

Now run the following queries 5 times each

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
declare @Col2 smallint
declare @Col1 int
 
select @Col2 = 23,@Col1 = 24
 
declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)
 
 
exec sp_executesql @SQL
Go
 
 
declare @Col2 smallint
declare @Col1 int
 
select @Col2 = 22,@Col1 = 23
 
declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)
 
 
 
exec sp_executesql @SQL
GO
declare @Col2 smallint
declare @Col1 int

select @Col2 = 23,@Col1 = 24

declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)


exec sp_executesql @SQL
Go


declare @Col2 smallint
declare @Col1 int

select @Col2 = 22,@Col1 = 23

declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = ' + convert(varchar(10),@Col2)+ '
and Col1 = ' + convert(varchar(10),@Col1)



exec sp_executesql @SQL
GO

Now check again for the plans

T-SQL
1
2
3
4
5
6
7
8
9
10
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'

As you can see we have 2 plans with a count of 5 for each. This is because we didn’t use sp_executesql correctly and the engine couldn’t reuse the plan. Here is what Books On Line has to say

sp_executesql can be used instead of stored procedures to execute a Transact-SQL statement a number of times when the change in parameter values to the statement is the only variation. Because the Transact-SQL statement itself remains constant and only the parameter values change, the Microsoft® SQL Server™ query optimizer is likely to reuse the execution plan it generates for the first execution.

Below is the query which is correctly parameterized. As you can see we have variables inside the string and at execution time we pass values by means of other variables to it.

First clear the cache again

T-SQL
1
dbcc freeproccache
dbcc freeproccache

Now run the following queries 5 times each

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
declare @Col2 smallint, @Col1 int
select @Col2 = 3,@Col1 = 4
 
 
declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = @InnerCol2 and Col1 = @InnerCol1' 
 
declare @ParmDefinition nvarchar(500)
SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int'
 
 
 
exec sp_executesql  @SQL,@ParmDefinition,
            @InnerCol2  = @Col2,
            @InnerCol1  = @Col1
go
 
 
declare @Col2 smallint, @Col1 int
select @Col2 = 3,@Col1 = 4
 
 
declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = @InnerCol2 and Col1 = @InnerCol1' 
 
declare @ParmDefinition nvarchar(500)
SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int'
 
 
 
exec sp_executesql  @SQL,@ParmDefinition,
            @InnerCol2  = @Col2,
            @InnerCol1  = @Col1
 
go
declare @Col2 smallint, @Col1 int
select @Col2 = 3,@Col1 = 4


declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = @InnerCol2 and Col1 = @InnerCol1' 

declare @ParmDefinition nvarchar(500)
SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int'



exec sp_executesql 	@SQL,@ParmDefinition,
			@InnerCol2 	= @Col2,
			@InnerCol1 	= @Col1
go


declare @Col2 smallint, @Col1 int
select @Col2 = 3,@Col1 = 4


declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = @InnerCol2 and Col1 = @InnerCol1' 

declare @ParmDefinition nvarchar(500)
SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int'



exec sp_executesql 	@SQL,@ParmDefinition,
			@InnerCol2 	= @Col2,
			@InnerCol1 	= @Col1

go

Check the plans again

T-SQL
1
2
3
4
5
6
7
8
9
10
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'

And you will see that we have only one plan with a count of 10

Instead of running the query like we did before we can also do the following. We only have to declare everything once and then we just need to change the values of the parameters before executing

First clear the cache yet again

T-SQL
1
dbcc freeproccache
dbcc freeproccache

Here is the rewritten query, execute it 5 times

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
declare @Col2 smallint, @Col1 int
select @Col2 = 3,@Col1 = 4
 
 
declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = @InnerCol2 and Col1 = @InnerCol1' 
 
declare @ParmDefinition nvarchar(500)
SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int'
 
 
 
exec sp_executesql  @SQL,@ParmDefinition,
            @InnerCol2  = @Col2,
            @InnerCol1  = @Col1
 
--change param values and run the same query
select @Col2 = 2,@Col1 = 3
exec sp_executesql  @SQL,@ParmDefinition,
            @InnerCol2  = @Col2,
            @InnerCol1  = @Col1
 
go 
declare @Col2 smallint, @Col1 int
select @Col2 = 3,@Col1 = 4


declare @SQL nvarchar(1000)
select @SQL = 'select * from dbo.test
where Col2 = @InnerCol2 and Col1 = @InnerCol1' 

declare @ParmDefinition nvarchar(500)
SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int'



exec sp_executesql 	@SQL,@ParmDefinition,
			@InnerCol2 	= @Col2,
			@InnerCol1 	= @Col1

--change param values and run the same query
select @Col2 = 2,@Col1 = 3
exec sp_executesql 	@SQL,@ParmDefinition,
			@InnerCol2 	= @Col2,
			@InnerCol1 	= @Col1

go 

And we will check the plans yet again

T-SQL
1
2
3
4
5
6
7
8
9
10
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'
select q.text,cp.usecounts,cp.objtype,p.*,
q.*,
cp.plan_handle
from
sys.dm_exec_cached_plans cp
cross apply sys.dm_exec_query_plan(cp.plan_handle) p
cross apply sys.dm_exec_sql_text(cp.plan_handle) as q
where
cp.cacheobjtype = 'Compiled Plan' and q.text  like '%dbo.test%'
and q.text  not like '%sys.dm_exec_cached_plans %'

As you can see we still have a count of 10 and only one plan.

As you can see sp_executesql can be beneficial for performance when used correctly. Using sp_executesql will also give you some additional features you can’t do with EXEC.

How would you get a count of rows in a table? with EXEC you need to use a temp table and populate that, with sp_executesql you can use an output variable

Take a look at the following queries

Here is the EXEC version

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--EXEC (SQL)
DECLARE @TableName VARCHAR(100),
@TableCount INT,
@SQL NVARCHAR(100)
 
 
CREATE TABLE #temp (Totalcount INT)
SELECT @TableName = 'test'
SELECT @SQL = 'Insert into #temp Select Count(*) from ' + @TableName
 
EXEC( @SQL)
 
SELECT @TableCount = Totalcount FROM #temp
 
SELECT @TableCount as TheCount
 
DROP TABLE #temp
GO
--EXEC (SQL)
DECLARE @TableName VARCHAR(100),
@TableCount INT,
@SQL NVARCHAR(100)
 
 
CREATE TABLE #temp (Totalcount INT)
SELECT @TableName = 'test'
SELECT @SQL = 'Insert into #temp Select Count(*) from ' + @TableName
 
EXEC( @SQL)
 
SELECT @TableCount = Totalcount FROM #temp
 
SELECT @TableCount as TheCount
 
DROP TABLE #temp
GO

Here is the sp_executesql version

T-SQL
1
2
3
4
5
6
7
8
9
10
11
12
--sp_executesql
DECLARE @TableName VARCHAR(100),
@TableCount INT,
@SQL NVARCHAR(100)
 
SELECT @TableName = 'Test'
SELECT @SQL = N'SELECT @InnerTableCount = COUNT(*) FROM ' + @TableName
 
EXEC SP_EXECUTESQL @SQL, N'@InnerTableCount INT OUTPUT', @TableCount OUTPUT
 
SELECT @TableCount
GO
--sp_executesql
DECLARE @TableName VARCHAR(100),
@TableCount INT,
@SQL NVARCHAR(100)
 
SELECT @TableName = 'Test'
SELECT @SQL = N'SELECT @InnerTableCount = COUNT(*) FROM ' + @TableName
 
EXEC SP_EXECUTESQL @SQL, N'@InnerTableCount INT OUTPUT', @TableCount OUTPUT
 
SELECT @TableCount
GO

There are more differences between EXEC and sp_executesql, one of the more important one is that sp_executesql can protect you from SQL Injection. I encourage you to read The curse and blessings of dynamic SQL to learn more stuff

I have written a follow up to this post that explains how to avoid conversions. Here is the link: Avoid Conversions In Execution Plans By Using sp_executesql Instead of Exec

*** If you have a SQL related question try our Microsoft SQL Server Programming forum or our Microsoft SQL Server Admin forum