<?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 'Undocumented' and 'Seeks'</title><link>http://sqlblog.com/search/SearchResults.aspx?o=DateDescending&amp;tag=Undocumented,Seeks&amp;orTags=0</link><description>Search results matching tags 'Undocumented' and 'Seeks'</description><dc:language>en-US</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><item><title>SQL Server, Seeks, and Binary Search</title><link>http://sqlblog.com/blogs/paul_white/archive/2011/08/08/sql-server-seeks-and-binary-search.aspx</link><pubDate>Mon, 08 Aug 2011 20:10:27 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:37622</guid><dc:creator>Paul White</dc:creator><description>&lt;p&gt;The following table summarizes the results from my last &lt;a href="http://sqlblog.com/blogs/paul_white/archive/2011/08/04/avoiding-uniqueness-for-performance.aspx"&gt;two&lt;/a&gt; blog entries, showing the CPU time used when performing 5 million clustered index seeks:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/image_2AB2C931.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Clustered Index Seek Test Results" border="0" alt="Clustered Index Seek Test Results" src="http://sqlblog.com/blogs/paul_white/image_thumb_41FDCDA2.png" width="315" height="64" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;In test 1, making the clustered index unique improved performance by around 40%. In test 2, making the same change reduced performance by around 70% (on 64-bit systems – more on that later).&amp;#160; As a reminder, both tests use nested loops to join a single-column BIGINT table of numbers to itself:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/image_15749DBC.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Query and Plans" border="0" alt="Query and Plans" src="http://sqlblog.com/blogs/paul_white/image_thumb_52491F8E.png" width="647" height="453" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;In test 1, the table contains numbers from 1 to 5 million inclusive; for test 2, the table contains the even numbers from 2 to 10 million (still 5 million rows).&amp;#160; In case you were wondering, the test 2 results are the same if we populate the table with odd numbers from 1 to 9,999,999 instead of even numbers.&amp;#160; How can we explain these results?&lt;/p&gt;  &lt;h3&gt;Performing an Index Seek&lt;/h3&gt;  &lt;p&gt;Perhaps we need to look a bit deeper into how SQL Server performs an index seek.&amp;#160; Most people know that SQL Server indexes are a &lt;a href="http://en.wikipedia.org/wiki/B%2B_tree"&gt;B+ tree&lt;/a&gt;, so let’s work through an example of locating the row containing the value 1 million, when the table contains values from 1 to 5 million, with a unique clustered index.&lt;/p&gt;  &lt;p&gt;Index seeks start at the index root page, which we can find in a number of ways (e.g. &lt;a href="http://msdn.microsoft.com/en-us/library/ms189051.aspx"&gt;sys.system_internals_allocation_units&lt;/a&gt;).&amp;#160; I happened to use &lt;a href="http://blogs.msdn.com/b/sqlserverstorageengine/archive/2006/12/13/more-undocumented-fun_3a00_-dbcc-ind_2c00_-dbcc-page_2c00_-and-off_2d00_row-columns.aspx"&gt;DBCC IND&lt;/a&gt; to determine that the root of the index was page &lt;strong&gt;117482&lt;/strong&gt; in file 1 (in Denali, we can use a new DMF: &lt;a href="http://sqlgeek.pl/2011/07/26/pl-denali-ctp3-nowe-lepsze-dbcc-ind/"&gt;sys.dm_db_database_page_allocations&lt;/a&gt;).&amp;#160; Anyway, whichever way you find it, we can use &lt;a href="http://support.microsoft.com/kb/83065"&gt;DBCC PAGE&lt;/a&gt; to look at the contents of the root page:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/SNAGHTMLd8089b_0F1DA161.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Index Root Page Contents" border="0" alt="Index Root Page Contents" src="http://sqlblog.com/blogs/paul_white/SNAGHTMLd8089b_thumb_36946DCB.png" width="489" height="213" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;The col1 (key) column shows the lowest key value that can possibly be present in the lower-level page specified by Child Page Id.&amp;#160; We can see that the lowest possible key value on page 117485 is 906305, and the next lowest key value possible is 1132881 on page 117486.&amp;#160; So, if a row containing the value 1 million exists, the next page to look at is &lt;strong&gt;117485&lt;/strong&gt;:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/SNAGHTMLdb085d_41DD8208.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Level 1 Index Contents" border="0" alt="Level 1 Index Contents" src="http://sqlblog.com/blogs/paul_white/SNAGHTMLdb085d_thumb_383513D2.png" width="486" height="232" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;We are now at level 1 of the index, and have to scroll down the output a bit further to see that the next page to look at is &lt;strong&gt;119580&lt;/strong&gt;.&amp;#160; This page is at the leaf level of the index, and we find our target value at position 399:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/image_07525C67.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Leaf level index record" border="0" alt="Leaf level index record" src="http://sqlblog.com/blogs/paul_white/image_thumb_5AC92C80.png" width="429" height="72" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;h3&gt;Binary Search&lt;/h3&gt;  &lt;p&gt;When performing an index seek, SQL Server obviously doesn’t run DBCC PAGE and scroll down the output window to find the next page to look at like we just did.&amp;#160; You may know that SQL Server can use binary search to locate index entries, but it’s worth taking a closer look at the index pages to see how this works in practice:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/image_6B61276E.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="SQL Server Page Structure Diagram" border="0" alt="SQL Server Page Structure Diagram" src="http://sqlblog.com/blogs/paul_white/image_thumb_61B8B938.png" width="486" height="478" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;The slot array contains a series of two-byte offsets pointing to the index records.&amp;#160; The index records are not necessarily stored in any particular order on the page; only the slot array entries are guaranteed to be in sorted order.&amp;#160; Note that the sorted slot array entries do not contain the index keys themselves, just offsets.&lt;/p&gt;  &lt;h4&gt;Performance&lt;/h4&gt;  &lt;p&gt;When SQL Server uses binary search to locate an index entry, it starts by checking the middle element of the slot array (shown in red in the example above).&amp;#160; It then follows that offset pointer to the index record, and compares the value it finds there with the sought value.&amp;#160; Assuming the value is not the one we are looking for, the sorted nature of the slot array allows SQL Server to narrow the search range in half after this first comparison.&lt;/p&gt;  &lt;p&gt;If the red index record happens to contain a value &lt;em&gt;higher&lt;/em&gt; than the sought value, we know the value we are looking for is in that half of the slot array that sorts lower than the red entry.&amp;#160; This process of cutting the search space in half continues until we find the record we are looking for, or until it becomes clear that the value does not exist.&lt;/p&gt;  &lt;p&gt;The average and worst case performance of binary search is very close to log&lt;sub&gt;2&lt;/sub&gt;N comparisons to find the item of interest in a sorted list of N items.&amp;#160; For the 476 index entries found in our test example index structure at the leaf level, that means at most 9 comparison operations.&amp;#160; That compares well to a linear search of the sorted slot array, which might require as many as 476 comparisons if we are unlucky, 238 on average.&lt;/p&gt;  &lt;p&gt;None of this offers any particular insight into why a binary search into a unique indexes containing only even (or odd!) numbers should be so slow.&amp;#160; One of the advantages of binary search is that it expects to perform around the same amount of work, regardless of the distribution of the values in the index.&amp;#160; Luckily, binary search is not the only game in town.&lt;/p&gt;  &lt;h3&gt;Linear Interpolation&lt;/h3&gt;  &lt;p&gt;As efficient as binary search is in the general case, in some cases we can do better.&lt;/p&gt;  &lt;p&gt;When searching for the value 1 million, we know from level 1 of the index that the lowest key value that can appear on the leaf level page we are interested in is 999,601.&amp;#160; We also know that the lowest key value on the next page is 1,000,077.&amp;#160; From the header information on our target page, we also know there are 476 entries in the slot array on that page.&amp;#160; Given these facts, we can make an immediate guess at which slot the value 1,000,000 ought to appear in:&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;expected slot = (1000077 - 999601) / 476 * (1000000 - 999601) = 399&lt;/p&gt; &lt;/blockquote&gt;  &lt;p&gt;This simple calculation takes us immediately to slot 399.&amp;#160; As we saw earlier, slot 399 does indeed contain the value 1,000,000:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/image_5EC35485.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Slot 399" border="0" alt="Slot 399" src="http://sqlblog.com/blogs/paul_white/image_thumb_323A249F.png" width="429" height="72" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;Naturally, the quality of the linear interpolation guess depends on how evenly distributed the values are within the page.&amp;#160; To a lesser extent, it also depends on how closely the minimum key value information at level 1 of the index matches reality.&amp;#160; In both tests shown in earlier blog entries, the values are precisely evenly distributed and the level 1 index key information is spot on.&lt;/p&gt;  &lt;h4&gt;Performance&lt;/h4&gt;  &lt;p&gt;Linear interpolation has the potential to be more efficient even than binary search, finding the target value in one comparison in this example, compared to nine comparisons for binary search.&amp;#160; Even where the data is not perfectly distributed, there is scope for linear interpolation to be superior, simply by applying the technique repeatedly on successively narrower ranges.&lt;/p&gt;  &lt;p&gt;A reasonable practical compromise might be to try linear interpolation two or three times, before falling back to linear or binary search on the remaining range.&amp;#160; The advantage of linear interpolation may not seem much, but consider that SQL Server makes these comparisons for every index seek operation – when doing many millions of seeks, the difference can soon add up.&amp;#160; It is probably not coincidence that the OLTP TPC benchmark tests perform a very great number of singleton index seeks.&lt;/p&gt;  &lt;h3&gt;Linear Regression&lt;/h3&gt;  &lt;p&gt;A natural extension to the idea of linear interpolation is to apply a linear regression (line of best fit) instead:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/image_42D21F8D.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="Linear Regression Diagram" border="0" alt="Linear Regression Diagram" src="http://sqlblog.com/blogs/paul_white/image_thumb_6717040F.png" width="646" height="461" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;In the diagram, the blue dotted line represents a linear interpolation based on the values of the smallest and highest values only.&amp;#160; The black line is obtained by a linear regression of &lt;em&gt;all&lt;/em&gt; the data values, and results in a much better fit.&lt;/p&gt;  &lt;p&gt;Since every straight line can be represented by a formula of the form y = mx + b, we can completely specify it by recording the values of the ‘m’ (slope) and ‘b’ (y-axis intercept) parameters.&amp;#160; The R&lt;sup&gt;2&lt;/sup&gt; value gives an idea of how good a fit the linear regression line is to the data.&amp;#160; In the present context, the ‘y’ value represents the indexed value, and the ‘x’ value is the slot position where that value can be found.&amp;#160; It’s easy to see that once we have a linear regression line, we can estimate the slot position for a particular sought index value by solving x = (y – b) / m.&lt;/p&gt;  &lt;p&gt;To use linear regression in a database, we might imagine storing the ‘b’ and ‘m’ parameter values somewhere (perhaps in a compact format in the page header), and deciding whether to apply the technique based on the R&lt;sup&gt;2&lt;/sup&gt; value.&lt;/p&gt;  &lt;h3&gt;What Does SQL Server Do?&lt;/h3&gt;  &lt;p&gt;All this is fine in theory, but does SQL Server &lt;em&gt;really&lt;/em&gt; ever use anything except binary search?&amp;#160; To see, I ran tests with a debugger attached to the SQL Server process, and stepped through the calls made while the seek test queries were running.&amp;#160; The call stack below was obtained when running 64-bit SQL Server:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/SNAGHTML385906e_329631C7.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="64-bit Call Stack" border="0" alt="64-bit Call Stack" src="http://sqlblog.com/blogs/paul_white/SNAGHTML385906e_thumb_28EDC391.png" width="450" height="225" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;By contrast, the following call stack comes from 32-bit SQL Server:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/SNAGHTML38c1c77_25F85EDE.png"&gt;&lt;img style="background-image:none;border-right-width:0px;padding-left:0px;padding-right:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px;" title="32-bit Call Stack" border="0" alt="32-bit Call Stack" src="http://sqlblog.com/blogs/paul_white/SNAGHTML38c1c77_thumb_1C4FF0A8.png" width="459" height="218" /&gt;&lt;/a&gt;&lt;/p&gt;    &lt;p&gt;Interestingly, SQL Server follows the same code paths (at least at the call stack level, which is the best we can do without source code) regardless of whether the index is defined as unique or not, and for all the numeric data types I tested (INT, BIGINT, REAL, FLOAT, DECIMAL with a zero scale).&lt;/p&gt;  &lt;p&gt;The performance problem on x64 servers appears to be most pronounced when the key values have a small fixed gap between them.&amp;#160; As we have seen, performance is &lt;em&gt;very&lt;/em&gt; much worse on x64 servers compared with x86 versions when the table contains just even or odd numbers (i.e. with a gap of 1 between index entries).&amp;#160; If we change the test to multiply the original entries by a factor of ten (to produce a sequence of 10, 20, 30, 40…) the performance penalty all but disappears, and the x64 unique index test is around 30% faster than the non-unique test.&lt;/p&gt;  &lt;h3&gt;Conclusions and Further Reading&lt;/h3&gt;  &lt;p&gt;From the evidence available, my best guess is that 64-bit SQL Server does use linear interpolation and/or linear regression instead of binary search under suitable conditions, but that the implementation has edge-case poor performance where the index keys are separated by a small fixed offset.&amp;#160; It seems that we can achieve best performance by using a unique index where possible, and ensuring that keys are, as far as practicable, sequential and contiguous.&lt;/p&gt;  &lt;p&gt;&lt;a href="http://academic.research.microsoft.com/Publication/2202072"&gt;B-tree Indexes, Interpolation Search, and Skew&lt;/a&gt; – Microsoft Research     &lt;br /&gt;&lt;a href="http://im.ufba.br/pub/MAT152/SemestreLetivo20052/intepolationSearch.pdf"&gt;Interpolation Search – a log log N Search&lt;/a&gt; (PDF)&lt;/p&gt;  &lt;p&gt;© 2011 Paul White&lt;/p&gt;  &lt;p&gt;email: &lt;a href="mailto:SQLkiwi@gmail.com"&gt;SQLkiwi@gmail.com&lt;/a&gt;     &lt;br /&gt;twitter: &lt;a href="http://twitter.com/SQL_Kiwi"&gt;@SQL_Kiwi&lt;/a&gt;&lt;/p&gt;</description></item><item><title>A Tale of Two Index Hints</title><link>http://sqlblog.com/blogs/paul_white/archive/2010/09/22/a-tale-of-two-index-hints.aspx</link><pubDate>Wed, 22 Sep 2010 21:44:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:28917</guid><dc:creator>Paul White</dc:creator><description>&lt;p&gt;If you look up &lt;a href="http://msdn.microsoft.com/en-us/library/ms187373.aspx"&gt;Table Hints&lt;/a&gt; in Books Online, you’ll find the following statement:&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;&lt;i&gt;If a clustered index exists, INDEX(0) forces a clustered index scan and INDEX(1) forces a clustered index scan or seek.        &lt;br /&gt;If no clustered index exists, INDEX(0) forces a table scan and INDEX(1) is interpreted as an error.&lt;/i&gt;&lt;/p&gt; &lt;/blockquote&gt;  &lt;p align="left"&gt;The interesting thing there is that &lt;i&gt;both&lt;/i&gt; hints can result in a scan.&amp;#160; If that is the case, you might wonder if there is any effective difference between the two.&amp;#160; This blog entry explores that question, and highlights an optimizer quirk that can result in a much less efficient query plan when using INDEX(0).&amp;#160; I’ll also cover some stuff about ordering guarantees.&lt;/p&gt;  &lt;h3&gt;Test Environment&lt;/h3&gt;  &lt;p&gt;As usual, we need to create a test table:&lt;/p&gt;  &lt;div style="border-bottom:silver 1px solid;text-align:left;border-left:silver 1px solid;padding-bottom:4px;line-height:12pt;background-color:#f4f4f4;margin:20px 0px 10px;padding-left:4px;width:97.5%;padding-right:4px;font-family:'Courier New', courier, monospace;direction:ltr;max-height:200px;font-size:8pt;overflow:auto;border-top:silver 1px solid;cursor:text;border-right:silver 1px solid;padding-top:4px;" id="codeSnippetWrapper"&gt;   &lt;pre style="border-bottom-style:none;text-align:left;padding-bottom:0px;line-height:12pt;background-color:#f4f4f4;margin:0em;border-left-style:none;padding-left:0px;width:100%;padding-right:0px;font-family:'Courier New', courier, monospace;direction:ltr;border-top-style:none;color:black;border-right-style:none;font-size:8pt;overflow:visible;padding-top:0px;" id="codeSnippet"&gt;&lt;span style="color:#0000ff;"&gt;USE&lt;/span&gt;     tempdb;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SET&lt;/span&gt;     NOCOUNT &lt;span style="color:#0000ff;"&gt;ON&lt;/span&gt;;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;IF&lt;/span&gt;      OBJECT_ID(N&lt;span style="color:#006080;"&gt;'Test.Orders'&lt;/span&gt;, N&lt;span style="color:#006080;"&gt;'U'&lt;/span&gt;)&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;IS&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NOT&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NULL&lt;/span&gt;&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;DROP&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;TABLE&lt;/span&gt; Test.Orders;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;IF&lt;/span&gt;      SCHEMA_ID(N&lt;span style="color:#006080;"&gt;'Test'&lt;/span&gt;)&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;IS&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NOT&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NULL&lt;/span&gt;&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;DROP&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;SCHEMA&lt;/span&gt; Test;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;CREATE&lt;/span&gt;  &lt;span style="color:#0000ff;"&gt;SCHEMA&lt;/span&gt; Test&lt;br /&gt;&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;CREATE&lt;/span&gt;  &lt;span style="color:#0000ff;"&gt;TABLE&lt;/span&gt; Orders&lt;br /&gt;                (&lt;br /&gt;                order_id    &lt;span style="color:#0000ff;"&gt;INTEGER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NOT&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NULL&lt;/span&gt;,&lt;br /&gt;                product_id  &lt;span style="color:#0000ff;"&gt;INTEGER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NOT&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NULL&lt;/span&gt;,&lt;br /&gt;                quantity    &lt;span style="color:#0000ff;"&gt;INTEGER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NOT&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NULL&lt;/span&gt;,&lt;br /&gt;                padding     &lt;span style="color:#0000ff;"&gt;CHAR&lt;/span&gt;(980) &lt;span style="color:#0000ff;"&gt;NOT&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;NULL&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;DEFAULT&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;SPACE&lt;/span&gt;(0)),&lt;br /&gt;                &lt;br /&gt;                &lt;span style="color:#0000ff;"&gt;CONSTRAINT&lt;/span&gt;  [PK Test.Orders order_id]&lt;br /&gt;                &lt;span style="color:#0000ff;"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;KEY&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;CLUSTERED&lt;/span&gt; (order_id &lt;span style="color:#0000ff;"&gt;ASC&lt;/span&gt;)&lt;br /&gt;                );&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;CREATE&lt;/span&gt;  &lt;span style="color:#0000ff;"&gt;NONCLUSTERED&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;  [IX Test.Orders quantity]&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;ON&lt;/span&gt;      Test.Orders (quantity &lt;span style="color:#0000ff;"&gt;ASC&lt;/span&gt;)&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;INCLUDE&lt;/span&gt; (padding);&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;DECLARE&lt;/span&gt; @Counter    &lt;span style="color:#0000ff;"&gt;INTEGER&lt;/span&gt; = 1,&lt;br /&gt;        @&lt;span style="color:#0000ff;"&gt;RowCount&lt;/span&gt;   &lt;span style="color:#0000ff;"&gt;INTEGER&lt;/span&gt; = 64 * 8;&lt;br /&gt;        &lt;br /&gt;&lt;span style="color:#0000ff;"&gt;WHILE&lt;/span&gt;   @Counter &amp;lt;= @&lt;span style="color:#0000ff;"&gt;RowCount&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;BEGIN&lt;/span&gt;&lt;br /&gt;        INSERT  Test.Orders&lt;br /&gt;                (order_id, product_id, quantity)&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;VALUES&lt;/span&gt;  (&lt;br /&gt;                @&lt;span style="color:#0000ff;"&gt;RowCount&lt;/span&gt; - @Counter,&lt;br /&gt;                @&lt;span style="color:#0000ff;"&gt;RowCount&lt;/span&gt; - @Counter,&lt;br /&gt;                @&lt;span style="color:#0000ff;"&gt;RowCount&lt;/span&gt; - @Counter&lt;br /&gt;                );&lt;br /&gt;                &lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;SET&lt;/span&gt;     @Counter += 1;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;END&lt;/span&gt;;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;

  &lt;br /&gt;&lt;/div&gt;

