<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://www2.sqlblog.com/utility/FeedStylesheets/atom.xsl" media="screen"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title type="html">Elisabeth Redei</title><subtitle type="html" /><id>http://www2.sqlblog.com/blogs/elisabeth_redei/atom.aspx</id><link rel="alternate" type="text/html" href="http://www2.sqlblog.com/blogs/elisabeth_redei/default.aspx" /><link rel="self" type="application/atom+xml" href="http://www2.sqlblog.com/blogs/elisabeth_redei/atom.aspx" /><generator uri="http://communityserver.org" version="2.1.61129.1">Community Server</generator><updated>2009-03-01T22:52:00Z</updated><entry><title>Lies, Damned Lies and Statistics – Part III (SQL Server 2008)</title><link rel="alternate" type="text/html" href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/12/17/lies-damned-lies-and-statistics-part-iii-sql-server-2008.aspx" /><id>http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/12/17/lies-damned-lies-and-statistics-part-iii-sql-server-2008.aspx</id><published>2009-12-17T11:14:00Z</published><updated>2009-12-17T11:14:00Z</updated><content type="html">In previous posts ( http://sqlblog.com/blogs/elisabeth_redei/archive/2009/08/10/lies-damned-lies-and-statistics-part-i.aspx and http://sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/lies-damned-lies-and-statistics-part-ii.aspx)I have talked about the performance problems that may surface because of low statistics sample rate on large tables. One of the things I wrote about are issues with naturally skewed data in an indexed column. In this context, skewed data means that some values are represented...(&lt;a href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/12/17/lies-damned-lies-and-statistics-part-iii-sql-server-2008.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=20058" width="1" height="1"&gt;</content><author><name>Elisabeth Redei</name><uri>http://www2.sqlblog.com/members/Elisabeth+Redei.aspx</uri></author><category term="statistics" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/statistics/default.aspx" /><category term="Skewed Data" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Skewed+Data/default.aspx" /><category term="query performance" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/query+performance/default.aspx" /></entry><entry><title>Lies damned lies and statistics - Part II</title><link rel="alternate" type="text/html" href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/08/10/lies-damned-lies-and-statistics-part-ii.aspx" /><link rel="enclosure" type="text/plain" length="1460" href="http://www2.sqlblog.com/blogs/elisabeth_redei/attachment/15921.ashx" /><id>http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/08/10/lies-damned-lies-and-statistics-part-ii.aspx</id><published>2009-08-10T12:29:00Z</published><updated>2009-08-10T12:29:00Z</updated><content type="html">Lies damned lies and statistics - Part II In previous post I listed situations where your statistics might need some special attention ( http://sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/lies-damned-lies-and-statistics-part-i.aspx ). As I mentioned in that post, the symptoms are typically related to more or less random variations in duration that cannot be explained by resource contention or blocking. A quick recap of the various problems you can encounter: 1. Statistics is out-of-date...(&lt;a href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/08/10/lies-damned-lies-and-statistics-part-ii.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=15921" width="1" height="1"&gt;</content><author><name>Elisabeth Redei</name><uri>http://www2.sqlblog.com/members/Elisabeth+Redei.aspx</uri></author><category term="statistics" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/statistics/default.aspx" /><category term="Estimated Rows" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Estimated+Rows/default.aspx" /><category term="Skewed Data" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Skewed+Data/default.aspx" /><category term="histogram" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/histogram/default.aspx" /><category term="Actual Rows" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Actual+Rows/default.aspx" /></entry><entry><title>Lies, Damned Lies And Statistics – Part I</title><link rel="alternate" type="text/html" href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/lies-damned-lies-and-statistics-part-i.aspx" /><link rel="enclosure" type="application/x-zip-compressed" length="984" href="http://www2.sqlblog.com/blogs/elisabeth_redei/attachment/12364.ashx" /><id>http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/lies-damned-lies-and-statistics-part-i.aspx</id><published>2009-03-01T19:51:00Z</published><updated>2009-03-01T19:51:00Z</updated><content type="html">&lt;P&gt;One of the biggest call generators at Microsoft SQL Server Support when SQL Server 7.0 was released was issues with statistics. In the beginning, they tended to be about the Autoupdate statistics feature interfering with user activity but when SQL Server 2000 came, and systems running SQL Server became larger, issues with skewed statistics/data and problems related to the sample rate used when gathering statistics emerged and in 2005, the challenges are the same. There are some new statistics features in 2008 but overall, this post applies to 2008 as well. &lt;/P&gt;
&lt;P&gt;First a few words on the symptoms that are typical for issues related to statistics. What they all have in common is seemingly strange variations in duration when the server has no resource problems and you have confirmed that there is no blocking:&lt;/P&gt;
&lt;P&gt;- The same query (with the same values in the search argument or WHERE clause) varies in duration without any related blocking or resource problem&lt;/P&gt;
&lt;P&gt;- Some queries degrade in performance as time passes since the last Index rebuild&lt;/P&gt;
&lt;P&gt;- The same procedure will perform different depending on what values are passed into it&lt;/P&gt;
&lt;P&gt;- Intermittent performance problem on some tables without any related blocking or resource problem&lt;/P&gt;
&lt;P&gt;Although all of these symptoms may be related to other issues, I always look into the quality of the statistics before I spend time on more time consuming troubleshooting.&lt;/P&gt;
&lt;P&gt;Background - Statistics Default Settings&lt;/P&gt;
&lt;P&gt;Even though the default settings (which is Autoupdate and Autocreate Statistics ON for all tables and indexes), will take you a long way, it is absolutely necessary to have a more granular approach as your databases and your workload grows. There is no tuning available that can work around a problem with lack of accurate statistics. The problems related to statistics (or lack thereof) are either about: &lt;/P&gt;
&lt;P&gt;- How often the statistics are updated&lt;/P&gt;
&lt;P&gt;- The sample rate used when the statistics is built&lt;/P&gt;
&lt;P&gt;One thing that you might not have thought about is when Autoupdate statistics will be triggered. A lot of people will say it is when a certain threshold for changes is reached (more details below) but that’s not the whole truth – it does not automatically start when the threshold is reached. It is part of the compilation or optimization process and will only be triggered when/if the threshold has been reached AND the index/column is touched by a query. The thresholds are really Recompilation thresholds and not Statistics thresholds.&lt;/P&gt;
&lt;P&gt;Simplified it works like this:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/LiesDamnedLiesAndStatisticsPartI_1413C/clip_image002_2.jpg"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=443 alt=clip_image002 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/LiesDamnedLiesAndStatisticsPartI_1413C/clip_image002_thumb.jpg" width=490 border=0&gt;&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&lt;I&gt;Picture based on diagram in http://technet.microsoft.com/en-us/library/cc966425.aspx&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;There are some other processes that will trigger an update of the statistics. And not only that, it will trigger an update of the statistics WITH FULLSCAN (i.e. using all rows in the index to build statistics as opposed to Autoupdate statistics which will use some sample rate):&lt;/P&gt;
&lt;P&gt;- DBCC REINDEX (not INDEXDEFRAG)&lt;/P&gt;
&lt;P&gt;- CREATE INDEX …. WITH DROP_EXISTING&lt;/P&gt;
&lt;P&gt;If you do any of the above on an index, you will get the best statistics possible and it is not too uncommon to see cases when switching from Index Rebuild to UPDATE STATISTICS … WITH FULLSCAN, gives the same performance boost. It is also important to know that once the Autoupdate statistics process runs, it will reduce the sample rate of the statistics down to the default.&lt;/P&gt;
&lt;P&gt;If your maintenance window is getting crowded, and index rebuild is part of your maintenance plans, you might want to experiment with a combination of index rebuilds and statistics updates (needless to say updating statistics is not going to do anything for fragmentation should that be an issue).&lt;/P&gt;
&lt;P&gt;Now to the reasons why Autoupdate statistics might not work well in all situations:&lt;/P&gt;
&lt;P&gt;1. Statistics gets out-of-date or stale&lt;/P&gt;
&lt;P&gt;This is a problem related to large tables. What happens is that it takes more and more modifications for the Recompilation Threshold to be met. The formula for tables with more than 500 rows is:&lt;/P&gt;
&lt;P&gt;Rowmodctr 500 + 20% of total rows (rowcnt)&lt;/P&gt;
&lt;P&gt;(the two columns, rowmodctr and rowcnt, can be found in the sys.sysindexes compatibility view).&lt;/P&gt;
&lt;P&gt;The problem following this, is that the Optimizer will have somewhat misleading information about the cardinality of tables and indexes (i.e. a WHERE or JOIN clause returns more or less rows than expected).&lt;/P&gt;
&lt;P&gt;2. Again for large tables, the default sample rate (used by the Autoupdate Statistics feature) may not be enough because it gets relatively lower as the tables grow larger (to avoid issues with Autoupdate Statistics process taking too long).&lt;/P&gt;
&lt;P&gt;3. Data in the column is naturally "skewed" (e.g. LastName; Smith vs. Redei for instance) &lt;/P&gt;
&lt;P&gt;4. The Autoupdate Statistics process interrupts your user activity &lt;/P&gt;
&lt;P&gt;How do you diagnose and address these issues? In this post I will explain how to do that for the first 2 items in the list above; stale statistics and problems with default sample rate.&lt;/P&gt;
&lt;P&gt;Stale statistics &lt;BR&gt;=========== &lt;BR&gt;To diagnose, you would typically look at two things:&lt;/P&gt;
&lt;P&gt;- When is the last time the statistics were updated?&lt;/P&gt;
&lt;P&gt;- What is the ratio rowcnt/rowmodctr (for tables/indexes with rowcnt &amp;gt; somenumber)?&lt;/P&gt;
&lt;P&gt;The solution to this is quite simple; you just need to schedule manual UPDATE STATISTICS jobs on the tables when a threshold that you deem reasonable is met:&lt;/P&gt;
&lt;DIV class=wlWriterSmartContent id=scid:0767317B-992E-4b12-91E0-4F059A8CECA8:cbb24e99-c012-4a0a-8239-ced251211c99 style="PADDING-RIGHT:0px;DISPLAY:inline;PADDING-LEFT:0px;PADDING-BOTTOM:0px;MARGIN:0px;PADDING-TOP:0px;"&gt;Technorati Tags: &lt;A href="http://technorati.com/tags/statistics" rel=tag&gt;statistics&lt;/A&gt;,&lt;A href="http://technorati.com/tags/autoupdate%20statistics" rel=tag&gt;autoupdate statistics&lt;/A&gt;,&lt;A href="http://technorati.com/tags/UPDATE%20STATISTICS" rel=tag&gt;UPDATE STATISTICS&lt;/A&gt;&lt;/DIV&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ssi.id&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, object_name(ssi.id) AS tblName&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, stats_date(ssi.id,ssi.indid) as StatsDate&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, ssi.indid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, ssi.rowcnt&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, ssi.rowmodctr&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, cast(ssi.rowmodctr as decimal)/cast(ssi.rowcnt as decimal) as ChangedRowsRatio&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, ss.no_recompute AS IsAutoUpdateOff&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM sys.sysindexes ssi left join sys.stats ss&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ON ssi.name = ss.name&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;WHERE ssi.id &amp;gt; 100&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;AND indid &amp;gt; 0&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;AND ssi.rowcnt &amp;gt; 500&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;AND (ssi.rowmodctr/ssi.rowcnt) &amp;gt; 0.15 -- enter a relevant number&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;ORDER BY 3 &lt;/P&gt;
&lt;P&gt;You would probably want to change the ssi.rowcnt and the ratio to something that is relevant for your database.&lt;/P&gt;
&lt;P&gt;The output will look something like this:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/LiesDamnedLiesAndStatisticsPartI_1413C/clip_image003_2.jpg"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=22 alt=clip_image003 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/LiesDamnedLiesAndStatisticsPartI_1413C/clip_image003_thumb.jpg" width=244 border=0&gt;&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;In this case, I have some statistics on a non-clustered index (indid &amp;gt; 1) which has not been updated for 3 years. The IsAutoUpdateOff = 0 tells me that Autoupdate statistics feature is ON for this particular index (this can also be viewed, or changed, with the sp_autostats procedure).&lt;/P&gt;
&lt;P&gt;Once you have confirmed that stale statistics is an issue, you have two options:&lt;/P&gt;
&lt;P&gt;- Update the statistics for all indexes in the database using sp_updatestats (if Autoupdate statistics is turned on, there is no point in using the ‘resample’ of option of sp_updatestats)&lt;/P&gt;
&lt;P&gt;- Update statistics only for the tables you have identified with UPDATE STATISTICS (use UPDATE STATISTICS &amp;lt;tablename&amp;gt; WITH ALL if you want to update all indexes of a table)&lt;/P&gt;
&lt;P&gt;Low Sample Rate&lt;/P&gt;
&lt;P&gt;=============&lt;/P&gt;
&lt;P&gt;This is slightly trickier than the problem with stale statistics; both to diagnose and to solve. The way you would know that sample rate is an issue, is if a query that you think have suspiciously high duration runs faster after you have run an UPDATE STATISTIC ... WITH FULLSCAN on it (or if you did an Index rebuild that gave you statistics with a full scan). &lt;/P&gt;
&lt;P&gt;It involves finding the point when the sample rate is high enough to create useful statistics but low enough for the process of updating statistics to fit into your maintenance window. You also need to decide how often the job should run.&lt;/P&gt;
&lt;P&gt;Your starting point should be the current sample rate which you will find with DBCC SHOW_STATISTICS:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;DBCC SHOW_STATISTICS ([schemaname.tablename], indexname) WITH STAT_HEADER&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Since this output cannot be presented with table results, I have attached a script that will present the output from DBCC SHOW_STATISTICS for all indexes for all tables and schemas in the current database. The output will look something like this:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/LiesDamnedLiesAndStatisticsPartI_1413C/clip_image004_2.jpg"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=54 alt=clip_image004 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/LiesDamnedLiesAndStatisticsPartI_1413C/clip_image004_thumb.jpg" width=636 border=0&gt;&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;What you want to look for is large discrepancies between Rows and RowsSampled (in the above output a full scan has been used to create the statistics so the numbers are equal).&lt;/P&gt;
&lt;P&gt;As mentioned, for the large tables where you believe that this might be an issue, you should start with some number above the default sample rate (RowsSampled/Rows) to get an idea of how long it will take to update the statistics with a certain sample rate. Then move the sample rate up until the execution time of UPDATE STATISTICS hits the threshold given by your maintenance window.&lt;/P&gt;
&lt;P&gt;Once you have found your ideal sample rate, you then need to set up jobs to run UPDATE STATISTICS ... WITH SAMPLE RATE XX, NO_RECOMPUTE and turn off the Autoupdate feature with:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;EXEC sp_autostats ‘tablename’, ‘OFF’&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;If the NO_RECOMPUTE clause is omitted, Autoupdate statistic will be switched on and the next time that particular index passes the Recompilation Threshold, Autoupdate Statistics will be run with the default sample rate (which you have just concluded is not enough to provide the optimizer with accurate statistics).&lt;/P&gt;
&lt;P&gt;Next post I will show you how you can diagnose issues with “skewed” data/statistics.&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=12364" width="1" height="1"&gt;</content><author><name>Elisabeth Redei</name><uri>http://www2.sqlblog.com/members/Elisabeth+Redei.aspx</uri></author><category term="autoupdate statistics" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/autoupdate+statistics/default.aspx" /><category term="performance" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/performance/default.aspx" /><category term="statistics" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/statistics/default.aspx" /><category term="UPDATE STATISTICS" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/UPDATE+STATISTICS/default.aspx" /></entry><entry><title>Bufferpool Performance Counters</title><link rel="alternate" type="text/html" href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/bufferpool-performance-counters.aspx" /><id>http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/bufferpool-performance-counters.aspx</id><published>2009-03-01T19:36:57Z</published><updated>2009-03-01T19:36:57Z</updated><content type="html">&lt;div class="wlWriterSmartContent" id="scid:0767317B-992E-4b12-91E0-4F059A8CECA8:57b72b4d-79ac-4450-9d71-7f64e929a064" style="padding-right:0px;display:inline;padding-left:0px;padding-bottom:0px;margin:0px;padding-top:0px;"&gt;Technorati Tags: &lt;a href="http://technorati.com/tags/bufferpool" rel="tag"&gt;bufferpool&lt;/a&gt;,&lt;a href="http://technorati.com/tags/performance%20monitor%20counters" rel="tag"&gt;performance monitor counters&lt;/a&gt;,&lt;a href="http://technorati.com/tags/memory" rel="tag"&gt;memory&lt;/a&gt;&lt;/div&gt;  &lt;p&gt;Memory configuration and investigating memory conditions is still something that seems to cause a lot of confusion. There are numerous articles, blog posts etc. that talk about SQL Server memory, virtual memory, AWE and other factors but not much about the one thing that I personally think give a great understanding of the different counters we have learned to use to look at when investigating SQL Server memory.&lt;/p&gt;  &lt;p&gt;In this post I will talk a little bit about some mechanisms in the Buffer pool &amp;#8211; i.e. the part of SQL Server memory where, above all, your data and index pages go and also at the performance monitor counters you can use when assessing the state of the Buffer pool.&lt;/p&gt;  &lt;p&gt;(When I refer to pages, I am exclusively talking about data and index pages. For log records, there is a special Log Manager and I will talk about how log records are written in another post.)&lt;/p&gt;  &lt;p&gt;First, let&amp;#8217;s look at some Buffer pool concepts. A simplistic view of the Buffer pool looks something like this:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/BufferpoolPerformanceCounters_13DEF/clip_image001_2.jpg"&gt;&lt;img style="border-right:0px;border-top:0px;border-left:0px;border-bottom:0px;" height="226" alt="clip_image001" src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/BufferpoolPerformanceCounters_13DEF/clip_image001_thumb.jpg" width="244" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;POOLS AND CACHE STORES&lt;/p&gt;  &lt;p&gt;Pools are used to cache homogenous, stateless data. All entries in the pool are considered equal - for example a pool of connections or a pool of network buffers. &lt;/p&gt;  &lt;p&gt;Cache stores are used to store stateful data and provide a set of memory allocation interfaces that can be reused by different consumers. For example the Procedure Cache which is divided into several different cache stores &amp;#8211; e.g. one for ad-hoc sql plans, another for plans for stored procedures/functions/triggers and a separate for external stored procedures.&lt;/p&gt;  &lt;p&gt;The Free List&lt;/p&gt;  &lt;p&gt;SQL Server keeps a minimum number of free pages on the Free List to be able to serve incoming requests without any delay. The amount of free pages that SQL Server tries to keep in the Free list (&amp;#8220;Min Free&amp;#8220; in DBCC MEMORYSTATUS output) is calculated based on the size of the Buffer pool and the amount of incoming requests (Page Life Expectancy, the expected lifetime of a page in cache, being one indicator).&lt;/p&gt;  &lt;p&gt;Writing and Freeing buffer pages&lt;/p&gt;  &lt;p&gt;SQL Server uses a Least Recently Used (LRU) algorithm for aging pages in the Buffer pool. Basically a counter is incremented every time a page is referenced and decremented every time the lazy writer process sweeps the page. &lt;/p&gt;  &lt;p&gt;Any worker thread will at certain points (for example when an asynchronous IO is issued) check the memory status of the Buffer pool to make sure that a healthy number of free pages are available for new incoming requests. If the number of free buffers is not satisfactory, two things can happen:&lt;/p&gt;  &lt;p&gt;If the upper limit of the Buffer pool has been reached (the limit being either &amp;#8220;max server memory&amp;#8221; or available memory in the operating system; both reflected in the SQL Server Memory Manager:Target Server Memory counter):&lt;/p&gt;  &lt;p&gt;- the lazywriter process (although run by the worker thread) will sweep parts of the Buffer pool (it keeps track of where it left off the last sweep) and based on when the page was last used either leave it or &amp;#8211; in case the page is dirty &amp;#8211; flush it to disk and then put the page on the free list. A dirty page will not be flushed until the corresponding log records have been written to disk (for more information, see Write-ahead Logging in Books Online).&lt;/p&gt;  &lt;p&gt;If the upper limit of the Buffer pool has not yet been reached:&lt;/p&gt;  &lt;p&gt;- &lt;i&gt;it will commit more reserved pages into the buffer pool rather than to move a page that has been used previously to the Free List.&lt;/i&gt;&lt;/p&gt;  &lt;p&gt;This is how the Page Life Expectancy can stay very high on a server with little workload (or plenty of memory available) and also why Process:Private Bytes (sqlservr.exe) and SQL Server Memory Manager: Total Server Memory (more on these counters later) will keep growing even on a server with little activity. &lt;/p&gt;  &lt;p&gt;The lion&amp;#8217;s part of writing and freeing buffer pages is actually done by &amp;#8220;normal&amp;#8221; worker threads.&lt;/p&gt;  &lt;p&gt;Lazywriter Thread&lt;/p&gt;  &lt;p&gt;Is a system thread that flushes out batches of dirty aged buffers and puts the corresponding buffers on the Free List in the same manner as described above. The main purpose of the Lazywriter is to maintain the Free List.&lt;/p&gt;  &lt;p&gt;Checkpoint process&lt;/p&gt;  &lt;p&gt;Is another system process that wakes up every minute and checks each database to see whether it has exceeded the Recovery interval. Its purpose is to flush dirty pages in order to keep the number transactions that have to be rolled forward during a potential recovery to a minimum (or rather to ensure that the sum of the recovery cost value for all transactions does not exceed the number of minutes specified in Recovery Interval). However, the Checkpoint process does NOT put buffers back on the free list.&lt;/p&gt;  &lt;p&gt;Eager Write&lt;/p&gt;  &lt;p&gt;Is a special write mechanism used for non-logged IO operations such as BULK INSERT and SELECT INTO. Its purpose is to avoid so called Buffer pool trashing (which is what happens when large amounts of data that is unlikely to be re-used gets read into cache).&lt;/p&gt;  &lt;p&gt;Now that we have some background &amp;#8211; let&amp;#8217;s look at the performance counters related to the buffer pool. You need to look at these counters altogether to get the complete picture. For instance, Page Life Expectancy is quite often all over the place - even dips to zero occasionally - even if there is no memory issue. &lt;/p&gt;  &lt;p&gt;SQL Server Buffer Manager:   &lt;br /&gt;Lazy writes/sec    &lt;br /&gt;Checkpoint/sec    &lt;br /&gt;Free List stalls/sec    &lt;br /&gt;Free Pages/sec&lt;/p&gt;  &lt;p&gt;Page Life Expectancy   &lt;br /&gt;(Page Writes/sec    &lt;br /&gt;Page Reads/sec)&lt;/p&gt;  &lt;p&gt;We already know that most writing and freeing buffers is made by individual worker threads. This means that on a very healthy system, Lazy writes/sec and Checkpoint pages/sec will be close to zero. However, if the worker threads cannot quite keep up because of the workload, you will see Lazy writes/sec climb up.&lt;/p&gt;  &lt;p&gt;You might also see Free Pages jumping up and down occasionally and that in itself is not a problem. SQL server can usually recover quickly from a zero Free pages situation. However, if you see Free List Stalls, you can be pretty sure that your workload would benefit from more memory or from tuning to reduce the memory requirements (whether that is index tuning, query tuning or maybe even just an update of statistics).&lt;/p&gt;  &lt;p&gt;To summarize, if Free Pages and Page Life Expectancy are intermittently dipping but you do not see any increase in Lazy writes/sec, the amount of memory available in the Buffer pool is probably enough for the workload (as measured by SQL Server Statistics: Batches/sec).&lt;/p&gt;  &lt;p&gt;The Page Writes/sec and Page Reads/sec gives you information of how much SQL Server is contributing to hard page faults on the system. Because I typically look at to me unknown workloads, I find it hard to say anything about the numbers as they are. I usually look at it in comparison to some benchmark numbers (or in comparison to hard page faults overall on the server).&lt;/p&gt;  &lt;p&gt;You might wonder why I have not included Buffer Manager: Buffer Cache hit ratio. The reason is simple; not all memory consumption will reflect in Buffer Cache Hit Ratio because not all, although most, memory is about data and index pages; you have things such as workspace memory or memory consumed by the optimizer. A high Buffer Cache Hit Ration doesn&amp;#8217;t necessarily mean that all is well although a &lt;i&gt;low&lt;/i&gt; cache hit ratio indicates issues with the Buffer pool (or rather the utilization of memory available in the buffer pool). Having said this, on a finely tuned OLTP system it works quite well as an indicator because you are typically not processing large amounts of data (that would require large workspace memory).&lt;/p&gt;  &lt;p&gt;If you look in Performance Monitor, you will notice you have a Buffer Partition and Buffer Node. This is because some structures are per processor and per NUMA node respectively. (Aside; on NUMA you will have one Lazywriter per NUMA node). &lt;/p&gt;  &lt;p&gt;There is another group of counters that concerns memory that requires special attention. On a finely tuned OLTP system, you normally don&amp;#8217;t have issues with this but you should keep an eye on these counters as well when you are investigating the Buffer pool.&lt;/p&gt;  &lt;p&gt;SQL Server Memory Manager:   &lt;br /&gt;Memory Grants Outstanding    &lt;br /&gt;Memory Grants Pending&lt;/p&gt;  &lt;p&gt;Certain memory intensive queries require &lt;i&gt;Memory Grants&lt;/i&gt; before executing and in order to balance the need for these queries with other user activity, SQL Server throttles the number of concurrent queries of this kind. Queries that require SORT or HASH operators will need a memory grant, or memory reservation, from the &lt;i&gt;Memory Broker&lt;/i&gt;. The number of pending grants should be zero. If it&amp;#8217;s not, it is a sign that some tuning or optimization should take place. &lt;/p&gt;  &lt;p&gt;Lastly, a recap on the memory counters for SQL Server overall memory:&lt;/p&gt;  &lt;p&gt;Process:Private Bytes:sqlservr.exe &amp;#8211; all committed memory including the Buffer pool&lt;/p&gt;  &lt;p&gt;Process:Working Set: sqlservr.exe - the part of committed memory (Private Bytes) which is mapped to physical RAM&lt;/p&gt;  &lt;p&gt;SQL Server Memory Manager : Total Server Memory - the amount of committed memory used by the Buffer pool&lt;/p&gt;  &lt;p&gt;SQL Server Memory Manager : Target Server Memory &amp;#8211; the amount of memory reserved for the Bufferpool. This number is set at SQL Server startup but will be adjusted if the server gets into a low-memory situation.&lt;/p&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=12363" width="1" height="1"&gt;</content><author><name>Elisabeth Redei</name><uri>http://www2.sqlblog.com/members/Elisabeth+Redei.aspx</uri></author></entry><entry><title>How To Get High Quality Information About Query Performance</title><link rel="alternate" type="text/html" href="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/how-to-get-high-quality-information-about-query-performance.aspx" /><id>http://www2.sqlblog.com/blogs/elisabeth_redei/archive/2009/03/01/how-to-get-high-quality-information-about-query-performance.aspx</id><published>2009-03-01T18:52:00Z</published><updated>2009-03-01T18:52:00Z</updated><content type="html">&lt;P&gt;&lt;B&gt;&lt;/B&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;P&gt;There are a zillion reasons why Profiler and its friends by far are the most potent tuning tools available for SQL Server DBAs. This post focuses on one of them but I will mention a few others at the end as well (download details for the tools mentioned are also listed at the end). &lt;/P&gt;
&lt;P&gt;With SQL Server 2005 a new world opened to SQL Server DBAs with the introduction of DMVs and there are even more available in SQL Server 2008. There are 2 groups of DMVs that has, right-fully, gained enormous popularity - the sys.dm_db_index_* DMV's and the sys.dm_exec_* DMVs. &lt;/P&gt;
&lt;P&gt;With the introduction of the DMVs, I have noticed that a lot of people are not inviting Profiler to their tuning parties anymore – I will show you why he (she?) will always be on my guest list.&lt;/P&gt;
&lt;P&gt;In this post, I am going to talk about the limitations of sys.dm_exec_query_stats and sys.dm_exec_cached_plans. I will then show you how you can get aggregated performance data with much higher quality using Profiler trace data and some of the easy-to-use lightweight tools that Microsoft and the community provide for free.&lt;/P&gt;
&lt;P&gt;Needless to say, there are situations when running a trace is not feasible and the sys.dm_exec_* DMV is a very welcome option. It’s never going to hurt anyone to tune whatever TOP queries you identify with these DMVs. For real-time troubleshooting, they are probably also your only option but then you need to be aware that sys.dm_exec_query_stats only contains aggregated data about query executions that has actually finished executing. Sys.dm_exec_request – or some combination - is a better option for real-time troubleshooting. &lt;/P&gt;
&lt;P&gt;However and whenever you use sys.dm_exec_query_stats, you need to be aware of its limitations. &lt;/P&gt;
&lt;P&gt;There are numerous scripts out there that queries sys.dm_exec_query_stats from different angles and they will typically look something like this:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;(total_elapsed_time/execution_count)/1000 AS [Avg Exec Time in ms]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, max_elapsed_time/1000 AS [MaxExecTime in ms]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, min_elapsed_time/1000 AS [MinExecTime in ms]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, (total_worker_time/execution_count)/1000 AS [Avg CPU Time in ms]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, qs.execution_count AS NumberOfExecs&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, (total_logical_writes+total_logical_Reads)/execution_count AS [Avg Logical IOs]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, max_logical_reads AS MaxLogicalReads&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, min_logical_reads AS MinLogicalReads&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, max_logical_writes AS MaxLogicalWrites&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, min_logical_writes AS MinLogicalWrites&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;,(&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT SUBSTRING(text,statement_start_offset/2,&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;(&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;CASE WHEN statement_end_offset = -1 &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;then LEN(CONVERT(nvarchar(max), text)) * 2 &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ELSE statement_end_offset &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;end -statement_start_offset)/2&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;)&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM sys.dm_exec_sql_text(sql_handle)&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;) AS query_text&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM sys.dm_exec_query_stats qs&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ORDER BY [Avg Exec Time in ms] DESC&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;The first time you ever used this DMV you probably thought that the gates of heaven just opened up in front of you. No more getting approval for running traces, no more copying and wading around in MBs or GBs of trace data. For a few chosen ones, this might have been the case but for most of us that was not really the case. Especially if you, like me, tend to look at many different systems and have limited amount of time to assess the workload.&lt;/P&gt;
&lt;P&gt;Depending on the access methods used on the particular server you are looking at, you might only see half of the truth, if you are lucky the output shows you something very close to the whole truth. But it will never show you the whole truth.&lt;/P&gt;
&lt;P&gt;&lt;U&gt;Why Not The Whole Truth?&lt;/U&gt;&lt;/P&gt;
&lt;P&gt;The inherent problem with with sys.dm_exec_query_stats is that it shows you aggregations from sys.dm_exec_cached_plans. And contrary to what you might believe, not all query plans will go into the procedure cache. Even if you did know this, you may not realize the full implication.&lt;/P&gt;
&lt;P&gt;Some of the things that will NEVER be cached are:&lt;/P&gt;
&lt;P&gt;1. DBCC and BACKUP statements&lt;/P&gt;
&lt;P&gt;2. Any query that was issued with the RECOMPILE statement&lt;/P&gt;
&lt;P&gt;3. So called "zero-cost" plan queries - i.e. queries that are very cheap to compile but NOT necessarily cheap to execute.&lt;/P&gt;
&lt;P&gt;In addition to this, &lt;/P&gt;
&lt;P&gt;&lt;I&gt;You cannot see the accumulated numbers for a query whose plan is never re-used (one that uses inline literals and is not parameterized implicitly or explicitly).&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;&lt;I&gt;Execution time for things like EXEC extended_stored_procedure or CREATE INDEX will not be included in sys.dm_exec_query_stats.total_worker_time. &lt;/I&gt;&lt;/P&gt;
&lt;P&gt;On top of this, a plan can get thrown out of cache at any point in time and once it is out, it will not show up in sys.dm_exec_query_stats. &lt;/P&gt;
&lt;P&gt;Unless you are certain that your ENTIRE WORKLOAD consists of queries that are parameterized and that none of your procedures contain “funky stuff” such as EXEC ext_stored_procedure; you absolutely have to use Profiler (or the corresponding sp_trace_* procedures) to get an accurate view of your workload. &lt;/P&gt;
&lt;P&gt;An example of an Ad-hoc query that would “escape” sys.dm_exec_query_stats is the following:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;USE Adventureworks&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;sp.salespersonid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, so.salesorderid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM sales.salesorderheader so JOIN sales.salesperson sp &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ON sp.salespersonid = so.salespersonid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;WHERE so.salesorderid = 43860&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;The above query will generate a new plan for each execution provided that the salesorderid is different and you will find a different entry in sys.dm_exec_query_stats and sys.dm_exec_cached_plan for each execution:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;total_worker_time as CPU&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, total_elapsed_time AS Duration&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, total_logical_writes AS TotWrites&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, total_logical_reads AS TotReads&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, qs.execution_count AS NoOfExecs&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, text&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM sys.dm_exec_query_stats qs&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;CROSS APPLY sys.dm_exec_sql_text(sql_handle)&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic1_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=74 alt=Pic1 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic1_thumb.jpg" width=610 border=0&gt;&lt;/A&gt; &lt;/P&gt;&lt;I&gt;&lt;/I&gt;
&lt;P&gt;&lt;I&gt;Fig 1. The two executions show up as separate queries in sys.dm_exec_query_stats.&lt;/I&gt;&lt;/P&gt;&lt;I&gt;&lt;/I&gt;
&lt;P&gt;For my second scenario, I created two stored procedures with the only difference that I added a WAITFOR (to simulate a call to e.g. an extended stored procedure) in one of them.&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;CREATE PROCEDURE uspGetSalesPerson2 (@p1 int)&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;AS&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;BEGIN&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;WAITFOR DELAY'00:00:10' -- uspGetSalesPerson WITHOUT the WAITFOR&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;sp.salespersonid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, so.salesorderid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM sales.salesorderheader so JOIN sales.salesperson sp &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ON sp.salespersonid = so.salespersonid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;WHERE so.salesorderid = @p1&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;END&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;And then ran them:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;uspGetSalesperson2 44501&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;go&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;uspGetSalesperson 44501&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic2_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=65 alt=Pic2 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic2_thumb.jpg" width=611 border=0&gt;&lt;/A&gt; &lt;/P&gt;&lt;I&gt;&lt;/I&gt;
&lt;P&gt;&lt;I&gt;Fig 2. As you can see, the 10 WAITFOR seconds in supGetSalesperson2 are not accounted for in the total_elapsed_time (Duration) at all&lt;/I&gt;.&lt;/P&gt;
&lt;P&gt;&lt;U&gt;What About SQL Server 2008?&lt;/U&gt;&lt;/P&gt;
&lt;P&gt;In SQL Server 2008, there is something called query fingerprints/query hash which will let you see aggregates for non-parameterized queries such as the above. It can also be used to identify which queries in your workload that would benefit from parameterization. &lt;/P&gt;
&lt;P&gt;However, &lt;I&gt;it still requires that the plan was put in cache in the first place&lt;/I&gt;. The Activity Monitor uses this new feature but the Management Datawarehouse does not in the RTM version (you can read more about this new feature in Bart Duncan’s blog, “The Best New SQL 2008 Feature You Never Heard Of”, &lt;A href="http://blogs.msdn.com/bartd/archive/2008/09/03/Query-Fingerprints-and-Plan-Fingerprints_3A00_-The-Best-New-SQL-2008-Feature-You_2700_ve-Never-Heard-Of.aspx"&gt;http://blogs.msdn.com/bartd/archive/2008/09/03/Query-Fingerprints-and-Plan-Fingerprints_3A00_-The-Best-New-SQL-2008-Feature-You_2700_ve-Never-Heard-Of.aspx&lt;/A&gt;).&lt;/P&gt;
&lt;P&gt;To address the problem with duration for statements other than SELECT/INSERT/UPDATE/DELETE, there is a new DMV in SQL Server 2008 called sys.dm_exec_procedure_stats which aggregates consumption for ALL statements executed within a stored procedure.&lt;/P&gt;
&lt;P&gt;&lt;U&gt;Enter Profiler And Friends&lt;/U&gt;&lt;/P&gt;
&lt;P&gt;Anyone who has used Profiler knows that it in itself can do nothing about the above – in fact it does anything BUT aggregate data. This is when the friends should join the party. For the above query (first executed with so.salesorderid = 44501 and then with so.salesorderid = 43860), the equivalent output from my personal favorite friends, SQLNexus and Reporter, look like this:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic3_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=61 alt=Pic3 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic3_thumb.jpg" width=599 border=0&gt;&lt;/A&gt; &lt;/P&gt;&lt;I&gt;&lt;/I&gt;
&lt;P&gt;&lt;I&gt;Fig 3. Notice the number of executes and how Readtrace has parsed the inline literal in the Query Template in order to be able aggregate the performance data.&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;And for my second scenario with the missing WAITFOR duration:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/pic4_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=54 alt=pic4 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/pic4_thumb.jpg" width=609 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 4. Notice the difference in Duration for uspGetSalesperson2 compared to Fig 2.&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;So what did I do? I first ran a Profiler trace with the default template adding just the StmtStarting and StmtCompleted events (although in a real world scenario you would want to add some more events and you might also save the trace definition as a script and use the sp_trace_* procedures to avoid the overhead of the Profiler GUI). &lt;/P&gt;
&lt;P&gt;I then ran SQLNexus, connected to one of the SQL Server instances on my machine and imported the trace file I just created. SQLNexus will at this point create a database called SQLNexus (default which can be changed) where the crunched trace data goes and then generate a number of reports. The number of reports available depends on what events you included in the trace. &lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/pic5_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=249 alt=pic5 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/pic5_thumb.jpg" width=580 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 5&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;If you switch to “ReadTrace_Main” you will find the output found in Fig3 and Fig4 at the bottom of the screen. As you can see in Fig 3 &amp;amp; 4, the information is somewhat limited and there is certainly some vital information missing. For instance, I like to know whether there are variations in Execution times to pinpoint intermittent bad execution plans, resource or blocking issues.&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic6_4.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=57 alt=Pic6 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic6_thumb_1.jpg" width=612 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 6.&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;By clicking the statement in the grid, you will get to the “Unique Batch Details” report which at the end has the following data:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic7_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=100 alt=Pic7 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic7_thumb.jpg" width=596 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 7.&lt;/I&gt;&lt;/P&gt;&lt;I&gt;&lt;/I&gt;
&lt;P&gt;In order to get that information, you can query the data in the SQLNexus database as well. In addition to all the data necessary to produce the reports in the SQLNexus GUI, it also contains a number of views you can query (or you can query the base tables directly). You can also make your own custom reports. &lt;/P&gt;
&lt;P&gt;To get the data in one single report, I ran the following SELECT statement:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;USE sqlnexus&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;vwU.NormText&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.CompletedEvents&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.Duration&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.AvgDuration&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.Reads&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.AvgReads&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.Writes&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.AvgWrites&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.CPU&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.AvgCPU&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;, vwB.HashID&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;FROM readtrace.vwBatchUtilization vwB &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;INNER JOIN readtrace.vwUniqueBatches vwU&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;ON vwB.hashid = vwU.hashid&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic8_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=45 alt=Pic8 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic8_thumb.jpg" width=613 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 8. Notice the HashID column to the far right – that is basically a “query fingerprint” for this particular query.&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;Using Reporter, Baseline and Comparison Data&lt;/P&gt;
&lt;P&gt;One really cool feature of Reporter, is that you can load two different trace data databases and look at comparative data. So you could have one baseline database and then at any point in time, capture a new trace and compare the results in Reporter:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic9_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=70 alt=Pic9 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic9_thumb.jpg" width=616 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 9. Comparative data from Reporter.&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;It will also identify which queries are found in which trace data set (i.e. if there are new TOPn queries on the scene or if any of your old TOPn queries are gone). Be aware that you can change the TOP parameter to show anything between TOP 3 to TOP 100.&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic10_2.jpg"&gt;&lt;IMG style="BORDER-TOP-WIDTH:0px;BORDER-LEFT-WIDTH:0px;BORDER-BOTTOM-WIDTH:0px;BORDER-RIGHT-WIDTH:0px;" height=244 alt=Pic10 src="http://sqlblog.com/blogs/elisabeth_redei/WindowsLiveWriter/1773eac9d4ed_125B3/Pic10_thumb.jpg" width=229 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;&lt;I&gt;Fig 10. Identifying new queries with Reporter.&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;More Information About the Tools&lt;/P&gt;
&lt;P&gt;As mentioned, SQLNexus has a number of drill-downable reports available and it can also import Performance Monitor output and WAITSTATS (sys.dm_os_wait_stats output) data. Reporter on the other hand, has the same drill-down reports but works only with trace data (and some extra features such as the comparison featured mentioned you will not find in SQLNexus). &lt;/P&gt;
&lt;P&gt;Both utilities require ReadTrace, one of the RML Utilities released by Microsoft and the data format for both Reporter and SQLNexus is the same. By default, ReadTrace creates a database name PerfAnalysis but if you run ReadTrace via SQLNexus, the name will, as mentioned earlier be SQLNexus by default.&lt;/P&gt;
&lt;P&gt;In SQLNexus, you can point to the trace files and it will import them for you whereas Reporter requires you to first run ReadTrace to create the database. If you are a little bit lazy like me (or “productivity focused” as I like to call it) you can use the GUI in SQLNexus to create the trace data database and then point Reporter to that database. &lt;/P&gt;
&lt;P&gt;Another perk is that once you have the trace data, you can easily feed it to the Database Tuning Advisor (which is a good idea considering the limitations of the sys.dm_db_index_* DMVs… but that’s another story… and an upcoming blog post). &lt;/P&gt;
&lt;P&gt;My strong recommendation is that you at least every now and then try to find a window where you can capture trace data to make a full assessment of the workload of your SQL Server(s).&lt;/P&gt;
&lt;P&gt;If the above is not necessary to convince you to bring along Profiler and his cool friends, I will mention a few other reasons:&lt;/P&gt;
&lt;OL&gt;&lt;/OL&gt;
&lt;LI&gt;
&lt;P&gt;The ability to Replay trace data. For instance, before applying a service pack, capture a trace of your critical workload and Replay it against an upgraded test server.&lt;/P&gt;&lt;/LI&gt;&lt;/OL&gt;
&lt;OL&gt;
&lt;OL&gt;&lt;/OL&gt;
&lt;P&gt;(With OStress, another RML Utility, or the built-in “Replay events using multiple threads” feature of Profiler, you can simulate multiple users.)&lt;/P&gt;
&lt;OL&gt;&lt;/OL&gt;
&lt;LI&gt;
&lt;P&gt;Profiler will also catch the number of rows returned to the client so you can identify queries that are returning over-the-top amounts of rows. These queries are not necessarily expensive to execute.&lt;/P&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;You can easily detect if cursors are used and in particular cursors with a low rowfetch size&lt;/P&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;It will give you information about runtime errors and performance related warnings (such as HASH and SORT warnings)&lt;/P&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;If you have a performance monitor log from the same time window, you can correlate the data from within the Profiler GUI.&lt;/P&gt;&lt;/LI&gt;&lt;/OL&gt;
&lt;OL&gt;&lt;/OL&gt;
&lt;P&gt;In essence, there is nothing else available that will give you a complete picture about the workload on the server. The best method to get to know a system is to gather and analyze trace data (with whatever tool you are comfortable with) and maybe even spend a bit of time just browsing through a trace of some of your more critical workload(s).&lt;/P&gt;
&lt;P&gt;More Information and Download Areas&lt;/P&gt;
&lt;P&gt;You can find (a lot) more information about the tools in the following article. At the end of the article you will find links to download areas:&lt;/P&gt;
&lt;P&gt;“Precision Performance for Microsoft SQL Server using RML Utilities 9.0”&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlcat.com/technicalnotes/archive/2008/02/01/precision-performance-for-microsoft-sitql-server-using-rml-utilities-9-0.aspx"&gt;http://sqlcat.com/technicalnotes/archive/2008/02/01/precision-performance-for-microsoft-sitql-server-using-rml-utilities-9-0.aspx&lt;/A&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=12360" width="1" height="1"&gt;</content><author><name>Elisabeth Redei</name><uri>http://www2.sqlblog.com/members/Elisabeth+Redei.aspx</uri></author><category term="Profiler" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Profiler/default.aspx" /><category term="Performance Data" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Performance+Data/default.aspx" /><category term="Reporter" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/Reporter/default.aspx" /><category term="SQLNexus" scheme="http://www2.sqlblog.com/blogs/elisabeth_redei/archive/tags/SQLNexus/default.aspx" /></entry></feed>