11

I have a recursive query which executes very fast if the WHERE clause contains a constant but becomes very slow if I replace the constant with a parameter having the same value.

Query #1 - with constant

;WITH Hierarchy (Id, ParentId, Data, Depth)
AS
( SELECT Id, ParentId, NULL AS Data, 0 AS Depth
  FROM Test
  UNION ALL
  SELECT h.Id, t.ParentId, COALESCE(h.Data, t.Data), Depth + 1 AS Depth
  FROM Hierarchy h
       INNER JOIN Test t ON t.Id = h.ParentId
)
SELECT *
FROM Hierarchy
WHERE Id = 69

Query #2 - with parameter

DECLARE @Id INT
SELECT @Id = 69

;WITH Hierarchy (Id, ParentId, Data, Depth)
AS
( SELECT Id, ParentId, NULL AS Data, 0 AS Depth
  FROM Test
  UNION ALL
  SELECT h.Id, t.ParentId, COALESCE(h.Data, t.Data), Depth + 1 AS Depth
  FROM Hierarchy h
       INNER JOIN Test t ON t.Id = h.ParentId
)
SELECT *
FROM Hierarchy
WHERE Id = @Id

In case of a table with 50,000 rows the query with the constant runs for 10 milliseconds and the one with the parameter runs for 30 seconds (3,000 times slower).

It is not an option to move the last WHERE clause to the anchor definition of the recursion, as I would like to use the query to create a view (without the last WHERE). The select from the view would have the WHERE clause (WHERE Id = @Id) - I need this because of Entity Framework, but that is another story.

Can anybody suggest a way to force query #2 (with the parameter) to use the same query plan as query #1 (with the constant)?

I already tried playing with indexes but that did not help.

If somebody would like I can post the table definition and some sample data as well. I am using SQL 2008 R2.

Thank you for your help in advance!

Execution plan - Query #1 - with constant

alt text

Execution plan - Query #2 - with parameter

alt text

Zoltan
  • 181
  • 1
  • 2
  • 11
  • Please, check newly added execution plans. – Zoltan Nov 20 '10 at 09:36
  • 3
    Looks like the good plan pushes the predicate a lot further down. Similar to the issue discussed in the 2nd part of this article http://jahaines.blogspot.com/2010/03/performance-tuning-101-what-you-will_17.html – Martin Smith Nov 21 '10 at 22:19
  • You are right, I just wanted to "push down the predicate". Creating a user defined table valued function and CROSS APPLY solved my problem, thank you! – Zoltan Nov 22 '10 at 13:36

4 Answers4

5

As Martin suggested in a comment under the question, the problem is that SQL server does not push down properly the predicate from the WHERE clause - see the link in his comment.

I ended up with creating a user defined table-valued function and use it with the CROSS APPLY operator for creating the view.

Let's see the solution itself.

User Defined Table-valued Function

CREATE FUNCTION [dbo].[TestFunction] (@Id INT)
RETURNS TABLE 
AS
RETURN 
(
    WITH
    Hierarchy (Id,  ParentId, Data, Depth)
    AS(
    SELECT Id, ParentId, NULL AS Data, 0 AS Depth FROM Test Where Id = @Id
    UNION ALL
    SELECT h.Id, t.ParentId, COALESCE(h.Data, t.Data), Depth + 1 AS Depth
        FROM Hierarchy h
            INNER JOIN Test t ON t.Id = h.ParentId
    )
    SELECT * FROM Hierarchy
)

View

CREATE VIEW [dbo].[TestView]
AS
SELECT t.Id, t.ParentId, f.Data, f.Depth
FROM
    Test AS t
    CROSS APPLY TestFunction(Id) as f

Query with constant

SELECT * FROM TestView WHERE Id = 69

Query with parameter

DECLARE @Id INT
SELECT @Id = 69
SELECT * FROM TestView WHERE Id = @Id

The query with the parmater executes basically as fast as the query with the constant.

