5

I have a query which I'm using with SQL Server 2008R2 via ADO.NET. When I use a LIKE clause inline, it works in less than a second, with 5 rows returned from 2 million. If I declare the paramater as I do in .NET at the start of the query in SSMS, it takes forever.

It's the same query, but parameterized.

The first (which works fine) is (which works fine):

;WITH Results_CTE AS (
    SELECT  ld.* , ROW_NUMBER() OVER (ORDER BY PK_ID) AS RowNum  
    FROM list..List_Data ld 
    WHERE Name IS NOT NULL  AND 
    Postcode LIKE 'SW14 1xx%' 
) SELECT * FROM Results_CTE 

The second which takes forever is:

declare @postcode varchar(10) = 'SW14 1xx'
;WITH Results_CTE AS (
    SELECT  ld.* , ROW_NUMBER() OVER (ORDER BY PK_ID) AS RowNum  
    FROM list..List_Data ld
    WHERE Name IS NOT NULL  AND 
    Postcode LIKE @postcode +'%' 
) SELECT * FROM Results_CTE 

I believe this has something to do with the inner workings of SQL Server but I really have no idea.

Echilon
  • 10,064
  • 33
  • 131
  • 217
  • Searching on `like` makes the optimiser less likely to use indexes. In the first case it can see that there is no wildcard in the beginning of the string, hence it will use the index. Maybe you can `hint` that the index should be used. – Klas Lindbäck Oct 24 '11 at 10:20
  • @KlasLindbäck - SQL Server implements a parametrised `LIKE` as a range seek so in the case that it has a non leading wildcard queries aren't penalised (in the case that there is a leading wildcard the range is the whole index). Presumably in this case though there is a non covering index on `Postcode` that it doesn't use as it over estimates the number of lookups that would be needed. – Martin Smith Oct 24 '11 at 10:22
  • @Echilon: What is the explain plan for the second query? One thing you could try is to add the '%' to `postcode` before you call the query (just in case the `+ '%'` is confusing the optimiser). Shouldn't make a difference, but it's worth a try. – Klas Lindbäck Oct 24 '11 at 10:32
  • When dealing with SQL Server performance problems you should always [get an execution plan](http://stackoverflow.com/questions/7359702/how-do-i-obtain-a-query-execution-plan) – Justin Oct 24 '11 at 10:46

4 Answers4

5

I was googling for potential problems with SqlCommand.Parameters.Add() in C#, and I found this page. I know this is an SQL Server post, but others might find it through google, and it may help them with C#.

For me, none of the above answers worked, so I tried another method.

Instead of:

cmd.Parameters.Add(new SqlParameter("@postcode", postcode));

I used this instead:

// Replace SqlDbType enumeration with whatever SQL Data Type you're using.
cmd.Parameters.Add("@postcode", SqlDbType.VarChar).Value = postcode;

And don't forget the namespace:

using System.Data;

Hope this helps someone!

  • Thanks, mate .. this one did the trick for me. Was going all in on the parameter sniffing problem, but with no luck. This, however, helped. – gimlichael Aug 17 '15 at 11:31
  • You could also use property initializer new SqlParameter("@postcode", postcode) { SqlDbType = SqlDbType.VarChar }. IMO cleaner. – gimlichael Aug 17 '15 at 11:38
3

Use

SELECT * 
FROM Results_CTE 
OPTION (RECOMPILE)

SQL Server does not sniff the value of the variable so it has no idea how selective it will be and will probably be assuming that the query will return significantly more rows than is actually the case and giving you a plan optimised for that.

In your case I'm pretty sure that in the good plan you will find it is using a non covering non clustered index to evaluate the PostCode predicate and some lookups to retrieve the missing columns whereas in the bad plan (as it guesses the query will return a greater number of rows) it avoids this in favour of a full table scan.

Martin Smith
  • 438,706
  • 87
  • 741
  • 845
  • 1
    Can this be used to speed up all parameterized queries? We had performance problems with a query that basically said: `WHERE DATECOL BETWEEN @D1 AND @D2`, and when we inserted the actual date values into the SQL as literals, it speeded up tremendously. Could we just tuck on `OPTION (RECOMPILE)` on that and it would work faster? I know I should test this, but we rewrote our SQL layer to automatically inject these dates so it's a bit of a pain to create a working test right now. – Lasse V. Karlsen Oct 24 '11 at 10:19
  • @LasseV.Karlsen: Recompiling a query is not free, so that would be a balacing act. A recompile adds around 30ms per query on the DB I work with. – Andomar Oct 24 '11 at 10:35
  • Yeah, but since we're injecting parameter values into the SQL, we're basically forcing a recompile anyway, aren't we? When the values change, the entire SQL changes, so it has to be recompiled? – Lasse V. Karlsen Oct 24 '11 at 10:36
  • @Andomar You'd need to figure out cost of compilation vs cost of using an inappropriate plan * probability of using an inappropriate plan to see if it was worthwhile. `@Lasse` - Yes, in your case you are compiling anyway (for each different set of parameters passed) and filling up the plan cache with similar adhoc queries that differ only in the parameter values. – Martin Smith Oct 24 '11 at 10:37
  • We parameterize the values, and build a SQL statement for each combination of search fields. So you get one cached plan per postal code search, and one per name+city, and so on. Works well but obviously a ton of complexity. – Andomar Oct 24 '11 at 10:46
  • @MartinSmith Actually later versions of SQL Server identify constants in ad-hoc queries and automaticaly parameterise them - you can see if this is happening by inspecting the plan cache. – Justin Oct 24 '11 at 10:59
  • @Justin - Only if the query is auto parameterised (which it isn't in Lasse's case or the fix they are using wouldn't work). Of course they may have optimize for adhoc workloads turned on which will mitigate the number of plans actually cached at the expense of additional compiles. – Martin Smith Oct 24 '11 at 11:00
  • Although this does answer my question, I find that if I use it via ADO.NET and use parameters for paging with Results_CTE it times out. Even though there are only 5 rows returned. Any ideas? – Echilon Oct 24 '11 at 11:26
  • 1
    @Echilon - What is the datatype of the column? What is the datatype of the parameter? (Be careful not to use `nvarchar` parameters against `varchar` columns as that will cause an implicit cast) [See this answer for details](http://stackoverflow.com/questions/7637907/query-extremely-slow-in-code-but-fast-in-ssms/7638059#7638059) – Martin Smith Oct 24 '11 at 11:33
  • The Results_CTE table has a RowNum colulm, and I'm adding integer parameters for `@Offset` and `@Limit` with a BETWEEN clause. If I run this through SSMS it works fine though, but not with .NET. – Echilon Oct 24 '11 at 11:42
  • @Echilon - But what is the datatype of the string parameter? If it is `nvarchar` vs `varchar` that's your problem solved (probably) otherwise post a new question with the full code you are using and the execution plan. – Martin Smith Oct 24 '11 at 11:44
2

You can use optimize for to have the parameterized query use the same execution plan as the one with a specific parameter:

SELECT * 
FROM Results_CTE 
OPTION (OPTIMIZE FOR (@postcode = 'SW14 1xx'))
Andomar
  • 232,371
  • 49
  • 380
  • 404
1

This looks like a problem caused by parameter sniffing - during plan compilation SQL Server "sniffs" the current parameters values and uses it to optimise the query. The most common problem that this might cause is if the query is run with an "odd" parameter value the first time its run / compiled in which case the query plan will be optimised for that parameter value, parameter sniffing can cause all other problems however

In your case if the query is run with an empty / null value for @postcode then the query is using a LIKE '%' clause, which is very likely to cause a table scan as a LIKE wildcard is being used at the start of the filter. It looks like either the plan was initially run / compiled with an empty @postcode parameter, or SQL Server is somehow getting confused by this parameter.

There are a couple of things you can try:

  1. Mark the query for recompilation and then run the query again with a non-null value for @postcode.
  2. "Mask" the parameter to try and prevent parameter sniffing,

for example:

declare @postcode varchar(10) = 'SW14 1xx'
declare @postcode_filter varchar(10) = @postcode + '%'
-- Run the query using @postcode_filter instead of @postcode

Although this query looks like it should behave in exactly the same way I've found that SQL Server deals with parameters in strange ways - the rules on when exactly parameter sniffing is used can be a tad strange at time so you may want to play around with variations on the above.

Justin
  • 84,773
  • 49
  • 224
  • 367
  • The posted code uses a **variable** not a parameter. variable values are only sniffed upon statement recompile. It is actually the fact that the value is masked which is causing the problem in the OP. – Martin Smith Oct 24 '11 at 11:14
  • Thanks, but this would be pretty fiddly to use with ADO.NET – Echilon Oct 24 '11 at 11:35