&lt;p align="left"&gt;The test table has 512 rows, padded so that eight rows fit on each 8KB data page.&amp;#160; The table is clustered on the &lt;i&gt;order_id&lt;/i&gt; column, and there is a nonclustered index on the &lt;i&gt;quantity&lt;/i&gt; column, with the &lt;i&gt;padding&lt;/i&gt; column INCLUDEd to again ensure that eight rows fit on each nonclustered index page.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/TableContents_2C94614A.png"&gt;&lt;img style="border-right-width:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;" title="Table-Contents" border="0" alt="Table-Contents" src="http://sqlblog.com/blogs/paul_white/TableContents_thumb_16CF9A4F.png" width="270" height="229" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script also deliberately generates a very high level of logical fragmentation, which we can see with the following query:&lt;/p&gt;

&lt;div style="border-bottom:silver 1px solid;text-align:left;border-left:silver 1px solid;padding-bottom:4px;line-height:12pt;background-color:#f4f4f4;margin:20px 0px 10px;padding-left:4px;width:97.5%;padding-right:4px;font-family:'Courier New', courier, monospace;direction:ltr;max-height:200px;font-size:8pt;overflow:auto;border-top:silver 1px solid;cursor:text;border-right:silver 1px solid;padding-top:4px;" id="codeSnippetWrapper"&gt;
  &lt;pre style="border-bottom-style:none;text-align:left;padding-bottom:0px;line-height:12pt;background-color:#f4f4f4;margin:0em;border-left-style:none;padding-left:0px;width:100%;padding-right:0px;font-family:'Courier New', courier, monospace;direction:ltr;border-top-style:none;color:black;border-right-style:none;font-size:8pt;overflow:visible;padding-top:0px;" id="codeSnippet"&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  SI.name,&lt;br /&gt;        IPS.index_type_desc,&lt;br /&gt;        IPS.avg_fragmentation_in_percent,&lt;br /&gt;        IPS.fragment_count,&lt;br /&gt;        IPS.page_count&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt;    sys.dm_db_index_physical_stats&lt;br /&gt;        (&lt;br /&gt;        DB_ID(),&lt;br /&gt;        OBJECT_ID(N&lt;span style="color:#006080;"&gt;'Test.Orders'&lt;/span&gt;, N&lt;span style="color:#006080;"&gt;'U'&lt;/span&gt;),&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;DEFAULT&lt;/span&gt;,&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;DEFAULT&lt;/span&gt;,&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;DEFAULT&lt;/span&gt;&lt;br /&gt;        ) IPS&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;JOIN&lt;/span&gt;    sys.indexes SI&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;ON&lt;/span&gt;  SI.[object_id] = IPS.[object_id]&lt;br /&gt;        &lt;span style="color:#0000ff;"&gt;AND&lt;/span&gt; SI.index_id = IPS.index_id&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;ORDER&lt;/span&gt;   &lt;span style="color:#0000ff;"&gt;BY&lt;/span&gt;&lt;br /&gt;        IPS.index_id;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;GO&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;

  &lt;br /&gt;&lt;/div&gt;

