<?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 : Query Tuning, Optimization</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Query+Tuning/Optimization/default.aspx</link><description>Tags: Query Tuning, 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>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>25</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></channel></rss>