<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="https://www.w3.org/2005/Atom" xmlns:sy="https://purl.org/rss/1.0/modules/syndication/" xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:wfw="https://wellformedweb.org/CommentAPI/" xmlns:slash="https://purl.org/rss/1.0/modules/slash/"
  version="2.0">
  <channel>
    <title>Craig Freedman's SQL Server Blog</title>
    <atom:link
      href="https://docs.microsoft.com/archive/blogs/craigfr/feed.xml"
      rel="self"
      type="application/rss+xml" />
    <link>https://docs.microsoft.com/archive/blogs/craigfr/feed.xml</link>
    <description>A discussion of query processing, query execution, and query plans in SQL Server.</description>
    <lastBuildDate>Fri, 29 Mar 2019 18:05:59 GMT</lastBuildDate>
    <language>en-US</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>We have moved!</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/we-have-moved</link>
      <pubDate>Fri, 29 Mar 2019 18:05:59 GMT</pubDate>
      <dc:creator><![CDATA[Pam Lahoud]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/?p=1535</guid>
      <description><![CDATA[Thanks for visiting! This blog has now been migrated...]]></description>
      <content:encoded><![CDATA[Thanks for visiting! This blog has now been migrated to:

<a href="https://techcommunity.microsoft.com/t5/SQL-Server/bg-p/SQLServer/label-name/SQLServerQueryProcessing">https://techcommunity.microsoft.com/t5/SQL-Server/bg-p/SQLServer/label-name/SQLServerQueryProcessing</a>

&nbsp;

&nbsp;]]></content:encoded>
    </item>
    <item>
      <title>More on Implicit Conversions</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/more-on-implicit-conversions</link>
      <pubDate>Wed, 20 Jan 2010 11:56:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2010/01/20/more-on-implicit-conversions/</guid>
      <description><![CDATA[Yesterday, a reader posted a question asking me to comment on SQL Server's algorithm for choosing...]]></description>
      <content:encoded><![CDATA[<P>Yesterday, a reader posted a <A title="re: Implicit Conversions" href="https://blogs.msdn.com/craigfr/archive/2008/06/05/implicit-conversions.aspx#9950488" mce_href="https://blogs.msdn.com/craigfr/archive/2008/06/05/implicit-conversions.aspx#9950488">question</A> asking me to comment on SQL Server's algorithm for choosing implicit conversions.&nbsp; When SQL Server encounters an expression with mismatched types, it has two options.&nbsp; It can execute the query with an implicit conversion or it can fail with an error.&nbsp; Before digging into the implicit conversion scenario, let's briefly consider the error case.</P>
<P>If an implicit conversion is not possible, there are two possible errors that SQL Server can generate.&nbsp; If the two data types are entirely incompatible (that is, if no conversion, implicit or explicit, is permitted between the two types), SQL Server generates the following error:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b DATE<BR>SET @a = @b</P></BLOCKQUOTE>
<P>Msg 206, Level 16, State 2, Line 3<BR>Operand type clash: date is incompatible with int</P>
<P>An attempt to remedy this situation with an explicit conversion, fails with the following error:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b DATE<BR>SET @a = CONVERT(INT,@b)</P></BLOCKQUOTE>
<P>Msg 529, Level 16, State 2, Line 3<BR>Explicit conversion from data type date to int is not allowed.</P>
<P>On the other hand, if the two data types are compatible, but require an explicit conversion, SQL Server generates the following error:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b DATETIME<BR>SET @a = @b</P></BLOCKQUOTE>
<P>Msg 257, Level 16, State 3, Line 3<BR>Implicit conversion from data type datetime to int is not allowed. Use the CONVERT function to run this query.</P>
<P>Introducing an explicit convert, resolves the problem:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b DATETIME<BR>SET @a = CONVERT(INT,@b)</P></BLOCKQUOTE>
<P>This <A title="CAST and CONVERT (Transact-SQL)" href="https://msdn.microsoft.com/en-us/library/ms187928.aspx" mce_href="https://msdn.microsoft.com/en-us/library/ms187928.aspx">Books Online page</A> includes a compatibility matrix showing how SQL Server handles all of the possible data type combinations.</P>
<P>If SQL Server introduces an implicit conversion into an expression, it must choose which side of the expression to convert and it must choose the target data type of the conversion.&nbsp; In principle, there are three options for comparison expressions:</P>
<OL>
<LI>Convert the right hand side to the type of the left hand side.</LI>
<LI>Convert the left hand side to the type of the right hand side.</LI>
<LI>Convert both sides to a third type.</LI></OL>
<P>For assignments, it only makes sense to convert the right hand side to the type of the left hand side.&nbsp; If this conversion is not permitted, the assignment must fail.&nbsp; It does not make sense to write:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b DATETIME<BR>SET CONVERT(DATETIME,@a) = @b</P></BLOCKQUOTE>
<P>Msg 156, Level 15, State 1, Line 3<BR>Incorrect syntax near the keyword 'convert'.</P>
<P>In practice, SQL Server chooses one of the first two options.&nbsp; Specifically, <A title="Data Type Precedence" href="https://msdn.microsoft.com/en-us/library/ms190309.aspx" mce_href="https://msdn.microsoft.com/en-us/library/ms190309.aspx">SQL Server ranks all types</A> and converts from the lower ranked to the higher ranked type.&nbsp; For example, since INT is lower ranked than DATETIME, the following query converts the INT variable to DATETIME:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b DATETIME<BR>SELECT 0 WHERE @a = @b</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1000]=(0)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Filter(WHERE:(STARTUP EXPR(<B>CONVERT_IMPLICIT(datetime,[@a],0)</B>=[@b])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Constant Scan</P>
<P>This approach does have some drawbacks.&nbsp; Specifically, while some conversions are lossless, others are lossy and some conversions fail outright.&nbsp; For example, converting a SMALLINT to an INT is lossless.&nbsp; The set of values that can be expressed by an INT is a superset of the set of values than can be expressed by a SMALLINT.&nbsp; Converting an INT to a REAL is lossy.&nbsp; There are large INT values that cannot be expressed with a REAL:</P>
<BLOCKQUOTE>
<P>DECLARE @a INT<BR>DECLARE @b REAL<BR>DECLARE @c INT<BR>SET @a = 1000000001<BR>SET @b = CONVERT(REAL,@a)<BR>SET @c = CONVERT(INT,@b)<BR>SELECT @a, @b, @c</P></BLOCKQUOTE><PRE>a                    b             c
-------------------- ------------- --------------------
1000000001           1E+09         1000000000</PRE>
<P>Converting a REAL to an INT can be lossy (an INT cannot represent a fraction) and can fail outright:</P>
<BLOCKQUOTE>
<P>DECLARE @a REAL<BR>DECLARE @b INT<BR>SET @a = 1e13<BR>SET @b = CONVERT(INT,@a)</P></BLOCKQUOTE>
<P>Msg 232, Level 16, State 3, Line 4<BR>Arithmetic overflow error for type int, value = 9999999827968.000000.</P>
<P>The type ranking used by SQL Server&nbsp;tries to minimize the risk of lossy or failed conversions, but sometimes&nbsp;data loss or&nbsp;failures are unavoidable as in the case of REAL and INT.</P>
<P>Conversions between string types (e.g., CHAR or VARCHAR) and numeric types (e.g., INT, REAL, or DECIMAL) are especially problematic.&nbsp; SQL Server ranks string types lower than numeric types and will always convert from a string type to a numeric type.&nbsp; This conversion can easily fail.&nbsp; Clearly, there are many strings that contain non-numeric data and cannot be converted.&nbsp; However, converting in the reverse direction is arguably worse.&nbsp; There are many ways to represent a numeric value as a string.&nbsp; All of the following strings represent the same number: ‘1', ‘˽1', ‘1˽ ‘, ‘+1', ‘1.0', and ‘01'.&nbsp; While SQL Server could certainly choose a canonical format for numeric to string conversions, without converting the string to a numeric type, there is no easy way to compare a string that is not already in a canonical format with a numeric value.&nbsp; For example, consider the results of the following comparisons:</P>
<P>DECLARE @a INT<BR>DECLARE @b CHAR(4)<BR>SET @a = 1<BR>SET @b = @a<BR>SELECT @a AS a, @b AS b,<BR>&nbsp;&nbsp;&nbsp; CASE WHEN @a = '1' &nbsp;THEN 'True' ELSE 'False' END AS [a = '1'],<BR>&nbsp;&nbsp;&nbsp; CASE WHEN @a = '+1' THEN 'True' ELSE 'False' END AS [a = '+1'],<BR>&nbsp;&nbsp;&nbsp; CASE WHEN @b = '1'&nbsp; THEN 'True' ELSE 'False' END AS [b = '1'],<BR>&nbsp;&nbsp;&nbsp; CASE WHEN @b = '+1' THEN 'True' ELSE 'False' END AS [b = '+1']</P><PRE>a           b    a = '1' a = '+1' b = '1' b = '+1'
----------- ---- ------- -------- ------- --------
1           1    True    True     True    False</PRE>
<P>Notice that, when compared as integers, 1 = +1 but, when compared as strings, ‘1' &lt;&gt; ‘+1'.</P>
<P>Returning to the three implicit conversion options above, SQL Server never implicitly converts both sides of an expression to a third type.&nbsp; In some cases, it may be possible to explicitly convert two sides of an expression to a "super" type such that both conversions are lossless, but there are cases where no such "super" type exists (at least in the SQL Server type system).</P>
<P>At times these conversion rules and behaviors may be inconvenient or quirky.&nbsp; In reality they evolved over many years (even decades) and many applications have been written based on these assumptions.&nbsp; To make changes now would certainly cause many existing applications to fail.</P>In general, wherever possible, new code should avoid using expressions with mismatched types both to avoid the above quirks and error cases and to maximize performance.]]></content:encoded>
    </item>
    <item>
      <title>Correction to my prior post on sys.dm_db_index_operational_stats</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/correction-to-my-prior-post-on-sys-dm_db_index_operational_stats</link>
      <pubDate>Wed, 29 Jul 2009 16:56:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2009/07/29/correction-to-my-prior-post-on-sys-dm_db_index_operational_stats/</guid>
      <description><![CDATA[In this post about the sys.dm_db_index_usage_stats and sys.dm_db_index_operational_stats DMVs, I...]]></description>
      <content:encoded><![CDATA[In <A title="What is the difference between sys.dm_db_index_usage_stats and sys.dm_db_index_operational_stats?" href="https://blogs.msdn.com/craigfr/archive/2008/10/30/what-is-the-difference-between-sys-dm-db-index-usage-stats-and-sys-dm-db-index-operational-stats.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2008/10/30/what-is-the-difference-between-sys-dm-db-index-usage-stats-and-sys-dm-db-index-operational-stats.aspx">this post</A> about the sys.dm_db_index_usage_stats and sys.dm_db_index_operational_stats DMVs, I wrote: 
<BLOCKQUOTE>
<P>Another less important difference between these DMVs is that sys.dm_db_index_usage_stats only reports on indexes that have been used at least once since the server was last restarted while <EM><STRONG>sys.dm_db_index_operational_stats reports on all indexes regardless of whether they have been used</STRONG></EM>.</P></BLOCKQUOTE>
<P>The <A title="sys.dm_db_index_operational_stats (Transact-SQL)" href="https://msdn.microsoft.com/en-us/library/ms174281.aspx" mce_href="https://msdn.microsoft.com/en-us/library/ms174281.aspx">Books Online page</A> for sys.dm_db_index_operational_stats similarly states about the object_id parameter: "Specify NULL to return information <B><EM>for all tables and views</EM></B> in the specified database."&nbsp; However, further down in the same page, it states:</P>
<BLOCKQUOTE>
<P>The data returned by <B>sys.dm_db_index_operational_stats</B> exists only as long as the metadata cache object that represents the heap or index is available. This data is neither persistent nor transactionally consistent. This means you cannot use these counters to determine whether an index has been used or not, or when the index was last used. For information about this, see <A href="https://msdn.microsoft.com/en-us/library/ms188755.aspx" mce_href="https://msdn.microsoft.com/en-us/library/ms188755.aspx">sys.dm_db_index_usage_stats (Transact-SQL)</A>.</P></BLOCKQUOTE>
<P>An observant reader pointed out that the sys.dm_db_index_operational_stats DMV was not behaving as I claimed - that it was not returning all rows - and further pointed out the apparent discrepancy in Books Online.&nbsp; I checked with one of the storage engine developers who confirmed for me that the DMV will only return rows for those objects that are currently in the metadata cache.</P>
<P>The metadata cache does not contain any user tables when the server is first started or, for a database, when it is first attached.&nbsp; Moreover, if a database contains many tables, the cache may not be large enough to store all of them at once and some may be evicted.&nbsp; If a table is evicted, it will cease to appear in the DMV output and its counters will be reset to zero.</P>
<P>Let's look at a simple example.&nbsp; First, let's create a test database:</P>
<BLOCKQUOTE>
<P>CREATE DATABASE DMVTest<BR>GO<BR>USE DMVTest<BR>GO<BR>CREATE TABLE T (A INT, B INT, C INT)<BR>CREATE UNIQUE CLUSTERED INDEX TA ON T(A)<BR>CREATE UNIQUE INDEX TB ON T(B)<BR>GO</P></BLOCKQUOTE>
<P>Since we just created this table, it should be in the metadata cache and should be listed by sys.dm_db_index_operational_stats:</P>
<BLOCKQUOTE>
<P>SELECT index_id, range_scan_count, singleton_lookup_count<BR>FROM sys.dm_db_index_operational_stats (DB_ID(), OBJECT_ID('T'), NULL, NULL)<BR>ORDER BY index_id</P></BLOCKQUOTE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           0                    0
2           0                    0</PRE>
<P mce_keep="true">Let's also run a simple query so that the table appears in sys.dm_db_index_usage_stats.&nbsp; Recall that sys.dm_index_usage_stats only lists tables and indexes that appear in a query plan and only when that query plan is actually executed.</P>
<BLOCKQUOTE>
<P>SELECT * FROM T</P>
<P>SELECT index_id, user_seeks, user_scans, user_lookups, user_updates<BR>FROM sys.dm_db_index_usage_stats<BR>WHERE database_id = DB_ID() and object_id = OBJECT_ID('T')<BR>ORDER BY index_id</P></BLOCKQUOTE><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------
1           0                    1                    0                    0</PRE>
<P mce_keep="true">Now, let's detach and reattach the database to clear the metadata cache:</P>
<BLOCKQUOTE>
<P>USE tempdb<BR>GO<BR>EXEC SP_DETACH_DB 'DMVTest'<BR>GO<BR>EXEC SP_ATTACH_DB 'DMVTest',<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 'C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\DMVTest.mdf',<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 'C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\DMVTest_log.ldf'<BR>GO<BR>USE DMVTest<BR>GO</P></BLOCKQUOTE>
<P>Rechecking the two DMVs shows that the table is not listed in either one:</P>
<BLOCKQUOTE>
<P>SELECT index_id, range_scan_count, singleton_lookup_count<BR>FROM sys.dm_db_index_operational_stats (DB_ID(), OBJECT_ID('T'), NULL, NULL)<BR>ORDER BY index_id</P></BLOCKQUOTE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------</PRE>
<BLOCKQUOTE>
<P>SELECT index_id, user_seeks, user_scans, user_lookups, user_updates<BR>FROM sys.dm_db_index_usage_stats<BR>WHERE database_id = DB_ID() and object_id = OBJECT_ID('T')<BR>ORDER BY index_id</P></BLOCKQUOTE><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------</PRE>
<P>Finally, we can repopulate the metadata cache by compiling a query that references the table.&nbsp; Note that compiling the query is all that is needed.&nbsp; There is no need to execute it.</P>
<BLOCKQUOTE>
<P>SET SHOWPLAN_TEXT ON<BR>GO<BR>SELECT * FROM T<BR>GO<BR>SET SHOWPLAN_TEXT OFF<BR>GO</P></BLOCKQUOTE>
<P>Rechecking sys.dm_db_index_operational_stats one final time shows that the table is once again listed:</P>
<P>SELECT index_id, range_scan_count, singleton_lookup_count<BR>FROM sys.dm_db_index_operational_stats (DB_ID(), OBJECT_ID('T'), NULL, NULL)<BR>ORDER BY index_id</P><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           0                    0
2           0                    0</PRE>
<P>Note that all of the indexes, not just the clustered index (which happens to be used by the table scan in the above query plan), are listed.&nbsp; In general, SQL Server loads all of a table's metadata as part of the compilation process.</P>In this simple example, I used database attach and detach to flush the metadata cache.&nbsp; Of course, SQL Server also starts with an empty metadata cache after a restart and, as I noted above,&nbsp;may evict metadata at any time due to memory pressure.&nbsp; Thus, Books Online is correct in stating that sys.dm_db_index_operational_stats may not offer reliable data regarding a table's past usage.]]></content:encoded>
    </item>
    <item>
      <title>Maximum Row Size and Query Hints</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/maximum-row-size-and-query-hints</link>
      <pubDate>Wed, 24 Jun 2009 12:26:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2009/06/24/maximum-row-size-and-query-hints/</guid>
      <description><![CDATA[In my last post (yes, that was two months ago), I gave an example of how a query hint could cause a...]]></description>
      <content:encoded><![CDATA[<P mce_keep="true">In <A title="Implied Predicates and Query Hints" href="https://blogs.msdn.com/craigfr/archive/2009/04/28/implied-predicates-and-query-hints.aspx">my last post</A> (yes, that was two months ago), I gave an example of how a query hint could cause a query to fail.&nbsp; In this post, I'll give another example of how query hints can cause problems.&nbsp; As with my last post, this post was inspired by a question submitted by a reader.</P>
<P>SQL Server has a <A title="Maximum Capacity Specifications for SQL Server" href="https://msdn.microsoft.com/en-us/library/ms143432.aspx">documented</A> row size limit of 8060 bytes.&nbsp; This <A title="Row-Overflow Data Exceeding 8 KB" href="https://msdn.microsoft.com/en-us/library/ms186981.aspx">row size limit</A> applies specifically to the fixed width or in-row portion of variable width columns.&nbsp; A web search yields plenty of articles and discussions about this limit.&nbsp; If you try to create a table that exceeds this limit, you will encounter the following error:</P>
<BLOCKQUOTE>
<P>CREATE TABLE T (A INT, B CHAR(8000), C CHAR(8000))</P>
<P>Msg 1701, Level 16, State 1, Line 1<BR>Creating or altering table 'T' failed because the minimum row size would be 16011, including 7 bytes of internal overhead. This exceeds the maximum allowable table row size of 8060 bytes.</P></BLOCKQUOTE>
<P>So, what does this row size limit have to do with query hints?&nbsp; Let's start with the following simple join:</P>
<BLOCKQUOTE>
<P>CREATE TABLE T1 (A INT, B CHAR(8000))<BR>CREATE TABLE T2 (A INT, B CHAR(8000))<BR>CREATE TABLE T3 (A INT, B CHAR(8000))</P>
<P>SELECT *<BR>FROM T1 JOIN T2 ON T1.A = T2.A</P></BLOCKQUOTE>
<P>&nbsp; |--Hash Match(Inner Join, HASH:([T2].[A])=([T1].[A]), RESIDUAL:([T2].[A]=[T1].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]))</P>
<P>So far, so good.&nbsp; Now let's suppose that we want to sort the results of this query:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM T1 JOIN T2 ON T1.A = T2.A<BR>ORDER BY T1.A</P></BLOCKQUOTE>
<P>&nbsp; |--Merge Join(Inner Join, MANY-TO-MANY MERGE:([T1].[A])=([T2].[A]), RESIDUAL:([T2].[A]=[T1].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Sort(ORDER BY:([T1].[A] ASC))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Sort(ORDER BY:([T2].[A] ASC))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]))</P>
<P>The optimizer has switched to a merge join.&nbsp; This plan may seem pretty reasonable.&nbsp; SQL Server needs to sort the data, so why not before the join?&nbsp; Recall that <A title="Summary of Join Properties" href="https://blogs.msdn.com/craigfr/archive/2006/08/16/702828.aspx">merge join is order preserving</A> so SQL Server does not need to sort again after the join.&nbsp; However, suppose that we really want the hash join after all.&nbsp; We might be tempted to add a hint forcing it:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM T1 JOIN T2 ON T1.A = T2.A<BR>ORDER BY T1.A<BR>OPTION (HASH JOIN)</P>
<P>Msg 8618, Level 16, State 2, Line 1<BR>The query processor could not produce a query plan because a worktable is required, and its minimum row size exceeds the maximum allowable of 8060 bytes. A typical reason why a worktable is required is a GROUP BY or ORDER BY clause in the query. If the query has a GROUP BY or ORDER BY clause, consider reducing the number and/or size of the fields in the clause. Consider using prefix (LEFT()) or hash (CHECKSUM()) of fields for grouping or prefix for ordering. Note however that this will change the behavior of the query.</P></BLOCKQUOTE>
<P>What happened?&nbsp; Recall that hash join is <B>not</B> order preserving.&nbsp; Thus, SQL Server must sort after the join.&nbsp; The sort requires a work table which must include all columns to be sorted.&nbsp; In this example, the columns include T1.B and T2.B.&nbsp; These two CHAR(8000) columns together exceed the 8060 byte row size limit, SQL Server cannot create the work table, and the query fails.&nbsp; Note that the hash join itself is not a problem (the original query above succeeds with a hash join) but rather it is the sort that causes the failure.</P>
<P>Let's try another example:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM (T1 JOIN T2 ON T1.A = T2.A) JOIN T3 ON T1.A = T3.A</P></BLOCKQUOTE>
<P>&nbsp; |--Nested Loops(Inner Join, WHERE:([T2].[A]=[T3].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Hash Match(Inner Join, HASH:([T3].[A])=([T1].[A]), RESIDUAL:([T1].[A]=[T3].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T3]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]))</P>
<P>Notice that this query uses one hash join and one nested loops join.&nbsp; Suppose that we really want two hash joins.&nbsp; We might again be tempted to use a hint:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM (T1 JOIN T2 ON T1.A = T2.A) JOIN T3 ON T1.A = T3.A<BR>OPTION (HASH JOIN)</P></BLOCKQUOTE>
<P>This query fails with the same 8618 error that we saw above.&nbsp; Why?&nbsp; Like a sort, a hash join also requires a work table.&nbsp; In fact, a <A title="Hash Join" href="https://blogs.msdn.com/craigfr/archive/2006/08/10/687630.aspx">hash join requires work tables</A> for both the build and probe (i.e., left and right inputs).&nbsp; No matter which two tables SQL Server joins first, the results of that join, which includes two CHAR(8000) columns, must be joined with the third and final table.&nbsp; This second join cannot use a hash join as SQL Server cannot build a work table that exceeds the 8060 byte row size limit.</P>
<P>There are many more examples possible where hints may cause a query to fail due to requiring a work table that exceeds the 8060 byte row size limit.&nbsp; One solution to this problem is, of course, to remove the offending hint. &nbsp;Another solution is to replace large fixed length CHAR columns with variable length VARCHAR columns.</P>]]></content:encoded>
    </item>
    <item>
      <title>Implied Predicates and Query Hints</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/implied-predicates-and-query-hints</link>
      <pubDate>Tue, 28 Apr 2009 17:45:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2009/04/28/implied-predicates-and-query-hints/</guid>
      <description><![CDATA[In this post, I want to take a look at how two seemingly unrelated features of SQL Server can...]]></description>
      <content:encoded><![CDATA[<P>In this post, I want to take a look at how two seemingly unrelated features of SQL Server can interact to cause a problem.&nbsp; The idea for this post came from a question submitted by a reader.&nbsp; Let's begin.&nbsp; Consider the following trivial schema and query:</P>
<BLOCKQUOTE>
<P>CREATE TABLE T1 (A INT, B INT)<BR>CREATE TABLE T2 (A INT, B INT)</P></BLOCKQUOTE>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM T1 INNER JOIN T2 ON T1.A = T2.A<BR>WHERE T1.B = 0<BR>OPTION (HASH JOIN)</P></BLOCKQUOTE>
<P>Not surprisingly, this query yields the following plan:</P>
<P>&nbsp; |--Hash Match(Inner Join, HASH:([T1].[A])=([T2].[A]), RESIDUAL:([T2].[A]=[T1].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]), WHERE:([T1].[B]=(0)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]))</P>
<P>In fact, this query yields this plan with or without the hint.&nbsp; Now let's make a small modification to the WHERE clause of the query see what happens:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM T1 INNER JOIN T2 ON T1.A = T2.A<BR><B>WHERE</B><B> T1.A = 0<BR></B>OPTION (HASH JOIN)</P></BLOCKQUOTE>
<P>Now this query yields the following error message:</P>
<BLOCKQUOTE>
<P>Msg 8622, Level 16, State 1, Line 1</P>
<P>Query processor could not produce a query plan because of the hints defined in this query. Resubmit the query without specifying any hints and without using SET FORCEPLAN.</P></BLOCKQUOTE>
<P>What happened?&nbsp; Why does this seemingly innocuous change to the query cause it to fail?&nbsp; To find the answer, let's run the query without the HASH JOIN hint and look at the plan:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM T1 INNER JOIN T2 ON T1.A = T2.A<BR>WHERE T1.A = 0</P></BLOCKQUOTE>
<P>&nbsp; |--Nested Loops(Inner Join)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]), <B>WHERE:([T2].[A]=(0))</B>)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]), WHERE:([T1].[A]=(0)))</P>
<P>There are two things about this plan that are especially notable:</P>
<P>First, the plan includes the predicate "T2.A = 0".&nbsp; Although we did not specify this predicate in the original query, SQL Server derives it from the predicates that we did specify.&nbsp; This derivation is a good thing.&nbsp; It allows SQL Server to filter out rows from the scan of T2 earlier than would otherwise be possible.</P>
<P>Second, the original equijoin predicate "T1.A = T2.A" appears nowhere in this plan.&nbsp; This predicate is redundant with the original predicate "T1.A = 0" and the derived predicate "T2.A = 0" so SQL Server eliminates it.&nbsp; Normally, eliminating a redundant predicate is a good thing.&nbsp; By the time the rows from the scans reach the join, there is no reason to evaluate this predicate.&nbsp; It would be a waste of time.&nbsp; Unfortunately, in this case, the eliminated predicate also happens to be the only equijoin predicate and hash joins (and merge joins) <A title="Summary of Join Properties" href="https://blogs.msdn.com/craigfr/archive/2006/08/16/702828.aspx">require at least one equijoin predicate</A>.&nbsp; Thus, the query with the HASH JOIN hint fails.</P>
<P>Note that the loss of the hash join and merge join alternatives for the above plan is not a big deal from a performance perspective.&nbsp; With or without the join predicate, the query is a cross join since all rows from T1 will join with all rows from T2.</P>
<P>If we throw in a third, briefly lived feature of SQL Server 2008, the situation gets even more complex.&nbsp; SQL Server 2008 RTM has an optimization that substitutes constants for parameter and variable values if we use the RECOMPILE hint.&nbsp; Note that this optimization was removed from SQL Server 2008 Cummulative Update 4 and Service Pack 1 to <A href="https://support.microsoft.com/kb/968693/">fix</A> an issue.&nbsp; If your instance of SQL Server has the fix, you will not be able to reproduce the following scenario.</P>
<P>Let's see a simple example of this optimization in action.&nbsp; Compare the plans for the following two identical queries:</P>
<BLOCKQUOTE>
<P>DECLARE @P INT<BR>SET @P = 0</P>
<P mce_keep="true">SELECT * FROM T1 WHERE T1.A = @P</P></BLOCKQUOTE>
<P mce_keep="true">&nbsp; |--Table Scan(OBJECT:([T1]), <B>WHERE:([T1].[A]=[@P])</B>)</P>
<BLOCKQUOTE>
<P mce_keep="true">SELECT * FROM T1 WHERE T1.A = @P<BR>OPTION (RECOMPILE)</P></BLOCKQUOTE>
<P>&nbsp; |--Table Scan(OBJECT:([T1]), <B>WHERE:([T1].[A]=(0))</B>)</P>
<P>Observe how the WHERE clause in the first plan references the @P variable while the WHERE clause in the second plan which includes the RECOMPILE hint references the constant 0.&nbsp; This optimization is safe since the plan generated for the query with the RECOMPILE hint will be used only once and then discarded rather than cached.</P>
<P>The preceding plans are "actual" rather than "estimated" query plans.&nbsp; That is, I collected them using SET STATISTICS PROFILE ON rather than SET SHOWPLAN_TEXT ON.&nbsp; The parameter substitution optimization can only be applied when the query is recompiled immediately prior to execution and the actual parameter values are known.</P>
<P>Now let's create a simple stored procedure:</P>
<BLOCKQUOTE>
<P>CREATE PROCEDURE MY_SP (@P1 INT, @P2 INT)<BR>AS<BR>SELECT *<BR>FROM T1 INNER JOIN T2 ON T1.A = T2.A<BR>WHERE T1.A BETWEEN @P1 AND @P2<BR>OPTION (HASH JOIN, RECOMPILE)</P></BLOCKQUOTE>
<P>If we run this stored procedure with two different parameters, it works just fine:</P>
<BLOCKQUOTE>
<P>EXEC MY_SP 0, 1</P></BLOCKQUOTE>
<P>Here is the query plan:</P>
<P>&nbsp; |--Hash Match(Inner Join, HASH:([T1].[A])=([T2].[A]), RESIDUAL:([T2].[A]=[T1].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]), WHERE:([T1].[A]&gt;=(0) AND [T1].[A]&lt;=(1)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]), WHERE:([T2].[A]&gt;=(0) AND [T2].[A]&lt;=(1)))</P>
<P mce_keep="true">However, if we run the stored procedure with the same parameter, it fails with the same error that we saw at the beginning of this post.&nbsp; For example, the following fails:</P>
<BLOCKQUOTE>
<P>EXEC MY_SP 0, 0</P></BLOCKQUOTE>
<BLOCKQUOTE>
<P>Msg 8622, Level 16, State 1, Line 1</P>
<P>Query processor could not produce a query plan because of the hints defined in this query. Resubmit the query without specifying any hints and without using SET FORCEPLAN.</P></BLOCKQUOTE>
<P>Why does the stored procedure work with some parameters but not with others?&nbsp; If we alter the stored to remove the HASH JOIN hint, we can see what has happened:</P>
<BLOCKQUOTE>
<P>ALTER PROCEDURE MY_SP (@P1 INT, @P2 INT)<BR>AS<BR>SELECT *<BR>FROM T1 INNER JOIN T2 ON T1.A = T2.A<BR>WHERE T1.A BETWEEN @P1 AND @P2<BR>OPTION (RECOMPILE)</P></BLOCKQUOTE>
<BLOCKQUOTE>
<P>EXEC MY_SP 0, 0</P></BLOCKQUOTE>
<P>&nbsp; |--Nested Loops(Inner Join)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T2]), WHERE:([T2].[A]=(0)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Table Scan(OBJECT:([T1]), WHERE:([T1].[A]=(0)))</P>
<P mce_keep="true">SQL Server has transformed the predicate "T1.A BETWEEN @P1 AND @P2" first into "T1.A BETWEEN 0 AND 0" and then into "T1.A = 0".&nbsp; Now we have exactly the same scenario that we had in the first failed query above.</P>Query hints can be powerful tools, but they can also backfire - often in mysterious ways.&nbsp; In these examples, two and even three separate features all interact to induce the error.&nbsp; Even a small change to a query or to a stored procedure's parameters can be the difference between success and failure.]]></content:encoded>
    </item>
    <item>
      <title>OPTIMIZED Nested Loops Joins</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/optimized-nested-loops-joins</link>
      <pubDate>Wed, 18 Mar 2009 10:34:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2009/03/18/optimized-nested-loops-joins/</guid>
      <description><![CDATA[In my past two posts, I explained how SQL Server may add a sort to the outer side of a nested loops...]]></description>
      <content:encoded><![CDATA[In my past two posts, I explained how SQL Server may add a sort to the outer side of a nested loops join and showed how <A title="Optimizing I/O Performance by Sorting - Part 2" href="https://blogs.msdn.com/craigfr/archive/2009/03/04/optimizing-i-o-performance-by-sorting-part-2.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2009/03/04/optimizing-i-o-performance-by-sorting-part-2.aspx">this sort can significantly improve performance</A>.&nbsp; In <A title="Random Prefetching" href="https://blogs.msdn.com/craigfr/archive/2008/10/07/random-prefetching.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2008/10/07/random-prefetching.aspx">an earlier post</A>, I discussed how SQL Server can use random prefetching to improve the performance of a nested loops join.&nbsp; In this post, I'm going to explore one more nested loops join performance feature.&nbsp; I'll use the same database that I used in <A title="Optimizing I/O Performance by Sorting - Part 1" href="https://blogs.msdn.com/craigfr/archive/2009/02/25/optimizing-i-o-performance-by-sorting-part-1.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2009/02/25/optimizing-i-o-performance-by-sorting-part-1.aspx">my two prior posts</A>.&nbsp; Let's start with the following simple query: 
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T<BR>WHERE RandKey &lt; 1000</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1011]=(0) THEN NULL ELSE [Expr1012] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1011]=COUNT_BIG([T].[Data]), [Expr1012]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK], [Expr1010]) <B>OPTIMIZED</B> WITH UNORDERED PREFETCH)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (1000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P>Notice that the nested loops join includes an extra keyword: OPTIMIZED.&nbsp; This keyword indicates that the nested loops join may try to reorder the input rows to improve I/O performance.&nbsp; This behavior is similar to the explicit sorts that we saw in my two previous posts, but unlike a full sort it is more of a best effort.&nbsp; That is, the results from an optimized nested loops join may not be (and in fact are highly unlikely to be) fully sorted.</P>
<P>SQL Server only uses an optimized nested loops join when the optimizer concludes based on its cardinality and cost estimates that a sort is most likely not required, but where there is still a possibility&nbsp;&nbsp; that a sort could be helpful in the event that the cardinality or cost estimates are incorrect.&nbsp; In other words, an optimized nested loops join may be thought of as a "safety net" for those cases where SQL Server chooses a nested loops join but would have done better to have chosen an alternative plan such as a full scan or a nested loops join with an explicit sort.&nbsp; For the above query which only joins a few rows, the optimization is unlikely to have any impact at all.</P>
<P>Let's look at an example where the optimization actually helps:</P>
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T<BR>WHERE RandKey &lt; 100000000 AND<BR>&nbsp;&nbsp;&nbsp; Flags &amp; 0x1 = 0x1 AND<BR>&nbsp;&nbsp;&nbsp; Flags &amp; 0x2 = 0x2 AND<BR>&nbsp;&nbsp;&nbsp; Flags &amp; 0x4 = 0x4 AND<BR>&nbsp;&nbsp;&nbsp; Flags &amp; 0x8 = 0x8</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1014]=(0) THEN NULL ELSE [Expr1015] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1014]=COUNT_BIG([T].[Data]), [Expr1015]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK], [Expr1013]) <B>OPTIMIZED</B> WITH UNORDERED PREFETCH)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (100000000)),&nbsp; WHERE:(([T].[Flags]&amp;(1))=(1) AND ([T].[Flags]&amp;(2))=(2) AND ([T].[Flags]&amp;(4))=(4) AND ([T].[Flags]&amp;(8))=(8)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P>The Flags column contains the value 0xFF in every row.&nbsp; Thus, every one of the bitwise AND predicates evaluates to true and this query returns about 2.5 million rows or 10% of the table.&nbsp; Ordinarily, when faced with a query like this one, SQL Server would resort to a sequential scan of the entire table.&nbsp; Indeed, if you try this query without the extra bitwise filters, you will get a sequential scan.&nbsp; However, SQL Server does not realize that these predicates are always true, estimates a much lower cardinality of less than 10,000 rows, and chooses a simple nested loops join plan.&nbsp; Note that I would generally recommend against using predicates like these ones in a real world application precisely because they will lead to cardinality estimation errors and poor plans.</P>
<P>To see what effect the optimized nested loops join has, let's compare the above plan with an "un-optimized" nested loops join.&nbsp; We can eliminate the optimization by using the following UPDATE STATISTICS statement to trick SQL Server into believing that the table is very small:</P>
<BLOCKQUOTE>
<P>UPDATE STATISTICS T WITH ROWCOUNT = 1, PAGECOUNT = 1</P></BLOCKQUOTE>
<P>I'll compare the above query with the following simpler query which uses essentially the same plan and touches the same data but has an "un-optimized" nested loops join:</P>
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T WITH (INDEX (IRandKey))<BR>WHERE RandKey &lt; 100000000</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1009]=(0) THEN NULL ELSE [Expr1010] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1009]=COUNT_BIG([T].[Data]), [Expr1010]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (100000000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P>We can reset the statistics using the following statement:</P>
<BLOCKQUOTE>
<P>UPDATE STATISTICS T WITH ROWCOUNT = 25600000, PAGECOUNT = 389323</P></BLOCKQUOTE>
<P>As in my last post, I'm going to simulate a larger table by reducing the memory available to the server to 1 GByte with SP_CONFIGURE 'MAX SERVER MEMORY' and I'm also going to flush the buffer pool between runs with DBCC DROPCLEANBUFFERS.</P>
<P><B>Note that you will NOT want to run these statements on a production server.</B></P>
<P>I ran both of the above queries with three different constants.&nbsp; Here are my results.&nbsp; Keep in mind that these results depend greatly on the specific hardware.&nbsp; If you try this experiment, your results may vary.</P>
<TABLE border=1 cellSpacing=0 cellPadding=0>
<TBODY>
<TR>
<TD vAlign=top rowSpan=2 width=255 colSpan=2>
<P mce_keep="true">&nbsp;</P></TD>
<TD vAlign=top width=255 colSpan=2>
<P align=center><B>Execution Time</B></P></TD>
<TD vAlign=bottom rowSpan=2 width=128>
<P align=right><B>Increase</B></P></TD></TR>
<TR>
<TD vAlign=top width=128>
<P align=right><B>OPTIMIZED</B></P></TD>
<TD vAlign=top width=128>
<P align=right><B>"un-OPTIMIZED"</B></P></TD></TR>
<TR>
<TD rowSpan=3 width=128>
<P><B>Constant</B></P></TD>
<TD vAlign=top width=128>
<P><B>10,000,000<BR>(1% of rows)</B></P></TD>
<TD vAlign=bottom width=128>
<P align=right>6.5 minutes</P></TD>
<TD vAlign=bottom width=128>
<P align=right>26 minutes</P></TD>
<TD vAlign=bottom width=128>
<P align=right>4x</P></TD></TR>
<TR>
<TD vAlign=top width=128>
<P><B>100,000,000<BR>(10% of rows)</B></P></TD>
<TD vAlign=bottom width=128>
<P align=right>10.4 minutes</P></TD>
<TD vAlign=bottom width=128>
<P align=right>4.3 hours</P></TD>
<TD vAlign=bottom width=128>
<P align=right>25x</P></TD></TR>
<TR>
<TD vAlign=top width=128>
<P><B>250,000,000<BR>(25% of rows)</B></P></TD>
<TD vAlign=bottom width=128>
<P align=right>11.3 minutes</P></TD>
<TD vAlign=bottom width=128>
<P align=right>10.6 hours</P></TD>
<TD vAlign=bottom width=128>
<P align=right>56x</P></TD></TR></TBODY></TABLE>
<P mce_keep="true">Clearly the optimized nested loops join can have a huge impact on performance.&nbsp; Moreover, as the plan touches more rows the benefit of the optimization grows dramatically.&nbsp; Although a full scan or a nested loops join with an explicit sort would be faster, the optimized nested loops join really is a safety net protecting against a much worse alternative.</P>]]></content:encoded>
    </item>
    <item>
      <title>Optimizing I/O Performance by Sorting – Part 2</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/optimizing-io-performance-by-sorting-part-2</link>
      <pubDate>Wed, 04 Mar 2009 12:27:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2009/03/04/optimizing-io-performance-by-sorting-part-2/</guid>
      <description><![CDATA[In my last post, I discussed how SQL Server can use sorts to transform random I/Os into sequential...]]></description>
      <content:encoded><![CDATA[<P>In my last post, I discussed how SQL Server can use sorts to transform random I/Os into sequential I/Os.&nbsp; In this post, I'll demonstrate directly how such a sort can impact performance.&nbsp; For the following experiments, I'll use the same 3 GByte database that I created <A title="Optimizing I/O Performance by Sorting - Part 1" href="https://blogs.msdn.com/craigfr/archive/2009/02/25/optimizing-i-o-performance-by-sorting-part-1.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2009/02/25/optimizing-i-o-performance-by-sorting-part-1.aspx">last week</A>.</P>
<P>The system I'm using to run this test has 8 GBytes of memory.&nbsp; To exaggerate the performance effects and simulate an even larger table that does not fit in main memory, I'm going to adjust the ‘MAX SERVER MEMORY' SP_CONFIGURE option to allow SQL Server to use just 1 GByte of memory.&nbsp; I'm going to use CHECKPOINT to ensure that the newly created database is completely flushed to disk before running any experiments.&nbsp; Finally, I'm going to run DBCC DROPCLEANBUFFERS before each test to ensure that none of the data is cached in the buffer pool between tests.</P>
<BLOCKQUOTE>
<P>CHECKPOINT</P>
<P mce_keep="true">EXEC SP_CONFIGURE 'SHOW ADVANCED OPTIONS', '1'<BR>RECONFIGURE<BR>EXEC SP_CONFIGURE 'MAX SERVER MEMORY', '1024'<BR>RECONFIGURE</P>
<P mce_keep="true">DBCC DROPCLEANBUFFERS</P></BLOCKQUOTE>
<P><B>Note that you will NOT want to run these statements on a production server.</B></P>
<P>As I discussed last week, SQL Server can use one of three plans for the following query depending on the value of the constant:</P>
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T<BR>WHERE RandKey &lt; <I>constant</I></P></BLOCKQUOTE>
<P>To recap, if the constant is small, SQL Server uses a non-clustered index seek and a bookmark lookup.&nbsp; If the constant is large, SQL Server uses a clustered index scan to avoid performing many random I/Os.&nbsp; Finally,&nbsp; if the constant is somewhere in the middle, SQL Server uses the non-clustered index seek but sorts the rows prior to performing the bookmark lookup to reduce the number of random I/Os.&nbsp; You can review last week's post to see examples of each of these plans.&nbsp; I'm going to focus on the third and final plan with the sort.</P>
<P>To demonstrate the benefit of the sort, I need to be able to run the same query with and without the sort.&nbsp; A simple way to make SQL Server remove the sort is to use the following UPDATE STATISTICS statement to trick SQL Server into believing that the table is really small.&nbsp; To ensure that I still get the plan with the non-clustered index seek and the bookmark lookup, I need to add an INDEX hint.&nbsp; I'm also adding a RECOMPILE query hint to ensure that SQL Server generates a new plan after I've altered the statistics.</P>
<BLOCKQUOTE>
<P>UPDATE STATISTICS T WITH ROWCOUNT = 1, PAGECOUNT = 1</P>
<P>SELECT SUM(Data)<BR>FROM T WITH (INDEX (IRandKey))<BR>WHERE RandKey &lt; <I>constant<BR></I>OPTION (RECOMPILE)</P></BLOCKQUOTE>
<P>I can also reset the statistics using the following statement:</P>
<BLOCKQUOTE>
<P>UPDATE STATISTICS T WITH ROWCOUNT = 25600000, PAGECOUNT = 389323</P></BLOCKQUOTE>
<P>Here is an example of the default plan with the real statistics and with the sort:</P>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1010]=(0) THEN NULL ELSE [Expr1011] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1010]=COUNT_BIG([T].[Data]), [Expr1011]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK], [Expr1009]) WITH UNORDERED PREFETCH)<BR><B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Sort(ORDER BY:([T].[PK] ASC))<BR></B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (2000000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P mce_keep="true">Here is an example of the plan after running UPDATE STATISTICS and without the sort:</P>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1009]=(0) THEN NULL ELSE [Expr1010] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1009]=COUNT_BIG([T].[Data]), [Expr1010]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (2000000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P mce_keep="true">Here are my results running this query with two values of the constant both with and without the sort.&nbsp; Keep in mind that these results depend greatly on the specific hardware.&nbsp; If you try this experiment, your results may vary.</P>
<TABLE border=1 cellSpacing=0 cellPadding=0>
<TBODY>
<TR>
<TD vAlign=top rowSpan=2 width=255 colSpan=2>
<P mce_keep="true">&nbsp;</P></TD>
<TD vAlign=top width=255 colSpan=2>
<P align=center><B>Execution Time</B></P></TD>
<TD vAlign=bottom rowSpan=2 width=128>
<P align=right><B>% Increase</B></P></TD></TR>
<TR>
<TD vAlign=top width=128>
<P align=right><B>with Sort</B></P></TD>
<TD vAlign=top width=128>
<P align=right><B>with<I>out</I> Sort</B></P></TD></TR>
<TR>
<TD rowSpan=2 width=128>
<P><B>Constant</B></P></TD>
<TD vAlign=top width=128>
<P><B>2,000,000<BR>(0.2% of rows)</B></P></TD>
<TD vAlign=bottom width=128>
<P align=right>91 seconds</P></TD>
<TD vAlign=bottom width=128>
<P align=right>352 seconds</P></TD>
<TD vAlign=bottom width=128>
<P align=right>286%</P></TD></TR>
<TR>
<TD vAlign=top width=128>
<P><B>4,000,000<BR>(0.4% of rows)</B></P></TD>
<TD vAlign=bottom width=128>
<P align=right>97 seconds</P></TD>
<TD vAlign=bottom width=128>
<P align=right>654 seconds</P></TD>
<TD vAlign=bottom width=128>
<P align=right>574%</P></TD></TR>
<TR>
<TD vAlign=top width=128>
<P><B>% Increase</B></P></TD>
<TD vAlign=top width=128>
<P>100%</P></TD>
<TD vAlign=top width=128>
<P align=right>6%</P></TD>
<TD vAlign=top width=128>
<P align=right>86%</P></TD>
<TD vAlign=top width=128>
<P mce_keep="true">&nbsp;</P></TD></TR></TBODY></TABLE>
<P mce_keep="true">There are a two points worth noting regarding these results.&nbsp; First, it should be very clear that the plan with the sort is significantly faster (up to 7 times faster) than the plan without the sort.&nbsp; This result clearly shows the benefit of sequential vs. random I/Os.&nbsp; Second, doubling the number of rows touched had hardly any effect on the execution time for the plan with the sort but nearly doubled the execution time for the plan without the sort.&nbsp; Adding additional I/Os to the plan with the sort adds only a small incremental cost since the I/Os are sequential and the disk head will pass over the required data exactly once either way.&nbsp; Adding additional I/Os to the plan without the sort adds additional disk seeks and increases the execution time proportionately to the increase in the number of rows.&nbsp; In fact, if the constant is increased further, the execution time of the plan with the sort will continue to increase only gradually with the execution time of the plan without the sort will continue to increase rapidly.</P>]]></content:encoded>
    </item>
    <item>
      <title>Optimizing I/O Performance by Sorting – Part 1</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/optimizing-io-performance-by-sorting-part-1</link>
      <pubDate>Wed, 25 Feb 2009 12:29:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2009/02/25/optimizing-io-performance-by-sorting-part-1/</guid>
      <description><![CDATA[In this post from last year, I discussed how random I/Os are slower than sequential I/Os...]]></description>
      <content:encoded><![CDATA[In <A title="Sequential Read Ahead" href="https://blogs.msdn.com/craigfr/archive/2008/09/23/sequential-read-ahead.aspx">this post</A> from last year, I discussed how random I/Os are slower than sequential I/Os (particularly for conventional rotating hard drives).&nbsp; For this reason, SQL Server often favors query plans that perform sequential scans of an entire table over plans that perform random lookups of only a portion of a table.&nbsp; (See the last example in <A title="Index Examples and Tradeoffs" href="https://blogs.msdn.com/craigfr/archive/2006/07/13/664902.aspx">this post</A> for a simple demonstration.)&nbsp; In other cases, instead of performing a sequential scan, SQL Server introduces a sort operator whose sole purpose is to convert random I/Os into sequential I/Os. 
<P>Let's look at an example of such a sort.&nbsp; To measure the performance effects, we'll need a reasonably large table.&nbsp; The following script creates a 25.6 million row table that consumes about 3 GBytes of storage.</P>
<BLOCKQUOTE>
<P>CREATE DATABASE IOTest<BR>&nbsp;&nbsp;&nbsp; ON ( NAME = IOTest_Data, FILENAME = '...\IOTest_Data.mdf', SIZE = 4 GB )<BR>&nbsp;&nbsp;&nbsp; LOG ON ( NAME = IOTest_Log, FILENAME = '...\IOTest_Log.ldf', SIZE = 200 MB )<BR>GO<BR>ALTER DATABASE IOTest SET RECOVERY SIMPLE<BR>GO<BR>USE IOTest<BR>GO<BR>CREATE TABLE T (<BR>&nbsp;&nbsp;&nbsp; PK INT IDENTITY PRIMARY KEY,<BR>&nbsp;&nbsp;&nbsp; RandKey INT,<BR>&nbsp;&nbsp;&nbsp; Flags TINYINT,<BR>&nbsp;&nbsp;&nbsp; Data INT,<BR>&nbsp;&nbsp;&nbsp; Pad CHAR(100))<BR>GO<BR>SET NOCOUNT ON<BR>DECLARE @I INT<BR>SET @I = 0<BR>WHILE @I &lt; 100000<BR>&nbsp; BEGIN<BR>&nbsp;&nbsp;&nbsp; WITH<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X2 (R) AS ( SELECT RAND() UNION ALL SELECT RAND() ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X4 (R) AS ( SELECT R FROM X2 UNION ALL SELECT R FROM X2 ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X8 (R) AS ( SELECT R FROM X4 UNION ALL SELECT R FROM X4 ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X16 (R) AS ( SELECT R FROM X8 UNION ALL SELECT R FROM X8 ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X32 (R) AS ( SELECT R FROM X16 UNION ALL SELECT R FROM X16 ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X64 (R) AS ( SELECT R FROM X32 UNION ALL SELECT R FROM X32 ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X128 (R) AS ( SELECT R FROM X64 UNION ALL SELECT R FROM X64 ),<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X256 (R) AS ( SELECT R FROM X128 UNION ALL SELECT R FROM X128 )<BR>&nbsp;&nbsp;&nbsp; INSERT T (RandKey, Flags, Data, Pad)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SELECT R * 1000000000, 0xFF, 1, '' FROM X256<BR>&nbsp;&nbsp;&nbsp; SET @I = @I + 1<BR>&nbsp; END<BR>GO<BR>CREATE INDEX IRandKey on T (RandKey, Flags)<BR>GO</P></BLOCKQUOTE>
<P>Due to the fixed width Pad column, each row of T consumes 113 bytes (plus overhead).&nbsp; Roughly 65 rows fit on a single 8 Kbyte page.&nbsp; (The Flags column is unused in this example, but I will make use of it in a subsequent post.)</P>
<P>The RandKey column, as the name suggests, contains random values.&nbsp; Notice that we have a non-clustered index on this column.&nbsp; Given a predicate on the RandKey column, SQL Server can use this index to fetch qualifying rows from the table.&nbsp; However, because the values in this column are random, the selected rows will be scattered randomly throughout the clustered index.</P>
<P>If we select just a few rows from the table using a filter on RandKey, SQL Server will use the non-clustered index:</P>
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T<BR>WHERE RandKey &lt; 1000</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1011]=(0) THEN NULL ELSE [Expr1012] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1011]=COUNT_BIG([T].[Data]), [Expr1012]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK], [Expr1010]) OPTIMIZED WITH UNORDERED PREFETCH)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (1000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P>The non-clustered index seek selects a few rows (the use of random keys means that the exact number may vary each time the table is loaded) and <A title="Bookmark Lookup" href="https://blogs.msdn.com/craigfr/archive/2006/06/30/652639.aspx">looks them up in the clustered index</A> to get the value of the Data column for the SUM aggregate.&nbsp; The non-clustered index seek is very efficient - it likely touches only one page - but the clustered index seek generates a random I/O for each row.</P>
<P>If we select a large number of rows, SQL Server recognizes that the random I/Os are too expensive and switches to a clustered index scan:</P>
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T<BR>WHERE RandKey &lt; 10000000</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1009]=(0) THEN NULL ELSE [Expr1010] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1009]=COUNT_BIG([T].[Data]), [Expr1010]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Scan(OBJECT:([T].[PK__T__...]), WHERE:([T].[RandKey]&lt;(10000000)))</P>
<P>This query touches only 1% of the data.&nbsp; Still, the query is going to touch more than half of the pages in the clustered index so it is faster to scan the entire clustered index than to perform on the order of 256,000 random I/Os.</P>
<P>Somewhere in between these two extremes things get a little more interesting:</P>
<BLOCKQUOTE>
<P>SELECT SUM(Data)<BR>FROM T<BR>WHERE RandKey &lt; 2500000</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1010]=(0) THEN NULL ELSE [Expr1011] END))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(DEFINE:([Expr1010]=COUNT_BIG([T].[Data]), [Expr1011]=SUM([T].[Data])))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[PK], [Expr1009]) WITH UNORDERED PREFETCH)<BR><B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Sort(ORDER BY:([T].[PK] ASC))<BR></B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([T].[IRandKey]), SEEK:([T].[RandKey] &lt; (2500000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([T].[PK__T__...]), SEEK:([T].[PK]=[T].[PK]) LOOKUP ORDERED FORWARD)</P>
<P>This query touches a mere 0.25% of the data.&nbsp; The plan uses the non-clustered index to avoid unnecessarily touching many rows.&nbsp; Yet, performing 64,000 random I/Os is still rather expensive so SQL Server adds a sort.&nbsp; By sorting the rows on the clustered index key, SQL Server transforms the random I/Os into sequential I/Os.&nbsp; Thus, we get the efficiency of the seek - touching only those rows that qualify - with the performance of the sequential scan.</P>
<P>It is worth pointing out that sorting on the clustered index key will yield rows that are in the logical index order.&nbsp; Due to fragmentation or due simply to the multiple layers of abstraction between SQL Server and the actual hard drives, there is no guarantee that the physical order on disk matches the logical order.</P>In my next post, I'll run some of these queries and demonstrate the performance implications of the sort.]]></content:encoded>
    </item>
    <item>
      <title>What is the difference between sys.dm_db_index_usage_stats and sys.dm_db_index_operational_stats?</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/what-is-the-difference-between-sys-dm_db_index_usage_stats-and-sys-dm_db_index_operational_stats</link>
      <pubDate>Thu, 30 Oct 2008 17:40:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2008/10/30/what-is-the-difference-between-sys-dm_db_index_usage_stats-and-sys-dm_db_index_operational_stats/</guid>
      <description><![CDATA[SQL Server includes two DMVs - sys.dm_db_index_usage_stats and sys.dm_db_index_operational_stats -...]]></description>
      <content:encoded><![CDATA[<P>SQL Server includes two DMVs - sys.dm_db_index_usage_stats and sys.dm_db_index_operational_stats - that are extremely useful for monitoring which indexes are used as well as how and when they are used.&nbsp; Both DMVs report similar statistics on information such as the number of scans, seeks, and updates to different indexes.&nbsp; These DMVs are documented in Books Online (see <A title=sys.dm_db_index_usage_stats href="https://msdn.microsoft.com/en-us/library/ms188755.aspx" mce_href="https://msdn.microsoft.com/en-us/library/ms188755.aspx">here</A> and <A title=sys.dm_db_index_operational_stats href="https://msdn.microsoft.com/en-us/library/ms174281.aspx" mce_href="https://msdn.microsoft.com/en-us/library/ms174281.aspx">here</A>) and a simple Web search reveals numerous other postings about these DMVs.&nbsp; However, in my own search, I did not find many direct explanations of the difference between these two valuable DMVs.&nbsp; (You will find a short explanation halfway through <A title="How can SQL Server 2005 help me evaluate and manage indexes?" href="https://blogs.msdn.com/sqlcat/archive/2006/02/13/531339.aspx" mce_href="https://blogs.msdn.com/sqlcat/archive/2006/02/13/531339.aspx">this post</A> on the Microsoft SQL Server Customer Advisory Team blog.)</P>
<P>The main difference between these DMVs is simple but important:</P>
<P>sys.dm_db_index_usage_stats records how many times the query optimizer uses an index in a plan.&nbsp; This usage information is recorded again each time the plan is executed.&nbsp; (Compiling a plan alone is not sufficient to record an index's usage.)&nbsp; However, and this is the important part, for the purposes of computing the statistics, it does matter how many times the query processor executes the specific operator that references the index.&nbsp; For that matter, it does not matter whether the query processor executes the operator at all.&nbsp; Mere execution of the plan counts as a single usage for each index used by the plan.</P>
<P>sys.dm_db_index_operational_stats records how many times the storage engine executes a specific operation on the index.&nbsp; These statistics do depend on how many times the query processor executes each operator.&nbsp; If an operator is never executed, the storage engine does not perform any operations on the index and the DMV reports that the index was not used.&nbsp; If an operator is executed multiple times, the storage engine performs multiple operations on the index and the DMV reports that the index was used multiple times.</P>
<P><STRONG>Update (7/29/2009): The following paragraph is incorrect.&nbsp; See </STRONG><A title="Correction to my prior post on sys.dm_db_index_operational_sta" href="https://blogs.msdn.com/craigfr/archive/2009/07/29/correction-to-my-prior-post-on-sys-dm-db-index-operational-stats.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2009/07/29/correction-to-my-prior-post-on-sys-dm-db-index-operational-stats.aspx"><STRONG>this post</STRONG></A><STRONG> for more information.</STRONG></P>
<P><STRIKE>(Another less important difference between these DMVs is that sys.dm_db_index_usage_stats only reports on indexes that have been used at least once since the server was last restarted while sys.dm_db_index_operational_stats reports on all indexes regardless of whether they have been used.)</STRIKE></P>
<P>Let's try an example to see this difference in action.&nbsp; I'll use the following simple schema:</P>
<BLOCKQUOTE>
<P>CREATE TABLE T (A INT, B INT, C INT)<BR>CREATE UNIQUE CLUSTERED INDEX TA ON T(A)<BR>CREATE UNIQUE INDEX TB ON T(B)</P></BLOCKQUOTE>
<P>As expected, immediately after creating this table, the stats are zero (or just non-existent):</P>
<BLOCKQUOTE>
<P>SELECT index_id, user_seeks, user_scans, user_lookups, user_updates<BR>FROM sys.dm_db_index_usage_stats<BR>WHERE database_id = DB_ID('tempdb') and object_id = OBJECT_ID('tempdb..t')<BR>ORDER BY index_id</P>
<P>SELECT index_id, range_scan_count, singleton_lookup_count<BR>FROM sys.dm_db_index_operational_stats (DB_ID('tempdb'), OBJECT_ID('tempdb..t'), NULL, NULL)<BR>ORDER BY index_id</P></BLOCKQUOTE><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------</PRE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           0                    0
2           0                    0</PRE>
<P>Now suppose that we do a scan of the clustered index:</P>
<BLOCKQUOTE>
<P>SELECT * FROM T</P></BLOCKQUOTE>
<P>&nbsp; |--Clustered Index Scan(OBJECT:([tempdb].[dbo].[T].[TA]))</P>
<P>Repeating the DMV queries, we see that the clustered index shows one scan in both DMVs.&nbsp; SQL Server records the scan even though the table contains no rows and the query returns an empty result:</P><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------
1           0                    1                    0                    0</PRE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           1                    0
2           0                    0</PRE>
<P>Next let's try a singleton lookup on the clustered index:</P>
<BLOCKQUOTE>
<P>SELECT * FROM T WHERE A = 1</P></BLOCKQUOTE>
<P>&nbsp; |--Clustered Index Seek(OBJECT:([tempdb].[dbo].[T].[TA]), SEEK:([tempdb].[dbo].[T].[A]=CONVERT_IMPLICIT(int,[@1],0)) ORDERED FORWARD)</P>
<P>Again the table contains no rows and the query returns an empty result.&nbsp; Nevertheless, the DMVs now report one seek and one singleton lookup:</P><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------
1           1                    1                    0                    0</PRE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           1                    1
2           0                    0</PRE>
<P>(Keep in mind that the DMV results are cumulative so you need to subtract the previous values from the current values as you run each of these experiments.&nbsp; Thus, we can disregard the scan that was already reported by the previous example.)</P>
<P>Now let's try something a little more interesting.&nbsp; Let's run a bookmark lookup:</P>
<BLOCKQUOTE>
<P>SELECT * FROM T WHERE B = 1</P></BLOCKQUOTE>
<P>&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([tempdb].[dbo].[T].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([tempdb].[dbo].[T].[TB]), SEEK:([tempdb].[dbo].[T].[B]=CONVERT_IMPLICIT(int,[@1],0)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([tempdb].[dbo].[T].[TA]), SEEK:([tempdb].[dbo].[T].[A]=[tempdb].[dbo].[T].[A]) LOOKUP ORDERED FORWARD)</P>
<P>As expected sys.dm_db_index_usage_stats reports a seek on index TB (index id 2) and a bookmark lookup on the clustered index (index id 1).&nbsp; However, sys.dm_db_index_operational_stats reports only the singleton lookup on index TB but does not report any new activity on the clustered index:</P><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------
1           1                    1                    1                    0
2           1                    0                    0                    0</PRE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           1                    1
2           0                    1</PRE>
<P>To understand what has happened, recall how a <A title="Nested Loops Join" href="https://blogs.msdn.com/craigfr/archive/2006/07/26/679319.aspx" mce_href="https://blogs.msdn.com/craigfr/archive/2006/07/26/679319.aspx">nested loops join</A> works.&nbsp; The server executes the seek (the singleton lookup) on index TB and, as in the previous example, both DMVs are updated even though the seek returns no rows.&nbsp; However, since the seek on index TB returns no rows, the nested loops join does not execute the clustered index seek (i.e., the bookmark lookup).&nbsp; The server updates sys.dm_db_index_usage_stats to indicate that it executed a query plan that includes a bookmark lookup on table T, but does not update sys.dm_db_index_operational_stats since the query did not actually perform any bookmark lookups.</P>
<P>Next, let's insert three rows into the table and run another bookmark lookup experiment.&nbsp; I'm using a hint to force a bookmark lookup plan.&nbsp; Without the hint, the optimizer would simply use a clustered index scan since the query returns all three rows in the table:</P>
<BLOCKQUOTE>
<P>INSERT T VALUES (0, 0, 0), (1, 1, 1), (2, 2, 2)<BR>SELECT * FROM T WITH (INDEX (TB))</P></BLOCKQUOTE>
<P>&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([tempdb].[dbo].[T].[A]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Scan(OBJECT:([tempdb].[dbo].[T].[TB]))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([tempdb].[dbo].[T].[TA]), SEEK:([tempdb].[dbo].[T].[A]=[tempdb].[dbo].[T].[A]) LOOKUP ORDERED FORWARD)</P>
<P>This time sys.dm_db_index_usage_stats reports a scan on index TB and a bookmark lookup on the clustered index (plus the updates from the insert statement).&nbsp; But, sys.dm_db_index_operational_stats reports a scan on index TB and <I>three</I> bookmark lookups on the clustered index:</P><PRE>index_id    user_seeks           user_scans           user_lookups         user_updates
----------- -------------------- -------------------- -------------------- --------------------
1           1                    1                    2                    1
2           1                    1                    0                    1</PRE><PRE>index_id    range_scan_count     singleton_lookup_count
----------- -------------------- ----------------------
1           1                    4
2           1                    1</PRE>
<P>When the server executes the above query, it runs the clustered index seek three times - once for each row returned by the index scan.&nbsp; We ran the query only once but it performed three bookmark lookups.&nbsp; Thus, as in the prior example, the server updates sys.dm_db_index_usage_stats to indicate that it executed a query plan that includes a bookmark lookup on table T, but unlike the prior example, it updates sys.dm_db_index_operational_stats to indicate that the query performed three actual bookmark lookups.</P>
<P>I've used bookmark lookups in the above examples, but any nested loops join will produce similar results.&nbsp; At this point, it should be clear that the statistics returned by these two DMVs can differ dramatically.</P>So, what is the important takeaway from all of these examples?&nbsp;&nbsp; Don't expect the data reported by these two DMVs to match.&nbsp; sys.dm_db_index_usage_stats tells us the proportion of query plans that were executed that use various indexes.&nbsp; This information is useful for concluding how many of the executed query plans might be affected if we drop an index but it does not tell us how many actual operations are performed using each index.&nbsp; sys.dm_db_index_operational_stats, on the other hand, tells us how often the indexes are actually used during the execution of plans and, thus, which indexes are directly contributing to server performance.&nbsp; But, even if sys.dm_db_index_operational_stats indicates that an index is not used very often (or perhaps even that an index is never used), do not automatically conclude that you can drop the index.&nbsp; First, be sure that sys.dm_db_index_usage_stats indicates that no queries depend on the index.&nbsp; In some cases, the presence of an index could change a query plan for the better even though the index itself is not used when the plan is executed.]]></content:encoded>
    </item>
    <item>
      <title>Random Prefetching</title>
      <link>https://docs.microsoft.com/archive/blogs/craigfr/random-prefetching</link>
      <pubDate>Tue, 07 Oct 2008 17:44:00 GMT</pubDate>
      <dc:creator><![CDATA[Craig Freedman]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/craigfr/2008/10/07/random-prefetching/</guid>
      <description><![CDATA[In my last post, I explained the importance of asynchronous I/O &nbsp;and described how SQL Server...]]></description>
      <content:encoded><![CDATA[In <A title="Sequential Read Ahead" href="https://blogs.msdn.com/craigfr/archive/2008/09/23/sequential-read-ahead.aspx">my last post</A>, I explained the importance of asynchronous I/O &nbsp;and described how SQL Server uses sequential read ahead to boost the performance of scans.&nbsp; In this post, I'll discuss how SQL Server uses random prefetching.&nbsp; Let's begin with a simple example of a query plan that performs many random I/Os.&nbsp; As in my prior post, all of the examples in this post use a 1GB scale factor <A href="https://www.tpc.org/tpch/default.asp">TPC-H</A> database.&nbsp; The following query returns the number of line items associated with each order placed on March 15, 1998: 
<BLOCKQUOTE>
<P>SELECT O_ORDERKEY, COUNT(*)<BR>FROM ORDERS JOIN LINEITEM ON O_ORDERKEY = L_ORDERKEY<BR>WHERE O_ORDERDATE = '1998-03-15'<BR>GROUP BY O_ORDERKEY</P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1012],0)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(GROUP BY:([ORDERS].[O_ORDERKEY]) DEFINE:([Expr1012]=Count(*)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([ORDERS].[O_ORDERKEY], [Expr1011]) OPTIMIZED <B>WITH UNORDERED PREFETCH</B>)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([ORDERS].[O_ORDERDATE_CLUIDX]), SEEK:([ORDERS].[O_ORDERDATE]='1998-03-15') ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([LINEITEM].[L_ORDERKEY_IDX]), SEEK:([LINEITEM].[L_ORDERKEY]=[ORDERS].[O_ORDERKEY]) ORDERED FORWARD)</P>
<P mce_keep="true">This query plan uses an index <A title="Nested Loops Join" href="https://blogs.msdn.com/craigfr/archive/2006/07/26/679319.aspx">nested loops join</A>.&nbsp; The clustered index seek on the ORDERS table returns the 661 orders that were placed on March 15, 1998.&nbsp; For each of these 661 orders, SQL Server performs an index seek on the LINEITEM table to lookup the records associated with this order.&nbsp; Each of these index seeks potentially represents a series of random I/Os to navigate from the root of the B-tree index to the leaf page(s) where the records for that order are stored.&nbsp; To minimize the cost of these I/Os, SQL Server enhances the nested loops join with prefetching.&nbsp; (Notice the WITH UNORDERED PREFETCH keywords associated with the nested loops join.)&nbsp; The prefetching mechanism peers ahead in the clustered index seek on the ORDERS table and issues asynchronous I/Os for the pages that will ultimately be needed by the index seek on the LINEITEM table.&nbsp; As in the sequential read ahead scenario, we can see the prefetching in action by checking the output of SET STATISTICS IO ON.&nbsp; Look at the read-ahead reads for the LINEITEM table:</P>
<P>Table 'LINEITEM'. Scan count 661, logical reads 5165, physical reads 2, <B>read-ahead reads 5000</B>, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.<BR>Table 'ORDERS'. Scan count 1, logical reads 15, physical reads 2, read-ahead reads 19, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.</P>
<P>You may have noticed that the prefetch in this example was UNORDERED.&nbsp; Indeed, there are two types of prefetch:&nbsp; UNORDERED and ORDERED.&nbsp; Although a nested loops join ordinarily preserves the order of the rows from its outer input (in this case the ORDERS table), a nested loops join WITH UNORDERED PREFETCH does not preserve order.&nbsp; Instead, the rows are returned in the order that the asynchronous I/Os complete.&nbsp; However, if the order of the rows is important, SQL Server can use a nested loops join WITH ORDERED PREFETCH.&nbsp; For example, observe what happens to the plan if we add an ORDER BY clause to the above query:</P>
<BLOCKQUOTE>
<P>SELECT O_ORDERKEY, COUNT(*)<BR>FROM ORDERS JOIN LINEITEM ON O_ORDERKEY = L_ORDERKEY<BR>WHERE O_ORDERDATE = '1998-03-15'<BR>GROUP BY O_ORDERKEY<BR><B>ORDER</B><B> BY O_ORDERKEY</B></P></BLOCKQUOTE>
<P>&nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1012],0)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Stream Aggregate(GROUP BY:([ORDERS].[O_ORDERKEY]) DEFINE:([Expr1012]=Count(*)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--Nested Loops(Inner Join, OUTER REFERENCES:([ORDERS].[O_ORDERKEY], [Expr1011]) <B>WITH ORDERED PREFETCH</B>)<BR><B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Sort(ORDER BY:([ORDERS].[O_ORDERKEY] ASC))<BR></B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([ORDERS].[O_ORDERDATE_CLUIDX]), SEEK:([ORDERS].[O_ORDERDATE]='1998-03-15 00:00:00.000') ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([LINEITEM].[L_ORDERKEY_IDX]), SEEK:([LINEITEM].[L_ORDERKEY]=[ORDERS].[O_ORDERKEY]) ORDERED FORWARD)</P>
<P>Notice that SQL Server chooses to push the sort below the nested loops join.&nbsp; For this sort to satisfy the ORDER BY clause, the nested loops join must preserve the order of the rows that it returns.&nbsp; Thus, this time SQL Server uses a nested loops join WITH ORDERED PREFETCH.</P>
<P>SQL Server can also use random prefetching to speed up <A title="Bookmark Lookup" href="https://blogs.msdn.com/craigfr/archive/2006/06/30/652639.aspx">bookmark lookups</A> and certain update and delete statements.&nbsp; For instance, consider the following two statements:</P>
<BLOCKQUOTE>
<P>SELECT *<BR>FROM LINEITEM<BR>WHERE L_ORDERKEY BETWEEN 5000000 AND 5001000</P></BLOCKQUOTE>
<P>&nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([Uniq1002], [LINEITEM].[L_SHIPDATE], [Expr1004]) OPTIMIZED <B>WITH UNORDERED PREFETCH</B>)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([LINEITEM].[L_ORDERKEY_IDX]), SEEK:([LINEITEM].[L_ORDERKEY] &gt;= (5000000) AND [LINEITEM].[L_ORDERKEY] &lt;= (5001000)) ORDERED FORWARD)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Clustered Index Seek(OBJECT:([LINEITEM].[L_SHIPDATE_CLUIDX]), SEEK:([LINEITEM].[L_SHIPDATE]=[LINEITEM].[L_SHIPDATE] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD)</P>
<BLOCKQUOTE>
<P>UPDATE LINEITEM<BR>SET L_DISCOUNT = 0.1<BR>WHERE L_ORDERKEY BETWEEN 5000000 AND 5001000</P></BLOCKQUOTE>
<P>&nbsp; |--Clustered Index Update(OBJECT:([LINEITEM].[L_SHIPDATE_CLUIDX]), SET:([LINEITEM].[L_DISCOUNT] = RaiseIfNull([ConstExpr1010])) <B>WITH UNORDERED PREFETCH</B>)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Compute Scalar(DEFINE:([ConstExpr1010]=CONVERT_IMPLICIT(money,[@1],0)))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Top(ROWCOUNT est 0)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |--Index Seek(OBJECT:([LINEITEM].[L_ORDERKEY_IDX]), SEEK:([LINEITEM].[L_ORDERKEY] &gt;= [@2] AND [LINEITEM].[L_ORDERKEY] &lt;= [@3]) ORDERED FORWARD)</P>
<P>Both plans use a non-clustered index on the LINEITEM table to identify rows that match the L_ORDERKEY predicate.&nbsp; In the case of the SELECT statement, SQL Server performs a bookmark lookup - recall that a bookmark lookup is just a special case of a nested loops join - to fetch the columns of the LINEITEM table that are not covered by the non-clustered index. &nbsp;In the case of the UPDATE statement, SQL Server needs to locate the correct page and row in the clustered index and update the L_DISCOUNT column.&nbsp; The resulting I/O sequence is the same as the bookmark lookup.&nbsp; In both cases, to minimize the cost of the I/Os, SQL Server adds prefetching to the plan.&nbsp; Just as in the original example above, the prefetch mechanism peers ahead in the non-clustered index seek on the LINEITEM table and issues asynchronous I/Os for the pages of the clustered index that will be needed.</P>For systems with many hard drives, random prefetching can dramatically improve performance.&nbsp; However, prefetching can adversely affect concurrency as I explained in <A title="Read Committed and Bookmark Lookup" href="https://blogs.msdn.com/craigfr/archive/2007/06/07/read-committed-and-bookmark-lookup.aspx">this post</A>.]]></content:encoded>
    </item>
  </channel>
</rss>