&lt;p&gt;The reason for the fragmentation will become apparent later on.&amp;#160; These are the results I saw on my machine:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/IndexPhysicalStats_764AEB4D.png"&gt;&lt;img style="border-right-width:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;" title="Index-Physical-Stats" border="0" alt="Index-Physical-Stats" src="http://sqlblog.com/blogs/paul_white/IndexPhysicalStats_thumb_5A3CBD13.png" width="629" height="61" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Background Information on Scans&lt;/h3&gt;

&lt;p&gt;A &lt;i&gt;scan&lt;/i&gt; of a table or index processes every row, whereas a &lt;i&gt;seek&lt;/i&gt; returns one or more rows from one or more ranges of the index.&amp;#160; &lt;/p&gt;

&lt;h4&gt;Scan Strategies&lt;/h4&gt;

&lt;p align="left"&gt;When the SQL Server optimizer produces a plan containing a scan of an index or clustered table, the Storage Engine may be able to choose from two different strategies: scanning the pages in logical order, or the order in which they were allocated.&amp;#160; In the first case, it follows the doubly-linked list at the leaf level of the index.&amp;#160; In the second case, it can use the &lt;a href="http://msdn.microsoft.com/en-us/library/ms187501.aspx"&gt;Index Allocation Map&lt;/a&gt; (IAM) pages to drive the scan.&lt;/p&gt;