Thank You Martin and for the others as well!

Zoltan
  • 181
  • 1
  • 2
  • 11
1

For your second Query try using the OPTIMIZE FOR or OPTION(RECOMPILE) query hint to see if that forces it to recomplile based on the provided parameter value.

kevchadders
  • 8,335
  • 4
  • 42
  • 61
  • If SQL would push down the WHERE clause of the query properly this would be a solution as for e.g. @Id = 69 the query runs really fast. However this is not the case. – Zoltan Nov 22 '10 at 15:07
1

You should use a plan guide to freeze the plan you want.

Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • How can this be generated? Is it straight forward to take the plan with a constant and then somehow hack the XML to incorporate the parameter? – Martin Smith Nov 21 '10 at 22:32
  • I ended up with the same conclusion as Martin, is it possible/easy to take the plan guide for the query with the literal and apply it for the query with the parameter? – Zoltan Nov 22 '10 at 15:05
1

This could be the worse suggestion ever, but have you considered creating a sproc to create your query as a string and execute it using sp_executesql?

I know nothing about the caching behaviour of SQL executed by sp_executesql, it was just the first thing to pop into my head.

David
  • 15,750
  • 22
  • 90
  • 150
  • Or even just the EXEC command. I'd be really interested to know if it helped. – David Nov 21 '10 at 20:51
  • The problem is that I would like to avoid totally stored procedures. It is a requirement to end up with a view as it is possible to write a SELECT FROM a view, but this is not true for a stored procedure. Regarding your suggestion, even though I did not try it, as far as I understand SQL server, it would not make any difference in case of my problem. The parameter would still be there in the query thus the execution plan would stay the same. – Zoltan Nov 22 '10 at 13:31
  • You missed my (probably unclear) point. You could use the parameter to write a parameterless query. If your parameter @Id equals 100 (say), you could write a SQL string "SELECT..... WHERE @Id =' + @Id, and execute the SQL string. – David Nov 22 '10 at 15:10
  • I may be missing something, but I don't see that it's relevant that you're really querying a view. Create the view without the parameter as you say, and make sure that when you query the view you use string concatenation to avoid using a parameter. – David Nov 22 '10 at 15:16
  • If you're using string concatenation to create queries, you need to think about SQL injection too. Assuming that @Id is an int, you could just check it's an int before doing the concatenation. That should sort you out. – David Nov 22 '10 at 15:17
  • Sorry, I'm making a real hash of explaining myself. Please email me if you think my approach might be helpful. – David Nov 22 '10 at 15:20
  • Ok, now I understand your suggestion. Your solution would actually work. However if I already have a parameterized stored procedure then I can write the SELECT a bit different way: include the WHERE Id = @Id in the recursive anchor and the query would already run fast and I would not need to put it together dynamically. – Zoltan Nov 22 '10 at 15:47
  • Unfortunately it is not possible to write a SELECT agains a stored procedure. And this is really a MUST in my case - I am using Entity Framework and it generates a WHERE clause by itself. I know that it is possible to call stored procedures from EF, but that totally does not fit in the architecture and thus I would like to have a view to select from. – Zoltan Nov 22 '10 at 15:47
  • I think I don't understand your requirement. I'm not talking about using a stored procedure. I'm talking about creating a view based on the SQL you provided but without the where clause, and then dynamically creating a SQL string which queries the view using a hardcoded value in the WHERE clause. – David Nov 22 '10 at 16:17
  • I just tried your suggestion and it is also fast. But my problem is that I cannot influence the sql this much, as it is generated by an Object Relational Mapper (Entity Framework). Entity Framework is really good for querying tables and views and generates nice sql queries from c# linq expressions. I really need to put my query logic into a view, and fortunately I found a solution for that - see the answer. – Zoltan Nov 22 '10 at 20:01
  • Great, I'm glad you're sorted. – David Nov 23 '10 at 12:49