<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://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>Search results matching tags 'performance' and 'Query Tuning'</title><link>http://sqlblog.com/search/SearchResults.aspx?o=DateDescending&amp;tag=performance,Query+Tuning&amp;orTags=0</link><description>Search results matching tags 'performance' and 'Query Tuning'</description><dc:language>en-US</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><item><title>Query Tuning Mastery at PASS Summit 2012: The Video</title><link>http://sqlblog.com/blogs/adam_machanic/archive/2012/11/13/query-tuning-mastery-at-pass-summit-2012-the-video.aspx</link><pubDate>Tue, 13 Nov 2012 14:49:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:46135</guid><dc:creator>Adam Machanic</dc:creator><description>&lt;p&gt;An especially clever community member was kind enough to reverse-engineer the video stream for me, and came up with a &lt;b&gt;direct link to the PASS TV video stream&lt;/b&gt; for my &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2012/11/12/query-tuning-mastery-at-pass-summit-2012-the-demos.aspx"&gt;Query Tuning Mastery: The Art and Science of Manhandling Parallelism&lt;/a&gt; talk, delivered at the PASS Summit last Thursday. I'm &lt;b&gt;not sure how long this link will work&lt;/b&gt;, but I'd like to share it for my readers who were unable to see it in person or live on the stream.&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;a href="http://pass.bethereglobal.com/demand/day2p1.mp4"&gt;Start here.&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Skip past the keynote, to the 149 minute mark.&lt;/b&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;Enjoy!&lt;br&gt;&lt;/p&gt;</description></item><item><title>Query Tuning Mastery at PASS Summit 2012: The Demos</title><link>http://sqlblog.com/blogs/adam_machanic/archive/2012/11/11/query-tuning-mastery-at-pass-summit-2012-the-demos.aspx</link><pubDate>Mon, 12 Nov 2012 04:00:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:46095</guid><dc:creator>Adam Machanic</dc:creator><description>&lt;p&gt;For the second year in a row, I was asked to deliver a &lt;b&gt;500-level "Query Tuning Mastery"&lt;/b&gt; talk in room 6E of the Washington State Convention Center, for the PASS Summit. (&lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2011/10/16/pass-summit-2011-zen-and-the-art-of-workspace-memory-demos.aspx"&gt;Here's some information about last year's talk, on workspace memory.&lt;/a&gt;) And for the second year in a row, I had to deliver said talk at 10:15 in the morning, in a room used as overflow for the keynote, following a keynote speaker that didn't stop speaking on time. Frustrating!&lt;/p&gt;&lt;p&gt;Last Thursday, after very, very quickly setting up and getting sound and video checks, the rest of the talk went surprisingly smoothly. My deck--a brand new version created specifically for PASS--helped me get across the message I wanted to communicate, my demos ran without any failure, and my jokes didn't drive too many people out of the room before the end of the talk. &lt;b&gt;I even received a round of applause when I managed to take a 26 minute query plan and, using a few query rewrites, deliver the same exact data in 9 seconds&lt;/b&gt;. That, I have to say, was pretty cool.&lt;/p&gt;&lt;p&gt;Here's the abstract for the session:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;b&gt;Query Tuning Mastery: The Art and Science of Manhandling Parallelism&lt;/b&gt; &lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;As a database developer, your job boils down to one word: 
performance. In today's multi-core-driven world, query performance is 
very much determined by how well you're taking advantage of the 
processing power at your disposal. Are your big queries using every 
clock tick, or are they lagging behind? And if your queries are already 
parallel, can they be rewritten for even greater speed?
&lt;br&gt;
&lt;br&gt;In this session, you'll learn to take full advantage of SQL Server 
query parallelism. After a terminology review and technology refresher, 
the session will go deep, covering T-SQL patterns that allow certain 
queries to scale almost linearly across your multi-core CPUs. You'll see
 when and why the optimizer makes a parallel plan choice and how to 