&lt;p align="left"&gt;A scan of a heap always uses the IAM pages of course (there is no clustered index to provide a logical page order), but this post is concerned mainly with indexes and clustered tables, so I won’t talk much more about heaps.&lt;/p&gt;

&lt;p align="left"&gt;The advantage of an IAM-driven scan is that pages will tend to be read in physical order on the disk, which generally results in sequential I/O, and promotes effective read-ahead.&amp;#160; How efficient scanning the pages in logical order is depends on the level of logical fragmentation: if fragmentation is high, the scan will tend to result in a high proportion of random I/Os, and read-ahead may be much less effective.&lt;/p&gt;

&lt;p align="left"&gt;The downside to the IAM-driven scan is that it is not guaranteed to return results in logical key order (in fact it usually won’t), and there is an extra start-up cost in accessing the IAM pages, and planning the scan.&lt;/p&gt;

&lt;h4&gt;IAM-driven scans&lt;/h4&gt;

&lt;p&gt;The Storage Engine may choose an IAM-driven scan if:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The index contains 64 or more pages (giving the method a chance to recover the start-up cost); &lt;/li&gt;

  &lt;li&gt;The optimizer indicates that an ordered scan is not required for correct results; and &lt;/li&gt;

  &lt;li&gt;Either the data cannot change during the scan, &lt;b&gt;or&lt;/b&gt; the query indicates that incorrect results are acceptable. &lt;/li&gt;
