views:

173

answers:

5

Let's say you have a stored procedure, and it takes an optional parameter. You want to use this optional parameter in the SQL query. Typically this is how I've seen it done:

SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND (@MyOptionalParam IS NULL OR t1.MyField = @MyOptionalParam)

This seems to work well, however it causes a high amount of logical reads if you run the query with STATISTICS IO ON. I've also tried the following variant:

SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND t1.MyField = CASE WHEN @MyOptionalParam IS NULL THEN t1.MyField ELSE @MyOptionalParam END

And it yields the same number of high reads. If we convert the SQL to a string, then call sp_ExecuteSQL on it, the reads are almost nil:

DECLARE @sql nvarchar(max)

SELECT @sql = 'SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = ''test'''

IF @MyOptionalParam IS NOT NULL
BEGIN
     SELECT @sql = @sql + ' AND t1.MyField = @MyOptionalParam '
END

EXECUTE sp_ExecuteSQL @sql, N'@MyOptionalParam', @MyOptionalParam

Am I crazy? Why are optional where clauses so hard to get right?

Update: I'm basically asking if there's a way to keep the standard syntax inside of a stored procedure and get low logical reads, like the sp_ExecuteSql method does. It seems completely crazy to me to build up a string... not to mention it makes it harder to maintain, debug, visualize..

+1  A: 

You're using "OR" clause (implicitly and explicitly) on the first two SQL statements. Last one is an "AND" criteria. "OR" is always more expensive than "AND" criteria. No you're not crazy, should be expected.

Mevdiven
`EXEC sp_executesql` **does** cache the query plan, since v2005: http://www.sommarskog.se/dynamic_sql.html#queryplans
OMG Ponies
You're right. I did not notice that he's using parameter on sp_ExecuteSQL.
Mevdiven
Changed my answer accordingly. Thanks.
Mevdiven
A: 

This is another variation on the optional parameter technique:

SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND t1.MyField = COALESCE(@MyOptionalParam, t1.MyField)

I'm pretty sure it will have the same performance problem though. If performance is #1 then you'll probably be stuck with forking logic and near duplicate queries or building strings which is equally painful in TSQL.

Michael Valenty
+1  A: 

If we convert the SQL to a string, then call sp_ExecuteSQL on it, the reads are almost nil...

  1. Because your query is no longer evaluating an OR, which as you can see kills sargability
  2. The query plan is cached when using sp_executesql; SQL Server doesn't have to do a hard parse...

Excellent resource: The Curse & Blessing of Dynamic SQL

As long as you are using parameterized queries, you should safe from SQL Injection attacks.

OMG Ponies
+1  A: 

EDIT: Adding link to similar question/answer with context as to why the union / if...else approach works better than OR logic (FYI, Remus, the answerer in this link, used to work on the SQL Server team developing service broker and other technologies)

Change from using the "or" syntax to a union approach, you'll see 2 seeks that should keep your logical read count as low as possible:

SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND @MyOptionalParam IS NULL 
union all
SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND t1.MyField = @MyOptionalParam

If you want to de-duplicate the results, use a "union" instead of "union all".

EDIT: Demo showing that the optimizer is smart enough to rule out scan with a null variable value in UNION:

if object_id('tempdb..#data') > 0
    drop table #data
go

-- Put in some data
select  top 1000000
     cast(a.name as varchar(100)) as thisField, cast(newid() as varchar(50)) as myField
into    #data
from    sys.columns a
cross join sys.columns b
cross join sys.columns c;
go

-- Shwo count
select count(*) from #data;
go

-- Index on thisField
create clustered index ixc__blah__temp on #data (thisField);
go

set statistics io on;
go

-- Query with a null parameter value
declare @MyOptionalParam varchar(50);
select  *
from    #data d 
where   d.thisField = 'test'
and  @MyOptionalParam is null;
go

-- Union query
declare @MyOptionalParam varchar(50);
select  *
from    #data d 
where   d.thisField = 'test'
and  @MyOptionalParam is null
union all
select  *
from    #data d 
where   d.thisField = 'test'
and  d.myField = '5D25E9F8-EA23-47EE-A954-9D290908EE3E';
go

-- Union query with value
declare @MyOptionalParam varchar(50);
select @MyOptionalParam = '5D25E9F8-EA23-47EE-A954-9D290908EE3E'
select  *
from    #data d 
where   d.thisField = 'test'
and  @MyOptionalParam is null
union all
select  *
from    #data d 
where   d.thisField = 'test'
and  d.myField = '5D25E9F8-EA23-47EE-A954-9D290908EE3E';
go

if object_id('tempdb..#data') > 0
    drop table #data
go
chadhoc
First query reads whole table. This is not a good way to minimize IO.
David B
This is more expensive method than the SQL statement depicted in the question.
Mevdiven
Sorry guys, but the optimizer will definitely NOT scan the whole table in the first query, it is smart enough to exclude the query based on a null value "AND' with the variable. A simple example with IO stat output will demonstrate, run locally and review on your own (note if you don't have a seekable index on ThisField, you'll always get a scan due to the query against it, hence that is assumed) - I've edited the answer with the sample to demonstrate-- Put in some dataselect top 1000000 cast(a.name as varchar(100)) as thisField, cast(newid() as varchar(50)) as myFieldinto #datafrom s
chadhoc
You have a non-conditional SARG-able filtering criteria.. so yes you do dodge a table scan. Original question asker doesn't know SARG and will wind up with table scans if he follows your advice.
David B
Not sure I'm following you David, "original question asker doesn't know SARG", can you clarify? Are you saying the original question asker doesn't know what a SARG is? Or are you saying the query isn't SARGable (which is the point of rewriting it to make it so)?
chadhoc
And I'd strongly recommend reading over Remus's answer in the newly linked q/a above as well
chadhoc
A: 

Change from using the "or" syntax to a two query approach, you'll see 2 different plans that should keep your logical read count as low as possible:

IF @MyOptionalParam is null
BEGIN

  SELECT *
  FROM dbo.MyTableName t1

END
ELSE
BEGIN

  SELECT *
  FROM dbo.MyTableName t1
  WHERE t1.MyField = @MyOptionalParam

END

You need to fight your programmer's urge to reduce duplication here. Realize you are asking for two fundamentally different execution plans and require two queries to produce two plans.

David B
But what if you have multiple optional parameters you want to filter on? I guess I do not understand why it is "two fundamentally different execution plans". If I'm a parser, I look at the variable, go "hey, it's null and will never be otherwise.. I can stop filtering on it." But I suppose it doesn't work that way, at least in SQL 2005.
Nicholas H
If you have multiple optional parameters, odds are that only a few are significant for the query plan... just branch on those. As for parameter sniffing: http://sqlblog.com/blogs/ben_nevarez/archive/2009/08/27/the-query-optimizer-and-parameter-sniffing.aspx Good luck with this approach though.
David B