impact the decision. Along the way, you’ll manipulate costs and row 
goals, challenge generally accepted tuning practices, and take complete 
control of your parallel queries.&lt;/span&gt; &lt;br&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Since the talk was being broadcast live on "PASS TV," I had &lt;a href="http://sqlblog.com/blogs/paul_white/"&gt;Paul White&lt;/a&gt; join me at the front of the room to moderate questions delivered via Twitter. This worked out reasonably well and I hope to do something similar in the future. &lt;b&gt;Huge thanks to Paul for helping out -- and for giving me a really ugly scowl when one of my jokes fell totally flat&lt;/b&gt;.&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Demos for the talk are attached.&lt;/b&gt; Let me know if you have any questions.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Thanks again to everyone who watched, either in person or at home. I had a blast. Hope you enjoyed it even half as much as I did!&lt;/b&gt; &lt;br&gt;&lt;/p&gt;</description></item><item><title>PASS Summit 2011 - Zen and the Art of Workspace Memory - Demos</title><link>http://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><description>&lt;p&gt;&lt;b&gt;What a rush&lt;/b&gt;. 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 and I had to reboot it two minutes into the talk--I found my rhythm and the entire 90 minutes went by in a flash. I wish I'd been able to take 90 more!&lt;/p&gt;&lt;p&gt;The scene? &lt;span style="font-weight:bold;"&gt;PASS Summit 2011&lt;/span&gt;. Friday, October 14, 10:15 a.m. (Room 6E, to be exact.) The last day of one of the best PASS Summits I've had the pleasure of attending.&lt;/p&gt;&lt;p&gt;The topic? A fairly obscure area of SQL Server, called &lt;b&gt;workspace memory&lt;/b&gt;. Here's the abstract for the talk:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;b&gt;Query Tuning Mastery: Zen and the Art of Workspace Memory&lt;/b&gt;&lt;/p&gt;&lt;p&gt;As SQL Server professionals, we often think of memory in vague, instance-level terms: buffer pool, procedure cache, Virtual Address Space, and so on. But certain tasks require a more in-depth focus, and query tuning is one of them. Large, complex queries need memory in which to work--workspace memory--and understanding the how's, when's, and why's of this memory can help you create queries that run in seconds rather than minutes. This session will teach you how to guide the query processor to grant enough memory for top performance, while also keeping things balanced for the sake of concurrency. You will learn advanced monitoring techniques, expert-level application of specialized query hints, and the memory internals needed to put it all together. If you work with large queries and are serious about achieving scalability and consistently great performance, you owe it to yourself to attend this session. &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;If you were in the audience,&lt;b&gt; I thank you for choosing my session &lt;/b&gt;over the many others that were running concurrently. I had a great time, and I hope you did too.&lt;/p&gt;&lt;p&gt;The &lt;b&gt;demos &lt;/b&gt;for the talk are attached to this post. Apologies, but I am not sharing the deck at this time as I'm going to be integrating it into a larger course that I hope to start delivering next year. (Through &lt;a href="http://dataeducation.com/"&gt;Data Education&lt;/a&gt;, naturally!)&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Enjoy!&lt;/b&gt; And as always, let me know in the comments if you have any questions.&lt;br&gt;&lt;/p&gt;</description></item><item><title>Automating dm_exec_query_stats and dm_db_index_usage_stats analysis</title><link>http://sqlblog.com/blogs/joe_chang/archive/2009/06/22/automating-dm-exec-query-stats-and-dm-db-index-usage-stats-analysis.aspx</link><pubDate>Mon, 22 Jun 2009 04:25:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:14815</guid><dc:creator>jchang</dc:creator><description>&lt;P&gt;Everyone should know by now how really useful the DMVs dm_exec_query_stats, and dm_db_index_usage_stats and the associated DMFs for obtaining the SQL and XML plan: dm_exec_sql_text, dm_exec_query_plan, and dm_exec_text_query_plan. Of course it has been explained that dm_exec_query_stats is not a replacement for SQL Server Profiler and SQL Trace. The DMV is only reliable if execution plans are not frequently evicted from the procedure cache and if SQL is not frequently recompiled. &lt;/P&gt;
&lt;P&gt;Still the DMV dm_exec_query_stats is popular because the results are available with a simple query. The complexity of setting up a trace, making sure it is not adversely impacting server performance, making sure space is available, and most of all, the effort of parsing the trace are all avoided.&lt;/P&gt;
&lt;P&gt;So what do we do next? From the top query statistics we can start looking at the SQL and the execution plans. One minor annoyance is that if we use CROSS APPLY to dm_exec_query_plan, each XML plan has to be saved individually. This can be annoying if one has to remote into to a PC in the server room and this connection is slow. &lt;BR&gt;There is much information in the XML plan that would be helpful if we could see it in a grid along with the execution statistics. &lt;/P&gt;
&lt;P&gt;From the index usage statistics, we can eliminate unused indexes. There might also be indexes that are infrequently used. Sometimes it is obvious that certain indexes can be consolidated. Other times, it is necessary to examine the execution plan to determine if another index is sufficient. The XML plan analysis cross-references in which plans each index is used. All of this is tedious work which can be automated. So this is the purpose of the SQLExecStats tool.&lt;/P&gt;
&lt;P&gt;A few people have tried it on production systems. There are still bugs, and feature requests, but hopefully I can turn this into something useful over time. &lt;/P&gt;
&lt;P&gt;lets use this url for the latest build rather than changing the url for each build&lt;/P&gt;
&lt;P&gt;&lt;A href="http://www.qdpma.com/tools/ExecStats.zip"&gt;http://www.qdpma.com/tools/ExecStats.zip&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;The above build is now 20090628. The (poor excuse for) documention is also online &lt;A href="http://www.qdpma.com/SQLExecStats.html"&gt;http://www.qdpma.com/SQLExecStats.html&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;Feedback is welcome, in particular, send me the output files so I can try to figure out if this is collecting the right information.&amp;nbsp;Support will be&amp;nbsp;based on&amp;nbsp;availability. &lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Note&lt;/STRONG&gt;: SQL Server 2008 sp1 fix now correctly reports parallel execution plan total_worker_time (CPU). It is not fixed on SQL Server 2005 sp3, latest cu?&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Limitations: &lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;European Localization &lt;/STRONG&gt;The XML plan is stored as a string, with numbers in US-en format. My program&amp;nbsp;extracts plan cost and estimated row numbers from the XML plan. The previous version did not specify any localization, but I think the default is the host environment. Hence the code double.Format(value) is interpreted as whatever the local environment is instead of US-en. The builds since 20090623&amp;nbsp;specifies the US-en format in most places. &lt;/P&gt;
&lt;P&gt;If some one with European number formats could run this program and send me the output, I would appreciate it.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Bug fixes, features etc&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;2009-06-22: The&amp;nbsp;password now should display *,&amp;nbsp;I don't do security work, and it shows. Some formatting was changed to right align, comma thousands separator, Very large numbers in&amp;nbsp;Excel&amp;nbsp;cannot be changed, 4 bytes integers are represented as numbers, larger numbers are represented as text.&lt;BR&gt;I will do more formatting changes later&lt;/P&gt;
&lt;DIV&gt;&lt;STRONG&gt;2009-06-28&lt;/STRONG&gt;&lt;/DIV&gt;
&lt;DIV&gt;The error described by Zen occurs with case sensitive collations. Apparently&amp;nbsp;one SQL query I issued from the executable was not a case correct with the columns I declared on the receiving side, and this is sufficient to cause the data table load to fail.&lt;/DIV&gt;
&lt;DIV&gt;The current build is really an intermediate build. I am adding the ability to collect table and index information for multiple databases and not everything is consistent yet.&lt;/DIV&gt;
&lt;DIV&gt;Be careful on trying to collect for too many databases, and too many execution plans, there are checks for over size yet.&lt;/DIV&gt;
&lt;DIV&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV&gt;&lt;STRONG&gt;2009-07-09&lt;/STRONG&gt;&lt;/DIV&gt;
&lt;DIV&gt;I am still in the middle of changing the tool to have the option of collecting table and index info from all databases. So some of the UI is not entirely consistent. However, all bug fixes are in the new version.&lt;/DIV&gt;
&lt;DIV&gt;Previously there were SQL queries that were not case correct with client-side table definitions, and when the default collation is case sensitive, an error occurred. This should be corrected.&lt;/DIV&gt;
&lt;DIV&gt;Also, previously I issued DBCC SHOW_STATISTICS([schema.table],[stat]) &lt;/DIV&gt;
&lt;DIV&gt;which&amp;nbsp;can generate an error if there is a hyphen&amp;nbsp;'-'&amp;nbsp;in the table or schema. I changed this to&lt;/DIV&gt;
&lt;DIV&gt;&amp;nbsp;('schema.table',[stat])&lt;BR&gt;I am not sure if this is the most safe format, should it be: ('schema.table','stat')?&lt;BR&gt;anyways, go to the main url, &lt;A href="http://www.qdpma.com/"&gt;http://www.qdpma.com/&lt;/A&gt;&lt;/DIV&gt;
&lt;DIV&gt;look for &lt;A href="http://www.qdpma.com/tools/ExecStats_20090709.zip"&gt;SQL Exec Stats Build 2009-07-09&lt;/A&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV&gt;&amp;nbsp;&lt;A href="http://www.qdpma.com/tools/ExecStats_20090709.zip"&gt;http://www.qdpma.com/tools/ExecStats_20090709.zip&lt;/A&gt;&lt;/DIV&gt;</description></item><item><title>Why Logical IO is a poor performance metric</title><link>http://sqlblog.com/blogs/joe_chang/archive/2008/09/10/why-logical-io-is-a-poor-performance-metric.aspx</link><pubDate>Wed, 10 Sep 2008 16:08:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:8834</guid><dc:creator>jchang</dc:creator><description>&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;Many years ago, I attended a conference where a very highly respected Oracle performance expert advocated the use of logical IO count as a performance metric. The argument was that a logical IO is a fairly substantial operation relative to others. I suppose that detailed logical IO was already collected supported this. So why would logical IO be advocated over CPU? On the Windows side, many people have probably noticed that Profiler frequently shows 0 CPU for low cost queries. &lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;(The resolution of Profiler in SQL Server 2000 was some approximate integer multiple of 10 ms for the single processor kernel and 15.625 ms for multi-processor kernel. It does turn out that averaging many calls yields a reasonably accurate CPU measure, possibly to the range of 1 msec.)&lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;I am not sure what the situation is for Oracle, which runs on many platforms, UNIX, Windows and other OS. It cannot be easy collecting performance counters whose meaning is uniform across all the different platforms.&lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;o:p&gt;&lt;FONT face="Times New Roman" size=3&gt;&amp;nbsp;&lt;/FONT&gt;&lt;/o:p&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;Anyways, the argument was convincing enough that I thought it warranted investigation on the SQL Server side. By comparing logical IO and average CPU (measured in full saturation load tests where SQL Server is driven to near 100% CPU), it was quickly apparent that Logical IO and true cost had no meaningful relation.&lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;o:p&gt;&lt;FONT face="Times New Roman" size=3&gt;&amp;nbsp;&lt;/FONT&gt;&lt;/o:p&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;Consider a table for which the indexes have depth 4, so that there is a root level, 2 intermediate levels and the leaf level. The index seek for a single index key (that is, no key lookup) would generate 4 logical IO (LIO). Now consider if a key lookup is required. If the table has a clustered index also of depth 4, each key lookup generates 4 LIO. If the table were a heap, each key lookup generates 1 LIO. In actuality, the key lookup to a heap is about 20-30% less expensive than a key lookup to a clustered index, not anywhere close to the 4:1 LIO ratio. Now consider a loop join between two tables. The index seek to outer source generates 4-5 LIO, 4 for the index depth, and possibly one or more IO for additional rows that do not fit in the same leaf level page. Each index seek to the inner source generates 4 LIO as the index is traversed. For a loop join involving 100 rows in a one-to-one join, (no key lookups) there are 404 or so LIO. Now consider a hash or merge join for the same number of rows (for which there is an explicit SARG on each source). There are 4-5 LIO to each table, for a total of 8-10 LIO. In actuality, the cost between the 3 join types at this row count is not far different (the hash join has a higher startup cost than a loop join, but lower cost per incremental row), but the LIO ratio is 404:10! &lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;o:p&gt;&lt;FONT face="Times New Roman" size=3&gt;&amp;nbsp;&lt;/FONT&gt;&lt;/o:p&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;The SQL Server Cost Based Optimizer itself clearly does not use LIO for its cost estimation. Just compare an index seek for tables and indexes with different index depth. The cost is the same, which is approximately true. After assembling a very deep collection of SQL Server performance measurements, I went back to the Oracle expert to ask about the LIO argument. He said that in the intervening time, a huge amount of evidence on the Oracle side also indicated LIO is a poor metric and this had been abandoned.&lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;o:p&gt;&lt;FONT face="Times New Roman" size=3&gt;&amp;nbsp;&lt;/FONT&gt;&lt;/o:p&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;The SQL Server 2005 DMV dm_exec_query_stats keep milli-sec resolution CPU stats, and micro-sec in SQL Server 2008. I strongly suggest people abandon LIO as a performance metric. Use CPU (total_worker_time) for non-parallel execution plans. For parallel plans, the DMV reports 1000. So I suppose one must revert to Profiler traces for parallel execution plans. (If any one know how to get parallel plan CPU from DMV, please advise.)&lt;/FONT&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;o:p&gt;&lt;FONT face="Times New Roman" size=3&gt;&amp;nbsp;&lt;/FONT&gt;&lt;/o:p&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN:0in 0in 0pt;"&gt;&lt;FONT face="Times New Roman" size=3&gt;Note also, the other post indicating physical IO has a completely different cost structure than logical IO.&lt;/FONT&gt;&lt;/P&gt;</description></item><item><title>SQL Server Query Processing Puzzle: LIKE vs ?</title><link>http://sqlblog.com/blogs/adam_machanic/archive/2008/04/22/sql-server-query-processing-puzzle-like-vs.aspx</link><pubDate>Tue, 22 Apr 2008 14:18:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:6344</guid><dc:creator>Adam Machanic</dc:creator><description>&lt;p&gt;How creative are you with manipulating your queries to produce more efficient plans? Try the following puzzle and e-mail your solution to me at [&amp;lt;do_not_mail&amp;gt; @ do_not_mail.com].  Make sure to include an explanation of why it works, as well as your mailing address. The best two solutions/explanations win a free copy of &lt;a href="http://www.amazon.com/dp/159059729X"&gt;Expert SQL Server 2005 Development&lt;/a&gt;, a wonderful feeling of accomplishment, plus eternal fame and glory when I reveal your solutions here on the blog.