&lt;/ol&gt;

&lt;p align="left"&gt;Hopefully the first point makes sense on it’s own.&amp;#160; The second point refers to a flag that the optimizer can set on a scanning iterator in a query plan.&amp;#160; This flag shows up as an “Ordered:True” or “Ordered:False” property of the iterator.&amp;#160; When set to true, it indicates that the Storage Engine &lt;i&gt;must&lt;/i&gt; perform a scan in logical key order for the plan to produce correct results.&amp;#160; When set to false, it means that the order does not matter.&amp;#160; In the latter case, the Storage Engine has the &lt;i&gt;choice&lt;/i&gt; of performing either type of scan.&amp;#160; The query plan does not show which type of scan was chosen.&lt;/p&gt;

&lt;p align="left"&gt;The third point comes in two parts, but both relate to the fact that an IAM-driven scan does not take the locks necessary to prevent other concurrent operations from changing the data in the table, or moving the pages around (perhaps as the result of a page split).&lt;/p&gt;

&lt;p align="left"&gt;The simplest way to satisfy the first part (to ensure that the data cannot be changed or moved) is to take a shared table lock.&amp;#160; The second part (acceptable incorrect results) refers to transaction isolation level.&amp;#160; If the scan runs at an effective isolation level of READ UNCOMMITTED (either explicitly or through the use of a NOLOCK hint) we are effectively saying that we don’t care about reading consistent data.&amp;#160; More than that, we imply that we are happy to read the same data twice, or not at all.&lt;/p&gt;

&lt;h4&gt;The Reason for the Fragmentation&lt;/h4&gt;

&lt;p align="left"&gt;Our test table has been created with a high level of fragmentation.&amp;#160; This allows us to see whether the Storage Engine used an IAM-driven scan or not.&amp;#160; If the results come back in logical key order, we can be sure (in these specific examples) that the scan was performed by following the doubly-linked list at the leaf level of the index.&amp;#160; If the results come back in allocation order, they will look very different, and we can infer that an IAM-driven scan was performed.&lt;/p&gt;

&lt;hr /&gt;

&lt;p align="left"&gt;&lt;b&gt;Important&lt;/b&gt;: Please don’t take any of that to mean that you can rely on query results to come back in a particular order in more general cases.&amp;#160; The very simple tests shown here are carefully crafted to encourage the ordering behaviour I have just described, but there are &lt;i&gt;no guarantees&lt;/i&gt;.&amp;#160; Never rely on plan details to provide ordering – always use an ORDER BY clause if you require ordered results.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;Scanning Examples&lt;/h3&gt;

