<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://www2.sqlblog.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Adam Machanic : Optimization</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx</link><description>Tags: Optimization</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><item><title>PASS Summit 2011 - Zen and the Art of Workspace Memory - Demos</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2011/10/16/pass-summit-2011-zen-and-the-art-of-workspace-memory-demos.aspx</link><pubDate>Mon, 17 Oct 2011 00:23:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:39085</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>18</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/39085.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=39085</wfw:commentRss><description>What a rush . Standing on the stage in an almost-full 1,000-person room, I (very) momentarily wondered what I'd been thinking when I submitted a 500-level talk for the biggest SQL Server conference in the world. But despite a rough start--my laptop crashed...(&lt;a href="http://www2.sqlblog.com/blogs/adam_machanic/archive/2011/10/16/pass-summit-2011-zen-and-the-art-of-workspace-memory-demos.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=39085" width="1" height="1"&gt;</description><enclosure url="http://www2.sqlblog.com/blogs/adam_machanic/attachment/39085.ashx" length="66600" type="application/x-zip-compressed" /><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Query+Tuning/default.aspx">Query Tuning</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/memory/default.aspx">memory</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/PASS/default.aspx">PASS</category></item><item><title>Where Can You Find Me the Rest of This Year?</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2011/08/21/where-can-you-find-me-the-rest-of-this-year.aspx</link><pubDate>Mon, 22 Aug 2011 00:28:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:37983</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>0</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/37983.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=37983</wfw:commentRss><description>Autumn is creeping inevitably closer here in the US, and that means that speaking season is about to kick into high gear. Here's my current schedule for the remainder of the year: September 8, 17:00 GMT (online) - 24 Hours of PASS webcast: "Baseline Basics...(&lt;a href="http://www2.sqlblog.com/blogs/adam_machanic/archive/2011/08/21/where-can-you-find-me-the-rest-of-this-year.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=37983" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/New+England/default.aspx">New England</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/TechEd/default.aspx">TechEd</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Speaking/default.aspx">Speaking</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/PASS/default.aspx">PASS</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/sql+saturday/default.aspx">sql saturday</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/no+more+guessing/default.aspx">no more guessing</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/atlanta/default.aspx">atlanta</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/sql+server+connections/default.aspx">sql server connections</category></item><item><title>TechEd 2011 - Performance Tuning and Optimization in SQL Server 2008 R2 and SQL Server Code Named "Denali"</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2011/05/18/teched-2011-performance-tuning-and-optimization-in-sql-server-2008-r2-and-sql-server-code-named-denali.aspx</link><pubDate>Wed, 18 May 2011 16:05:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:35708</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>2</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/35708.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=35708</wfw:commentRss><description>Thanks to everyone who took the time out of their conference experience to join Mike Wachal and me for yesterday's session on SQL Server performance tuning! For those who weren't there, we focused in on troubleshooting techniques, highlighting some of...(&lt;a href="http://www2.sqlblog.com/blogs/adam_machanic/archive/2011/05/18/teched-2011-performance-tuning-and-optimization-in-sql-server-2008-r2-and-sql-server-code-named-denali.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=35708" width="1" height="1"&gt;</description><enclosure url="http://www2.sqlblog.com/blogs/adam_machanic/attachment/35708.ashx" length="89557" type="application/x-zip-compressed" /><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/TechEd/default.aspx">TechEd</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/PASS/default.aspx">PASS</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/no+more+guessing/default.aspx">no more guessing</category></item><item><title>Revisiting ISNULL, COALESCE, and the Perils of Micro-Optimization</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2010/06/30/isnull-coalesce-and-micro-optimization.aspx</link><pubDate>Wed, 30 Jun 2010 17:12:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:25637</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>9</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/25637.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=25637</wfw:commentRss><description>Almost six years ago--in November of 2004--I posted what would turn out to be one of my most popular blog posts in terms of number of reads, " Performance: ISNULL vs. COALESCE ." (If you're curious, the post is dated July 2006 because I was too lazy it...(&lt;a href="http://www2.sqlblog.com/blogs/adam_machanic/archive/2010/06/30/isnull-coalesce-and-micro-optimization.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=25637" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/ISNULL/default.aspx">ISNULL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/COALESCE/default.aspx">COALESCE</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/nullability/default.aspx">nullability</category></item><item><title>What Happened Today? DATE and Date Ranges Over DATETIME</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2009/10/20/what-happened-today-date-and-date-ranges-over-datetime.aspx</link><pubDate>Tue, 20 Oct 2009 18:21:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:18023</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>14</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/18023.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=18023</wfw:commentRss><description>A few days ago Aaron posted yet another fantastic entry in his Bad Habits series, this one discussing mishandling of date ranges in queries . This is a topic near and dear to me, having had to clean up a lot of poorly thought out code in the past few...(&lt;a href="http://www2.sqlblog.com/blogs/adam_machanic/archive/2009/10/20/what-happened-today-date-and-date-ranges-over-datetime.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=18023" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/datetime/default.aspx">datetime</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/date/default.aspx">date</category></item><item><title>Exploring the secrets of intermediate materialization</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/10/03/exploring-the-secrets-of-intermediate-materialization.aspx</link><pubDate>Wed, 04 Oct 2006 02:18:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:267</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>24</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/267.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=267</wfw:commentRss><description>When working with SQL Server 2000, I used to have this little trick I'd pull out after exhausting all other ideas for tuning a query.&amp;nbsp; And I thought that my little trick was dead in SQL Server 2005, but thanks to fellow SQL Server MVP &lt;a href="http://robfarley.blogspot.com/"&gt;Rob Farley&lt;/a&gt;, I am officially reviving my trick from the dead here and now, in this blog post.&lt;br&gt;&lt;br&gt;... But first, let's start with an example query.&amp;nbsp; Here's the scenario: You work for AdventureWorks, and management has asked you to create a report to find out how many peers each employee in the company has. You see, AdventureWorks management seems to believe that if two employees are
managed by the same person, they must have exactly the same job
function, and they can do each others' jobs equally well.&amp;nbsp; So what they want to do is find out which employees have too many peers (might as well downsize some of that extraneous fluff), and at the same time find out which employees, should they be hit by a bus tomorrow, could be immediately substituted for by a colleague. Now, whether or not management's belief is utterly moronic or not is beyond the scope of this post, so dash any such thoughts from your head until you've read to the end, and then resume pondering along those lines, which I'm sure will end up putting a smile on your face.&amp;nbsp; &lt;br&gt;&lt;br&gt;But smiles are for later.&amp;nbsp; At this point I've managed to go off on a horribly involved tangent, so let's get back to the query at hand:&lt;br&gt;&lt;br&gt;&lt;pre class="code"&gt;SELECT &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.EmployeeId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; COUNT(*) AS TheCount&lt;br&gt;FROM HumanResources.Employee x&lt;br&gt;JOIN HumanResources.Employee y on y.ManagerId = x.ManagerId&lt;br&gt;GROUP BY x.EmployeeId&lt;/pre&gt;&lt;br&gt;&lt;br&gt;What we're doing here is finding all employees managed by the same manager, and then taking a count of those employees.&amp;nbsp; Yes, it would have made more sense to simply find out how many employees are managed by each manager, but that's not what management asked for, and management clearly thinks better than you do. So go run the report!&lt;br&gt;&lt;br&gt;But what does any of this have to do with query tuning tricks, you ask (while tidying up your resume a bit)?&amp;nbsp; To answer that question, let's take a quick peek at the I/Os our query is using, in addition to the query plan.&amp;nbsp; First, a baseline for the I/Os:&lt;br&gt;&lt;br&gt;&lt;pre class="code"&gt;SELECT *&lt;br&gt;FROM HumanResources.Employee&lt;/pre&gt;&lt;br&gt;&lt;br&gt;Result, obtained via Profiler: 20 logical reads.&amp;nbsp; OK, and now the test query in question: 827 logical reads. Quite a jump for a query that only uses the one table -- we're clearly wasting a lot of resources.&amp;nbsp; And looking at the query plan, it's obvious we can do better.&amp;nbsp; An outer table scan, looped to find the count for each employee -- that's a lot of index operations.&lt;br&gt;&lt;br&gt;A common way to start tuning this kind of query is to move the aggregation into a derived table. After peering at this query for a while, one might come to the conclusion that there's no reason to aggregate on ManagerId more than once per manager. Why do it once per employee?&lt;br&gt;&lt;br&gt;&lt;pre class="code"&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.EmployeeId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; y.theCount&lt;br&gt;FROM HumanResources.Employee x&lt;br&gt;JOIN&lt;br&gt;(&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; p.ManagerId, &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; COUNT(*)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM HumanResources.Employee p&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; GROUP BY p.ManagerId&lt;br&gt;) y (ManagerId, theCount) ON y.ManagerId = x.ManagerId&lt;/pre&gt;&lt;br&gt;&lt;br&gt;Seems better, but upon running it, we see that it produces 819 logical reads and almost exactly the same query plan. Not much of an improvement.&amp;nbsp; And alas, there's not much more we can do here.&amp;nbsp; There just aren't too many ways to skin this query, and each of them requires some kind of loop to get the count, either implied or otherwise... Right?&lt;br&gt;&lt;br&gt;And now we're almost to "dirty trick" territory.&amp;nbsp; But let's first try a not-so-dirty trick. A temp table might eliminate some of the overhead, right?&amp;nbsp; Then we'll only have to query the base tables once...&lt;br&gt;&lt;br&gt;&lt;pre class="code"&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; p.ManagerId, &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; COUNT(*) AS theCount&lt;br&gt;INTO #y&lt;br&gt;FROM HumanResources.Employee p&lt;br&gt;GROUP BY p.ManagerId&lt;br&gt;&lt;br&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.EmployeeId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; y.theCount&lt;br&gt;FROM HumanResources.Employee x&lt;br&gt;JOIN #y y ON y.ManagerId = x.ManagerId&lt;/pre&gt;&lt;br&gt;&lt;br&gt;207 logical reads.&amp;nbsp; Quite an improvement!&amp;nbsp; But the temp table is still using a nested loop, and a merge would be so much nicer, wouldn't it?&amp;nbsp; A MERGE JOIN hint drops the number of reads to 115, but I still feel that we can do even &lt;i&gt;better&lt;/i&gt;. &lt;br&gt;&lt;br&gt;Now in SQL Server 2000 at about this point in my query tuning excercise, I might try forcing &lt;i&gt;intermediate materialization&lt;/i&gt; of the derived table, sans the temp table, by using TOP 100 PERCENT in conjunction with ORDER BY. Unfortunately, the SQL Server query optimizer team &lt;a href="http://blogs.msdn.com/queryoptteam/archive/2006/03/24/560396.aspx"&gt;decided that this wasn't a good idea&lt;/a&gt;, and the optimizer now ignores such attempts.&lt;br&gt;&lt;br&gt;And until earlier today, I thought the game was over. Until I was reminded by Rob that TOP takes a number of rows in addition to a percent. The trick, then?&amp;nbsp; Use a bigger number of rows than you'll ever actually get back... Say, the maximum value for SQL Server's INTEGER type (2147483647)?&lt;br&gt;&lt;br&gt;By applying TOP and ORDER BY within the derived table, we can force SQL Server to perform intermediate materialization of the results.&amp;nbsp; And by playing with the ORDER BY properly, we can even prompt the optimizer to choose a merge...&lt;br&gt;&lt;br&gt;&lt;pre class="code"&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.EmployeeId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; y.theCount&lt;br&gt;FROM HumanResources.Employee x&lt;br&gt;JOIN&lt;br&gt;(&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT TOP (2147483647)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; p.ManagerId, &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; COUNT(*)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM HumanResources.Employee p&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; GROUP BY p.ManagerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY p.ManagerId&lt;br&gt;) y (ManagerId, theCount) on y.ManagerId = x.ManagerId&lt;/pre&gt;&lt;br&gt;&lt;br&gt;And the result of all of this hard labor?&amp;nbsp; &lt;i&gt;10 logical reads&lt;/i&gt; (1000% improvement over the next best method), a merge operation, and if I do say so myself, a very good topic for a blog post.&lt;br&gt;&lt;br&gt;The usual caveats apply.&amp;nbsp; Do not try this at home.&amp;nbsp; Do not rely on this undocumented behavior.&amp;nbsp; Do not pass Go.&amp;nbsp; Do not fail to hire me to tune your databases if this trick doesn't fix all of your problems.&amp;nbsp; And, lest I forget, do not waste time reading this blog when management needs that report &lt;i&gt;yesterday&lt;/i&gt;!&lt;br&gt;&lt;br&gt;Anyway, until next time, enjoy!&lt;br&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=267" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Query+Tuning/default.aspx">Query Tuning</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category></item><item><title>Controlling Stored Procedure Caching with ... Dyanmic SQL?!?</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/controlling-stored-procedure-caching-with-dyanmic-sql.aspx</link><pubDate>Thu, 13 Jul 2006 01:19:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:80</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>3</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/80.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=80</wfw:commentRss><description>&lt;p&gt;Tell me if this situation sends a chill down your spine: You've written
a stored procedure, tested it against a variety of inputs, and finally
rolled it out in production. All is well... Or so you think. You start
getting complaints from some users that it's taking forever to return.
But other users are having no problem. What the..?
&lt;/p&gt;&lt;p&gt;Veteran DBAs will know right away what's going on (even without
reading the title of this post!) -- but for those of you who haven't
had the &lt;i&gt;pleasure&lt;/i&gt;
of debugging these kinds of things, the answer is that cached execution
plans are not always as wonderful for performance as we might like.
&lt;/p&gt;&lt;p&gt;For any given query, there are numerous possible execution
plans that the query optimizer can come up with. Some of them are
optimal, some are less than optimal. But in the end, it's the job of
the query optimizer to decide which one to use (hopefully, the optimal
one). If a stored procedure is executed and its does not have a query
plan in cache, whatever execution plan the optimizer decides to use
will be cached for next time. This is usually a good thing -- it can be
quite a bit of work for the optimizer to make that decision.
&lt;/p&gt;&lt;p&gt;But in some cases, this is where the trouble begins. One of the
main factors the optimizer uses is index statistics vs. what parameters
are being used for the query. This can greatly affect what the
'correct' execution plan is -- the optimizer must decide such things as
which index should be used, whether a seek or a scan should be
performed, what types of joins are most efficient, etc. But as
parameters change, so can the most appropriate choices.
&lt;/p&gt;&lt;p&gt;
To illustrate this better, some sample data will be useful.  Break out your &lt;a href="http://sqljunkies.com/WebLog/amachanic/articles/NumbersTable.aspx" target="#"&gt;numbers table&lt;/a&gt; and run the following script, which will create a table with three columns, around 20 million rows, and a couple of indexes...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT &lt;br&gt;	Number, &lt;br&gt;	DATEADD(ss, Number, 0) AS TheDate&lt;br&gt;INTO DateTbl&lt;br&gt;FROM Numbers&lt;br&gt;&lt;br&gt;DECLARE &lt;br&gt;	@Num INT,&lt;br&gt;	@Incr INT&lt;br&gt;&lt;br&gt;SELECT &lt;br&gt;	@Num = MAX(Number) + 1,&lt;br&gt;	@Incr = MAX(Number) + 1&lt;br&gt;FROM DateTbl&lt;br&gt;&lt;br&gt;WHILE @Num &amp;lt; 20000000&lt;br&gt;BEGIN&lt;br&gt;	INSERT DateTbl (Number, TheDate)&lt;br&gt;	SELECT Number + @Num, DATEADD(ss, Number + @Num, 0) AS TheDate&lt;br&gt;	FROM Numbers&lt;br&gt;&lt;br&gt;	SET @Num = @Num + @Incr&lt;br&gt;END&lt;br&gt;&lt;br&gt;&lt;br&gt;CREATE UNIQUE CLUSTERED INDEX IX_Date ON DateTbl(TheDate)&lt;br&gt;&lt;br&gt;CREATE UNIQUE NONCLUSTERED INDEX IX_Number ON DateTbl(Number)&lt;br&gt;&lt;br&gt;ALTER TABLE DateTbl &lt;br&gt;ADD AnotherCol VARCHAR(40) NULL&lt;br&gt;&lt;br&gt;UPDATE DateTbl&lt;br&gt;SET AnotherCol = CONVERT(VARCHAR, Number) + CONVERT(VARCHAR, TheDate)&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;Okay! Now that your hard drive's workout is done, let's take a look
at what we have... DateTbl has three columns: A sequential number, a
datetime column, and a character column. You should have one row for
every second between January 1, 1900 and sometime around August 21,
1900, depending on how big your numbers table is. The date column and
the number column are indexed (we'll be using those as predicates in
the WHERE clause of the example queries), but the character column is
not. That's on purpose, to force a bookmark lookup. What can I say --
this is a totally contrived example!
&lt;/p&gt;&lt;p&gt;
Put Query Analyzer into Show Execution Plan mode and check out the following:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT &lt;br&gt;	Number,&lt;br&gt;	TheDate,&lt;br&gt;	AnotherCol&lt;br&gt;FROM DateTbl&lt;br&gt;WHERE TheDate BETWEEN '19000201 09:35:00' AND '19000201 09:36:00'&lt;br&gt;	OR Number = 10&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;Before I proceed, I would just like to say that anyone who comments
or e-mails me saying that this query can be re-written with a UNION to
get consistently better execution plans will be slapped upside the head
with a trout. YES, this is a bad query, but as I said, this is a very
simple example. In real life, these situations are usually much more
difficult to re-write. So if you don't like my example, go write your
own article!
&lt;/p&gt;&lt;p&gt;
... Now that that's taken care of ...
&lt;/p&gt;&lt;p&gt;The execution plan you should see will have a seek on each
index. Makes sense -- we're looking at a very small chunk of data in
each place. But what if we change the query to use a much larger date
range?
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT &lt;br&gt;	Number,&lt;br&gt;	TheDate,&lt;br&gt;	AnotherCol&lt;br&gt;FROM DateTbl&lt;br&gt;WHERE TheDate BETWEEN '19000101' AND '19000201'&lt;br&gt;	OR Number = 10&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;A seek on the date index no longer makes sense. A range scan is a
better option. And why bother seeking on the Number column? The row
with the number 10 is already found within the selected range. SQL
Server agrees with me on this, and performs only a scan of the
clustered date index.
&lt;/p&gt;&lt;p&gt;
But now let's see what happens when we throw this into a stored procedure.  Create the following:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE PROCEDURE GetStuff&lt;br&gt;	@StartDate DateTime,&lt;br&gt;	@EndDate DateTime,&lt;br&gt;	@Number INT&lt;br&gt;AS&lt;br&gt;	SELECT &lt;br&gt;		Number,&lt;br&gt;		TheDate,&lt;br&gt;		AnotherCol&lt;br&gt;	FROM DateTbl&lt;br&gt;	WHERE TheDate BETWEEN @StartDate AND @EndDate&lt;br&gt;		OR Number = @Number&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
... And run the following in Query Analyzer with Show Execution Plan turned on ...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;EXEC GetStuff '19000201 09:35:00', '19000201 09:36:00', 10&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Same execution plan as before!  That's great, right?  Well...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;EXEC GetStuff '19000101', '19000201', 10&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
... ... ...
&lt;/p&gt;&lt;p&gt;
This is taking a while ...
&lt;/p&gt;&lt;p&gt;
Enjoy the break?  Good, now get back to work!  
&lt;/p&gt;&lt;p&gt;Check out the execution plan. I guess the cached one wasn't
optimal for the second query. So how do we satisfy BOTH sets of
arguments?
&lt;/p&gt;&lt;p&gt;
One way is to force the stored procedure to recompile each time:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;ALTER PROCEDURE GetStuff&lt;br&gt;	@StartDate DateTime,&lt;br&gt;	@EndDate DateTime,&lt;br&gt;	@Number INT&lt;br&gt;WITH RECOMPILE&lt;br&gt;AS&lt;br&gt;	SELECT &lt;br&gt;		Number,&lt;br&gt;		TheDate,&lt;br&gt;		AnotherCol&lt;br&gt;	FROM DateTbl&lt;br&gt;	WHERE TheDate BETWEEN @StartDate AND @EndDate&lt;br&gt;		OR Number = @Number&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
You'll notice that I added &lt;b&gt;WITH RECOMPILE&lt;/b&gt;. And while that's
probably not a big deal for this example stored procedure, it isn't a
good idea for the types of really complex stored procedures where these
problems crop up in the real world. Recompilation can be quite
intensive, and I really don't want it happening every time an active
stored procedure is called.
&lt;/p&gt;&lt;p&gt;But you already knew that wasn't the solution, because in
elementary school you were taught how to read, and the title of this
article isn't "Controlling Stored Procedure Caching with ... WITH
RECOMPILE".
&lt;/p&gt;&lt;p&gt;
No, instead the title is, "Controlling Stored Procedure Caching with ... Dyanmic SQL?!?"
&lt;/p&gt;&lt;p&gt;
Yes, dynamic SQL.  If you don't know about dynamic SQL, go read &lt;a href="http://www.sommarskog.se/dynamic_sql.html" target="#"&gt;this article&lt;/a&gt; right now and come back when you're finished.
&lt;/p&gt;&lt;p&gt;
You may have heard about a system stored procedure called &lt;b&gt;sp_executesql&lt;/b&gt;.
It lets you evaluate dynamic SQL, but it happens to also cache its
execution plan. In addition, due to the fact that it accepts
parameters, it makes SQL injection nearly impossible if correctly used.
So it's good stuff. We could evaluate our test query using
sp_executesql like this:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;DECLARE @SQL NVARCHAR(300)&lt;br&gt;&lt;br&gt;SET @SQL = '' +&lt;br&gt;	'SELECT ' +&lt;br&gt;		'Number, ' +&lt;br&gt;		'TheDate, ' +&lt;br&gt;		'AnotherCol ' +&lt;br&gt;	'FROM DateTbl ' +&lt;br&gt;	'WHERE TheDate BETWEEN @StartDate AND @EndDate ' +&lt;br&gt;		'OR Number = @Number'&lt;br&gt;&lt;br&gt;EXEC sp_executesql &lt;br&gt;	@SQL, &lt;br&gt;	N'@StartDate DATETIME, @EndDate DATETIME, @Number INT', &lt;br&gt;	@StartDate = '19000201 09:35:00',&lt;br&gt;	@EndDate = '19000201 09:36:00', &lt;br&gt;	@Number = 10&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
... And that's just wonderful, but it gives us &lt;i&gt;absolutely nothing&lt;/i&gt;, because if you re-run it with the other parameters you'll find that you have the same problem as the stored procedure version:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;DECLARE @SQL NVARCHAR(300)&lt;br&gt;&lt;br&gt;SET @SQL = '' +&lt;br&gt;	'SELECT ' +&lt;br&gt;		'Number, ' +&lt;br&gt;		'TheDate, ' +&lt;br&gt;		'AnotherCol ' +&lt;br&gt;	'FROM DateTbl ' +&lt;br&gt;	'WHERE TheDate BETWEEN @StartDate AND @EndDate ' +&lt;br&gt;		'OR Number = @Number'&lt;br&gt;&lt;br&gt;EXEC sp_executesql &lt;br&gt;	@SQL, &lt;br&gt;	N'@StartDate DATETIME, @EndDate DATETIME, @Number INT', &lt;br&gt;	@StartDate = '19000101',&lt;br&gt;	@EndDate = '19000201', &lt;br&gt;	@Number = 10&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Time for another coffee break...
&lt;/p&gt;&lt;p&gt;But let us not lose hope yet, because we're still in the article
that's talking about how to control caching and recompilation and you
know that I wouldn't have written this article if I didn't know the
answer.
&lt;/p&gt;&lt;p&gt;
So what's &lt;i&gt;really&lt;/i&gt; being cached here?  Let's take a look:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT sql&lt;br&gt;FROM master..syscacheobjects&lt;br&gt;WHERE sql LIKE '%datetbl%'&lt;br&gt;	AND cacheobjtype = 'Executable Plan'&lt;br&gt;&lt;br&gt;-----------------------------------------------------&lt;br&gt;&lt;br&gt;(@StartDate DATETIME, @EndDate DATETIME, @Number INT)&lt;br&gt;SELECT &lt;br&gt;	Number, &lt;br&gt;	TheDate, &lt;br&gt;	AnotherCol &lt;br&gt;FROM DateTbl &lt;br&gt;WHERE TheDate BETWEEN @StartDate &lt;br&gt;	AND @EndDate OR Number = @Number&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;The cached plan is cached for not just the query, but also a
parameter list -- and not just any parameter list, but the very
parameter list that was passed in to sp_executesql. So how could we
force SQL Server to cache a different plan for the same query?
&lt;/p&gt;&lt;p&gt;
... Change the parameter list!
&lt;/p&gt;&lt;p&gt;The parameter list, of course, is correlated to the actual
parameters passed in. But what you may not realize is that if you
satisfy a parameter within the list, sp_executesql will not expect a
correlated parameter to be passed in. For instance, the following is
perfectly valid:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;DECLARE @SQL NVARCHAR(300)&lt;br&gt;&lt;br&gt;SET @SQL = '' +&lt;br&gt;	'SELECT @TheParam AS TheParam'&lt;br&gt;&lt;br&gt;EXEC sp_executesql &lt;br&gt;	@SQL, &lt;br&gt;	N'@TheParam VARCHAR(100) = ''This is the param'''&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Not only that, but it's been cached:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT sql&lt;br&gt;FROM master..syscacheobjects&lt;br&gt;WHERE sql LIKE '%param%'&lt;br&gt;	AND cacheobjtype = 'Executable Plan'&lt;br&gt;&lt;br&gt;-----------------------------------------------------&lt;br&gt;&lt;br&gt;(@TheParam VARCHAR(100) = 'This is the param')&lt;br&gt;SELECT @TheParam AS TheParam&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
So what happens if we change our parameter's value?
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;DECLARE @SQL NVARCHAR(300)&lt;br&gt;&lt;br&gt;SET @SQL = '' +&lt;br&gt;	'SELECT @TheParam AS TheParam'&lt;br&gt;&lt;br&gt;EXEC sp_executesql &lt;br&gt;	@SQL, &lt;br&gt;	N'@TheParam VARCHAR(100) = ''This is the other_param'''&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Same query, but...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT sql&lt;br&gt;FROM master..syscacheobjects&lt;br&gt;WHERE sql LIKE '%other_param%'&lt;br&gt;	AND cacheobjtype = 'Executable Plan'&lt;br&gt;&lt;br&gt;-----------------------------------------------------&lt;br&gt;&lt;br&gt;(@TheParam VARCHAR(100) = 'This is the other_param')&lt;br&gt;SELECT @TheParam AS TheParam&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;Yes, a second cached execution plan! Exciting, isn't it? Kind of
like winning the lottery, only even better, because you don't have to
worry about how to spend all of that extra cash!
&lt;/p&gt;&lt;p&gt;So how do we put this all together? A quick recap: We know that
the query requires at least two execution plans; one for big date
ranges, and one for smaller date ranges. There might be more, but we
haven't tested that, so I'll leave it as an exercise for the reader. We
also know that sp_executesql will cache a second, third, or &lt;i&gt;Nth&lt;/i&gt;
execution plan whenever the parameter list is changed. So all we need
to do is change the parameter list depending on the inputs...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;ALTER PROC GetStuff&lt;br&gt;	@StartDate DATETIME,&lt;br&gt;	@EndDate DATETIME,&lt;br&gt;	@Number INT&lt;br&gt;AS&lt;br&gt;	DECLARE @SQL NVARCHAR(300)&lt;br&gt;	DECLARE @Params NVARCHAR(100)&lt;br&gt;&lt;br&gt;	SET @SQL = '' +&lt;br&gt;		'SELECT ' +&lt;br&gt;			'Number, ' +&lt;br&gt;			'TheDate, ' +&lt;br&gt;			'AnotherCol ' +&lt;br&gt;		'FROM DateTbl ' +&lt;br&gt;		'WHERE TheDate BETWEEN @StartDate AND @EndDate ' +&lt;br&gt;			'OR Number = @Number'&lt;br&gt;&lt;br&gt;	IF DATEDIFF(hh, @StartDate, @EndDate) &amp;lt;= 2&lt;br&gt;		SET @Params = '@StartDate DATETIME, @EndDate DATETIME, @Number INT, @dX1 INT = 1'&lt;br&gt;	ELSE&lt;br&gt;		SET @Params = '@StartDate DATETIME, @EndDate DATETIME, @Number INT, @dX1 INT = 2'&lt;br&gt;&lt;br&gt;	EXEC sp_executesql &lt;br&gt;		@SQL, &lt;br&gt;		@Params, &lt;br&gt;		@StartDate, &lt;br&gt;		@EndDate, &lt;br&gt;		@Number&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;Pretending that we've actually tested for the correct thresholds
(which you should do if you use this technique), you'll notice that
we're forcing a different execution plan if the time between start date
and end date is less than or equal to two hours (that will be an index
seek) or more than two hours (that will be an index scan).
&lt;/p&gt;&lt;p&gt;Since forcing evaluation of a new execution plan in this case
is simply a matter of changing the value of @dX1, you can add as many
conditions as necessary to control which cached plan is used for any
given set of arguments. Two hours is almost certainly not the best
choice here, but really, does it matter?
&lt;/p&gt;&lt;p&gt;So in conclusion, blah, blah, blah... No one reads this far,
you stopped after you saw the final stored procedure, didn't you? Have
a nice day, and enjoy your new, more dynamic stored procedures.&lt;/p&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=80" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Performance/default.aspx">Performance</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Optimization/default.aspx">Optimization</category></item></channel></rss>