&lt;/p&gt;&lt;p&gt;Run the following T-SQL to create two tables in TempDB:
&lt;/p&gt;&lt;pre style="margin-left:40px;"&gt;USE TempDB&lt;br&gt;GO&lt;br&gt;&lt;br&gt;CREATE TABLE b1 (blat1 CHAR(5) NOT NULL)&lt;br&gt;CREATE TABLE b2 (blat2 VARCHAR(200) NOT NULL)&lt;br&gt;GO&lt;br&gt;&lt;br&gt;INSERT b1&lt;br&gt;SELECT LEFT(AddressLine1, 5) AS blat1&lt;br&gt;FROM AdventureWorks.Person.Address&lt;br&gt;&lt;br&gt;INSERT b2&lt;br&gt;SELECT AddressLine1 AS blat2&lt;br&gt;FROM AdventureWorks.Person.Address&lt;br&gt;GO&lt;/pre&gt;
Now consider the following query:
&lt;pre style="margin-left:40px;"&gt;SELECT *&lt;br&gt;FROM b1&lt;br&gt;JOIN b2 ON&lt;br&gt;    b2.blat2 LIKE b1.blat1 + '%'&lt;/pre&gt;&lt;p&gt;
This query takes around three minutes to run on my notebook, and does over 1.8 million logical reads.  Can you figure out a way to re-write it so that it performs better?  No modification of the base tables or addition of any other objects is allowed (sorry, no indexed views!) -- the challenge is to tune this by doing nothing more than re-writing the query.