&lt;p&gt;Let’s look first at two simple queries that return all columns and all rows from our test table:&lt;/p&gt;

&lt;div style="border-bottom:silver 1px solid;text-align:left;border-left:silver 1px solid;padding-bottom:4px;line-height:12pt;background-color:#f4f4f4;margin:20px 0px 10px;padding-left:4px;width:97.5%;padding-right:4px;font-family:'Courier New', courier, monospace;direction:ltr;max-height:200px;font-size:8pt;overflow:auto;border-top:silver 1px solid;cursor:text;border-right:silver 1px solid;padding-top:4px;" id="codeSnippetWrapper"&gt;
  &lt;pre style="border-bottom-style:none;text-align:left;padding-bottom:0px;line-height:12pt;background-color:#f4f4f4;margin:0em;border-left-style:none;padding-left:0px;width:100%;padding-right:0px;font-family:'Courier New', courier, monospace;direction:ltr;border-top-style:none;color:black;border-right-style:none;font-size:8pt;overflow:visible;padding-top:0px;" id="codeSnippet"&gt;&lt;span style="color:#008000;"&gt;-- Query 1&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.*&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt;    Test.Orders T;&lt;br /&gt; &lt;br /&gt;&lt;span style="color:#008000;"&gt;-- Query 2&lt;/span&gt;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.*&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt;    Test.Orders T &lt;br /&gt;&lt;span style="color:#0000ff;"&gt;ORDER&lt;/span&gt;   &lt;span style="color:#0000ff;"&gt;BY&lt;/span&gt; &lt;br /&gt;        T.order_id &lt;span style="color:#0000ff;"&gt;ASC&lt;/span&gt;;&lt;br /&gt;&lt;/pre&gt;

  &lt;br /&gt;&lt;/div&gt;

&lt;p align="left"&gt;Both queries produce very similar execution plans, but the first specifies “Ordered:False” while the second specifies “Ordered:True”.&amp;#160; The second example avoids an expensive explicit Sort iterator by requiring the Storage Engine to return rows from the scan in logical key order (the clustered index is on &lt;i&gt;order_id&lt;/i&gt;, remember).&lt;/p&gt;

&lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/Example1_44E1985C.png"&gt;&lt;img style="border-right-width:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;" title="Example-1" border="0" alt="Example-1" src="http://sqlblog.com/blogs/paul_white/Example1_thumb_41659823.png" width="660" height="418" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p align="left"&gt;Both queries return records ordered by the clustering key; Query 1 because the Storage Engine cannot perform an IAM-driven scan (we haven’t met all the conditions); and Query 2 because an ordered scan was requested by the optimizer.&lt;/p&gt;

&lt;p align="left"&gt;Query 1 is &lt;i&gt;not&lt;/i&gt; guaranteed to return records in that order, but Query 2 (almost) is.&amp;#160; In case you are wondering what might cause Query 1 to return records in a different order, consider the &lt;a href="http://blogs.msdn.com/b/conor_cunningham_msft/archive/2008/08/27/no-seatbelt-expecting-order-without-order-by.aspx"&gt;effect of a parallel plan&lt;/a&gt;, or Enterprise Edition’s &lt;a href="http://msdn.microsoft.com/en-us/library/ms191475.aspx"&gt;Advanced Scan&lt;/a&gt; feature.&amp;#160; For anyone that follows that second link, I should mention that Advanced Scan is only possible when the Ordered flag is set to False.&lt;/p&gt;

&lt;h4&gt;An IAM-driven Scan&lt;/h4&gt;

&lt;p align="left"&gt;Query 1 above meets two of the conditions for an IAM-driven scan: the index contains at least 64 pages, and the optimizer did not specify Ordered:True.&amp;#160; We can meet the third condition by adding a WITH (TABLOCK) hint:&lt;/p&gt;

&lt;div style="border-bottom:silver 1px solid;text-align:left;border-left:silver 1px solid;padding-bottom:4px;line-height:12pt;background-color:#f4f4f4;margin:20px 0px 10px;padding-left:4px;width:97.5%;padding-right:4px;font-family:'Courier New', courier, monospace;direction:ltr;max-height:200px;font-size:8pt;overflow:auto;border-top:silver 1px solid;cursor:text;border-right:silver 1px solid;padding-top:4px;" id="codeSnippetWrapper"&gt;
  &lt;pre style="border-bottom-style:none;text-align:left;padding-bottom:0px;line-height:12pt;background-color:#f4f4f4;margin:0em;border-left-style:none;padding-left:0px;width:100%;padding-right:0px;font-family:'Courier New', courier, monospace;direction:ltr;border-top-style:none;color:black;border-right-style:none;font-size:8pt;overflow:visible;padding-top:0px;" id="codeSnippet"&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.*&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt;    Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (TABLOCK);&lt;br /&gt;&lt;/pre&gt;

  &lt;br /&gt;&lt;/div&gt;

&lt;p&gt;We receive the following result set:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/Query1withTABLOCK_1DCE8B2D.png"&gt;&lt;img style="border-right-width:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;" title="Query-1-with-TABLOCK" border="0" alt="Query-1-with-TABLOCK" src="http://sqlblog.com/blogs/paul_white/Query1withTABLOCK_thumb_4432FF29.png" width="244" height="237" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p align="left"&gt;Each group of eight records (each 8KB page) comes back ordered by the clustering key, but the overall order of pages is in the order the pages were written.&amp;#160; This is the result of an IAM-driven scan: pages in allocation order, records within the pages in clustered key order.&amp;#160; As you may have guessed, neither of these two observed orderings can be relied on either – I mention them for pure geek interest value.&lt;/p&gt;

&lt;p align="left"&gt;Adding the TABLOCK hint to Query 2 does &lt;i&gt;not&lt;/i&gt; result in an IAM-driven scan, because the optimizer continues to specify Ordered:True on the scan (to avoid the sort that would be necessary to honour the ORDER BY clause).&lt;/p&gt;

