{"id":794,"date":"2010-02-04T00:00:00","date_gmt":"2010-02-04T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/simple-query-tuning-with-statistics-io-and-execution-plans\/"},"modified":"2026-06-02T16:12:22","modified_gmt":"2026-06-02T16:12:22","slug":"simple-query-tuning-with-statistics-io-and-execution-plans","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/performance-sql-server\/simple-query-tuning-with-statistics-io-and-execution-plans\/","title":{"rendered":"SQL Server STATISTICS IO and Execution Plans: Query Tuning Lessons with Logical Reads, Physical Reads, and Plan Analysis"},"content":{"rendered":"\n<p><b>SET STATISTICS IO is one of the two essential SQL Server query-tuning diagnostics (the other is the execution plan). When enabled for a session, it returns per-object I\/O statistics for every query &#8211; scan count (number of times each index was accessed), logical reads (pages read from the buffer cache), physical reads (pages read from disk), read-ahead reads (pages prefetched), and LOB variants of those for large-object data. <\/b><\/p>\n\n\n\n<p><b>Interpreting these numbers in combination with the execution plan is how you identify which queries are expensive, which specific objects are driving the expense, and whether the expense comes from I\/O volume, non-cached reads, or large-object access. <\/b><\/p>\n\n\n\n<p><b>This article teaches the skill in three lessons: <\/b><br><br><b>(1) breaking down each STATISTICS IO metric with working examples of what the numbers mean; <\/b><br><br><b>(2) dissecting an execution plan alongside STATISTICS IO output, connecting the plan operators to the I\/O statistics;<\/b><br><b> <\/b><br><b>(3) a real-world tuning example showing how a query&#8217;s STATISTICS IO and plan together diagnose the performance problem and validate the fix. <\/b><\/p>\n\n\n\n<p><strong>This article is<\/strong> <b>beginner-friendly with executable examples &#8211; no prior query-tuning experience assumed, SQL Server basics sufficient.<\/b><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-introduction\">Introduction<\/h2>\n\n\n\n<div id=\"pretty\">\n<p>\u00a0I once worked at a large financial institution where we had a policy in place that went &#8230;<\/p>\n<p><b><i>&#8220;All procedures that are to be promoted must have their execution plan and STATISTICS IO data attached with the promote form, for review by DBA group&#8221;.<\/i><\/b><\/p>\n<p>\u00a0A policy that, at first, might seem an intolerable imposition turned out to be a great habit to foster. We, the DBA group, could easily identify poorly performing queries, make notes and return it to the developers for them to modify.\u00a0 \u00a0The developers soon realized that they learned better techniques over time by studying the execution plans and STATISTICS IO data; \u00a0<\/p>\n<p>In this article, I&#8217;d like to pass on what I learned about these two simple sources of information from SQL Server, because they are specifically designed to assist the developer to speed up the performance of queries:<\/p>\n<p>You need the outputs from both together to make best use of them. In this article, I&#8217;ll take a real example of a large SQL query to illustrate how they will help solve the puzzle, and, hopefully, provide a nice clear picture rather than the jumbled puzzle pieces.<\/p>\n<p>STATISTICS IO provides detailed information about the impact that your query has on SQL Server.\u00a0 It tells you the number of logical reads (including LOB), physical reads (including read-ahead and LOB), and how many times a table was scanned.\u00a0 This information helps you to establish whether or not the choices made by the optimizer are as efficient as possible at the time.\u00a0<\/p>\n<p>This is powerful information to have when used together with the Execution plan. \u00a0Sometimes, for example, you&#8217;ll run a query and find that the Execution plan displays an index being used, yet STATISTICS IO shows that the index is doing 10 million logical reads.\u00a0 At that point, you can re-evaluate the index choice and make sure there is no better way to write the query to use a more efficient index (less IOs).\u00a0 The ideal solution is to use the least number of logical reads to perform your operation.\u00a0 The fewer the logical reads, the faster the response and the lesser the impact on the Server.<\/p>\n<h1>Lesson 1: Breakdown of &#8216;STATISTICS IO&#8217;<\/h1>\n<p>STATISTICS IO can be set as an option when you execute a query. A message is sent via the connection that made a query , telling you the cost of the query in terms of the actual number of physical reads from \u00a0the disk and logical reads from memory, by the query. In SQL Server Management Studio, it will appear with the result of the query in the results pane\u00a0 (under the messages tab, if you are using the grid)<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-StatsIO.jpg\" alt=\"934-StatsIO.jpg\" width=\"627\" height=\"393\" \/><\/figure>\n<p>\u00a0<\/p>\n<p><b>STATISTICS IO<\/b> helps you to understand how your query performed by telling you what actually happened.\u00a0 This shows you the IO that was incurred for each object, including the number of times it read a given object, the amount of logical\/physical IO, and the order of access.\u00a0<\/p>\n<h2>What Statistics IO means<\/h2>\n<p>I took a single line from the output of <b>STATISTICS IO<\/b> and it is shown below.\u00a0 We will take each phrase in the order in which they appear from left to right, though this is not necessarily the order that you will concentrate on, once you begin your tuning.\u00a0 I&#8217;ll explain that later.\u00a0<\/p>\n<pre>Table 'T023_RepStatement'. Scan count 208450, logical reads 716751, physical reads 1421, read-ahead reads 996, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.<\/pre>\n<h3>Scan count (208,450)<\/h3>\n<p class=\"MsoSubtitle\">This number tells us that the optimizer has chosen a plan that caused this object to be read repeatedly\u00a0 \u00a0\u00a0This number is used as a gauge later on in the process and you will see what object it is being scanned when I go over the execution plan. This number does not change unless you alter the query.<\/p>\n<h3>Logical Reads (716751)<\/h3>\n<p>This number tells us the actual number of pages read from the data cache.\u00a0\u00a0 This is the number to focus on because it does not change unless you change the actual query structure or index structures.\u00a0 Most common changes are the joins within the WHERE clause, parameter values, or \u00a0index structures.<\/p>\n<h3>Physical Reads (1421)<\/h3>\n<p>This is the number of pages actually read from the disk.\u00a0 These are the pages that weren&#8217;t already in cache and so it is an interesting figure to monitor as it has a direct effect on the performance of the query.\u00a0 SQL Server does all of its work within its caches.\u00a0 If there is a requested page that is not in cache, it will read it from disk and place it in cache, then use that page.\u00a0 If you were to run your query multiple times in a row, you would see your physical reads decrease and ultimately become 0 (so long as there is enough room in memory to store all of the pages required).\u00a0 Because the physical reads change based upon memory pressure and not query design, I tend to ignore this figure.<\/p>\n<h3><span class=\"MsoSubtleEmphasis\">Read-Ahead Reads (996)<\/span><\/h3>\n<p>This number tells us how many of the physical reads were satisfied by SQL Servers &#8216;Read-ahead&#8217; mechanism.\u00a0 This is directly tied to physical reads, so if there are no physical reads, you will have 0 for Read-Ahead reads.\u00a0 I ignore this just like I ignore the Physical Reads.\u00a0 This number will fluctuate as pages are swapped in\/out of memory.\u00a0\u00a0 Although this is considered a type of physical read and whether or not SQL Server will do a physical read is based upon if the page exists in memory or not, index fragmentation will affect this number.<\/p>\n<h3>LOB Logical Reads (0)<\/h3>\n<p>We are not reading in any Large Objects (t<b>ext<\/b>, <b>ntext,<\/b> <b>image<\/b>, <b>varchar(max)<\/b>, <b>nvarchar(max) <\/b>and <b>varbinary(max))<\/b> in this particular example so this number will be 0.\u00a0 \u00a0\u00a0The query used later on in this document does not request any Large Objects so this number is 0.\u00a0 Should the query you are tuning at the time request large objects, you will see this number grow.\u00a0 Pay attention to this number, just like the <b>Logical Reads<\/b> show above.<\/p>\n<h3>LOB Physical Reads (0)<\/h3>\n<p><span class=\"MsoSubtleEmphasis\">This is the number of physical reads the server performed to fetch the necessary pages to satisfy the query.\u00a0 Again, being physical we have no control over this.\u00a0 Ignore it.<\/span><\/p>\n<h3><span class=\"MsoSubtleEmphasis\">LOB Read-Ahead Reads (0)<\/span><\/h3>\n<p>This represents the number of physical reads satisfied by the Read-Ahead mechanism.\u00a0 Nothing you can affect, without tuning the physical server, nothing to look to tune.<\/p>\n<h2>The STATISTICS IO Output<\/h2>\n<p>Here is part of a real query that I&#8217;ve chosen because it represents the reality of what has to be tackled in the working day of a DBA or developer. The real query is over 700 lines long so I&#8217;ll just show the tail of the query from the &#8216;JOIN&#8217; clause onwards.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">SELECT  \n----a lot of code that isn't directly relevant to this article ----\nfrom (select getdate() as mdate) as d,\n\u00a0\u00a0\u00a0\u00a0 HCPGlobalDW.dbo.T002_ConfirmedSalesDetail o\n\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0inner join RepManagement.dbo.T023_RepStatement cl on (cl.C023_deliveryacct=o.C002_AddressNumberShipTo)-- and cl.c023_statement=o.C002_AddressNumber)\n\u00a0\u00a0\u00a0\u00a0 inner join dbo.T017_ISBNRange e on ( e.C017_SellingCompany=o.C002_SellingCompany and e.C017_ISBN=o.C002_SecondItemNumber and e.C017_MisCompanyCode=o.C002_MisCompanyCode)\nwhere\u00a0 o.C002_PackFlag in ('C','')\n\u00a0 and o.C002_DataType='SL'\n\u00a0 and exists (select 1\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 from HCPGlobalMasterFilesDB.dbo.T008_JDEProductMaster,\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Repmanagement.dbo.T521_OrgGrouping\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 where C521_GroupID=C023_SellingGroupNo \n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 and C521_TAPCd=C008_DivisionCode\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 and C521_TARCd=C008_ProductGroup\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 and (C521_TACCd='' or C521_TACCd=C008_Category)\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0 and (C521_TasCd='' or C521_TasCd=C008_ProgramCode)\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 and C521_RowStatus='1'\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 and C008_ISBN=e.C017_ISBN\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0 and e.C017_SellingCompany=C008_SellingCompany)\n<\/pre>\n<p>.\u00a0 Listed below is the actual output of the STATISTICS IO request from executing this query..<\/p>\n<pre>Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n\u00a0  \nTable 'T023_RepStatement'. Scan count 208450, logical reads 716751, physical reads 1421, read-ahead reads 996, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n\u00a0  \nTable 'T002_ConfirmedSalesDetail'. Scan count 1, logical reads 3959, physical reads 2, read-ahead reads 3952, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n\u00a0  \nTable 'T017_ISBNRange'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n\u00a0  \nTable 'T521_OrgGrouping'. Scan count 5, logical reads 18, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n\u00a0  \nTable 'T008_JDEProductMaster'. Scan count 1, logical reads 4, physical reads 4, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n<\/pre>\n<h2>Key Items of STATISTICS IO<\/h2>\n<p>Now that we have gone over each item that the <b>STATISTICS IO<\/b> provides, we&#8217;ll home in on what I call the <u>key items<\/u>, those that you can effect by \u00a0tuning your query.\u00a0 The items are the scan count and the \u00a0logical reads.\u00a0 I use these numbers in conjunction with the Execution plan output.\u00a0 Now that you understand what each item within the <b>STATISTICS IO <\/b>output represents, I will now dig into the <b>Execution plan<\/b> of the above query.\u00a0 You might be thinking to yourself &#8220;Why do that now?\u00a0 We just started with the <b>STATISTICS IO p<\/b>art&#8221;.\u00a0 The answer is that you cannot properly alter any of the key items\u00a0 (scan count and logical IO) without an understanding of Execution plans. \u00a0You need to use both outputs together.<\/p>\n<h1>Lesson 2: Execution plan Dissection<\/h1>\n<p>Now it&#8217;s time to take a Execution plan apart.\u00a0 This will be your most important guide for identifying and fixing problem queries \u00a0You will begin to see the how you can affect the figures being returned by STATISTICS IO if you determine the best index and join usage .\u00a0 Here is the Execution plan that corresponds to the STATISTICS IO output shown above.\u00a0 To properly fit onto the page it&#8217;s hard to see the individual components.\u00a0 Click on it to see it full-size\u00a0 Since the output of the Execution plan is too large to display clearly on the page, I will break it into individual sections.<\/p>\n<figure><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-showplan_51410_01.jpg\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image002.png\" alt=\"934-image002.png\" width=\"640\" height=\"340\" \/><\/a><\/figure>\n<p class=\"illustration\">\u00a0<\/p>\n<p>Start with the right-most top piece, since the end result of the Execution plan will be on the left.\u00a0<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image004.png\" alt=\"934-image004.png\" width=\"640\" height=\"311\" \/><\/figure>\n<p class=\"illustration\">\u00a0<\/p>\n<p>There are two things that you ought to keep in mind when you start inspecting the output. You will want to check the main access points (table scan, index scan, index seek, type of index) and the little % below each step.\u00a0 All steps must add up to 100% and you will find, as you reduce one step, then another step will surely increase.\u00a0 It&#8217;s the nature of the beast and you just want to make each step as efficient as you can<\/p>\n<p>Taking a look at the execution fragment above, you will see that we are accessing two tables via an index seek. This is a more efficient way \u00a0to use an index than an index scan. The execution plan output \u00a0gives you the fully-qualified &#8216;database.owner.object.index&#8217; syntax for each object.\u00a0 If you mouse-over one of the images you will see something like this:<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image006.png\" alt=\"934-image006.png\" width=\"545\" height=\"729\" \/><\/figure>\n<p class=\"illustration\">\u00a0<\/p>\n<p>This gives a great deal of information about <b>how<\/b> SQL Server is accessing that index, and about what it is expecting to retrieve.\u00a0 If you are stumped as to the reason why an index might be scanning \u00a0instead of seeking, or why SQL Server is doing a table scan instead of using an index, this is where you go.<\/p>\n<p>Let&#8217;s take a moment to dissect the information presented by the image above using the highlighted sections as guides..\u00a0 The first piece of information is the type of index access, which in this case is a seek.\u00a0 This means that SQL Server knows exactly where to start looking for the information within the index; \u00a0much the same way if you were looking up my last name in the telephone book.\u00a0 You could go straight to &#8216;Richberg&#8217; and then look at each listing until you found &#8216;Josef&#8217;.\u00a0<\/p>\n<p>The next highlighted section is <b>Actual Number of Rows<\/b>, which is exactly as it sounds.\u00a0 SQL Server found 4 records that match all of the criteria given\u00a0 I find this very useful when trying to determine how effective a given index is or specific criteria are\u00a0 The <b>Predicate <\/b>section is next.\u00a0 This shows you the all the pieces this index uses to qualify a row.\u00a0 It will show you any known values, value ranges, and joins to other tables (and on what columns).\u00a0 You will see this on an <b>Index Seek <\/b>and an <b>Index Scan<\/b>.\u00a0 This is an area I use to help determine if you index is being use as effectively as possibly.\u00a0<\/p>\n<p>You might look at this and notice &#8216;I am missing a join to table X&#8217; or &#8216;I shouldn&#8217;t be joining to table y&#8217;.\u00a0 You may not see this section at all.\u00a0 That would mean the index is being scanned in its entirety and there are no criteria to restrict the rows at this point.\u00a0 The <b>Output List<\/b> shows you what columns the index will be returning.\u00a0 If the index can satisfy all of the columns requested, it is considered a &#8216;Covered&#8217; index, otherwise the index will need to get the additional columns from the underlying structure (clustered index if it exists or the table). \u00a0\u00a0The final section is <b>Seek <\/b>Predicates, which shows the actual columns, values, and criteria (&lt;,&gt;,=) used to satisfy the seek.\u00a0 If this where an <b>Index Scan<\/b> you would not see the <b>Seek Predicates<\/b> section. I can tell by reading this section, I am looking for <code>C521_RowStatus='1' and C521_TASCD &gt; [Expr1167] and C521_TASCD&lt;[Expr1168].<\/code>\u00a0 I would have to go back to the query to see what the actual values of <code>[Expr1167] and [Expr1168] <\/code>are, but I would know where to look, because it would be near &gt; and &lt;.\u00a0 \u00a0.\u00a0<\/p>\n<p>Understanding how and why SQL Server determines particular paths between objects to satisfy your query and ultimately provide your data, you can tune your queries and database structures to be more efficient in their use of resources, which is the ultimate goal in tuning.<\/p>\n<p>The next set of objects is below.<\/p>\n<p class=\"illustration\">Here you can see that \u00a0<b>T017_ISBNRange<\/b> is accessed via a clustered index seek and that <b>T002_ConfirmedSalesDetail<\/b> is access via a non-clustered index seek.\u00a0 They are joined together by a Nested Loop.\u00a0 The output of that is then joined to the output of the non-clustered index seek of <b>T023_RepStatement<\/b>I would like you to take note as we walk through the graphical execution plan how each step from the right connects to objects on the left and each object from the bottom connects upwards towards the top.\u00a0 The sequence logically moves from left to right, bottom to top and eventually towards, in this case a <b>select<\/b>.\u00a0 Continuing, the output of all 3 indexes is joined again to a non-clustered index seek on <b>T008_JDEProductMaster<\/b>.\u00a0 This is shown in the image below. <img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image011.jpg\" alt=\"934-image011.jpg\" width=\"624\" height=\"241\" \/><\/p>\n<p>\u00a0We are now at the end of the diagram.\u00a0 A nested loop is used to join the result coming from <b>T008_JDEProductMaster<\/b> and that of the nested loop below it, shown by the orange arrow.\u00a0 The result of that output is joined with the results of other operations below, shown by the fatter line pointed to by the blue arrow.\u00a0\u00a0 A final hash match is used, taking up 9% of the total cost, then <b>Sort<\/b> (I have a few <b>group by<\/b>s in the query creating the need for a sort) and then the <b>Select<\/b>, which is the result, being sent back to the calling application.\u00a0 Another piece of useful information can be gleaned by clicking on the lines between each set.<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image012.png\" alt=\"934-image012.png\" width=\"659\" height=\"172\" \/><\/figure>\n<p class=\"illustration\">\u00a0<\/p>\n<p>This information tells me that there are 208,453 qualifying rows that are the result of joining <b>T017_ISBNRange<\/b> and <b>T002_ConfirmedSalesDetail<\/b>.\u00a0 If there is a significant difference between the estimated and actual figures, check the statistics of your indexes as they might be stale. \u00a0The other pieces of information, estimated row size and estimated data size can only be modified if you adjust your join columns or select statement.\u00a0 \u00a0\u00a0<\/p>\n<h1>Lession 3: Into the Wild Blue Yonder (Real World Example)<\/h1>\n<p>I am going to switch up gears a little bit to show you how you can<\/p>\n<ul>\n<li>\n<p class=\"MsoListParagraphCxSpFirst\">use the Execution plan output to determine what changes to make<\/p>\n<\/li>\n<li>\n<p class=\"MsoListParagraphCxSpLast\">use your STATISTICS IO output to make sure that change was correct.\u00a0<\/p>\n<\/li>\n<\/ul>\n<p>On rare occasions you will find \u00a0that a query strategy that looks sensible in the S proves to be poor in the IO department, making you scratch your head and re-think your choices.\u00a0 I find this situation most often is the use of an in &#8216;incorrect&#8217; index.\u00a0 You might be asking yourself, &#8216;How can an index be incorrect&#8217;? \u00a0\u00a0Let&#8217;s look at the images below and use those as examples to better explain.\u00a0 .<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">select C017_DateLastReceived,C017_SecondItemNumber,C017_ThirdItemNumber  \n\u00a0 from dbo.tmp_T017_TradeReceiving  \n\u00a0where C017_DateLastReceived='4\/29\/2002 12:00:00 AM'\n<\/pre>\n<p>The above query produces the following execution plan and STATISTICS IO output.<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image014.png\" alt=\"934-image014.png\" width=\"372\" height=\"107\" \/><\/figure>\n<p class=\"illustration\">\u00a0<\/p>\n<pre>Table 'tmp_T017_TradeReceiving'. Scan count 1, logical reads 4663, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n<\/pre>\n<p>The optimizer is looking for the most efficient path to obtain your information.\u00a0 The first choice it has is to go directly to the table for the information, which requires a table scan.\u00a0\u00a0 A table with no index is akin to a book with no index.\u00a0 If I asked you to find me the pumpkin pie recipe in the &#8216;Great Recipe Book&#8217;, but the publisher left out the index, you would have to start at the first page and leaf through each and every page until you found it.\u00a0 You would most likely have to leaf through the whole book, since you won&#8217;t know if there is more than one recipe.<\/p>\n<p>Scanning through our &#8216;recipe book (<b>T017_TradeReceiving<\/b>)&#8217; required 4,661 logical reads.\u00a0 While it only took 1 second ,you want to keep in mind the fewer resources \u00a0used by each and every query, the overall better performance you will get out of your entire system.\u00a0 Knowing this is a table scan, you want to try to see if you can put some index on this which will reduce the effort it takes SQL Server to retrieve the data you want.<\/p>\n<p>We decide to call the editor and say &#8216;This book needs an index of some sort&#8217;.\u00a0 The editor agrees and publishes a new book with an index on categories.\u00a0 Now when you go to look up pumpkin pie, you go to the back and look at &#8216;pies pgs 120-155&#8217;. This index is more efficient, but you still have work to do, you have to look at 35 pages.\u00a0<\/p>\n<p>Continuing this example going forward, we decide to put an index on the\u00a0 column &#8216;C017_DateLastReceived&#8217;.\u00a0<\/p>\n<p>The syntax for creating the index is:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">create nonclustered index IDX1 on dbo.tmp_T017_TradeReceiving(C017_DateLastReceived)  \n<\/pre>\n<p>You can see that the query produces a table scan, which results in 4,663 logical reads.\u00a0 There is no physical IO so the time we get (1 sec) is &#8216;as good as it gets &#8216; for this query.\u00a0 The first thing that comes to mind is that there may not be an index on <b>C017_DateLastRecived<\/b>.\u00a0 It turns out that there isn&#8217;t one, so I will put one on the table<\/p>\n<p>Just as in the cookbook example above, were you decide to use the index to retrieve your pie information, the optimizer has decided to use the index to retrieve the data instead of doing a table scan.\u00a0 The output of the execution plan gives insight into the new path.\u00a0 The output of the STATISTICS IO verifies that it is a better path by showing a reduction in logical reads.<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image016.png\" alt=\"934-image016.png\" width=\"562\" height=\"178\" \/><\/figure>\n<p>\u00a0<\/p>\n<pre>Table 'tmp_T017_TradeReceiving'. Scan count 1, logical reads 120, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.  \n<\/pre>\n<p>We are now down to 120 logical reads, a significant improvement over the original 4,663, but in looking at the graphical execution plan, \u00a0\u00a0I can see there is additional work being done.\u00a0 The RID Lookup tells me that SQL Server needs more information than is being supplied by the index.\u00a0 Back to our cookbook example, even though the index tells you the exact pages to go for pie recipes, you must go back into the cookbook to read the actual pumpkin pie recipe.\u00a0 To find out what is missing in our real world example we need to highlight the RID Lookup image and our pop-up looks like this:<\/p>\n<p><b>\u00a0<\/b>If you look at the highlighted section, you will see the missing columns,<b> C017_SecondItemNumber and C017_ThirdItemNumber<\/b>.\u00a0 This tells me we can improve this one step further.\u00a0 Back to our cookbook example.<\/p>\n<p>You have cooked so many pies by now, you don&#8217;t need the instructions on how to cook a pumpkin pie, you just want an ingredients list.\u00a0 You contact the editor again and say &#8216;Can you add an ingredients list for each recipe?&#8217;.\u00a0 The editor takes your suggestion and sends you the new book.\u00a0 Now when you go the new new &#8216;recipie appendix&#8217;, you look up pumpkin pie and see all the ingredients right there.\u00a0 You don&#8217;t have to go back to page xxx to find them.\u00a0 This is optimal.\u00a0 Let&#8217;s do the same thing with out table.<\/p>\n<p>Let&#8217;s modify the index to provide the necessary information.\u00a0 Here is the statement to drop the current index:<\/p>\n<pre>Drop index dbo.Tmp_T017_TradeRecieving.idx1<\/pre>\n<p>We will recreate the index to include the missing columns.\u00a0 We will be using the syntax for creating an index with &#8216;included columns&#8217;, which is available in SQL Server 2005 or greater.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">create nonclustered index IDX1 on dbo.tmp_T017_TradeReceiving(C017_DateLastReceived)  \ninclude (C017_SecondItemNumber,C017_ThirdItemNumber)\n<\/pre>\n<p>You will notice the keyword &#8216;included columns&#8217;.\u00a0 This is a new space saving \u00a0feature in SQL Server 2005 and higher.\u00a0 The columns listed in the <b>include <\/b>section can only be used to satisfy columns requested in the select clause of your SQL statement, not the WHERE clause.\u00a0 This is because the included columns\u00a0 are created in the leaf of the index and not the intermediate levels.\u00a0 This means the optimizer has no path to them.\u00a0 In the cookbook example, I cannot use an ingredient to find a recipe.<\/p>\n<p>In SQL Server 2000, the columns in an index existed at all levels (root, intermediate, leaf) taking up much more space than needed.<\/p>\n<p>If you are using SQL Server 2000, your create index statement would be:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">create nonclustered index IDX1 on dbo.tmp_T017_TradeReceiving(C017_DateLastReceived,C017_SecondItemNumber,C017_ThirdItemNumber)  \n<\/pre>\n<p>After creating the new index we re-run the select statement and find the following output from execution plan and STATISTICS IO.<\/p>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/934-image018.png\" alt=\"934-image018.png\" width=\"373\" height=\"85\" \/><\/figure>\n<p>\u00a0<\/p>\n<pre>Table 'tmp_T017_TradeReceiving'. Scan count 1, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n<\/pre>\n<p>Modifying the non-clustered index to <b>include<\/b> those two columns reduced the logical reads to 4, a reduction in resource use by over 1000%.\u00a0 You might be asking, &#8216;Why didn&#8217;t you use a clustered index?&#8217;\u00a0 The answer is simple:\u00a0 I example I was looking to illustrate was just because an index is show as being using in the graphical execution plan, doesn&#8217;t mean there I no tuning to be done.\u00a0 I wanted to show you the progression from a table with no indexes, to a table with a moderately efficient index, to a table with a very efficient index and the path to get there.\u00a0 A clustered index is by it&#8217;s very nature \u00a0is &#8216;covered&#8217; and would have left out the 2nd step.<\/p>\n<p>You will have seen that your query has used over 1000 times less resources just by improving the access through an index\u00a0 and thereby reducing your final reads to \u00a04.\u00a0<\/p>\n<h1>Conclusion<\/h1>\n<p>While you have cut your time from 1 sec to 0 seconds which seems negligible, you have achieved \u00a0a dramatic reduction in the load on the server. Queries must return data as fast as possible, but must use resources efficiently too.\u00a0 If each query is designed to be as resource efficient as possible your sever will be able to do more, in a shorter period of time, with less.\u00a0 Consider SQL Server a community where every query\u00a0 has an impact, either direct or indirect on the all the other queries running.\u00a0 Having a single poorly performing query can affect any number of optimized queries, since they are reside in the same SQL Server.\u00a0 They share the same resource pool (memory, disk, CPU, etc).\u00a0 Having a poorly designed query take millions of more reads than it needs, puts \u00a0undue pressure on the I\/O subsystem and the cache, which has a trickling-down effect on the other queries, which results in the entire server looking &#8216;slow&#8217;..<\/p>\n<p>I hope this article will lead to the production DBAs getting calls at 3 am about performance issues and then for them to have to use some of the complicated DMVs to find those slow\u00a0 queries.\u00a0 Tuning your queries properly, prior to their introduction into production is a much better practice, than looking to correct the problem after the application goes live.\u00a0\u00a0<\/p>\n<\/div>\n\n\n\n<section id=\"my-first-block-block_3016f280b2959b628ef71dcd3dba13fd\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Simple Talk is brought to you by Redgate Software<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/solutions\/overview\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Discover how Redgate can help you: Simple Talk is brought to you by Redgate Software\">Discover how Redgate can help you<\/a>\n                    <\/div>\n    <\/div>\n<\/section>","protected":false},"excerpt":{"rendered":"<p>Learning SQL Server query tuning with SET STATISTICS IO and execution plans &#8211; breaking down each statistic (scan count, logical reads, physical reads, read-ahead reads, LOB variants), reading execution plans, and applying both to tune a real-world query. Beginner-friendly three-lesson tutorial.&hellip;<\/p>\n","protected":false},"author":221876,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143529],"tags":[4206,4150],"coauthors":[131791],"class_list":["post-794","post","type-post","status-publish","format-standard","hentry","category-performance-sql-server","tag-performance","tag-sql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/794","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221876"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=794"}],"version-history":[{"count":6,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/794\/revisions"}],"predecessor-version":[{"id":111163,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/794\/revisions\/111163"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=794"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=794"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=794"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=794"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}