&lt;/p&gt;&lt;p&gt;Good luck!  I'll leave the contest open for submissions until May 1.&lt;/p&gt;</description></item><item><title>Who's On First? Solving the Top per Group Problem (Part 1: Technique)</title><link>http://sqlblog.com/blogs/adam_machanic/archive/2008/02/08/who-s-on-first-solving-the-top-per-group-problem-part-1-technique.aspx</link><pubDate>Fri, 08 Feb 2008 23:09:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:4992</guid><dc:creator>Adam Machanic</dc:creator><description>&lt;p&gt;Relative comparison is a simple matter of human nature. From early childhood we compare and contrast what we see in the world around us, building a means by which to rate what we experience. And as it turns out, this desire to discover top and bottom, rightmost and leftmost, or best and worst happens to extend quite naturally into business scenarios. Which product is the top seller? How about the one that's simply not moving off the shelves? Which of our customers has placed the most expensive order? What are the most recent orders placed at each of our outlets?&lt;/p&gt;&lt;p&gt;In the world of common business questions, the edge cases are generally of most interest. What's in the middle is unimportant; it's often too difficult for the mind to compare and comprehend when there are hundreds, thousands, or even millions of items, transactions, or facts that are all within a similar range. Instead, we focus on those that stick out in some extraordinary way.&lt;/p&gt;&lt;p&gt;Those of us who work with SQL products on a regular basis are faced with solving this same problem time and again as we work through various business requirements. Over time, I have noticed four basic query patterns that can be used to solve the problem; each are logically equivalent (within certain restrictions -- more on that later), but can have surprisingly different performance characteristics depending on the data being queried. In this first post, I will outline the available patterns/methods. In the following posts, I will show the results of testing each pattern against a variety of scenarios in an attempt to discover where and when each should be used.&lt;/p&gt;&lt;p&gt;The four basic patterns are outlined below. Each of the methods is illustrated using a query to show all customers' names, plus their most recent order date, and the amount of that order. I've included notes that indicate where logic differences can arise among the various methods.&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Method 1: Join to full group and use correlated subquery with a MIN/MAX aggregate to filter&lt;/b&gt;&lt;/p&gt;&lt;p&gt;In this method we use an inner join to get all required columns, then filter the resultant set using a correlated subquery in the WHERE clause. &lt;br&gt;&lt;/p&gt;&lt;blockquote&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.FirstName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.LastName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderAmount&lt;br&gt;FROM Customers c&lt;br&gt;JOIN Orders o ON o.CustomerId = c.CustomerId&lt;br&gt;WHERE &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderDate&amp;nbsp; =&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT MAX(o1.OrderDate)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Orders o1&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o1.CustomerId = o.CustomerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;br&gt;&lt;/blockquote&gt;&lt;p&gt;Logic notes: With this method ties are automatically included in the output, unless a tiebreaker is specified (which can be tricky given that you only have one column to work with). This method does not allow you to pull back an arbitrary number of rows, such as top 10 per customer; you are limited to the edge and any ties that might exist. &lt;b&gt;&lt;/b&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Method 1a: Join to full group and use correlated subquery with TOP(n) and ORDER BY to filter&lt;/b&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;This method is almost identical to Method 1 (which is why it is classified here as 1a), but the TOP and ORDER BY allow for a bit more flexibility than the aggregates.&lt;br&gt;&lt;/p&gt;&lt;blockquote&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.FirstName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.LastName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderAmount&lt;br&gt;FROM Customers c&lt;br&gt;JOIN Orders o ON o.CustomerId = c.CustomerId&lt;br&gt;WHERE &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderDate&amp;nbsp; =&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT TOP(1)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o1.OrderDate&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Orders o1&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o1.CustomerId = o.CustomerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o1.OrderDate DESC&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;/blockquote&gt;&lt;p&gt;Logic notes: With this method you can more easily integrate a tiebreaker than with Method 1; the comparison column can be anything, including a primary key, and you can still order on whatever column makes most sense. In addition, you can take more rows than with Method 1 by using IN instead of = in the WHERE clause, and increasing the argument value to TOP.&lt;br&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Method 2: CROSS APPLY to ordered TOP(n)&lt;/b&gt;&lt;/p&gt;&lt;p&gt;In this method, SQL Server 2005's CROSS APPLY operator is used. This operator allows us to essentially create a table-valued correlated subquery -- something that impossible in previous versions of SQL Server. By using TOP in conjunction with ORDER BY we can get as many rows per group as needed.&lt;/p&gt;&lt;blockquote&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.FirstName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.LastName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.OrderAmount&lt;br&gt;FROM Customers c&lt;br&gt;CROSS APPLY&lt;br&gt;(&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT TOP(1)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderAmount&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Orders o&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.CustomerId = c.CustomerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderDate DESC&lt;br&gt;) x&lt;br&gt;&lt;/blockquote&gt;&lt;p&gt;Logic notes: This method is almost identical, from a logic point of view, with Method 1a modified to use IN on a primary key column. With both methods WITH TIES can be added to the TOP in order to get ties.&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Method 3: Join to derived table that uses a partitioned, ordered windowing function, and filter in the outer query based on the row number&lt;/b&gt;&lt;/p&gt;&lt;p&gt;In this method a derived table or CTE is used, in conjunction with a windowing function partitioned based on the required grain of the final query. So for the "most recent order per customer" query, the row number is partitioned based on the customer. This gives us a count starting at 1 for each customer, which can be filtered in the outer query.&lt;br&gt;&lt;/p&gt;&lt;blockquote&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.FirstName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.LastName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.OrderAmount&lt;br&gt;FROM Customers c&lt;br&gt;INNER 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;&amp;nbsp; o.OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.OrderAmount,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.CustomerId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ROW_NUMBER() OVER&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PARTITION BY o.CustomerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY o.OrderDate DESC&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ) AS r&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Orders o&lt;br&gt;) x ON&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.CustomerId = c.CustomerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND x.r = 1&lt;br&gt;&lt;/blockquote&gt;&lt;p&gt;Logic notes: If ties are important, use DENSE_RANK instead of ROW_NUMBER. ROW_NUMBER is good for arbitrary TOP(n), similar to Method 2. Unlike the previously described methods, in conjunction with DENSE_RANK this method can return an arbitrary TOP(n) rows, all of which can include ties. So if you would like to see the three most recent order dates and each happens to have multiple orders, this method will be able to return them all by simply filtering on x.r = 3. This would not be directly possible with any of the other methods described here.&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Method 4: "Carry-along sort"&lt;/b&gt;&lt;/p&gt;&lt;p&gt;This is the only "tricky" method, and not one that I recommend using, except as a last resort. I'm including it here only for completeness and comparison because it happens to be a very high performance method in some cases. This method involves converting each of the required inner columns into a string, concatenating them, then applying an aggregate to the string as a whole. By putting the "sort" column first, the other data is "carried along" -- thus the name for the method. The concatenated data is then "unpacked" in the outer query. This can be surprisingly efficient from an I/O standpoint, but the resultant code is a maintenance nightmare and it is quite easy to introduce errors. In addition, this method can only return the top 1 per group -- no ties or multiple return items are supported.&lt;br&gt;&lt;/p&gt;&lt;blockquote&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.FirstName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; c.LastName,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CONVERT(DATETIME, SUBSTRING(x.OrderInfo, 1, 8)) AS OrderDate,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CONVERT(MONEY, SUBSTRING(x.OrderInfo, 9, 15)) AS OrderAmount&lt;br&gt;FROM Customers c&lt;br&gt;INNER 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;&amp;nbsp; o.CustomerId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; MAX&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; CONVERT(CHAR(8), OrderDate, 112) +&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; CONVERT(CHAR(15), SubTotal)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ) OrderInfo&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Orders o&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; GROUP BY&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; o.CustomerId&lt;br&gt;) x ON&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; x.CustomerId = c.CustomerId&lt;br&gt;&lt;/blockquote&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;This post is just the beginning; watch this space in the coming days for a series of performance tests and analysis of these methods, and some results that I personally found to be quite surprising.&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description></item><item><title>Medians, ROW_NUMBERs, and performance</title><link>http://sqlblog.com/blogs/adam_machanic/archive/2006/12/18/medians-row-numbers-and-performance.aspx</link><pubDate>Mon, 18 Dec 2006 19:52:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:437</guid><dc:creator>Adam Machanic</dc:creator><description>A couple of days ago, Aaron Bertrand posted about &lt;a href="http://sqlblog.com/blogs/aaron_bertrand/archive/2006/12/15/428.aspx"&gt;a method for calculating medians in SQL Server 2005&lt;/a&gt; using the ROW_NUMBER function in conjunction with the COUNT aggregate. This method (credited to Itzik Ben-Gan) is interesting, but I discovered an even better way to attack the problem in &lt;a href="http://www.amazon.com/Celkos-Analytics-Kaufmann-Management-Systems/dp/0123695120/sr=8-1/qid=1166482464/ref=sr_1_1/105-6595410-7450029?ie=UTF8&amp;amp;s=books"&gt;Joe Celko's Analytics and OLAP in SQL&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Rather than using a COUNT aggregate in conjunction with the ROW_NUMBER function, Celko's method uses ROW_NUMBER twice: Once with an ascending sort, and again with a descending sort. The output rows can then be matched based on the ascending row number being within +/- 1 of the descending row number.&amp;nbsp; This becomes clearer with a couple of small examples:&lt;br&gt;&lt;br&gt;