&lt;p align="left"&gt;For Query 2, the optimizer does still &lt;i&gt;consider&lt;/i&gt; a plan with an unordered scan followed by a sort, but rejects it as being more expensive than an ordered scan.&amp;#160; For very large tables, the optimizer may calculate that an IAM-driven scan might save more time than would be consumed by the extra sort, and a plan featuring the unordered scan + sort &lt;i&gt;would&lt;/i&gt; be chosen.&amp;#160; This is a heuristic optimization: the optimizer knows nothing about the actual fragmentation level of an index (though it does know how many pages it occupies).&lt;/p&gt;

&lt;h3&gt;Index Hints&lt;/h3&gt;

&lt;p&gt;Let’s now run the Query 1 test again, this time specifying INDEX(0) or INDEX(1) hints, as well as choosing to specify TABLOCK or not:&lt;/p&gt;

&lt;div style="border-bottom:silver 1px solid;text-align:left;border-left:silver 1px solid;padding-bottom:4px;line-height:12pt;background-color:#f4f4f4;margin:20px 0px 10px;padding-left:4px;width:97.5%;padding-right:4px;font-family:'Courier New', courier, monospace;direction:ltr;max-height:200px;font-size:8pt;overflow:auto;border-top:silver 1px solid;cursor:text;border-right:silver 1px solid;padding-top:4px;" id="codeSnippetWrapper"&gt;
  &lt;pre style="border-bottom-style:none;text-align:left;padding-bottom:0px;line-height:12pt;background-color:#f4f4f4;margin:0em;border-left-style:none;padding-left:0px;width:100%;padding-right:0px;font-family:'Courier New', courier, monospace;direction:ltr;border-top-style:none;color:black;border-right-style:none;font-size:8pt;overflow:visible;padding-top:0px;" id="codeSnippet"&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(0));&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(1));&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(0), TABLOCK);&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(1), TABLOCK);&lt;br /&gt;&lt;/pre&gt;

  &lt;br /&gt;&lt;/div&gt;

&lt;p align="left"&gt;In all cases, the results are the same: if we specify TABLOCK, we meet the conditions for an IAM-driven scan, and the records comes back in physical order.&amp;#160; If we omit TABLOCK, we happen to get results in clustered-key order – but we know that is just coincidental.&amp;#160; All four query plans look exactly the same as the simple clustered index scans shown earlier, and all feature “Ordered:False”.&lt;/p&gt;

&lt;p&gt;Let’s try the same thing with the ORDER BY clause (Query 2):&lt;/p&gt;

&lt;div style="border-bottom:silver 1px solid;text-align:left;border-left:silver 1px solid;padding-bottom:4px;line-height:12pt;background-color:#f4f4f4;margin:20px 0px 10px;padding-left:4px;width:97.5%;padding-right:4px;font-family:'Courier New', courier, monospace;direction:ltr;max-height:200px;font-size:8pt;overflow:auto;border-top:silver 1px solid;cursor:text;border-right:silver 1px solid;padding-top:4px;" id="codeSnippetWrapper"&gt;
  &lt;pre style="border-bottom-style:none;text-align:left;padding-bottom:0px;line-height:12pt;background-color:#f4f4f4;margin:0em;border-left-style:none;padding-left:0px;width:100%;padding-right:0px;font-family:'Courier New', courier, monospace;direction:ltr;border-top-style:none;color:black;border-right-style:none;font-size:8pt;overflow:visible;padding-top:0px;" id="codeSnippet"&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(0)) &lt;span style="color:#0000ff;"&gt;ORDER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;BY&lt;/span&gt; T.order_id;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(0), TABLOCK) &lt;span style="color:#0000ff;"&gt;ORDER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;BY&lt;/span&gt; T.order_id;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(1)) &lt;span style="color:#0000ff;"&gt;ORDER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;BY&lt;/span&gt; T.order_id;&lt;br /&gt;&lt;span style="color:#0000ff;"&gt;SELECT&lt;/span&gt;  T.* &lt;span style="color:#0000ff;"&gt;FROM&lt;/span&gt; Test.Orders T &lt;span style="color:#0000ff;"&gt;WITH&lt;/span&gt; (&lt;span style="color:#0000ff;"&gt;INDEX&lt;/span&gt;(1), TABLOCK) &lt;span style="color:#0000ff;"&gt;ORDER&lt;/span&gt; &lt;span style="color:#0000ff;"&gt;BY&lt;/span&gt; T.order_id;&lt;br /&gt;&lt;/pre&gt;

  &lt;br /&gt;&lt;/div&gt;

&lt;p&gt;All four queries produce the same results, in the same order, and are guaranteed to do so thanks to the ORDER BY clause.&amp;#160; The execution plans are rather different, however:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/INDEXhints_7EB08FAE.png"&gt;&lt;img style="border-right-width:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;" title="INDEX-hints" border="0" alt="INDEX-hints" src="http://sqlblog.com/blogs/paul_white/INDEXhints_thumb_392E2034.png" width="644" height="83" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both INDEX(1) plans produce the plan shown on the left, with the scan showing “Ordered:True”, and a cost of 0.05. 
  &lt;br /&gt;Both INDEX(0) plans produce the plan shown on the right, with the scan showing “Ordered:False”, and a cost of 0.07.&lt;/p&gt;

&lt;h3&gt;Optimizer Bug, Limitation, or “By Design”?&lt;/h3&gt;

&lt;p align="left"&gt;It seems that INDEX(0) forces “Ordered:False” on the scan.&amp;#160; As a consequence, we get an explicit Sort iterator when the ORDER BY clause is present.&amp;#160; Why should INDEX(0) force an unordered scan?&lt;/p&gt;

