<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>SQL in the Wild</title>
	<atom:link href="http://sqlinthewild.co.za/index.php/feed/" rel="self" type="application/rss+xml" />
	<link>http://sqlinthewild.co.za</link>
	<description>A discussion on SQL Server</description>
	<lastBuildDate>Tue, 31 Aug 2010 17:26:19 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>Come and gone</title>
		<link>http://sqlinthewild.co.za/index.php/2010/08/31/come-and-gone/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/08/31/come-and-gone/#comments</comments>
		<pubDate>Tue, 31 Aug 2010 14:00:59 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Execution Plans]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=643</guid>
		<description><![CDATA[Or &#8220;Plan cache monitoring &#8211; insert and remove&#8221; Previously I took a look at the CacheHit and CacheMiss events to see how they behave and how to identify what&#8217;s been searched for in the cache. in this follow up, I want to take a similar look at the CacheInsert and CacheRemove events, see when they [...]]]></description>
			<content:encoded><![CDATA[<p>Or &#8220;<em>Plan cache monitoring &#8211; insert and remove</em>&#8221;</p>
<p>Previously I took a look at the <a href="http://sqlinthewild.co.za/index.php/2010/07/27/hit-and-miss/">CacheHit and CacheMiss</a> events to see how they behave and how to identify what&#8217;s been searched for in the cache. in this follow up, I want to take a similar look at the CacheInsert and CacheRemove events, see when they fire and how to identify the objects that they relate to.</p>
<p>Again, a word of caution, these can be frequently occurring events on busy servers and so traces should be kept short and to a minimum of events and columns. That said, these should occur a lot less often than the CacheHit and CacheMiss events. If they are occurring very frequently it may indicate that the SQL Server is not reusing plans efficiently.</p>
<h3>CacheInsert</h3>
<p>The CacheInsert event fires after a CacheMiss. The search for a matching plan in the cache failed, firing a CacheMiss event. Since there&#8217;s no plan, the optimiser is invoked to generate one and then that plan is inserted into the plan cache before the Query Execution engine begins execution.</p>
<p>The event is fairly simple, though of course there are a few surprises (what in SQL doesn&#8217;t have?).</p>
<pre class="brush: sql;">Exec FireCacheEvents
GO

SELECT ID, SomeDate, Status
FROM TestingCacheEvents
WHERE Status =  'C'</pre>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/08/CacheInsert.png"><img style="display: inline; border-width: 0px;" title="CacheInsert" src="http://sqlinthewild.co.za/wp-content/uploads/2010/08/CacheInsert_thumb.png" border="0" alt="CacheInsert" width="490" height="83" /></a></p>
<p><span id="more-643"></span></p>
<p>Three CacheInsert events for two batches. The first is simple enough, it&#8217;s the insert of the plan for the stored procedure. The procedure name is in the TextData column and the procedure&#8217;s ID is in the ObjectID column. The ObjectName column is not populated for this event.</p>
<p>The second and third Cacheinsert events are the interesting ones. The second one shows a parameterised version of the ad-hoc SQL statement, while the third shows an unparameterised version of the same ad-hoc SQL statement. Clearly this query was simple enough to qualify for auto-parameterisation. So, are there two plans for this been inserted into cache? To answer that one, I need to switch over to SSMS and query the plan cache.</p>
<pre class="brush: sql;">select usecounts, size_in_bytes, cacheobjtype, objtype, st.text,  qp.query_plan
 from sys.dm_exec_cached_plans cp
 cross apply  sys.dm_exec_sql_text(cp.plan_handle) st
 cross apply  sys.dm_exec_query_plan(cp.plan_handle) qp
 where st.text not like  '%sys.dm_exec%'</pre>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/08/ExecCachedPlans.png"><img style="display: inline; border-width: 0px;" title="ExecCachedPlans" src="http://sqlinthewild.co.za/wp-content/uploads/2010/08/ExecCachedPlans_thumb.png" border="0" alt="ExecCachedPlans" width="484" height="56" /></a></p>
<p>Well, there are three entries and the Cache Object Type is listed as compiled plan for all three, but there&#8217;s something different between them. Take a look at the size in bytes for the ad-hoc and prepared statements. The ad-hoc is less than half the size of the prepared. That&#8217;s not the only thing different. If I open up the execution plans, the exec plan for the prepared statement looks normal. There&#8217;s a clustered index scan (there are no nonclustered indexes on this table yet) and a select operator. The plan for the adhoc statement however…</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/08/NotaPlan.png"><img style="display: inline; border-width: 0px;" title="NotaPlan" src="http://sqlinthewild.co.za/wp-content/uploads/2010/08/NotaPlan_thumb.png" border="0" alt="NotaPlan" width="475" height="100" /></a></p>
<p>There&#8217;s something missing here, like the rest of the plan. Ok, so the entry with the ad-hoc type is not a complete plan, so what is it?</p>
<p>There doesn&#8217;t seem to be anything on MSDN on this (at least nothing I could find), but two of Kalen&#8217;s books<sup>1</sup>. (Inside Microsoft SQL Server 2005: Query Tuning and Optimisation, page 283; Microsoft SQL Server 2008 Internals, page 533) mention this. These ad-hoc entries are shell queries, cached just to make the parameterised form of the query easier to find. All the execution plan contains for these shell queries is a pointer to the plan for the parameterised version of the query.</p>
<p>For the ad-hoc query that gets inserted as a shell plan and the parameterised query, the objectID is, as with the CacheHit and CacheMiss events, a hash of the query text and does not match to any object in the database. The full text of the batch is then given in the TextData column.</p>
<p>I think that&#8217;s enough for CacheInsert. On to it&#8217;s opposite.</p>
<h3>CacheRemove</h3>
<p>The CacheRemove event fires, as one would expect, when a plan is removed from cache. The interesting question is, when does that occur?</p>
<p>Is it every event that could possibly result in an inefficient or invalid plan, or just memory pressure and explicit cache flushes that trigger the CacheRemove event?</p>
<p>Let&#8217;s see…</p>
<p>As an aside, I&#8217;m going to add a nonclustered index to the table that I&#8217;m using, so that the update of statistics will have an effect. Otherwise the only execution path possible is a table scan, and changing statistics won&#8217;t affect that.</p>
<pre class="brush: sql;">CREATE NONCLUSTERED INDEX idx_TestingCacheEvents_Status
 ON  TestingCacheEvents (Status)
GO</pre>
<p>Right, onto the testing.</p>
<pre class="brush: sql;">Exec FireCacheEvents
GO
SELECT ID, SomeDate, Status
FROM  TestingCacheEvents
WHERE Status = 'C'
GO
DBCC FREEPROCCACHE -- Gone.  All gone
WAITFOR DELAY '00:00:05'
GO

Exec FireCacheEvents
GO
SELECT ID, SomeDate, Status
FROM  TestingCacheEvents
WHERE Status = 'C'
GO
DBCC  FREEPROCCACHE(0x0500060063A9355540614285000000000000000000000000) -- Just one removed, the proc
WAITFOR DELAY '00:00:05'
GO

Exec FireCacheEvents
GO
SELECT ID, SomeDate, Status
FROM  TestingCacheEvents
WHERE Status = 'C'
GO
exec sp_recompile  'TestingCacheEvents' -- recompile on the table, both proc and ah-hoc plans  invalidated.
WAITFOR DELAY '00:00:05'
GO

Exec FireCacheEvents
GO
SELECT ID, SomeDate, Status
FROM  TestingCacheEvents
WHERE Status = 'C'
GO
UPDATE TOP(1)  TestingCacheEvents
 SET Status = 'C'
 WHERE Status = 'B'
UPDATE STATISTICS TestingCacheEvents WITH FULLSCAN
WAITFOR DELAY  '00:00:05'
GO

Exec FireCacheEvents
GO
SELECT ID, SomeDate, Status
FROM  TestingCacheEvents
WHERE Status = 'C'
GO
ALTER TABLE  TestingCacheEvents ALTER COLUMN FILLER CHAR(325)
WAITFOR DELAY '00:00:05'
GO
Exec FireCacheEvents
GO
SELECT ID, SomeDate, Status
FROM  TestingCacheEvents
WHERE Status = 'C'
GO</pre>
<p>Well, this is interesting…</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/08/CacheRemove.png"><img style="display: inline; border-width: 0px;" title="CacheRemove" src="http://sqlinthewild.co.za/wp-content/uploads/2010/08/CacheRemove_thumb.png" border="0" alt="CacheRemove" width="492" height="230" /></a></p>
<p>The only things that caused the CacheRemove event to fire were the two DBCC FreeProcCache, the first that, as can be seen from the EventSubClass, cleared the entire plan cache, and the second that cleared a single plan.</p>
<p>If I go back and look at the whitepaper <a href="http://msdn.microsoft.com/en-us/library/ee343986.aspx">Plan Caching in SQL Server 2008</a>, it differentiates between removing a plan from cache (which memory pressure and cache flushes do) and invalidating the plan (which recompiles, schema changes, stats changes and the like do). Based on that whitepaper and these results, I would conclude that the CacheRemove event only fires when the plan is actually removed from cache, not when it&#8217;s just invalidated. The invalidation of a plan simply results in a recompile next time the plan is needed.</p>
<p>Now that the question of when it appears is answered, let&#8217;s finish with what it looks like.</p>
<p>There are two possible formats for this event, and it&#8217;s the EventSubClass that shows which of the two the particular event is.</p>
<p>The first possibility is when all of the plan cache has been flushed, identified by a EventSubClass of &#8220;2 &#8211; ProcCacheFlush&#8221;. This will be a result of a DBCC FREEPROCCACHE and some server reconfiguration events. In this case, there will be no ObjectID and the text data will simply state that the cache has been flushed.</p>
<p>The second possibility is more interesting, especially if monitoring plans being aged out of memory. An EventSubClass of &#8220;1 &#8211; Compplan Remove&#8221; identifies a single plan being removed from cache. These are fired when either a single plan or part of the plan cache is cleared. Examples here are DBCC FREEPROCCACHE with a plan or SQL handle passed to it, DBCC FLUSHPROCINDB, database reconfigurations (restore, detach, offline)</p>
<p>With the one, the ObjectID does have a value and it&#8217;s the ObjectID of the pbject whose plan is being thrown out of cache. As with all the other cache events, for ad-hoc queries the ObjectID is just a hash of the query text and only really useful for matching to CacheMiss, CacheHit and CacheInsert events and the object name or ad-hoc batch appears in the TextData</p>
<p>That&#8217;s more than I planned to write, but CacheRemove turned out to be a little more complex than I expected. Next up, the last part of this sort series &#8211; the recompile events.</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/08/31/come-and-gone/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Hit and miss</title>
		<link>http://sqlinthewild.co.za/index.php/2010/07/27/hit-and-miss/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/07/27/hit-and-miss/#comments</comments>
		<pubDate>Tue, 27 Jul 2010 14:00:10 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Execution Plans]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=377</guid>
		<description><![CDATA[Or &#8220;Monitoring plan cache usage&#8221; For people interested in the details of how SQL is using and reusing execution plans, there are some useful events in profiler for watching this in detail, under the Stored procedure group: SP:CacheMiss SP:CacheInsert SP:CacheHit SP:CacheRemove SP:Recompile SP:StmtRecompile Additionally there&#8217;s the SQL:StmtRecompile event under the TSQL group. For now, I [...]]]></description>
			<content:encoded><![CDATA[<p>Or &#8220;<em>Monitoring plan cache usage</em>&#8221;</p>
<p>For people interested in the details of how SQL is using and reusing execution plans, there are some useful events in profiler for watching this in detail, under the Stored procedure group:</p>
<ul>
<li>SP:CacheMiss</li>
<li>SP:CacheInsert</li>
<li>SP:CacheHit</li>
<li>SP:CacheRemove</li>
<li>SP:Recompile</li>
<li>SP:StmtRecompile</li>
</ul>
<p>Additionally there&#8217;s the SQL:StmtRecompile event under the TSQL group.</p>
<p>For now, I just want to look briefly at the CacheMiss and CacheHit events.</p>
<p>One word of caution early on, these are frequently occurring events and it may not be a good idea to trace these on busy production servers. If you do need to, keep the duration of the trace short and the columns to a minimum.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheEvents.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="CacheEvents" src="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheEvents_thumb.png" border="0" alt="CacheEvents" width="244" height="157" /></a></p>
<h3>CacheMiss</h3>
<p>The cache miss event fires any time SQL looks for the execution plans for an object or ad-hoc batch and does not find it in the plan cache.</p>
<p>For an object (scalar function, multi-statement table-valued function, stored procedure or trigger) the match is done on the object ID (along with some of the connection&#8217;s SET options and possibly the database user and c couple other factors<sup>1</sup>). For an ad-hoc batch, the match is done on a hash of the text of the batch (along with some of the connection&#8217;s SET options and possibly the database user)</p>
<p>When testing stored procedures from Management Studio (or another SQL querying tool), two CacheMiss events will appear in the trace.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheMiss.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="CacheMiss" src="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheMiss_thumb.png" border="0" alt="CacheMiss" width="490" height="87" /></a></p>
<p>What&#8217;s going on here?</p>
<p><span id="more-377"></span></p>
<p>Let&#8217;s start from the bottom and work up. The last event there, the SP:Completed records the completion of the stored procedure and lists both the ObjectID and ObjectName and, if I check in the database, ObjectID 1301579675 does indeed belong to the stored procedure FireCacheEvents.</p>
<p>The second event (the second cache miss) has the same ObjectID and ObjectName. So this is the failed cache lookup for the stored procedure (failed because this is the first time I ran it and the plan was, as a result, not in cache).</p>
<p>The first entry is the one that&#8217;s curious. The ObjectName is not populated and the ObjectID doesn&#8217;t match anything in the database. So what is the cache lookup trying to find?</p>
<p>The TextData column give a hint. What it&#8217;s trying to find is a cached plan for the ad-hoc batch containing the whole of the text submitted to SQL (In this case just &#8216;EXEC FireCacheEvents&#8217;). This too could contain queries (though it doesn&#8217;t in this case) and needs a plan lookup. With just an EXEC in the batch, it won&#8217;t have a plan and hence won&#8217;t be found in cache, but there&#8217;s still a lookup for it.</p>
<p>If there was any SELECT, INSERT, UPDATE or DELETE (or MERGE on SQL 2008) statement within that ad-hoc batch, the batch would also be cached and future cache lookups would succeed but, since it&#8217;s just an EXEC, there&#8217;s no plan to cache.</p>
<p>This additional cache lookup won&#8217;t occur if the execution is via RPC (e.g. A .Net call with the SQLCommand.CommandType = CommandType.StoredProcedure) but it will any time there is an ad-hoc SQL batch submitted (e.g. from Management Studio or .Net if the SQLCommand.CommandType is set to CommandType.Text)</p>
<p>That&#8217;s ad-hoc batches and stored procs. Let&#8217;s try the third possibility &#8211; a parameterised query.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheMissparameterised.png"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="CacheMiss parameterised" src="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheMissparameterised_thumb.png" border="0" alt="CacheMiss parameterised" width="496" height="61" /></a></p>
<p>This looks semi-familiar. The first CacheMiss is again for the entire of the ad-hoc batch (consisting of the DECLARE and the EXEC in this case). The second looks similar, something in TextData and no ObjectName. This is the parameterised query, identifiable as parameterised by the (@Status Char(1)) parameter definition right at the beginning. Again here the ObjectID is a hash of the text and doesn&#8217;t match to a real object in the database.</p>
<p>It is worth noting that, if both the CacheMiss and CacheInsert events are traced, the CacheMiss events will only appear if there is no subsequent CacheInsert for the same ObjectID. If I ran the exact same script as above, but had the SP:Completed, SP:CacheMiss and SP:CacheInsert events in the trace, there would still be only three events recorded, the cache miss for the ad-hoc batch (as that&#8217;s not cached there&#8217;s no matching CacheInsert event), the CacheInsert for the stored proc and then the SP:Completed. The CacheMiss for the procedure wouldn&#8217;t appear, though it&#8217;s presence can be intuited from the presence of the CacheInsert.</p>
<p>That should about cover it for the CacheMiss. If the failed cache lookup is for is an ad-hoc batch or parameterised query, the TextData column will be populated with the contents of the batch or query and the ObjectID will be a hash of the text (and shouldn&#8217;t match any object in the database). If the failed cache lookup is for a procedure (or function or trigger), the object name column is usually populated (not always) with the name of the object, TextData is blank and the ObjectID matches the ObjectID of the object in the database</p>
<h3>CacheHit</h3>
<p>Onto the CacheHit event. This, as its name implies, is the opposite to the CacheMiss. The CacheMiss indicates that a lookup to the plan cache failed to find a matching plan. The CacheHit indicates that a lookup to the plan cache succeeded in finding a matching plan (based on object id, set options, maybe user, and various other conditions).</p>
<p>It&#8217;s not certain, even if the cache lookup succeeds, that the plan will indeed be used for the execution of the query/batch/procedure as there are a number of stability and optimality related checks that will be done before the plan is used.</p>
<p>So let&#8217;s see how this event looks.</p>
<pre class="brush: sql;">EXEC FireCacheEvents
GO

SELECT ID, SomeDate, Status
FROM dbo.TestingCacheEvents
WHERE Status = 'B'
GO </pre>
<p>Two ad-hoc batches, first with just a stored procedure call, second with just an ad-hoc SQL statement.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheHit.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="CacheHit" src="http://sqlinthewild.co.za/wp-content/uploads/2010/07/CacheHit_thumb.png" border="0" alt="CacheHit" width="492" height="85" /></a></p>
<p>No big surprises here, not after looking at the CacheMiss events. The first is the CacheMiss for the first of the ad-hoc batches, the one with just the EXEC in it. Since that is not cached, there will be a CacheMiss every time that executes.</p>
<p>The first of the CacheHit events is for the stored procedure. As with the CacheMiss, this CacheHit for the stored procedure has an ObjectID that matches the ObjectID for that procedure in the database, and the ObjectName column is populated with (surprise) the object name while the TextData is blank.</p>
<p>The second CacheHit is for the ad-hoc batch with the SELECT statement. As is probably expected by this point, the ObjectID there is just a hash of the text, the ObjectName is blank and the TextData is populated with the full text of the batch.</p>
<h3>In Conclusion</h3>
<p>If you&#8217;re monitoring cache usage with the CacheMiss and CacheHit events, there are two different ways to identify what the lookup was looking for.</p>
<p>If the lookup was for the plan of a stored procedure, trigger or function, the ObjectID column contains a value that matches the ObjectID for that object in the database. The TextData column is blank and the ObjectName column is (usually) populated with the name of the object. I did encounter a couple of cases where the ObjectName was blank for a CacheMiss event for a stored procedure, not quite sure why. More investigation is necessary.</p>
<p>If the lookup was for the plan of an ad-hoc batch or parameterised query, the ObjectID contains a meaningless value, the ObjectName column is blank and the TextData contains the entire of the batch/query.</p>
<p>It may also be worth mentioning that the DatabaseID column is populated for all CacheMiss and CacheHit events, regardless of what the lookup is looking for. Additionally the DatabaseName column is populated for all CacheHit events (but is not an available column for the CacheMiss)</p>
<p>(1) For anyone who wants more information, there are two excellent resources available from Microsoft:</p>
<ul>
<li><a href="http://technet.microsoft.com/en-gb/library/cc966425.aspx">Batch Compilation, Recompilation, and Plan Caching Issues in SQL Server 2005</a></li>
<li><a href="http://msdn.microsoft.com/en-us/library/ee343986.aspx">Plan Caching in SQL Server 2008</a></li>
</ul>
<p>Both go extremely deep into caching, what influences matching of plans, plan reuse, recompilations and the plan cache itself.</p>
<p>Reproduction code:</p>
<pre class="brush: sql;">CREATE TABLE TestingCacheEvents (
 ID INT IDENTITY PRIMARY KEY,
 SomeDate DATETIME,
 Status CHAR(1),
 Filler CHAR(300) DEFAULT ' '
);
GO

INSERT INTO TestingCacheEvents (SomeDate, Status)
SELECT TOP (10000)
 DATEADD(dd,FLOOR(RAND(a.object_id+b.column_id*5000)*500),'2000/01/01'),
 CHAR(65+FLOOR(RAND(b.object_id+a.column_id*5000)*10))
 FROM master.sys.columns a CROSS JOIN master.sys.columns b;
GO

CREATE PROCEDURE FireCacheEvents
AS
 SELECT ID, SomeDate, Status
 FROM TestingCacheEvents
 WHERE Status = 'G'

GO</pre>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/07/27/hit-and-miss/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Does a missing data file send a database suspect?</title>
		<link>http://sqlinthewild.co.za/index.php/2010/06/29/does-a-missing-data-file-send-a-database-suspect/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/06/29/does-a-missing-data-file-send-a-database-suspect/#comments</comments>
		<pubDate>Tue, 29 Jun 2010 14:00:22 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Corruption]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=612</guid>
		<description><![CDATA[Short answer: No I keep seeing this come up on various forums when people ask what makes a database go suspect or what could have caused their database to be marked suspect. I can almost guarantee when that question comes up, one or more people will answer &#8216;missing files&#8217;. That may have been true on [...]]]></description>
			<content:encoded><![CDATA[<p>Short answer: No</p>
<p>I keep seeing this come up on various forums when people ask what makes a database go suspect or what could have caused their database to be marked suspect. I can almost guarantee when that question comes up, one or more people will answer &#8216;missing files&#8217;. That may have been true on earlier versions (I don&#8217;t have a SQL 2000 or earlier instance to play with), but it is not true in the current versions of SQL. A missing file may result in the database being inaccessible (depending what file), but it will not result in a suspect database.</p>
<p>Let&#8217;s prove it.</p>
<p>I&#8217;m going to create a database with three files, two of which are in the primary filegroup, with one user table on each filegroup. (T-SQL code at the end) Before each test I&#8217;ll begin a transaction, modify data in both tables, shut SQL down so that there&#8217;s an uncommitted transaction in the log (database cannot be cleanly shut down), then rename a file before restarting SQL and see what happens.</p>
<p>I&#8217;m not going to play with the transaction log. That I&#8217;ve <a href="http://sqlinthewild.co.za/index.php/2009/06/09/deleting-the-transaction-log/">done before</a>. In SQL 2005/2008, if the transaction log is missing and the database was cleanly shut down, SQL will recreate it. If the the transaction log is missing and the database was not cleanly shut down, the database goes into the RECOVERY_PENDING state, so no SUSPECT here.</p>
<p>Let&#8217;s try the file in the secondary filegroup first.</p>
<p><span id="more-612"></span></p>
<pre class="brush: sql;">BEGIN TRANSACTION
  UPDATE dbo.Table1 SET SomeColumn = 'aaa'
  UPDATE dbo.Table2 SET SomeColumn = 'aaa'</pre>
<p>Shut SQL down (SHUTDOWN WITH NOWAIT), then off to explorer and rename &#8216;Secondary.ndf&#8217; to &#8216;Secondary.missing.ndf&#8217; and restart the SQL instance.</p>
<p>It&#8217;s definitely not online, the plus sign is missing.<br />
<a href="http://sqlinthewild.co.za/wp-content/uploads/2010/06/DatabaseState.png"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="DatabaseState" src="http://sqlinthewild.co.za/wp-content/uploads/2010/06/DatabaseState_thumb.png" border="0" alt="DatabaseState" width="178" height="244" /></a></p>
<p>According to sys.databases (the state_desc column), the database state is &#8216;RECOVERY_PENDING&#8217;.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/06/NotSuspect.png"><img class="alignnone size-medium wp-image-613" title="Database State" src="http://sqlinthewild.co.za/wp-content/uploads/2010/06/NotSuspect-300x84.png" alt="Database State" width="300" height="84" /></a></p>
<p>Due to the uncommitted transaction that was running at the time SQL was shut down, SQL needs to run restart recovery on this database before bringing it online. It cannot do that because of the missing file. Hence the database state is recovery pending, recovery has not started. A look at the SQL error logs gives a clear reason why.</p>
<blockquote><p>Unable to open the physical file &#8220;D:\Develop\Databases\Secondary.ndf&#8221;. Operating system error 2: &#8220;2(failed to retrieve text for this error. Reason: 15105)&#8221;.</p></blockquote>
<p>Operating system error 2 is &#8220;File not found&#8221;</p>
<p>The important thing to note about the RECOVERY_PENDING state is that it is not fatal. If I fix the underlying reason, (in this case the renamed file) I can restart the database and it will come online. To do that I&#8217;m simply going to take it offline, rename the file back to what it should be, then bring the DB back online.</p>
<pre class="brush: sql;">ALTER DATABASE TestingSuspect SET OFFLINE</pre>
<p>Rename the secondary data file back to what it should be.</p>
<pre class="brush: sql;">ALTER DATABASE TestingSuspect SET ONLINE</pre>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/06/DatabaseState2.png"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="DatabaseState 2" src="http://sqlinthewild.co.za/wp-content/uploads/2010/06/DatabaseState2_thumb.png" border="0" alt="DatabaseState 2" width="104" height="244" /></a></p>
<p>Back online and fully usable.</p>
<p>Next up, the second file in the primary filegroup. Again, begin transaction, do updates, stop SQL, rename &#8216;Primary2.ndf&#8217; to &#8216;Primary2.missing.ndf&#8217;, restart SQL.</p>
<p>Again the database is inaccessible and again the state is RECOVERY_PENDING. Again taking the database offline, fixing the filename and bringing it online makes it fully available with no problems.</p>
<p>The final test, the primary file itself, &#8216;Primary.mdf&#8217;. Same process as before. Just as in the earlier two cases, the database comes up in the RECOVERY_PENDING state, not SUSPECT and the same process as before allows the DB to be brought online without any problems.</p>
<p>So in conclusion…</p>
<p>For a database to go suspect after a SQL startup, one or of the database files must be actually damaged. Just having the files missing will not result in the database being marked suspect. Missing or inaccessible files results in the database being marked recovery_pending only</p>
<p>Database code:</p>
<pre class="brush: sql;">CREATE DATABASE [TestingSuspect]
ON PRIMARY
 (NAME = N'Primary',  FILENAME = N'D:\Develop\Databases\Primary.mdf'),
 (NAME = N'Primary2',  FILENAME = N'D:\Develop\Databases\Primary2.ndf'),
FILEGROUP [Secondary]
 (NAME = N'Secondary', FILENAME = N'D:\Develop\Databases\Secondary.ndf')
LOG ON
 (NAME = N'TestingSuspect_log', FILENAME =  N'D:\Develop\Databases\TestingSuspect_log.ldf')
GO
USE [TestingSuspect]
GO

CREATE TABLE Table1 (
 ID INT IDENTITY,
 SomeColumn CHAR(200)  -- filler
)
ON [Primary]

CREATE TABLE Table2 (
 ID INT IDENTITY,
 SomeColumn CHAR(200)  -- filler
)
ON [Secondary]

INSERT INTO Table1 (SomeColumn)
SELECT TOP 50000 ' '
 FROM  master.sys.columns a CROSS JOIN master.sys.columns b
INSERT INTO Table2  (SomeColumn)
SELECT TOP 50000 ' '
 FROM master.sys.columns a CROSS  JOIN master.sys.columns b</pre>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/06/29/does-a-missing-data-file-send-a-database-suspect/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>DBA Hell</title>
		<link>http://sqlinthewild.co.za/index.php/2010/06/15/dba-hell/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/06/15/dba-hell/#comments</comments>
		<pubDate>Tue, 15 Jun 2010 14:00:46 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Rants]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=605</guid>
		<description><![CDATA[On the first day of DBA hell, the server gave to me A database with damaged system tables and no good backups (1) On the second day of DBA hell, the server gave to me Two databases with widespread corruption, no backups (1, 2) On the third day of DBA hell, the server gave to [...]]]></description>
			<content:encoded><![CDATA[<p>On the first day of DBA hell, the server gave to me<br />
A database with damaged system tables and no good backups (<a href="http://www.sqlservercentral.com/Forums/Topic922090-146-1.aspx">1</a>)</p>
<p>On the second day of DBA hell, the server gave to me<br />
Two databases with widespread corruption, no backups (<a href="http://www.sqlservercentral.com/Forums/Topic930861-266-1.aspx">1</a>, <a href="http://www.sqlservercentral.com/Forums/Topic926497-266-1.aspx">2</a>)</p>
<p>On the third day of DBA hell, the server gave to me<br />
Three suspect databases, no backups (<a href="http://www.sqlservercentral.com/Forums/Topic930198-266-1.aspx">1</a>,<a href="http://www.sqlservercentral.com/Forums/Topic930240-391-1.aspx">2</a>,<a href="http://www.sqlservercentral.com/Forums/Topic922754-146-1.aspx">3</a>)</p>
<p>On the fourth day of DBA hell, the server gave me nothing, cause I didn&#8217;t have a job any longer…</p>
<p>How does one end up with a critical production database that has no backups? I could kinda understand if the backups were damaged, if the corruption went undetected for long enough that it was in the backups as well, but to have no backups at all? Of an important database?</p>
<p>The only excuse for having no backups is if the database can be trivially and completely recreated from another source with minimal impact to the users. This is not the normal scenario.</p>
<p>There&#8217;s an immense amount of information available on backup and restore strategies.</p>
<ul>
<li>&#8220;<a href="http://msdn.microsoft.com/en-us/library/ms191239.aspx">Introduction to backup and restore strategies</a>&#8221; on MSDN</li>
<li>Paul Randal&#8217;s Technet article &#8220;<a href="http://technet.microsoft.com/en-us/magazinebeta/2009.07.sqlbackup.aspx">Understanding SQL Server backups</a>&#8220;</li>
<li>Paul Randal&#8217;s presentation to the SQLPass DBA Virtual Chapter &#8220;<a href="http://dba.sqlpass.org/MeetingArchive.aspx">Building the Right Backup Strategy</a>&#8221; (not available for download at this point, check back in a few days)</li>
</ul>
<p>That&#8217;s just a quick list, there&#8217;s far more information available than that. Enough that there&#8217;s really no good excuse to not have backups when they&#8217;re needed.</p>
<p>As Steve Jones (<a href="http://www.sqlservercentral.com/blogs/steve_jones/">blog</a>|<a href="http://twitter.com/way0utwest">twitter</a>) is fond of saying &#8220;Good backup, good resume. You only need one&#8221;</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/06/15/dba-hell/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Redgate&#039;s Exceptional DBA competition</title>
		<link>http://sqlinthewild.co.za/index.php/2010/05/07/redgates-exceptional-dba-competition/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/05/07/redgates-exceptional-dba-competition/#comments</comments>
		<pubDate>Fri, 07 May 2010 16:00:55 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Community]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=595</guid>
		<description><![CDATA[Redgate&#8217;s Exceptional DBA competition is back for a third year! I was one of the judges for this last year and, while I&#8217;m not judging it this year, I do have some advice for anyone considering entering. Be Explicit and detailed This is not a competition won by luck. There are no dice rolled, no [...]]]></description>
			<content:encoded><![CDATA[<p>Redgate&#8217;s <a href="http://www.exceptionaldba.com/">Exceptional DBA competition</a> is back for a third year! I was one of the judges for this last year and, while I&#8217;m not judging it this year, I do have some advice for anyone considering entering.</p>
<h3>Be Explicit and detailed</h3>
<p>This is not a competition won by luck. There are no dice rolled, no coins tossed, no numbers drawn from a hat.</p>
<p>As an entrant, you need to convince the judges that you (or the person you are nominating) are the best of the best. The only thing that you can use to do that are the answers on the entry form.</p>
<p>The more the better (within reason). To give an idea, last year the answers to one question (What&#8217;s the hallmarks of an exceptional DBA?) ranged from one word to half a page. Which of those two do you think the judges rated higher?</p>
<p>If you can, get a colleague to read over your answers before submitting them. Ask them for their opinion, ask them if there are any pieces that they&#8217;d change or add to show you (or the person you are nominating) in their very best light.</p>
<h3>Spell check</h3>
<p>Please, please, please run a spell check and grammar check over your entries before submitting. This goes double if English (or American) is not your first language. There is nothing that makes an entry look bad more than por speeling thet teh jugdes mast spand tyme desifering.</p>
<p>No, not all of us speak English fluently, but there are enough grammar and spell checkers available (hint Firefox includes one if you download the dictionary) that not bothering shows a lack of interest and professionalism. Besides, if the judges can&#8217;t work out what you&#8217;re saying, they&#8217;re not going to rate your entry highly.</p>
<p>On this point, watch the l33t speak and SMS/twitter style word-shortening. They&#8217;re harder to read that fully written out words, and space is not at a premium for these entries. Again, you should be trying to show that you are a professional, much like you would when writing up a CV.</p>
<h3>Watch the humour</h3>
<p>What&#8217;s funny for one person may be annoying or offensive to another. A joke about &#8216;cleaning up after those incompetent developers&#8217; may not be funny to a judge who is a developer or comes from a development background. Again, keep it professional, imagine that these answers are going to be seen by the CIO/owner/MD of the company you work for.</p>
<p>Along the same lines, funny answers aren&#8217;t. One entry last year, for the question &#8220;Why do you deserve to win?&#8221;, gave as an answer &#8220;No idea <img src='http://sqlinthewild.co.za/wp-includes/images/smilies/icon_wink.gif' alt=';-)' class='wp-smiley' /> &#8221;</p>
<p>Well dude, if you don&#8217;t know why you should win, I sure as hell don&#8217;t.</p>
<h3>In Conclusion</h3>
<p>If you&#8217;re planning to enter this competition, you have one chance to make an impression with the judges &#8211; your answers. Make it the best impression that you can.</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/05/07/redgates-exceptional-dba-competition/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>In, Exists and join &#8211; a roundup</title>
		<link>http://sqlinthewild.co.za/index.php/2010/04/27/in-exists-and-join-a-roundup/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/04/27/in-exists-and-join-a-roundup/#comments</comments>
		<pubDate>Tue, 27 Apr 2010 16:30:47 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>
		<category><![CDATA[T-SQL]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=586</guid>
		<description><![CDATA[Over the last several months I&#8217;ve had a look at IN, Exists, Join and their opposites to see how they perform and whether there&#8217;s any truth in the advice that is often seen on forums and blogs advocating replacing one with the other. Previous parts of this series can be found: Exists vs In In [...]]]></description>
			<content:encoded><![CDATA[<p>Over the last several months I&#8217;ve had a look at IN, Exists, Join and their opposites to see how they perform and whether there&#8217;s any truth in the advice that is often seen on forums and blogs advocating replacing one with the other.</p>
<p>Previous parts of this series can be found:</p>
<ul>
<li><a href="http://sqlinthewild.co.za/index.php/2009/08/17/exists-vs-in/">Exists vs In</a></li>
<li><a href="http://sqlinthewild.co.za/index.php/2010/01/12/in-vs-inner-join/">In vs Inner Join</a></li>
<li><a href="http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/">Not Exists vs Not In</a></li>
<li><a href="http://sqlinthewild.co.za/index.php/2010/03/23/left-outer-join-vs-not-exists/">Not Exists vs Left Outer Join … Is Null</a></li>
</ul>
<p>In this roundup post, I&#8217;m going to do multiple tests on the 6 query forms, with different numbers of rows, indexes, no indexes and, for the negative forms (NOT IN, NOT EXISTS), nullable and non-nullable join columns.</p>
<p>In the individual tests, I used 250000 rows in the first table and around 3000 rows in the secondary table. In this roundup, I&#8217;m going to use 3 different row counts, 1000000 rows, 50000 rows and 2500 rows. That should give a reasonable idea for performance at various table sizes. (Not much point in going smaller than 2500 rows. Everything&#8217;s fast on 100 rows)</p>
<p>Some notes on the tests.</p>
<ul>
<li>The version of SQL is SQL Server 2008 SP1 x64 Developer Edition.</li>
<li>The tests were run on a laptop. Core-2 Duo, 3 GB memory. SQL limited to 1 processor, so no parallelism possible.</li>
<li>Each query will be run 10 times, reads, cpu and duration measured by profiler and averaged.</li>
<li>Each query will be run once before the tests start to ensure that the data is in cache and the execution plans are generated and cached.</li>
<li>Reproduction scripts will be available for download.</li>
</ul>
<h3><span id="more-586"></span>Exists vs. In vs. Inner Join</h3>
<p>First, no indexes on the join columns</p>
<table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><strong>Table Size</strong></td>
<td valign="top"><strong>Operator</strong></td>
<td valign="top"><strong>CPU</strong></td>
<td valign="top"><strong>Reads </strong></td>
<td valign="top"><strong>Duration</strong></td>
</tr>
<tr>
<td rowspan="3">Large</td>
<td valign="top">IN</td>
<td valign="top">1293</td>
<td valign="top">14585</td>
<td valign="top">9649</td>
</tr>
<tr>
<td valign="top">Exists</td>
<td valign="top">1260</td>
<td valign="top">14585</td>
<td valign="top">9573</td>
</tr>
<tr>
<td valign="top">Inner Join</td>
<td valign="top">1302</td>
<td valign="top">14585</td>
<td valign="top">9716</td>
</tr>
<tr>
<td rowspan="3">Medium</td>
<td valign="top">IN</td>
<td valign="top">59</td>
<td valign="top">747</td>
<td valign="top">538</td>
</tr>
<tr>
<td valign="top">Exists</td>
<td valign="top">78</td>
<td valign="top">747</td>
<td valign="top">574</td>
</tr>
<tr>
<td valign="top">Inner Join</td>
<td valign="top">69</td>
<td valign="top">747</td>
<td valign="top">523</td>
</tr>
<tr>
<td rowspan="3">Small</td>
<td valign="top">IN</td>
<td valign="top">7</td>
<td valign="top">41</td>
<td valign="top">65</td>
</tr>
<tr>
<td valign="top">Exists</td>
<td valign="top">3</td>
<td valign="top">41</td>
<td valign="top">91</td>
</tr>
<tr>
<td valign="top">Inner Join</td>
<td valign="top">4</td>
<td valign="top">41</td>
<td valign="top">65</td>
</tr>
</tbody>
</table>
<p>Now with indexes on the join columns</p>
<table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><strong>Table Size</strong></td>
<td valign="top"><strong>Operator</strong></td>
<td valign="top"><strong>CPU</strong></td>
<td valign="top"><strong>Reads </strong></td>
<td valign="top"><strong>Duration </strong></td>
</tr>
<tr>
<td rowspan="3">Large</td>
<td valign="top">IN</td>
<td valign="top">973</td>
<td valign="top">1760</td>
<td valign="top">9707</td>
</tr>
<tr>
<td valign="top">Exists</td>
<td valign="top">956</td>
<td valign="top">1760</td>
<td valign="top">9483</td>
</tr>
<tr>
<td valign="top">Inner Join</td>
<td valign="top">1173</td>
<td valign="top">1760</td>
<td valign="top">9539</td>
</tr>
<tr>
<td rowspan="3">Medium</td>
<td valign="top">IN</td>
<td valign="top">43</td>
<td valign="top">100</td>
<td valign="top">516</td>
</tr>
<tr>
<td valign="top">Exists</td>
<td valign="top">53</td>
<td valign="top">100</td>
<td valign="top">548</td>
</tr>
<tr>
<td valign="top">Inner Join</td>
<td valign="top">59</td>
<td valign="top">100</td>
<td valign="top">498</td>
</tr>
<tr>
<td rowspan="3">Small</td>
<td valign="top">IN</td>
<td valign="top">3</td>
<td valign="top">9</td>
<td valign="top">64</td>
</tr>
<tr>
<td valign="top">Exists</td>
<td valign="top">1</td>
<td valign="top">9</td>
<td valign="top">80</td>
</tr>
<tr>
<td valign="top">Inner Join</td>
<td valign="top">4</td>
<td valign="top">9</td>
<td valign="top">67</td>
</tr>
</tbody>
</table>
<h3>Not Exists vs. Not In vs. Left Outer Join &#8230; Is Null</h3>
<p>First test with the columns join columns nullable, no indexes</p>
<table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><strong>Table Size</strong></td>
<td valign="top"><strong>Operator</strong></td>
<td valign="top"><strong>CPU</strong></td>
<td valign="top"><strong>Reads </strong></td>
<td valign="top"><strong>Duration </strong></td>
</tr>
<tr>
<td rowspan="3">Large</td>
<td valign="top">NOT IN</td>
<td valign="top">3194</td>
<td valign="top">2014622</td>
<td valign="top">3251</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">820</td>
<td valign="top">14585</td>
<td valign="top">837</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">962</td>
<td valign="top">14585</td>
<td valign="top">1025</td>
</tr>
<tr>
<td rowspan="3">Medium</td>
<td valign="top">NOT IN</td>
<td valign="top">174</td>
<td valign="top">100765</td>
<td valign="top">217</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">54</td>
<td valign="top">747</td>
<td valign="top">121</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">53</td>
<td valign="top">747</td>
<td valign="top">79</td>
</tr>
<tr>
<td rowspan="3">Small</td>
<td valign="top">NOT IN</td>
<td valign="top">12</td>
<td valign="top">5043</td>
<td valign="top">13</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">4</td>
<td valign="top">41</td>
<td valign="top">6</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">3</td>
<td valign="top">41</td>
<td valign="top">5</td>
</tr>
</tbody>
</table>
<p>Then with join columns nullable with indexes</p>
<table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><strong>Table Size</strong></td>
<td valign="top"><strong>Operator</strong></td>
<td valign="top"><strong>CPU</strong></td>
<td valign="top"><strong>Reads </strong></td>
<td valign="top"><strong>Duration </strong></td>
</tr>
<tr>
<td rowspan="3">Large</td>
<td valign="top">NOT IN</td>
<td valign="top">2677</td>
<td valign="top">2001762</td>
<td valign="top">2726</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">569</td>
<td valign="top">1760</td>
<td valign="top">586</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">949</td>
<td valign="top">1760</td>
<td valign="top">1029</td>
</tr>
<tr>
<td rowspan="3">Medium</td>
<td valign="top">NOT IN</td>
<td valign="top">137</td>
<td valign="top">100102</td>
<td valign="top">164</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">40</td>
<td valign="top">100</td>
<td valign="top">104</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">48</td>
<td valign="top">100</td>
<td valign="top">69</td>
</tr>
<tr>
<td rowspan="3">Small</td>
<td valign="top">NOT IN</td>
<td valign="top">11</td>
<td valign="top">5011</td>
<td valign="top">12</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">3</td>
<td valign="top">9</td>
<td valign="top">4</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">6</td>
<td valign="top">9</td>
<td valign="top">6</td>
</tr>
</tbody>
</table>
<p>Now, let&#8217;s make the join columns not nullable. Again, no indexes to start with.</p>
<table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><strong>Table Size</strong></td>
<td valign="top"><strong>Operator</strong></td>
<td valign="top"><strong>CPU</strong></td>
<td valign="top"><strong>Reads </strong></td>
<td valign="top"><strong>Duration </strong></td>
</tr>
<tr>
<td rowspan="3">Large</td>
<td valign="top">NOT IN</td>
<td valign="top">741</td>
<td valign="top">14585</td>
<td valign="top">753</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">784</td>
<td valign="top">14585</td>
<td valign="top">790</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">884</td>
<td valign="top">14585</td>
<td valign="top">937</td>
</tr>
<tr>
<td rowspan="3">Medium</td>
<td valign="top">NOT IN</td>
<td valign="top">43</td>
<td valign="top">747</td>
<td valign="top">103</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">49</td>
<td valign="top">747</td>
<td valign="top">120</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">53</td>
<td valign="top">747</td>
<td valign="top">74</td>
</tr>
<tr>
<td rowspan="3">Small</td>
<td valign="top">NOT IN</td>
<td valign="top">4</td>
<td valign="top">41</td>
<td valign="top">4</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">1</td>
<td valign="top">41</td>
<td valign="top">5</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">1</td>
<td valign="top">41</td>
<td valign="top">5</td>
</tr>
</tbody>
</table>
<p>and finally, join columns not nullable, with indexes</p>
<table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><strong>Table Size</strong></td>
<td valign="top"><strong>Operator</strong></td>
<td valign="top"><strong>CPU</strong></td>
<td valign="top"><strong>Reads </strong></td>
<td valign="top"><strong>Duration </strong></td>
</tr>
<tr>
<td rowspan="3">Large</td>
<td valign="top">NOT IN</td>
<td valign="top">578</td>
<td valign="top">1382</td>
<td valign="top">588</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">585</td>
<td valign="top">1382</td>
<td valign="top">597</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">953</td>
<td valign="top">1382</td>
<td valign="top">1006</td>
</tr>
<tr>
<td rowspan="3">Medium</td>
<td valign="top">NOT IN</td>
<td valign="top">37</td>
<td valign="top">80</td>
<td valign="top">79</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">34</td>
<td valign="top">80</td>
<td valign="top">79</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">39</td>
<td valign="top">80</td>
<td valign="top">84</td>
</tr>
<tr>
<td rowspan="3">Small</td>
<td valign="top">NOT IN</td>
<td valign="top">3</td>
<td valign="top">8</td>
<td valign="top">4</td>
</tr>
<tr>
<td valign="top">NOT Exists</td>
<td valign="top">1</td>
<td valign="top">8</td>
<td valign="top">5</td>
</tr>
<tr>
<td valign="top">Outer Join</td>
<td valign="top">4</td>
<td valign="top">8</td>
<td valign="top">5</td>
</tr>
</tbody>
</table>
<p>These results seem to pretty much confirm the earlier conclusions.</p>
<p>Exists and IN perform much the same, whether there are indexes on the join column or not. When there are indexes on the join columns, the INNER JOIN is slightly (very slightly) slower, which is more noticeable on the large tables, much less on the medium or small ones. (Note I&#8217;m mostly looking at CPU time, as the duration is also affected by sending of results to client, in this case, lots and lots of results)</p>
<p>When it comes to NOT In and NOT Exists they perform much the same when the columns involved are not nullable. If the columns are nullable, Not In is significantly slower because it has a different behaviour when nulls are present.</p>
<p>The join is slightly slower than Not Exists (or Not In on non-nullable columns), again only noticeable on the large table, probably because the optimiser has to do a full join with a secondary filter rather than the anti-semi join that it can use for Not Exists and Not In.</p>
<p>My conclusion from earlier posts stands. If all you are doing is looking for matching or non-matching rows and you don&#8217;t need any columns from the second table, use IN or Exists (or their negations), as appropriate for the situation. Only when you need columns from the second table should Join be used.</p>
<p>I think (and hope) that this adequately concludes the discussion on the Exists and In and joins, both behaviour and performance.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/04/Reproduction-scripts.zip">Reproduction scripts</a></p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/04/27/in-exists-and-join-a-roundup/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Running SQL faster</title>
		<link>http://sqlinthewild.co.za/index.php/2010/04/01/running-sql-faster/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/04/01/running-sql-faster/#comments</comments>
		<pubDate>Thu, 01 Apr 2010 06:00:47 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Misc]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=579</guid>
		<description><![CDATA[Poor performance is not uncommon in most SQL Server environments. The data sizes are growing, the hardware isn&#8217;t and traditional methods of performance tuning are time-consuming and difficult. So what&#8217;s the solution? Well, throwing hardware at the problem is an old favourite. There are few workloads that a nice 256-processor Itanium with a terabyte or [...]]]></description>
			<content:encoded><![CDATA[<p>Poor performance is not uncommon in most SQL Server environments. The data sizes are growing, the hardware isn&#8217;t and traditional methods of performance tuning are time-consuming and difficult.</p>
<p>So what&#8217;s the solution? Well, throwing hardware at the problem is an old favourite. There are few workloads that a nice 256-processor Itanium with a terabyte or two of memory won&#8217;t handle, but servers like that are a little on the expensive side and lots of money spent on expensive hardware means less that can be spent on annual bonuses.</p>
<p>There is another option, a hidden, undocumented option that can improve query performance, maybe a little, maybe substantially.</p>
<p>First thing that you need to do to get this one is to enable the hidden options in sp_configure. This is done much the same way as the advanced options.</p>
<pre class="brush: sql;">exec sp_configure 'show hidden options', 1
RECONFIGURE WITH EXTREME OVERRIDE</pre>
<p>Once that&#8217;s done, the undocumented option can be enabled.</p>
<pre class="brush: sql;">exec sp_configure 'run queries faster', 101010
 RECONFIGURE WITH EXTREME OVERRIDE</pre>
<p>How much improvement this will give depends on the kind of queries being run. OLTP systems usually see a greater improvement than decision-support, unless there&#8217;s full text search or spatial queries, in which case there will likely be substantially less of a gain.</p>
<p>Now, there are a few things to consider.</p>
<ol>
<li>This is obviously undocumented and that means unsupported.</li>
<li>It may not work on the next version of SQL.</li>
<li>If you call support, disable the option first and <strong>don&#8217;t tell them you were running it!</strong></li>
</ol>
<p>.</p>
<p>Happy <a href="http://en.wikipedia.org/wiki/April_fools_day">April Fools&#8217; day</a>.</p>
<p>.</p>
<p>..</p>
<p>&#8230;</p>
<p>&#8230;.</p>
<p>Seriously now, there&#8217;s no options that, when enabled, makes SQL run queries faster. There is no silver bullet for performance problems, there is no one-size-fits-all fix.</p>
<p>Fixing performance problems involves finding the current bottleneck and removing it, then repeating that operation until performance is acceptable. It&#8217;s a complex area and there&#8217;s a lot to it. Simply throwing hardware at the problem may not produce much, if any, performance gain, especially if the hardware wasn&#8217;t the bottleneck.</p>
<p>If you have a query performance problem and don&#8217;t know where to start, ask on one of the SQL forums (like <a href="http://www.sqlservercentral.com/">SQLServerCentral</a>) if it&#8217;s not an urgent problem. If it is, or if there are serious problems, consider getting a consultant in to help out. One of the quickest ways to learn is to learn from someone who knows what they are doing.</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/04/01/running-sql-faster/feed/</wfw:commentRss>
		<slash:comments>31</slash:comments>
		</item>
		<item>
		<title>Left outer join vs NOT EXISTS</title>
		<link>http://sqlinthewild.co.za/index.php/2010/03/23/left-outer-join-vs-not-exists/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/03/23/left-outer-join-vs-not-exists/#comments</comments>
		<pubDate>Tue, 23 Mar 2010 14:00:58 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>
		<category><![CDATA[T-SQL]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=575</guid>
		<description><![CDATA[And to wrap up the miniseries on IN, EXISTS and JOIN, a look at NOT EXISTS and LEFT OUTER JOIN for finding non-matching rows. For previous parts, see In vs Exists In vs Inner Join Not in vs Not Exists I&#8217;m looking at NOT EXISTS and LEFT OUTER JOIN, as opposed to NOT IN and [...]]]></description>
			<content:encoded><![CDATA[<p>And to wrap up the miniseries on IN, EXISTS and JOIN, a look at NOT EXISTS and LEFT OUTER JOIN for finding non-matching rows.</p>
<p>For previous parts, see</p>
<ul>
<li><a href="http://sqlinthewild.co.za/index.php/2009/08/17/exists-vs-in/">In vs Exists</a></li>
<li><a href="http://sqlinthewild.co.za/index.php/2010/01/12/in-vs-inner-join/">In vs Inner Join</a></li>
<li><a href="http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/">Not in vs Not Exists</a></li>
</ul>
<p>I&#8217;m looking at NOT EXISTS and LEFT OUTER JOIN, as opposed to NOT IN and LEFT OUTER JOIN, because, as shown in the <a href="http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/">previous part</a> of this series, NOT IN behaves badly in the presence of NULLs. Specifically, if there are any NULLs in the result set, NOT IN returns 0 matches.</p>
<p>The LEFT OUTER JOIN, like the NOT EXISTS can handle NULLs in the second result set without automatically returning no matches. It behaves the same regardless of whether the join columns are nullable or not. Seeing as NULL does not equal anything, any rows in the second result set that have NULL for the join column are eliminated by the join and have no further effect on the query.</p>
<p>It is important, when using the LEFT OUTER JOIN … IS NULL, to carefully pick the column used for the IS NULL check. It should either be a non-nullable column (the primary key is a somewhat classical choice) or the join column (as nulls in that will be eliminated by the join)</p>
<p>Onto the tests</p>
<p>The usual test tables…</p>
<pre class="brush: sql;">
CREATE TABLE BigTable (
id INT IDENTITY PRIMARY KEY,
SomeColumn char(4) NOT NULL,
Filler CHAR(100)
)

CREATE TABLE SmallerTable (
id INT IDENTITY PRIMARY KEY,
LookupColumn char(4) NOT NULL,
SomeArbDate Datetime default getdate()
)

INSERT INTO BigTable (SomeColumn)
SELECT top 250000
char(65+FLOOR(RAND(a.column_id *5645 + b.object_id)*10)) + char(65+FLOOR(RAND(b.column_id *3784 + b.object_id)*12)) +
char(65+FLOOR(RAND(b.column_id *6841 + a.object_id)*12)) + char(65+FLOOR(RAND(a.column_id *7544 + b.object_id)*8))
from master.sys.columns a cross join master.sys.columns b

INSERT INTO SmallerTable (LookupColumn)
SELECT DISTINCT SomeColumn
FROM BigTable TABLESAMPLE (25 PERCENT)
-- (3918 row(s) affected)
</pre>
<p>First without indexes</p>
<pre class="brush: sql;">-- Query 1
SELECT BigTable.ID, SomeColumn
	FROM BigTable LEFT OUTER JOIN SmallerTable ON BigTable.SomeColumn = SmallerTable.LookupColumn
	WHERE LookupColumn IS NULL

-- Query 2
SELECT ID, SomeColumn FROM BigTable
WHERE NOT EXISTS (SELECT LookupColumn FROM SmallerTable WHERE SmallerTable.LookupColumn = BigTable.SomeColumn)
</pre>
<p>Let&#8217;s take a look at the execution plans</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/03/LeftOuterJoinNotIN_NotIndexed.png"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="LeftOuterJoinNotIN_NotIndexed" src="http://sqlinthewild.co.za/wp-content/uploads/2010/03/LeftOuterJoinNotIN_NotIndexed_thumb.png" border="0" alt="LeftOuterJoinNotIN_NotIndexed" width="244" height="143" /></a></p>
<p><span id="more-575"></span></p>
<p>The plans are almost the same. There&#8217;s an extra filter in the JOIN and the logical join types are different. Why the different joins?</p>
<p>If we look at the execution plan for the NOT EXISTS, the join type is Right Anti-Semi join (a bit of a mouthful). This is a special join type used by the NOT EXISTS and NOT IN and it&#8217;s the opposite of the semi-join that I discussed back when I looked at <a href="http://sqlinthewild.co.za/index.php/2010/01/12/in-vs-inner-join/">IN and INNER JOIN</a></p>
<p>An anti-semi join is a partial join. It does not actually join rows in from the second table, it simply checks for, in this case, the absence of matches. That&#8217;s why it&#8217;s an <strong>anti</strong>-semi join. A semi-join checks for matches, an anti-semi join does the opposite and checks for the absence of matches.</p>
<p>The extra filter in the LEFT OUTER JOIN query is because the join in that execution plan is a complete right join, i.e. it&#8217;s returned matching rows (and possibly duplicates) from the second table. The filter operator is doing the IS NULL filter.</p>
<p>That&#8217;s the major difference between these two. When using the LEFT OUTER JOIN … IS NULL technique, SQL can&#8217;t tell that you&#8217;re only doing a check for nonexistance. Optimiser&#8217;s not smart enough (yet). Hence it does the complete join and then filters. The NOT EXISTS filters as part of the join.</p>
<p>Technical discussion done, now how did they actually perform?</p>
<blockquote><p>&#8211; Query 1: LEFT OUTER JOIN<br />
Table &#8216;Worktable&#8217;. Scan count 0, logical reads 0, physical reads 0.<br />
Table &#8216;BigTable&#8217;. Scan count 1, logical reads 3639, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 15, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 157 ms,  elapsed time = 486 ms.</p>
<p>&#8211; Query 2: NOT EXISTS<br />
Table &#8216;Worktable&#8217;. Scan count 0, logical reads 0, physical reads 0.<br />
Table &#8216;BigTable&#8217;. Scan count 1, logical reads 3639, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 15, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 156 ms,  elapsed time = 358 ms.</p></blockquote>
<p>Can&#8217;t make a big deal out of that.</p>
<p>Now, index on the join columns</p>
<pre class="brush: sql;">CREATE INDEX idx_BigTable_SomeColumn
ON BigTable (SomeColumn)

CREATE INDEX idx_SmallerTable_LookupColumn
ON SmallerTable (LookupColumn)</pre>
<p>and the same queries</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/03/LeftOuterJoinNotIN_Indexed.png"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="LeftOuterJoinNotIN_Indexed" src="http://sqlinthewild.co.za/wp-content/uploads/2010/03/LeftOuterJoinNotIN_Indexed_thumb.png" border="0" alt="LeftOuterJoinNotIN_Indexed" width="244" height="130" /></a></p>
<p>With indexes added, the execution plans are even more different. The LEFT OUTER JOIN is still doing the complete outer join with a filter afterwards. It&#8217;s interesting to note that it&#8217;s still a hash join, even though both inputs are sorted in the order of the join keys.</p>
<p>The Not Exists now has a stream aggregate (because duplicate values are irrelevant for an EXISTS/NOT EXISTS) and an anti-semi join. The join here is no longer hash, it&#8217;s now a merge join.</p>
<p>This echoes what I found when looking at <a href="http://sqlinthewild.co.za/index.php/2010/01/12/in-vs-inner-join/">IN vs Inner join</a>. When the columns were indexed, the inner join still went for a hash join but the IN changed to a merge join. At the time, I thought it to be a fluke, I&#8217;m not so sure any longer. More tests on this are required…</p>
<p>The costing of the plans indicates that the optimiser believes that the LEFT OUTER JOIN form is more expensive. Do the execution stats carry the same conclusion?</p>
<blockquote><p>&#8211; Query 1: LEFT OUTER JOIN<br />
Table &#8216;Worktable&#8217;. Scan count 0, logical reads 0, physical reads 0.<br />
Table &#8216;BigTable&#8217;. Scan count 1, logical reads 342, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 8, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 172 ms,  elapsed time = 686 ms.</p>
<p>&#8211; Query 2: NOT EXISTS<br />
Table &#8216;BigTable&#8217;. Scan count 1, logical reads 342, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 8, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 78 ms,  elapsed time = 388 ms.</p></blockquote>
<p>Well, yes, they do.</p>
<p>The reads (ignoring the existence of the worktable for the hash join) are the same. That&#8217;s to be expected, both queries executed with a single scan of each index.</p>
<p>The CPU time figures are not. The CPU time of the LEFT OUTER JOIN form is almost twice that of the NOT EXISTS.</p>
<h3>In conclusion…</h3>
<p>If you need to find rows that don&#8217;t have a match in a second table, and the columns are nullable, use NOT EXISTS. If you need to find rows that don&#8217;t have a match in a second table, and the columns are not nullable, use NOT EXISTS or NOT IN.</p>
<p>The LEFT OUTER JOIN … IS NULL method is slower when the columns are indexed and it&#8217;s perhaps not as clear what&#8217;s happening. It&#8217;s reasonably clear what a NOT EXISTS predicate does, with LEFT OUTER JOIN it&#8217;s not immediately clear that it&#8217;s a check for non-matching rows, especially if there are several where clause predicates.</p>
<p>I think that&#8217;s about that for this series. I&#8217;m going to do one more post summarising all the findings, probably in a week or two.</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/03/23/left-outer-join-vs-not-exists/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>The Root of all Evil</title>
		<link>http://sqlinthewild.co.za/index.php/2010/03/11/the-root-of-all-evil/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/03/11/the-root-of-all-evil/#comments</comments>
		<pubDate>Thu, 11 Mar 2010 14:00:51 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[Performance]]></category>
		<category><![CDATA[Rants]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=563</guid>
		<description><![CDATA[Or “Shot gun query tuning” There have been a fair few forums questions in recent months asking for help in removing index scans, loop joins, sorts or other, presumed, slow query operators. There have been just as many asking how to replace a subquery with a join or a join with a subquery or similar [...]]]></description>
			<content:encoded><![CDATA[<p>Or “<em>Shot gun query tuning</em>”</p>
<p>There have been a fair few forums questions in recent months asking for help in removing index scans, loop joins, sorts or other, presumed, slow query operators. There have been just as many asking how to replace a subquery with a join or a join with a subquery or similar aspects of a query usually for performance reasons.</p>
<p>The first question that I have to ask when looking at requests like that is &#8220;Why?&#8221;</p>
<p>Why is removing a particular query operator the goal? Why is changing a where clause predicate the goal? If it’s to make the query faster, has the query been examined and has it been confirmed that query operator or predicate really is the problem?</p>
<p>The title of this post refers to a comment I’ve seen again and again in blogs or articles about front-end development. &#8220;<em>Premature optimisation is the root of all evils.</em>&#8221; It’s true in the database field as well.</p>
<p><span id="more-563"></span></p>
<p>While optimisation is very important in database development, trying to optimise queries without any idea where the problem with the query is, or even if the query is a problem at all is about as effective in fixing a database performance problem as using a shotgun from 100 meters is in killing mosquitoes. If you hit the problem, it’s by shear luck and nothing else.</p>
<p>There&#8217;s two sides to this problem.</p>
<p>The first aspect of this is, during development, spending time on optimising a query (or stored procedure) without any idea whether or not the query is inefficient and no idea whether or not the changes made make any improvement or not.</p>
<p>Firstly this is a waste of time that could be better spent developing other queries. Second it creates an incorrect impression that the queries have been optimised when in fact nothing of the sort has been done.</p>
<p>The second aspect when, with a production database that is performing badly, queries are modified almost at random in an attempt to fix the performance problem quickly.</p>
<p>This almost never works. It wastes time fixing stuff that very likely isn&#8217;t broken in the first place all the while the database performance deteriorates and management curses SQL Server as &#8216;nonscalable&#8217;</p>
<p>So, what is the right approach for the above two scenarios?</p>
<ol>
<li>Don&#8217;t optimise queries without knowing if they need it.</li>
<li>Don&#8217;t optimise queries without knowing if they need it. <sup>1</sup></li>
</ol>
<h3>New development</h3>
<p>When writing queries and stored procedures they need to be tested against a representative data set on a server with representative workload and their performance characteristics evaluated to see if they are acceptable. If the query&#8217;s performance characteristics are acceptable, then that query requires no optimisation<sup>2</sup></p>
<p>This doesn&#8217;t mean write bad code and push it to production. It means write good, solid code, following accepted coding standards, ensure that it runs acceptably against production-volumes of data, and do not spend hours or days trying to get it running a couple of milliseconds faster.</p>
<p>And if the query doesn&#8217;t perform acceptable, identify the problematic portion and fix that, don&#8217;t flail around rewriting bits of the query in the hope that the problem will magically go away.</p>
<p>The execution plan is the primary tool here, along with the output of Statistics IO.</p>
<h3>Fixing existing code</h3>
<p>When evaluating existing databases with know performance problems, limit the performance tuning to queries that really are performing badly and need optimisation. It&#8217;s often true that fixing the top 5-10 worst performing queries will have massive effects in overall system performance, far more than tuning twice that number of queries that aren&#8217;t really a problem.</p>
<p>The best tool for finding which queries really are the worst offenders is SQL Trace.</p>
<p>When looking at queries that are a problem, identify the portions that are inefficient and target attempts at optimisation towards those problems.</p>
<h3>In conclusion</h3>
<p>Measure Twice.<br />
Optimise if necessary.</p>
<hr />
<p>(1) No, that wasn&#8217;t a typo.</p>
<p>(2) At that time. Later changes to schema or data volume may require existing queries to be revised.</p>
<p>For more details on exactly how to identify problematic queries, refer to the <a href="http://www.simple-talk.com/sql/performance/finding-the-causes-of-poor-performance-in-sql-server,-part-1/">series I wrote at Simple Talk</a> last year.</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/03/11/the-root-of-all-evil/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>NOT EXISTS vs NOT IN</title>
		<link>http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/</link>
		<comments>http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/#comments</comments>
		<pubDate>Thu, 18 Feb 2010 14:00:32 +0000</pubDate>
		<dc:creator>Gail</dc:creator>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Syndication]]></category>
		<category><![CDATA[T-SQL]]></category>

		<guid isPermaLink="false">http://sqlinthewild.co.za/?p=553</guid>
		<description><![CDATA[Continuing with the mini-series on query operators, I want to have a look at NOT EXISTS and NOT IN. Just one note before diving into that. The examples I’m using are fairly simplistic and that’s intentional. I’m trying to find what, if any, are the performance differences in a benchmark-style setup. I’ll have some comments [...]]]></description>
			<content:encoded><![CDATA[<p>Continuing with the mini-series on query operators, I want to have a look at NOT EXISTS and NOT IN.</p>
<p>Just one note before diving into that. The examples I’m using are fairly simplistic and that’s intentional. I’m trying to find what, if any, are the performance differences in a benchmark-style setup. I’ll have some comments on more complex examples in a later post.</p>
<p>The most important thing to note about NOT EXISTS and NOT IN is that, unlike EXISTS and IN,  they are not equivalent in all cases. Specifically, when NULLs are involved they will return different results. To be totally specific, when the subquery returns even one null, NOT IN will not match any rows.</p>
<p>The reason for this can be found by looking at the details of what the NOT IN operation actually means.</p>
<p>Let’s say, for illustration purposes that there are 4 rows in the table called t, there’s a column called ID with values 1..4</p>
<pre class="brush: sql;">WHERE SomeValue NOT IN (SELECT AVal FROM t)
is equivalent to
WHERE (
SomeValue != (SELECT AVal FROM t WHERE ID=1)
AND
SomeValue != (SELECT AVal FROM t WHERE ID=2)
AND
SomeValue != (SELECT AVal FROM t WHERE ID=3)
AND
SomeValue != (SELECT AVal FROM t WHERE ID=4)
)</pre>
<p>Let’s further say that AVal is NULL where ID = 4. Hence that != comparison returns UNKNOWN. The logical truth table for AND states that UNKNOWN and TRUE is UNKNOWN, UNKNOWN and FALSE is FALSE. There is no value that can be AND’d with UNKNOWN to produce the result TRUE</p>
<p>Hence, if any row of that subquery returns NULL, the entire NOT IN operator will evaluate to either FALSE or NULL and no records will be returned</p>
<p>So what about EXISTS?</p>
<p><span id="more-553"></span></p>
<p>Exists cannot return NULL. It’s checking solely for the presence or absence of a row in the subquery and, hence, it can only return true or false. Since it cannot return NULL, there’s no possibility of a single NULL resulting in the entire expression evaluating to UNKNOWN.</p>
<p>Hence, when the column in the subquery that’s used for comparison with the outer table can have nulls in it, consider carefully which of Not Exists or Not in you want to use.</p>
<p>Ok, but say there are no nulls in the column. How do they compare speed-wise. I’m going to do two tests, one where the columns involved in the comparison are defined as NULL and one where they are defined as NOT NULL. There will be no null values in the columns in either case. In both cases, the join columns will be indexed. After all, we all index our join columns, right?</p>
<p>So, first test, non-nullable columns. First some setup</p>
<pre class="brush: sql;">CREATE TABLE BigTable (
id INT IDENTITY PRIMARY KEY,
SomeColumn char(4) NOT NULL,
Filler CHAR(100)
)

CREATE TABLE SmallerTable (
id INT IDENTITY PRIMARY KEY,
LookupColumn char(4) NOT NULL,
SomeArbDate Datetime default getdate()
)

INSERT INTO BigTable (SomeColumn)
SELECT top 250000
char(65+FLOOR(RAND(a.column_id *5645 + b.object_id)*10)) + char(65+FLOOR(RAND(b.column_id *3784 + b.object_id)*12)) +
char(65+FLOOR(RAND(b.column_id *6841 + a.object_id)*12)) + char(65+FLOOR(RAND(a.column_id *7544 + b.object_id)*8))
from master.sys.columns a cross join master.sys.columns b

INSERT INTO SmallerTable (LookupColumn)
SELECT DISTINCT SomeColumn
FROM BigTable TABLESAMPLE (25 PERCENT)
-- (3898 row(s) affected)

CREATE INDEX idx_BigTable_SomeColumn
ON BigTable (SomeColumn)
CREATE INDEX idx_SmallerTable_LookupColumn
ON SmallerTable (LookupColumn)</pre>
<p>Then the queries</p>
<pre class="brush: sql;">-- Query 1
SELECT ID, SomeColumn FROM BigTable
WHERE SomeColumn NOT IN (SELECT LookupColumn FROM SmallerTable)

-- Query 2
SELECT ID, SomeColumn FROM BigTable
WHERE NOT EXISTS (SELECT LookupColumn FROM SmallerTable WHERE SmallerTable.LookupColumn = BigTable.SomeColumn)</pre>
<p>The first thing to note is that the execution plans are identical.</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/02/ExecPlansNOTNULL.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="ExecPlansNOTNULL" src="http://sqlinthewild.co.za/wp-content/uploads/2010/02/ExecPlansNOTNULL_thumb.png" border="0" alt="ExecPlansNOTNULL" width="244" height="130" /></a></p>
<p>The execution characteristics are also identical.</p>
<blockquote><p><strong>Query 1<br />
</strong>Table &#8216;BigTable&#8217;. Scan count 1, logical reads 342, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 8, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 156 ms,  elapsed time = 221 ms.</p>
<p><strong>Query 2<br />
</strong>Table &#8216;BigTable&#8217;. Scan count 1, logical reads 342, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 8, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 156 ms,  elapsed time = 247 ms.</p></blockquote>
<p>So, at least for the case where the columns are defined as NOT NULL, these two perform the same.</p>
<p>What about the case where the columns are defined as nullable? I&#8217;m going to simply alter the two columns involved without changing anything else, then test out the two queries again.</p>
<pre class="brush: sql;">ALTER TABLE BigTable
 ALTER COLUMN SomeColumn char(4) NULL

ALTER TABLE SmallerTable
 ALTER COLUMN LookupColumn char(4) NULL</pre>
<p>And the same two queries</p>
<pre class="brush: sql;">-- Query 1
&lt;pre&gt;SELECT ID, SomeColumn FROM BigTable
WHERE SomeColumn NOT IN (SELECT LookupColumn FROM SmallerTable)

-- Query 2
SELECT ID, SomeColumn FROM BigTable
WHERE NOT EXISTS (SELECT LookupColumn FROM SmallerTable WHERE SmallerTable.LookupColumn = BigTable.SomeColumn)</pre>
<p>And as for their performance…</p>
<p><a href="http://sqlinthewild.co.za/wp-content/uploads/2010/02/ExecPlansNull.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="ExecPlansNull" src="http://sqlinthewild.co.za/wp-content/uploads/2010/02/ExecPlansNull_thumb.png" border="0" alt="ExecPlansNull" width="244" height="123" /></a></p>
<blockquote><p><strong>Query 1</strong><br />
Table &#8216;SmallerTable&#8217;. Scan count 3, logical reads 500011, physical reads 0.<br />
Table &#8216;BigTable&#8217;. Scan count 1, logical reads 437, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 827 ms,  elapsed time = 825 ms.</p>
<p><strong>Query 2<br />
</strong>Table &#8216;BigTable&#8217;. Scan count 1, logical reads 437, physical reads 0.<br />
Table &#8216;SmallerTable&#8217;. Scan count 1, logical reads 9, physical reads 0.</p>
<p>SQL Server Execution Times:<br />
CPU time = 156 ms,  elapsed time = 228 ms.</p></blockquote>
<p>Radically different execution plans, radically different performance characteristics. The NOT IN took over 5 times longer to execute and did thousands of times more reads.</p>
<p>Why is that complex execution plan required when there may be nulls in the column? I can&#8217;t answer that one, probably only one of the query optimiser developer can, however the results are obvious. When the columns allow nulls but has none, the NOT IN performs significantly worse than NOT EXISTS.</p>
<p>So, take-aways from this?</p>
<p>Most importantly, NOT EXISTS and NOT IN do not have the same behaviour when there are NULLs involved. Chose carefully which you want.</p>
<p>Columns that will never contain NULL values should be defined as NOT NULL so that SQL knows there will never be NULL values in them and so that it doesn’t have to produce complex plans to handle potential nulls.</p>
<p>On non-nullable columns, the behaviour and performance of NOT IN and NOT EXISTS are the same, so use whichever one works better for the specific situation.</p>
]]></content:encoded>
			<wfw:commentRss>http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/feed/</wfw:commentRss>
		<slash:comments>14</slash:comments>
		</item>
	</channel>
</rss>