&lt;table class="MsoTableGrid" style="border:medium none;border-collapse:collapse;" cellpadding="0" cellspacing="0"&gt;
 &lt;tr&gt;
  &lt;td style="border:1pt solid windowtext;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;A&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:solid solid solid none;border-color:windowtext windowtext windowtext -moz-use-text-color;border-width:1pt 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;1&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:solid solid solid none;border-color:windowtext windowtext windowtext -moz-use-text-color;border-width:1pt 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;4&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;B&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;2&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;3&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;C&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;3&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;2&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;D&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;4&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;1&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
&lt;/table&gt;

&lt;p class="MsoNormal"&gt;&lt;o:p&gt;&amp;nbsp;&lt;/o:p&gt;&lt;/p&gt;

&lt;table class="MsoTableGrid" style="border:medium none;border-collapse:collapse;" cellpadding="0" cellspacing="0"&gt;
 &lt;tr&gt;
  &lt;td style="border:1pt solid windowtext;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;A&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:solid solid solid none;border-color:windowtext windowtext windowtext -moz-use-text-color;border-width:1pt 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;1&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:solid solid solid none;border-color:windowtext windowtext windowtext -moz-use-text-color;border-width:1pt 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;5&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;B&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;2&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;4&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;C&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;3&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;3&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;"&gt;
  &lt;p class="MsoNormal"&gt;D&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;4&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;"&gt;
  &lt;p class="MsoNormal"&gt;2&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr style="height:14.35pt;"&gt;
  &lt;td style="border-style:none solid solid;border-color:-moz-use-text-color windowtext windowtext;border-width:medium 1pt 1pt;padding:0in 5.4pt;width:23.4pt;height:14.35pt;"&gt;
  &lt;p class="MsoNormal"&gt;E&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;height:14.35pt;"&gt;
  &lt;p class="MsoNormal"&gt;5&lt;/p&gt;
  &lt;/td&gt;
  &lt;td style="border-style:none solid solid none;border-color:-moz-use-text-color windowtext windowtext -moz-use-text-color;border-width:medium 1pt 1pt medium;padding:0in 5.4pt;width:27pt;height:14.35pt;"&gt;
  &lt;p class="MsoNormal"&gt;1&lt;/p&gt;
  &lt;/td&gt;
 &lt;/tr&gt;