&lt;p align="left"&gt;One can imagine a line of reasoning that says we would normally specify INDEX(0) only on a heap table, where an ordered scan is not possible.&amp;#160; The flaw in this argument is that INDEX(1) does not enable us to force a &lt;i&gt;scan&lt;/i&gt; of a clustered table – it will result in a seek if at all possible.&lt;/p&gt;

&lt;h4&gt;INDEX(0) and Ordered:True&lt;/h4&gt;

&lt;p align="left"&gt;Nevertheless, it is &lt;i&gt;possible&lt;/i&gt; to produce “Ordered:True” when INDEX(0) is specified, in an INSERT, UPDATE or DELETE query.&amp;#160; Let’s look at three alternative formulations of a trivial UPDATE query on our test table:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://sqlblog.com/blogs/paul_white/UPDATEExamples_65D96ABE.png"&gt;&lt;img style="border-right-width:0px;display:inline;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;" title="UPDATE-Examples" border="0" alt="UPDATE-Examples" src="http://sqlblog.com/blogs/paul_white/UPDATEExamples_thumb_62C99D7A.png" width="644" height="332" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p align="left"&gt;All three plans feature the “Ordered:True” flag, but the one with INDEX(0) includes a sort on &lt;i&gt;order_id&lt;/i&gt;.&amp;#160; That sort is probably redundant, since the Clustered Index Scan (with “Ordered:True”) ought to be guaranteed to produce rows in that same order.&amp;#160; Not only will that sort slow down your query, it will require a memory grant, and might even spill to &lt;i&gt;tempdb&lt;/i&gt;.&lt;/p&gt;

&lt;h4&gt;A Data Modification Optimization&lt;/h4&gt;

&lt;p align="left"&gt;When performing our UPDATE query, the optimizer takes into account the effect of ordering.&amp;#160; If rows arrive at the update iterator in clustered index order, the pattern of updates will likely result in sequential I/O.&amp;#160; If rows are in a different order, the updates will likely cause random I/O as the index is updated row by row.&lt;/p&gt;

&lt;p align="left"&gt;Often, the optimizer may choose to pre-sort the records on the clustered index key to optimize for sequential I/O.&amp;#160; On other occasions, it may decide that the cost of the sort would exceed the savings made – this is seen when the number of rows to be updated is very small, and the rows can be located most efficiently from a nonclustered index.&lt;/p&gt;

&lt;p align="left"&gt;When the rows are sourced from the clustered index, the optimizer &lt;i&gt;always&lt;/i&gt; adds the “Ordered:True” flag to maximize the chances for sequential I/O, regardless of the estimated number of rows.&amp;#160; This explains how the Clustered Index Scan above manages to acquire the “Ordered:True” flag when INDEX(0) was specified.&amp;#160; It seems that the optimizer misses the fact that by adding the ordering flag, the sort is now redundant.&lt;/p&gt;

&lt;h3&gt;Final Thoughts&lt;/h3&gt;

&lt;p align="left"&gt;Although I have concentrated very much on clustered indexes in this entry, I want to stress that the two scanning strategies apply to both clustered and non-clustered indexes.&amp;#160; I created a nonclustered index on the test table so you can verify for yourself that IAM-driven scans are possible with nonclustered indexes, and the rules are exactly the same.&lt;/p&gt;

&lt;p align="left"&gt;It is also not necessary to specify TABLOCK explicitly to get an IAM-driven scan: if the index in question happens to have row and page locks disabled (using ALTER INDEX (…) SET ALLOW_ROW_LOCKS = OFF, ALLOW_PAGE_LOCKS = OFF) the Storage Engine will recognise that a table lock has to be taken.&amp;#160; Other circumstances like the database being read-only will also meet the underlying requirement that the data cannot change during the scan.&lt;/p&gt;

&lt;p align="left"&gt;Beware of using NOLOCK when the other conditions for an IAM-driven scan are met.&amp;#160; Not only might you read data that has not been committed yet, you can also read &lt;i&gt;committed&lt;/i&gt; data twice, or &lt;i&gt;not at all&lt;/i&gt;.&amp;#160; Your query might also fail completely with the dreaded error: “Could not continue scan with NOLOCK due to data movement”.&lt;/p&gt;

&lt;p align="left"&gt;Finally, watch out for unnecessary sorts in plans that use the INDEX(0) table hint – consider using INDEX(1) instead, or rewriting the query to avoid the hint altogether.&lt;/p&gt;

&lt;p&gt;Paul White 
  &lt;br /&gt;Email: &lt;a href="mailto:SQLkiwi@gmail.com"&gt;SQLkiwi@gmail.com&lt;/a&gt; 

  &lt;br /&gt;Twitter: &lt;a href="http://twitter.com/SQL_Kiwi"&gt;@SQL_Kiwi&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;Further Reading&lt;/h5&gt;

&lt;p&gt;&lt;a href="http://www.sqlmag.com/article/sql-server/quaere-verum-clustered-index-scans-part-i.aspx"&gt;Clustered Index Scans&lt;/a&gt; – Itzik Ben-Gan 

  &lt;br /&gt;&lt;a href="http://msdn.microsoft.com/en-us/library/ms191475.aspx"&gt;Advanced Scanning&lt;/a&gt; – Books Online 

  &lt;br /&gt;&lt;a href="http://blogs.msdn.com/b/sqlserverstorageengine/archive/2006/11/09/when-can-allocation-order-scans-be-used.aspx"&gt;When IAM Scans Can Be Used&lt;/a&gt; – SQL Server Storage Engine Team 

  &lt;br /&gt;&lt;a href="http://msdn.microsoft.com/en-us/library/ms187501.aspx"&gt;IAM Pages&lt;/a&gt; – Books Online 

  &lt;br /&gt;&lt;a href="http://www.sqlmag.com/article/news2/beware-the-nolock-hint.aspx"&gt;Beware the NOLOCK hint&lt;/a&gt; – Itzik Ben-Gan&lt;/p&gt;</description></item></channel></rss>