21

EDIT: I've updated the example code and provided complete table and view implementations for reference, but the essential question remains unchanged.

I have a fairly complex view in a database that I am attempting to query. When I attempt to retrieve a set of rows from the view by hard-coding the WHERE clause to specific foreign key values, the view executes very quickly with an optimal execution plan (indexes are used properly, etc.)

SELECT * 
FROM dbo.ViewOnBaseTable
WHERE ForeignKeyCol = 20

However, when I attempt to add parameters to the query, all of a sudden my execution plan falls apart. When I run the query below, I'm getting index scans instead of seeks all over the place and the query performance is very poor.

DECLARE @ForeignKeyCol int = 20

SELECT * 
FROM dbo.ViewOnBaseTable
WHERE ForeignKeyCol = @ForeignKeyCol 

I'm using SQL Server 2008 R2. What gives here? What is it about using parameters that is causing a sub-optimal plan? Any help would be greatly appreciated.

For reference, here are the object definitions for which I'm getting the error.

CREATE TABLE [dbo].[BaseTable]
(
    [PrimaryKeyCol] [uniqueidentifier] PRIMARY KEY,
    [ForeignKeyCol] [int] NULL,
    [DataCol] [binary](1000) NOT NULL
)

CREATE NONCLUSTERED INDEX [IX_BaseTable_ForeignKeyCol] ON [dbo].[BaseTable]
(
    [ForeignKeyCol] ASC
)

CREATE VIEW [dbo].[ViewOnBaseTable]
AS
SELECT
    PrimaryKeyCol,
    ForeignKeyCol,
    DENSE_RANK() OVER (PARTITION BY ForeignKeyCol ORDER BY PrimaryKeyCol) AS ForeignKeyRank,
    DataCol
FROM
    dbo.BaseTable

I am certain that the window function is the problem, but I am filtering my query by a single value that the window function is partitioning by, so I would expect the optimizer to filter first and then run the window function. It does this in the hard-coded example but not the parameterized example. Below are the two query plans. The top plan is good and the bottom plan is bad.

Query Execution Plans

Martin Smith
  • 438,706
  • 87
  • 741
  • 845