&lt;/table&gt;

&lt;p class="MsoNormal"&gt;&lt;o:p&gt;&lt;br&gt;&lt;/o:p&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;o:p&gt;In the first table (even number of rows), the median rows are B and C. These can be matched based on [Ascending Column] IN ([Descending Column] + 1, [Descending Column] - 1). In the second table (odd number of rows), the median row is C, which is matched where [Ascending Column] = [Descending Column]. Note that in the second table, the match criteria &lt;/o:p&gt;for the first table does not apply -- so the generic expression to match either case is the combination of the two:&amp;nbsp; [Ascending Column] IN ([Descending Column], [Descending Column] + 1, [Descending Column] - 1).&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;We can apply this logic within the AdventureWorks database to find the median of the "TotalDue" amount in the Sales.SalesOrderHeader table, for each customer:&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;/p&gt;&lt;div class="code"&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp; CustomerId,&lt;br&gt;&amp;nbsp;&amp;nbsp; AVG(TotalDue)&lt;br&gt;FROM&lt;br&gt;(&lt;br&gt;&amp;nbsp;&amp;nbsp; SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; CustomerId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; TotalDue,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ROW_NUMBER() OVER (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PARTITION BY CustomerId &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY TotalDue ASC, SalesOrderId ASC) AS RowAsc,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ROW_NUMBER() OVER (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PARTITION BY CustomerId &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY TotalDue DESC, SalesOrderId DESC) AS RowDesc&lt;br&gt;&amp;nbsp;&amp;nbsp; FROM Sales.SalesOrderHeader SOH&lt;br&gt;) x&lt;br&gt;WHERE &lt;br&gt;&amp;nbsp;&amp;nbsp; RowAsc IN (RowDesc, RowDesc - 1, RowDesc + 1)&lt;br&gt;GROUP BY CustomerId&lt;br&gt;ORDER BY CustomerId;&lt;/div&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;The equivalent logic using Itzik Ben-Gan's method follows:&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;div class="code"&gt;&lt;br&gt;SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp; CustomerId,&lt;br&gt;&amp;nbsp;&amp;nbsp; AVG(TotalDue)&lt;br&gt;FROM&lt;br&gt;(&lt;br&gt;&amp;nbsp;&amp;nbsp; SELECT&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; CustomerId,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; TotalDue,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ROW_NUMBER() OVER (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PARTITION BY CustomerId&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ORDER BY TotalDue) AS RowNum,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; COUNT(*) OVER (&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PARTITION BY CustomerId) AS RowCnt&lt;br&gt;&amp;nbsp;&amp;nbsp; FROM Sales.SalesOrderHeader&lt;br&gt;) x&lt;br&gt;WHERE&lt;br&gt;&amp;nbsp;&amp;nbsp; RowNum IN ((RowCnt + 1) / 2, (RowCnt + 2) / 2)&lt;br&gt;GROUP BY CustomerId&lt;br&gt;ORDER BY CustomerId;&lt;/div&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;Taking a look at the estimated execution plans for these two queries, we might believe that Ben-Gan's method is superior: Celko's algorithm requires an expensive intermediate sort operation and has an estimated cost of 4.96, compared to 3.96 for Ben-Gan's. &lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;Remember that these are merely estimates. And as it turns out, this is one of those times that the Query Optimizer's cost estimates are are totally out of line with the reality of what
happens when you actually run the queries. Although the performance
difference is not especially noticeable on a set of data as small as
that in Sales.SalesOrderHeader, check out the STATISTICS IO output. Celko's version does 703 logical reads; Ben-Gan's does an astonishing 140110!&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;&lt;br&gt;&lt;/p&gt;&lt;p class="MsoNormal"&gt;There is a good lesson to be learned from this: &lt;i&gt;Cost-based optimization is far from perfect!&lt;/i&gt; Never completely trust what estimates tell you; they've come a long way, but clearly there is still some work to do in this area. The only way to actually determine that one query is better than another is to run it against a realistic set of data and look at how much IO and CPU time is actually used.&lt;br&gt;&lt;/p&gt;&lt;br&gt;In this case, Ben-Gan's query probably should perform better than Celko's. It seems odd that the Query Processor can't collect the row counts at the same time it processes the row numbers. Regardless, as of today this is the best way to solve this problem... Not that I've ever needed a median in any production application I've worked on. But I suppose that's beside the point!&lt;br&gt;</description></item><item><title>Announcing SQLQueryStress: A simple query load tool</title><link>http://sqlblog.com/blogs/adam_machanic/archive/2006/10/21/announcing-sqlquerystress-a-simple-query-load-tool.aspx</link><pubDate>Sat, 21 Oct 2006 04:29:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:327</guid><dc:creator>Adam Machanic</dc:creator><description>On October 20, 2004 -- two years ago -- &lt;a href="http://www.sqljunkies.com/WebLog/amachanic/archive/2004/10/20/4699.aspx"&gt;I announced that I was considering writing my own query load testing tool&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Clearly,
my follow through on this promise has been incredibly delayed.&amp;nbsp; But all
is not lost, and I'm happy to report that I am true to my word.&amp;nbsp; As of
today, &lt;a href="http://www.datamanipulation.net/SQLQueryStress/"&gt;beta-1 of my new SQLQueryStress tool is available for download&lt;/a&gt;.&lt;br&gt;&lt;br&gt;SQLQueryStress
is a free tool that provides a simple means of load testing individual
queries.&amp;nbsp; It provides some support for query parameterization and
options for collection of I/O and CPU metrics.&amp;nbsp; It is not intended to
replace tools such as Visual Studio Team System's load tests, but
rather to be a simple and easy-to-use tool in the DBA or database
developer's kit. I think it lives up to that goal, and initial feedback
I've gotten from a few people I've shown it to has been positive.&amp;nbsp; I've
been working on this project on and off for around the last eight
months, so I'm quite excited to finally release it to the public!&lt;br&gt;&lt;br&gt;Please &lt;a href="http://www.datamanipulation.net/SQLQueryStress/"&gt;check it out&lt;/a&gt;
and provide me with any feedback you have: What works?&amp;nbsp; What doesn't
work?&amp;nbsp; What other features would you like to see?&amp;nbsp; I'm planning to do
quite a bit more with this tool, so I'm definitely interested in what
users think would be useful.&amp;nbsp; And of course, please let me know if you
find any bugs (but given the rigor with which I've tested this project,
there couldn't &lt;i&gt;possibly &lt;/i&gt;be any!)&lt;br&gt;&lt;br&gt;Thanks, and enjoy!&lt;br&gt;</description></item><item><title>Exploring the secrets of intermediate materialization</title><link>http://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><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;</description></item></channel></rss>