John Bledsoe
  • 17,142
  • 5
  • 42
  • 59
  • does `option(recompile)` help? – Joel Coehoorn Nov 29 '12 at 22:06
  • @JoelCoehoorn not at all. Neither does updating statistics, rebuilding indexes or things like that. The optimizer is convinced that it has the best plan and I need to convince it otherwise. – John Bledsoe Mar 08 '13 at 18:12
  • My only other suggestion is to look into index hints (http://blog.sqlauthority.com/2009/02/07/sql-server-introduction-to-force-index-query-hints-index-hint/). I don't think an index hint will work directly, but if you can get Sql Server to complain to you _why_ it was unable to use a hint, that could point you in the direction you need to find a solution. – Joel Coehoorn Mar 08 '13 at 18:20
  • @JoelCoehoorn that's a good suggestion. I've been trying to use those to force the plan that I want and SQL Server does complain that it can't create a plan with that hint, but it doesn't give any indication as to why. I'm going to try to reduce the problem to the smallest possible instance and work from there. – John Bledsoe Mar 08 '13 at 18:36
  • It would be interesting to see the view definition and the good and the bad query plans. – Mikael Eriksson Mar 08 '13 at 18:42
  • @MikaelEriksson right...there is a lot going on in this view. I'll see if I can reduce it to a state that still has the problem and post it. – John Bledsoe Mar 08 '13 at 18:52
  • A difference is that the value for the fast query is known in the query plan where the bad query plan is generated without a "compiled value" for the parameter. The suggestion by @JoelCoehoorn does in fact work for me to get the same plan for both queries. Using that means the plan is not cached and it is regenerated for every execution. You can also use `option (optimize for (@ForeignKeyCol = 20))` to make the value to use know to the optimizer but that does surprisingly (to me at least) **not** work. – Mikael Eriksson Mar 08 '13 at 21:18
  • I have no idea why the optimizer chooses a different plan if the value is known or not. Could be something to do with how statistics is used. I think this question is a good candidate to be migrated to http://dba.stackexchange.com/ where it will get the attention of some guys that really know the way around a execution plan. If you agree you can flag the question to be migrated and a moderator will hopefully make that happen. – Mikael Eriksson Mar 08 '13 at 21:18
  • @MikaelEriksson Good idea. It seems that this site is more populated but since I haven't had a good answer I'm fine with migration. I will flag the post. – John Bledsoe Mar 08 '13 at 21:31
  • For the time being I'm going to write a stored procedure that builds a dynamic SQL statement to hard-code the parameters, thus getting the optimal execution plan. It goes against every single one of my DBA sensibilities but it does solve the problem so I'm going with it until a better answer emerges. – John Bledsoe Mar 08 '13 at 21:35
  • 3
    @MikaelEriksson [Some related links here](http://dba.stackexchange.com/a/33572/3690). Particularly [this one](http://www.sqlskills.com/blogs/jonathan/ctes-window-functions-and-views/) – Martin Smith Mar 08 '13 at 22:31
  • @MartinSmith Yes, using a derived table instead of a view shows the same issues with a variable **but** moving the where clause into the derived table makes a query plan with the seek instead of a scan. – Mikael Eriksson Mar 08 '13 at 22:43
  • @MikaelEriksson - Yes and using an inline TVF containing a parameterised `WHERE` clause (instead of a view) would probably also work. I'm not sure why SQL Server can push the predicate for literals and using `OPTION(RECOMPILE)` but not for variables/parameters. – Martin Smith Mar 08 '13 at 22:47
  • @MartinSmith That is the only thing left to this mystery. It's not the fact that the value is known because `optimize for` did not work. Perhaps a different way of finding a query plan when Auto-Parameterization is used. – Mikael Eriksson Mar 08 '13 at 22:52
  • @MikaelEriksson - `OPTIMIZE FOR` doesn't guarantee the value that will be passed at run time though. The value passed at execution time could be different (e.g. `NULL`) . Though as far as I can see it would be valid to use the pushed plan if that was the case. – Martin Smith Mar 08 '13 at 22:58
  • @MikaelEriksson - I've just realised why you were talking about auto parameterisation. I hadn't noticed that the plan with the literal value shows up as `WHERE [ForeignKeyCol]=@1`. That is even stranger that the manually parameterised plan is different. I notice that `WHERE ForeignKeyCol = NULL` doesn't use the same auto parameterised template so still thinking it might be an optimisation that is only applied when SQL Server knows at compile time the parameter will never be `NULL` – Martin Smith Mar 09 '13 at 12:11
  • @MartinSmith I tried to fool the optimizer with `isnull` but it did not work. (Unless I accidentally tried with `nullif`, have to test again when at a computer.) – Mikael Eriksson Mar 09 '13 at 13:51
  • @MikaelEriksson - I tried that one as well! At the moment I'm trying to work it out from the info in Paul White's [optimiser series](http://sqlblog.com/blogs/paul_white/archive/2012/04/28/query-optimizer-deep-dive-part-1.aspx). [This script here](http://pastebin.com/yFB6F0F0) shows slightly different results as per my second comment on his answer. I suspect it might have to be `ScaOp_Const` for the optimisation to be applied. – Martin Smith Mar 09 '13 at 13:55
  • Have you tried the following approach `WHERE ForeignKeyCol = ISNULL(@ForeignKeyCol, ForeignKeyCol)`? I just recently managed to fool the optimizer by using a similar construct. – JodyT Mar 10 '13 at 01:28

3 Answers3

25

When using OPTION (RECOMPILE) be sure to look at the post-execution ('actual') plan rather than the pre-execution ('estimated') one. Some optimizations are only applied when execution occurs:

DECLARE @ForeignKeyCol int = 20;

SELECT ForeignKeyCol, ForeignKeyRank
FROM dbo.ViewOnBaseTable
WHERE ForeignKeyCol = @ForeignKeyCol
OPTION (RECOMPILE);

Pre-execution plan:

Pre-execution plan

Post-execution plan:

Post-execution plan

Tested on SQL Server 2012 build 11.0.3339 and SQL Server 2008 R2 build 10.50.4270

Background & limitations

When windowing functions were added in SQL Server 2005, the optimizer had no way to push selections past these new sequence projections. To address some common scenarios where this caused performance problems, SQL Server 2008 added a new simplification rule, SelOnSeqPrj, which allows suitable selections to be pushed where the value is a constant. This constant may be a literal in the query text, or the sniffed value of a parameter obtained via OPTION (RECOMPILE). There is no particular problem with NULLs though the query may need to have ANSI_NULLS OFF to see this. As far as I know, applying the simplification to constant values only is an implementation limitation; there is no particular reason it could not be extended to work with variables. My recollection is that the SelOnSeqPrj rule addresssed the most commonly seen performance problems.

Parameterization

The SelOnSeqPrj rule is not applied when a query is successfully auto-parameterized. There is no reliable way to determine if a query was auto-parameterized in SSMS, it only indicates that auto-param was attempted. To be clear, the presence of place-holders like [@0] only shows that auto-parameterization was attempted. A reliable way to tell if a prepared plan was cached for reuse is to inspect the plan cache, where the 'parameterized plan handle' provides the link between ad-hoc and prepared plans.

For example, the following query appears to be auto-parameterized in SSMS:

SELECT *
FROM dbo.ViewOnBaseTable
WHERE ForeignKeyCol = 20;

But the plan cache shows otherwise:

WITH XMLNAMESPACES
(
    DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan'
)
SELECT
    parameterized_plan_handle =
        deqp.query_plan.value('(//StmtSimple)[1]/@ParameterizedPlanHandle', 'nvarchar(64)'), 
    parameterized_text =
        deqp.query_plan.value('(//StmtSimple)[1]/@ParameterizedText', 'nvarchar(max)'),
    decp.cacheobjtype,
    decp.objtype,
    decp.plan_handle
FROM sys.dm_exec_cached_plans AS decp
CROSS APPLY sys.dm_exec_sql_text(decp.plan_handle) AS dest
CROSS APPLY sys.dm_exec_query_plan(decp.plan_handle) AS deqp
WHERE
    dest.[text] LIKE N'%ViewOnBaseTable%'
    AND dest.[text] NOT LIKE N'%dm_exec_cached_plans%';

Adhoc plan cache entry

If the database option for forced parameterization is enabled, we get a parameterized result, where the optimization is not applied:

ALTER DATABASE Sandpit SET PARAMETERIZATION FORCED;
DBCC FREEPROCCACHE;

SELECT *
FROM dbo.ViewOnBaseTable
WHERE ForeignKeyCol = 20;

Forced parameterization plan

The plan cache query now shows a parameterized cached plan, linked by the parameterized plan handle:

Parameterized plan cache

Workaround

Where possible, my preference is to rewrite the view as an in-line table-valued function, where the intended position of the selection can be made more explicit (if necessary):

CREATE FUNCTION dbo.ParameterizedViewOnBaseTable
    (@ForeignKeyCol integer)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
    SELECT
        bt.PrimaryKeyCol,
        bt.ForeignKeyCol,
        ForeignKeyRank = DENSE_RANK() OVER (
            PARTITION BY bt.ForeignKeyCol 
            ORDER BY bt.PrimaryKeyCol),
        bt.DataCol
    FROM dbo.BaseTable AS bt
    WHERE
        bt.ForeignKeyCol = @ForeignKeyCol;

The query becomes:

DECLARE @ForeignKeyCol integer = 20;
SELECT pvobt.*
FROM dbo.ParameterizedViewOnBaseTable(@ForeignKeyCol) AS pvobt;

With the execution plan:

Function plan

  • Thanks for the excellent explanation @PaulWhite. Amazing to me that OPTION (RECOMPILE) just takes care of this issue. Unfortunately, my real query was generated by Entity Framework and therefore I can't just stick that option into the generated SQL, so I had to create a stored procedure to include the recompilation option, but that broke me out of my gridlock. – John Bledsoe Mar 11 '13 at 16:49
  • But then you can no longer easily pass in where expressions, or things like "ForeignKeyCol < 50". Great if the particular use case you have is one scalar value only, not so great if you need query flexibility... – marknuzz Dec 05 '13 at 19:55
  • @Nuzzolilo If you need the flexibility of entirely dynamic WHERE clauses, use dynamic SQL. You can still put OPTION (RECOMPILE) on it and get different plans depending on the parameters and the eventual where clauses. This should still avoid having all plans against the view suffer from one single parameter sniffing problem. – Aaron Bertrand Aug 25 '14 at 21:21
  • 1
    The issues described in this post were first fixed in SQL Server 2017 CU 30. Query optimizer fixes need to be enabled e.g. by enabling trace flag 4199 or setting the database scoped configuration option. With the fix, `SelOnSeqPrj` is no longer limited to constants. – Paul White Jul 16 '22 at 10:20
1

You could always go the CROSS APPLY way.

ALTER VIEW [dbo].[ViewOnBaseTable]
AS
SELECT
    PrimaryKeyCol,
    ForeignKeyCol,
    ForeignKeyRank,
    DataCol
FROM (
    SELECT DISTINCT
        ForeignKeyCol
    FROM dbo.BaseTable
) AS Src
CROSS APPLY (
    SELECT
        PrimaryKeyCol,
        DENSE_RANK() OVER (ORDER BY PrimaryKeyCol) AS ForeignKeyRank,
        DataCol
    FROM dbo.BaseTable AS B
    WHERE B.ForeignKeyCol = Src.ForeignKeyCol
) AS X
Serge
  • 6,554
  • 5
  • 30
  • 56
-3

I think in this particular case it may be because the data types between your parameters and your table do not match exactly so SQL Server has to do an implicit conversion which is not a sargable operation.

Check your table data types and make your parameters the same type. Or do the cast yourself outside the query.

Francisco Soto
  • 10,277
  • 2
  • 37
  • 46
  • Actually, in this example KeyCol1 is of type uniqueidentifier, so I would think that if anything the first query (the one with a good plan) would have this problem and not the second (the one with the bad plan). However, the query run by the application is actually using sp_executesql, so I will double-check the data types of the parameters. – John Bledsoe Nov 29 '12 at 22:46
  • Running into this problem again and parameter data types are definitely not the issue. – John Bledsoe Mar 07 '13 at 15:05