<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://www2.sqlblog.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Kalen Delaney : indexes</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx</link><description>Tags: indexes</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><item><title>Geek City: Join With Me!</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2013/02/14/index-intersection.aspx</link><pubDate>Thu, 14 Feb 2013 22:16:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:47727</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>3</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/47727.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=47727</wfw:commentRss><description>I remember one of the most surprising changes in SQL Server 2000 2005 was how the graphical plans showed the use of a nonclustered index to seek, and the plan included something that looked like a JOIN to find rows in the base table. Here’s an example....(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2013/02/14/index-intersection.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=47727" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/nonclustered+index/default.aspx">nonclustered index</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/query+plans/default.aspx">query plans</category></item><item><title>Geek City: Build a Big Table with a Columnstore Index</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2012/04/26/Build-a-Big-Table-with-a-Columnstore-Index.aspx</link><pubDate>Thu, 26 Apr 2012 18:18:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:43013</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>6</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/43013.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=43013</wfw:commentRss><description>I was looking all over to find a big table to use for my columnstore examples in my SQL Server 2012 book, and the only one I found was from someone at Microsoft and was not publicly available. When I demonstrate code in my writings, I like to have the...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2012/04/26/Build-a-Big-Table-with-a-Columnstore-Index.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=43013" width="1" height="1"&gt;</description><enclosure url="http://www2.sqlblog.com/blogs/kalen_delaney/attachment/43013.ashx" length="7788" type="text/plain" /><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/columnstore/default.aspx">columnstore</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/SQL+Server+2012/default.aspx">SQL Server 2012</category></item><item><title>Geek City: What gets logged for index rebuild operations?</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2011/03/08/what-gets-logged-for-index-rebuilds.aspx</link><pubDate>Tue, 08 Mar 2011 18:54:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:33967</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>13</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/33967.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=33967</wfw:commentRss><description>This blog post was inspired by a question from a future student. Someone who was already booked for my SQL Server Internals class in June asked for some information on a current problem he was having with transaction log writes causing excessive wait...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2011/03/08/what-gets-logged-for-index-rebuilds.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=33967" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/internals/default.aspx">internals</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/recovery+models/default.aspx">recovery models</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/transaction+log/default.aspx">transaction log</category></item><item><title>Geek City: Ordered Seeks and Scans</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2011/01/21/ordered-seeks-and-scans.aspx</link><pubDate>Fri, 21 Jan 2011 23:28:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:32833</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>5</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/32833.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=32833</wfw:commentRss><description>I got a couple of really great questions during my SSWUG Workshop this morning , as I was discussing seeks and scans, and since the answers to the two questions are very related, I decided to address both of them in more detail in this blog post. Most...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2011/01/21/ordered-seeks-and-scans.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=32833" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/query+plans/default.aspx">query plans</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/seminars/default.aspx">seminars</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/showplan/default.aspx">showplan</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/training/default.aspx">training</category></item><item><title>Did You Know: My Online Seminar Lasted Almost 5 Hours!</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/07/24/my-online-seminar-lasted-almost-5-hours.aspx</link><pubDate>Sun, 25 Jul 2010 00:06:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:27274</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>0</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/27274.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=27274</wfw:commentRss><description>Yesterday was a rebroadcast of the index internals seminar I presented last February, and I had forgotten how long it was. I was in a chat room the whole time, and the questions just kept coming, fast and (not so) furious! Thanks to all the participants...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/07/24/my-online-seminar-lasted-almost-5-hours.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=27274" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/internals/default.aspx">internals</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/seminars/default.aspx">seminars</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/training/default.aspx">training</category></item><item><title>Geek City: Index Internals</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/07/19/seminar-on-index-internals.aspx</link><pubDate>Mon, 19 Jul 2010 14:15:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:27136</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>0</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/27136.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=27136</wfw:commentRss><description>Once again, I'll be presenting an online seminar through SSWUG . This on is on Index Internals . The presentation lasts over 3 and a half hours, and I'll be online for live chat for over 4 hours. Ask away! We'll look at juicy details of exactly how clustered...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/07/19/seminar-on-index-internals.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=27136" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/seminars/default.aspx">seminars</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/training/default.aspx">training</category></item><item><title>Did You Know? I'm doing 3 more online seminars with SSWUG!</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/06/11/3-more-online-seminars-with-sswug.aspx</link><pubDate>Fri, 11 Jun 2010 22:46:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:26122</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>2</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/26122.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=26122</wfw:commentRss><description>As I told you in April , I recorded two more seminars with Stephen Wynkoop, on aspects of Query Processing. The first one will be broadcast on June 30 and the second on August 27. In between, we'll broadcast my Index Internals seminar, on July 23. Workshops...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/06/11/3-more-online-seminars-with-sswug.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=26122" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/plan+cache/default.aspx">plan cache</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/query+plans/default.aspx">query plans</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/seminars/default.aspx">seminars</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/training/default.aspx">training</category></item><item><title>Geek City: More About Nonclustered Index Keys</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/03/07/more-about-nonclustered-index-keys.aspx</link><pubDate>Mon, 08 Mar 2010 00:04:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:22920</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>21</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/22920.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=22920</wfw:commentRss><description>I thought I had said almost all that could be said about nonclustered index keys in a post made almost exactly two years ago , on March 16, 2008. But there's more. To get all the benefit from today's post, you'll really have to read that one, but I'll...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/03/07/more-about-nonclustered-index-keys.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=22920" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/internals/default.aspx">internals</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/storage/default.aspx">storage</category></item><item><title>Did You Know? I just finished my first online seminar</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/02/12/i-just-finished-my-first-online-seminar.aspx</link><pubDate>Fri, 12 Feb 2010 23:21:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:22255</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>17</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/22255.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=22255</wfw:commentRss><description>I am really jazzed! The seminar was over 3.5 hours in length, and we had 3 15-20 minute breaks, during which questions just kept coming in the chat room. I barely got out to refill my coffee! But there were some great questions and a really enthusiastic...(&lt;a href="http://www2.sqlblog.com/blogs/kalen_delaney/archive/2010/02/12/i-just-finished-my-first-online-seminar.aspx"&gt;read more&lt;/a&gt;)&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=22255" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/internals/default.aspx">internals</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/seminars/default.aspx">seminars</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/training/default.aspx">training</category></item><item><title>Geek City: Controlling Lock Granularity</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2009/05/03/controlling-lock-granularity.aspx</link><pubDate>Sun, 03 May 2009 12:51:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:13729</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>6</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/13729.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=13729</wfw:commentRss><description>&lt;P&gt;In all versions of SQL Server since 7.0, the engine could choose to lock rows, pages or the entire table. In addition, even if it starts out your query execution by obtaining row or page locks, if too many locks are acquired, SQL Server could escalate to a table lock.&amp;nbsp; Each lock uses memory, so when escalation replaces the thousands of fine-grained locks with one table lock, there can be substantial resource savings. On the other hand, once a table is exclusively locked, no other processes can access any data at all from it, which drastically reduces concurrency. &lt;/P&gt;
&lt;P&gt;Way back in SQL Server 6/6.5, we had some controls over what percentage of a table had to be locked in order to induce escalation. These controls were implemented as configuration options but they were removed in SQL 7. So what controls do we have now? &lt;/P&gt;
&lt;P&gt;Lock escalation occurs in the following situations (taken from "SQL Server 2008 Internals", MS Press 2009): &lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;The number of locks held by a single statement on one object, or on one partition of one object, exceeds a threshold. Currently that threshold is 5000 locks, but it might change in future service packs. The lock escalation will not occur if the locks are spread over multiple objects in the same statement—for example, 3000 locks in one index and 3000 in another. &lt;FONT size=1&gt;&amp;nbsp;&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Memory taken by lock resources exceeds 40 percent of the non-AWE (32-bit) or regular (64-bit) enabled memory and the locks configuration option is set to 0. (In this case, the lock memory is allocated dynamically as needed, so the 40 percent value is not a constant.) If the locks option is set to a non-zero value, memory reserved for locks is statically allocated when SQL Server starts. Escalation will occur when SQL Server is using more than 40 percent of the reserved lock memory for lock resources. &lt;/LI&gt;&lt;/UL&gt;
&lt;P&gt;Most of the discussion of escalation that I have seen indicates that people want to prevent escalation, to maximize access to the data. One way to do this is to enable trace flag 1211 on your instance, which prevents lock escalation from occurring under any circumstances. You need to be really careful with this one, because it affects every table in every database on the instance. Another option is to trick SQL Server into thinking the table is in use, because if there is even one row locked by a different connection, escalation cannot occur. So you could add a dummy row to your table and start a process that starts a transaction, updates the dummy rows, and then keeps the transaction open. Although this can keep anyone from acquiring a table lock, this method has its own nasty side-effects. Namely, as long as the transaction is open, the log cannot truncated past that point. But if you can synchronize it with the operation updating the table, and make sure this dummy update gets closed as soon as the 'real' update finishes, you might be ok.&lt;/P&gt;
&lt;P&gt;SQL Server 2008 provides us with a bit more control, with a new option to ALTER TABLE:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face=Consolas color=#000080 size=1&gt;ALTER TABLE &amp;lt;table_name&amp;gt;&lt;BR&gt;SET (LOCK_ESCALATION = [TABLE | AUTO |DISABLE]);&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;The default is escalation is TABLE, and that was the only possibility for escalation prior to SQL 2008. If you set the option to AUTO, locks can escalate to a table or to a partition, if the table is partitioned. The third option is to completely disable escalation for this table. This option is much more manageable that my trick of having another transaction lock a single row, and much more fine-grained that disallowing escalation on the entire instance. 
&lt;P&gt;But what if you want the opposite behavior? What if you always want to take table locks in order to conserve resources? Yes, there are lock hints available to request TABLOCK or TABLOCKX, but those must be specified in every query. What if there are only a few tables that you want to always lock at the table level. 
&lt;P&gt;SQL Server provides an option to ALTER INDEX to disallow ROW and PAGE locks.&amp;nbsp; (In SQL 7 and 2000 you could use the system procedure sp_indexoption to do the same thing.) If the index is a clustered index, this means ROW and PAGE locks will be disallowed for the table. But, if the table is a heap, you can't use this option.&amp;nbsp; 
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face=Consolas color=#000080 size=1&gt;ALTER INDEX &amp;lt;index_name&amp;gt; ON &amp;lt;table_name&amp;gt; &lt;BR&gt;SET (ALLOW_PAGE_LOCKS = OFF);&amp;nbsp; &lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Also note that if you set ALLOW_PAGE_LOCKS to OFF, you &lt;STRONG&gt;cannot &lt;/STRONG&gt;REORGANIZE (defragment) the index. &lt;/P&gt;
&lt;P&gt;I usually mention this ALTER INDEX option in my Internals and Tuning class, and also mention that the only time I really had to use it was when clients turned off PAGE or ROW locks inappropriately and ended up with far too many unexplained table locks. Once you check sys.indexes (there are columns called allow_row_locks and allow_page_locks, with possible values of only 0 and 1), the behavior is no longer unexplained. (For versions prior to SQL Server 2005 you can use the INDEXPROPERTY function to check the status of these options.)&amp;nbsp; It was several years ago that I last encountered someone using this option to turn off finer grained locks, and I was considering removing mention of it from my course.&amp;nbsp; 
&lt;P&gt;And then, just last week, in my Oslo class, one of students had been asking about why her queries with CTEs ended up getting TABLE locks all the time. I didn't believe it had anything to do with the CTEs, but suggested she wait until we discussed locking, which we do on the fourth day of the class. She paid very close attention, and on Friday morning she came back and told me that the problem had not been the CTEs after all, but that 'someone' had turned off the ALLOW_PAGE_LOCKS and ALLOW_ROW_LOCKS options! &lt;/P&gt;
&lt;P&gt;So I guess I continue to mention this possibility in future classes. &lt;/P&gt;
&lt;P&gt;&lt;FONT color=#ff00ff size=4&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=13729" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/escalation/default.aspx">escalation</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/locking/default.aspx">locking</category></item><item><title>Geek City: Too Many Indexes!</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2009/01/18/too-many-indexes.aspx</link><pubDate>Sun, 18 Jan 2009 20:37:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:11244</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>1</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/11244.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=11244</wfw:commentRss><description>&lt;P&gt;I wrote about &lt;A href="http://sqlblog.com/blogs/kalen_delaney/archive/2008/09/20/too-many-columns.aspx" target=_blank&gt;"Too Many Columns"&lt;/A&gt; last September, and along with changes in SQL Server 2008 that allow lots of columns, there is also the ability to create more than the old maximum of 249 nonclustered indexes on a table. I knew this fact, but somehow I overlooked it when updating the chapter on table structures in my new book. My omission also got past all the editors, and I just realized my mistake when reviewing the Indexes chapter, which was being updated for SQL 2008 by Kimberly Tripp.&amp;nbsp; &lt;/P&gt;
&lt;P&gt;I found that Books Online has not been completely updated (and I have filed a doc bug on this already). This page still says the limit is 249&amp;nbsp; nonclustered indexes per table:&lt;/P&gt;
&lt;P&gt;&lt;A title=http://msdn.microsoft.com/en-us/library/ms190197.aspx href="http://msdn.microsoft.com/en-us/library/ms190197.aspx"&gt;http://msdn.microsoft.com/en-us/library/ms190197.aspx&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;However, the page for CREATE TABLE at &lt;A title=http://msdn.microsoft.com/en-us/library/ms174979.aspx href="http://msdn.microsoft.com/en-us/library/ms174979.aspx"&gt;http://msdn.microsoft.com/en-us/library/ms174979.aspx&lt;/A&gt; does say:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;Each table can contain a maximum of 999 nonclustered indexes, and 1 clustered index. These include the indexes generated to support any PRIMARY KEY and UNIQUE constraints defined for the table.&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;You can take my script to create a table with lots of columns, and edit it to create a UNIQUE constraint on every column. 
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;DECLARE @create varchar(max);&lt;BR&gt;DECLARE @tabname sysname;&lt;BR&gt;DECLARE @numcols int; &lt;BR&gt;DECLARE @col int;&lt;BR&gt;SELECT @numcols = 900;&lt;BR&gt;SELECT @tabname = 'wide' + CONVERT(varchar, @numcols); &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT @create = 'CREATE TABLE ' + @tabname + &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ' (ID int IDENTITY, '; &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT @col = 1;&lt;BR&gt;WHILE @col &amp;lt; @numcols BEGIN&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; IF (@col % 3) = 0 &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT @create = @create + 'col' + CONVERT(varchar, @col) +&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ' int UNIQUE,';&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; IF (@col % 3) = 1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT @create = @create + 'col' + CONVERT(varchar, @col) +&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ' char(5) UNIQUE,';&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; IF (@col % 3) = 2&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT @create = @create + 'col' + CONVERT(varchar, @col) +&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ' varchar(25) UNIQUE,';&lt;BR&gt;SELECT @col = @col + 1;&lt;BR&gt;END;&lt;BR&gt;SELECT @create = @create + 'col' + CONVERT(varchar, @col) + &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ' int default 0);'&lt;BR&gt;PRINT @create&amp;nbsp; &lt;BR&gt;EXECUTE (@create) &lt;/FONT&gt;
&lt;P&gt;After the table is created, you can examine &lt;EM&gt;sys.indexes&lt;/EM&gt;: 
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SELECT name, index_id, type_desc&lt;BR&gt;FROM sys.indexes&lt;BR&gt;WHERE object_id = object_id('wide900');&lt;/FONT&gt; 
&lt;P&gt;You'll definitely see more than 249 indexes! In addition, you might note that there is a gap in the &lt;EM&gt;index_id&lt;/EM&gt; values. The values 251 - 255 are not used, and this is for backward compatibility. In much older versions, &lt;EM&gt;index_id&lt;/EM&gt; (or &lt;EM&gt;indid&lt;/EM&gt;) 255 was reserved for the LOB data belonging to a table, and the values from 251 - 254 were reserved. So that range is still not used. But you can see that I have &lt;EM&gt;index_id&lt;/EM&gt; values from 256 - 905 in this table. 
&lt;P&gt;So, thanks Kim~ 
&lt;P&gt;One more place that Books Online could use a little tweaking is in the section on Programmability Enhancements: 
&lt;P&gt;&lt;A title=http://msdn.microsoft.com/en-us/library/cc645577.aspx href="http://msdn.microsoft.com/en-us/library/cc645577.aspx"&gt;http://msdn.microsoft.com/en-us/library/cc645577.aspx&lt;/A&gt; 
&lt;P&gt;The section on "Wide Tables" seems to imply that you need to have an XML column set in order to get 1000 indexes. It doesn't actually say that, so it's not really a bug, but it does seem to imply it: 
&lt;BLOCKQUOTE&gt;
&lt;P&gt;Wide tables are tables that contain one or more column sets. A wide table can contain up to 30000 columns, 1000 indexes, and 30000 statistics.&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Because you can only reach the 30000 column limit with a wide table, it seems to imply that the 1000 index limit is only for wide tables. But as my script shows, it is not. You can have up to 1024 columns in a non-wide table, and you can now have indexes on every one of those columns. 
&lt;P&gt;But do you want that many? That's for another post, another time... 
&lt;P&gt;Have fun! 
&lt;P&gt;&lt;FONT color=#ff00ff size=4&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=11244" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/books/default.aspx">books</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/nonclustered+index/default.aspx">nonclustered index</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/SQL+Server+2008/default.aspx">SQL Server 2008</category></item><item><title>Geek City QUIZ: What happens when you change a column in an index from a key column to an included column?</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2008/11/24/change-a-column-in-an-index-from-a-key-column-to-an-included-column.aspx</link><pubDate>Mon, 24 Nov 2008 07:15:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:10065</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>5</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/10065.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=10065</wfw:commentRss><description>&lt;P&gt;This quiz is not really a generic question about changing an index key column to be an included column; it's about a behavior noticed by a reader in one particular query.&amp;nbsp; Dejan Nakarada-Kordic from New Zealand sent me a very interesting puzzle. He had a reproducible query for which he thought an existing nonclustered index should be used, and it only ended up being used if the column was defined as an INCLUDED column instead of a key column. Here is the script Dejan sent me:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;-- First, create the table&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=1&gt;IF&amp;nbsp; EXISTS (SELECT * FROM sys.tables &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE schema_id = 1 and name = 'Table1') &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; DROP TABLE dbo.Table1;&lt;BR&gt;GO&lt;BR&gt;CREATE TABLE dbo.Table1(&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Transaction_Serial_No [bigint IDENTITY(1,1) NOT NULL,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Channel] char(4) NOT NULL,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Retry_Counter int NOT NULL,&lt;BR&gt;CONSTRAINT PK_Table1 PRIMARY KEY CLUSTERED &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (Transaction_Serial_No)); &lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=1&gt;GO &lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;--populate the table with 10000 rows&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=1&gt;SET NOCOUNT ON;&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=1&gt;declare @counter int;&lt;BR&gt;set @counter = 0;&lt;BR&gt;while (@counter &amp;lt; 10000)&lt;BR&gt;begin&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; set @counter = @counter + 1;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; insert into&amp;nbsp; dbo.Table1(channel, retry_counter)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; values('ch1', 0);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; insert into&amp;nbsp; dbo.Table1(channel,&amp;nbsp; retry_counter)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; values('ch2', 0);&lt;BR&gt;end;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;-- Now build a nonclustered index on &lt;BR&gt;IF&amp;nbsp; EXISTS (SELECT * FROM sys.indexes &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE object_id = OBJECT_ID('dbo.Table1') AND name = 'IX_Channel')&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; DROP INDEX IX_Channel ON dbo.Table1;&lt;BR&gt;GO&lt;BR&gt;CREATE NONCLUSTERED INDEX IX_Channel ON dbo.Table1 (Channel, Retry_Counter);&lt;BR&gt;GO&lt;BR&gt;&lt;BR&gt;-- Examine the query plan for this query:&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=1&gt;SELECT TOP(20) Transaction_Serial_No&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM dbo.Table1&amp;nbsp; &lt;BR&gt;WHERE Channel = 'ch2'&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND Retry_Counter &amp;lt;= 10&amp;nbsp; &lt;BR&gt;ORDER BY Transaction_Serial_No;&lt;/FONT&gt;&lt;BR&gt;&lt;/P&gt;
&lt;P&gt;The plan should show you a Clustered Index Scan being performed, even though the nonclustered index appears to be a covering index. &lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityQUIZWhathappenswhenyouchangeacol_13A9B/image_2.png"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=67 alt=image src="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityQUIZWhathappenswhenyouchangeacol_13A9B/image_thumb.png" width=261 border=0&gt;&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;The query contains all 3 columns used in the query. The columns &lt;EM&gt;Channel&lt;/EM&gt; and &lt;EM&gt;Retry_Counter&lt;/EM&gt; are defined index keys, and because the table has a clustered index, the clustered key &lt;EM&gt;Transaction_Serial_No&lt;/EM&gt; is also part of the index. &lt;/P&gt;
&lt;P&gt;If you rebuild the index to use &lt;EM&gt;Retry_Counter&lt;/EM&gt; as an INCLUDED column, the plan changes.&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;CREATE NONCLUSTERED INDEX IX_Channel ON dbo.Table1 (Channel)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; INCLUDE ( Retry_Counter)&amp;nbsp; WITH&amp;nbsp;&amp;nbsp; (DROP_EXISTING = ON);&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;-- Examine the plan again:&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;SELECT TOP(20) Transaction_Serial_No&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM dbo.Table1&amp;nbsp; &lt;BR&gt;WHERE Channel = 'ch2'&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND Retry_Counter &amp;lt;= 10&amp;nbsp; &lt;BR&gt;ORDER BY Transaction_Serial_No;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;The plan now shows a nonclustered index seek:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityQUIZWhathappenswhenyouchangeacol_13A9B/image_4.png"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=48 alt=image src="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityQUIZWhathappenswhenyouchangeacol_13A9B/image_thumb_1.png" width=244 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;WHY should changing the column from a key column to an INCLUDED column change the plan?&lt;/P&gt;
&lt;P&gt;You can try to figure out the answer for yourself, or read on. &lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-------------------------------------------------------------------------------------------------------------------------&lt;/P&gt;
&lt;P&gt;It turns out that there are two extra factors to consider. &lt;/P&gt;
&lt;P&gt;1) Although covering indexes are a great thing, and the optimizer will choose them over other possible indexes most of the time, the cost of sorting can sometimes outweigh the benefit of a covering index.&lt;/P&gt;
&lt;P&gt;2) When a column is an INCLUDED column in an index, it comes after all the key columns and after the clustered index key that is always part of a nonclustered index on a table that has a clustered index.&lt;/P&gt;
&lt;P&gt;So, the original index on (Channel, Retry_Counter) is the same as an index on (Channel, Retry_Counter, Transaction_Serial_No), but moving &lt;EM&gt;Retry_Counter&lt;/EM&gt; to be an INCLUDED column changes the index to be on the columns (Channel,&amp;nbsp; Transaction_Serial_No), with Retry_Counter in last place, and unsorted.&lt;/P&gt;
&lt;P&gt;The query wants the data sorted on &lt;EM&gt;Transaction_Serial_No&lt;/EM&gt;, and scanning the clustered index gives us the data in &lt;EM&gt;Transaction_Serial_No&lt;/EM&gt; order.&amp;nbsp; The first&amp;nbsp; nonclustered index is sorted first by &lt;EM&gt;Channel&lt;/EM&gt;, then by &lt;EM&gt;Retry_Counter&lt;/EM&gt;, then by &lt;EM&gt;Transaction_Serial_No&lt;/EM&gt; so that index is not useful for the sort.&amp;nbsp; &lt;/P&gt;
&lt;P&gt;The second nonclustered index is sorted first by &lt;EM&gt;Channel&lt;/EM&gt;, then by &lt;EM&gt;Transaction_Serial_No. &lt;/EM&gt;BUT the query has limited the data to only data with the &lt;EM&gt;Channel&lt;/EM&gt; value equal to 'ch2', so since the first column values we're retrieving are all the same, returning data in the nonclustered index order will return the data in &lt;EM&gt;Transaction_Serial_No&lt;/EM&gt; order and no sorting needs to be done. &lt;/P&gt;
&lt;P&gt;You might notice in this data set that all the values for &lt;EM&gt;Retry_Counter&lt;/EM&gt; are also identical, so you might then think we should be able to use the first nonclustered index to avoid having to sort the data, but the optimizer cannot be sure that the &lt;EM&gt;Retry_Counter&lt;/EM&gt; values are all identical. The statistics might indicate that all the values are the same, but the optimizer can't be sure the statistics are 100% up to date. &lt;/P&gt;
&lt;P&gt;Here are some things to try:&lt;/P&gt;
&lt;P&gt;1) If you remove the ORDER BY, you will see that the first index, with no INCLUDED column, is used.&lt;/P&gt;
&lt;P&gt;2) If you change the second condition to searching for a constant (AND Retry_Counter = 0), the first index can be used. If both of the first two index columns are constants in the retrieved data, it will be sorted by the third column (&lt;EM&gt;Transaction_Serial_No&lt;/EM&gt;).&lt;/P&gt;
&lt;P&gt;3) If you force the query to use the nonclustered index when &lt;EM&gt;Retry_Counter&lt;/EM&gt; is a key, you'll see that SQL Server has to do a sort:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=1&gt;DROP INDEX IX_Channel ON dbo.Table1;&lt;BR&gt;GO&lt;BR&gt;CREATE NONCLUSTERED INDEX IX_Channel ON dbo.Table1 (Channel, Retry_Counter);&lt;BR&gt;GO&lt;BR&gt;SELECT TOP(20) Transaction_Serial_No&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM dbo.Table1 WITH (index = ix_channel)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE Channel = 'ch2'&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND Retry_Counter &amp;lt;=&amp;nbsp; 10&lt;BR&gt;ORDER BY Transaction_Serial_No;&lt;BR&gt;GO&lt;/FONT&gt; 
&lt;P&gt;&lt;BR&gt;&lt;A href="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityQUIZWhathappenswhenyouchangeacol_13A9B/image_6.png"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=47 alt=image src="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityQUIZWhathappenswhenyouchangeacol_13A9B/image_thumb_2.png" width=244 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;Notice that the SORT operation is estimated to be 95% of the total query cost, so SQL Server would definitely like to avoid that if possible.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;So the issue wasn't really because of INCLUDED columns, it was because of the order the columns appeared in the index, and the fact that SQL Server wants to try to avoid sorting whenever possible.&lt;/P&gt;
&lt;P&gt;Have fun!&lt;/P&gt;
&lt;P&gt;&lt;FONT color=#ff00ff size=4&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=10065" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/included+columns/default.aspx">included columns</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/query+tuning/default.aspx">query tuning</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/sort/default.aspx">sort</category></item><item><title>Did You Know? Things Keep Changing</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/things-keep-changing.aspx</link><pubDate>Thu, 19 Jun 2008 19:07:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:7392</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>0</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/7392.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=7392</wfw:commentRss><description>&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;A href="http://www.sqlmag.com/Article/ArticleID/99513/sql_server_99513.html" target=_blank&gt;My Thursday commentary for the SQL Server Magazine Update e-newsletter&lt;/A&gt; discussed quiet changes in SQL Server 2005 and I just realized there's another one I wanted to mention. &lt;/P&gt;
&lt;P&gt;If you use Indexed Views at all, you're probably aware that there is a set of SET options that must on set appropriately in order for your indexed views to work as planned. One of the requirements is that ARITHABORT must be ON. In SQL Server 2000, this value had to be set explicitly. (But frequently it was set by your connection, so you didn't have to worry about it.)&amp;nbsp; &lt;/P&gt;
&lt;P&gt;In SQL 2005, if you (or your connection) enabled the setting ANSI_WARNINGS, ARITHABORT is automatically enabled. &lt;/P&gt;
&lt;P&gt;I dug around and found the reference to this change in the BOL at &lt;BR&gt;&lt;A title=http://msdn.microsoft.com/en-us/library/ms190306.aspx href="http://msdn.microsoft.com/en-us/library/ms190306.aspx"&gt;http://msdn.microsoft.com/en-us/library/ms190306.aspx&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;What the article doesn't make completely clear is that even if you explicitly set ARITHABORT to OFF, as long as ANSI_WARNINGS is ON, SQL Server will behave as if ARITHABORT is ON.&amp;nbsp; &lt;/P&gt;
&lt;P&gt;The SET options are particularly important when updating a table on which an indexed view is built. If the SET options have the incorrect settings, the update will actually fail.&lt;/P&gt;
&lt;P&gt;Let's look at an example on SQL Server 2005, using the old pubs database. &lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;-- First, create the view and the index&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;USE pubs&lt;BR&gt;GO&lt;BR&gt;CREATE VIEW sum_sales WITH SCHEMABINDING&lt;BR&gt;AS&lt;BR&gt;SELECT type, sum(isnull(ytd_sales,0)) AS total_sales, &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count_big(*) AS number_sales&lt;BR&gt;FROM dbo.titles&lt;BR&gt;GROUP BY type; &lt;BR&gt;GO&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;CREATE UNIQUE CLUSTERED INDEX idxv_sales_by_type &lt;BR&gt;&amp;nbsp;&amp;nbsp; ON sum_sales(type);&lt;BR&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;-- Next, verify the settings&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;SELECT ansi_warnings, arithabort &lt;BR&gt;FROM sys.dm_exec_sessions&lt;BR&gt;WHERE session_id = @@SPID;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;-- You should see that both options are ON (1).&amp;nbsp; &lt;BR&gt;-- Update the titles table, and it should succeed.&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;UPDATE dbo.titles&lt;BR&gt;SET ytd_sales = ytd_sales + 1&lt;BR&gt;WHERE title_id = 'BU1032';&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;--Now change ARITHABORT TO OFF, verify the settings, and update:&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;SET ARITHABORT OFF;&lt;BR&gt;GO&lt;BR&gt;SELECT ansi_warnings, arithabort &lt;BR&gt;FROM sys.dm_exec_sessions&lt;BR&gt;WHERE session_id = @@SPID;&lt;BR&gt;GO&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;UPDATE dbo.titles&lt;BR&gt;SET ytd_sales = ytd_sales - 1&lt;BR&gt;WHERE title_id = 'BU1032';&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;-- The above should succeed. &lt;BR&gt;-- Now change ANSI_WARNINGS to OFF:&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;SET ANSI_WARNINGS OFF;&lt;BR&gt;GO&lt;BR&gt;SELECT ansi_warnings, arithabort &lt;BR&gt;FROM sys.dm_exec_sessions&lt;BR&gt;WHERE session_id = @@SPID;&lt;BR&gt;GO&lt;BR&gt;UPDATE dbo.titles&lt;BR&gt;SET ytd_sales = ytd_sales - 1&lt;BR&gt;WHERE title_id = 'BU1032';&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;-- The above should fail. &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;-- If we change to SQL 2000 compatibility level, &lt;BR&gt;-- just setting ARITHABORT OFF will cause the update to fail:&lt;/FONT&gt; 
&lt;P&gt;&lt;FONT face="Courier New" color=#000080 size=2&gt;EXEC sp_dbcmptlevel pubs, 80;&lt;BR&gt;GO&lt;BR&gt;SET ANSI_WARNINGS ON;&lt;BR&gt;GO&lt;BR&gt;SET ARITHABORT OFF;&lt;BR&gt;GO&lt;BR&gt;SELECT ansi_warnings, arithabort &lt;BR&gt;FROM sys.dm_exec_sessions&lt;BR&gt;WHERE session_id = @@SPID;&lt;BR&gt;GO&lt;BR&gt;UPDATE dbo.titles&lt;BR&gt;SET ytd_sales = ytd_sales + 1&lt;BR&gt;WHERE title_id = 'BU1032';&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;I'm sure there are lots more quiet changes. They're just so quiet I haven't found them yet! 
&lt;P&gt;Have fun 
&lt;P&gt;&lt;FONT color=#ff00ff size=4&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=7392" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/compatibility+level/default.aspx">compatibility level</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/SET+Options/default.aspx">SET Options</category></item><item><title>Geek City: Clustered or Nonclustered? Why not both?</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2008/04/24/clustered-or-nonclustered-why-not-both.aspx</link><pubDate>Thu, 24 Apr 2008 21:51:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:6399</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>2</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/6399.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=6399</wfw:commentRss><description>&lt;P&gt;I had been thinking of this post all day, and then noticed that &lt;A href="http://sqlblog.com/blogs/denis_gobo/archive/2008/04/24/6385.aspx" target=_blank&gt;Denis wrote a post with almost the same name&lt;/A&gt;. I was worried he might have written about something similar, but it turns out not to be the case. &lt;/P&gt;
&lt;P&gt;A group of colleagues have been having a discussion about this topic recently, which was spurred by the fact that the&amp;nbsp; Microsoft supplied &lt;EM&gt;Northwind&lt;/EM&gt; database has duplicates of many of its indexes. If Microsoft does this, many new database users might assume it's a good idea. It's not! Having two identical indexes gives you no additional benefit, but does give you lots of additional overhead when those indexes need to be maintained. In SQL Server 2008, you will be able to create a policy to detect and prohibit this behavior if you choose. But that of course, is still in the future.&lt;/P&gt;
&lt;P&gt;But what if the indexes are not quite identical? What if one is a clustered index and one is nonclustered? My colleagues report seeing this behavior frequently when someone declares a Primary Key on a column, and then doesn't realize that automatically builds an index (clustered by default), so she then builds a nonclustered on the same key.&amp;nbsp; Is this necessarily all bad? The clustered index is useful when most of the columns of many of the rows need to be returned, based on the value in the PK column, or when the data needs to be returned sorted by the PK. But what if you only need a few rows, and you need only key columns? For example, what about a count(*) query?&lt;/P&gt;
&lt;P&gt;In general, when satisfying a count(*) query, the SQL Server optimizer will choose the index with the smallest number of pages at the leaf level. A nonclustered index typically will have far fewer leaf level rows that a clustered, but still contains an entry for every single row, so the count(*) value will be accurate. So consider this example:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Lucida Console" color=#0000a0&gt;USE AdventureWorks;&lt;BR&gt;-- create a big table by copying another one &lt;BR&gt;IF EXISTS (SELECT * FROM sys.tables WHERE name = 'newdetails')&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; DROP TABLE newdetails;&lt;BR&gt;SELECT * INTO newdetails FROM Sales.SalesOrderDetail; &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Lucida Console" color=#0000a0&gt;-- Build a PK and a NC index on the same column&lt;BR&gt;ALTER TABLE newdetails &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ADD CONSTRAINT PK_Detail PRIMARY KEY (SalesOrderDetailID);&lt;BR&gt;CREATE UNIQUE INDEX UNQ_Detail ON newdetails (SalesOrderDetailID); &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Lucida Console" color=#0000a0&gt;-- Look at the plan; the optimizer will choose the NC index&lt;BR&gt;SET SHOWPLAN_TEXT ON;&lt;BR&gt;SELECT count(*) FROM newdetails &lt;BR&gt;WHERE SalesOrderDetailID BETWEEN 1000 and 2000; &lt;BR&gt;SET SHOWPLAN_TEXT OFF;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Lucida Console" color=#0000a0&gt;-- Look at the performance; the nonclustered is performing better&lt;BR&gt;-- Even though the NC index will be chosen without the hint, &lt;BR&gt;--&amp;nbsp;&amp;nbsp; i included it here to make it more obvious which index is chosen&lt;BR&gt;SET STATISTICS IO ON;&lt;BR&gt;SELECT count(*) FROM newdetails WITH (INDEX = UNQ_Detail)&lt;BR&gt;WHERE SalesOrderDetailID BETWEEN 1000 and 2000&lt;BR&gt;SELECT count(*) FROM newdetails WITH (INDEX = PK_Detail)&lt;BR&gt;WHERE SalesOrderDetailID BETWEEN 1000 and 2000&lt;BR&gt;SET STATISTICS IO OFF;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;I am absolutely not recommending that you always duplicate your PK index with another one, but rather illustrating that it is not always completely pointless or harmful to do so. Choosing the best indexes takes a lot of careful consideration of your data distribution and your query usage patterns, and there is no one-size-fits-all answer.&lt;/P&gt;
&lt;P&gt;I hope this is useful to you,&lt;/P&gt;
&lt;P&gt;&lt;FONT color=#ff00ff size=4&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=6399" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/index+hints/default.aspx">index hints</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category></item><item><title>Geek City: Nonclustered Index Keys</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2008/03/16/nonclustered-index-keys.aspx</link><pubDate>Mon, 17 Mar 2008 01:53:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:5618</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>10</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/5618.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=5618</wfw:commentRss><description>&lt;P&gt;I recently received a question about the storage of nonclustered index keys. I am assuming you are aware of the fact that if your table has a clustered index, SQL Server uses the clustered index key (all of its columns, if it is a composite index) as a 'bookmark' in your nonclustered indexes,&amp;nbsp; to allow your nonclustered indexes to uniquely identify the row that the index is pointing to. &lt;/P&gt;
&lt;P&gt;In particular, this question was about a couple of statements in &lt;EM&gt;Inside SQL Server 2005: Query Tuning and Optimization&lt;/EM&gt;, Chapter 3. Now, I didn't write Chapter 3, but I thought I knew all about how index keys were stored.&amp;nbsp; But (you may want to be sitting down for this) I WAS WRONG.&lt;/P&gt;
&lt;P&gt;Here are the statements:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;“…when you create a &lt;B&gt;nonunique&lt;/B&gt; nonclustered index on a table with a clustered index, we append the clustered index keys to the nonclustered index keys if they are not explicitly part of the nonclustered index keys” &lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;And then a bit later on, there is another quote discussing this same effect: 
&lt;BLOCKQUOTE&gt;
&lt;P&gt;“Note that the key columns for each of the nonclustered indexes on T_clu include the clustered index key column a &lt;B&gt;with the exception of T_clu_f, which is a unique index&lt;/B&gt;.” &lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Although I had read this chapter when we were putting the book together, I must have read this last sentence too fast. The first quoted sentence is basically what I said in my lead-in paragraph. But the second sentence now seemed wrong. I thought it was saying that if your nonclustered index is UNIQUE, the clustered key columns are not stored in the index at all. I knew that wasn't true, so I contacted the author of Chapter 3. It turns out he was using a very specific definition of key. &lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;KEY COLUMN:&lt;/STRONG&gt; a column that is stored in the leaf pages and the interior (node, or non-leaf) pages of the B-tree and that you can use in an index seek.&amp;nbsp; &lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;He also uses another term in some of the related sections of the chapter:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;COVERED COLUMN:&lt;/STRONG&gt; a column that can be returned by the index without performing a bookmark lookup; it may or may not be a key column but it is stored in the leaf pages of the B-tree. &lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;The definition of an index key includes the the property that it is stored at all levels of an index, not just the leaf level. Covered columns are in the leaf, so we have the potential of a covering index, and the possibility of retrieving the data we need without accessing the table itself. But not all covered columns are key columns. &lt;/P&gt;
&lt;P&gt;This was news to me. I assumed that the fact that the clustered index key was in the nonclustered index meant that it was part of the key. And all my previous testing seemed to bear that out; I must never have tested at this level of detail with a unique nonclustered index. What this means is that I have been wrong in the following claim: "Explicitly declaring your clustered key columns as part of your nonclustered indexes does not make any difference in the storage of the index." That statement is true for nonunique indexes, but &lt;STRONG&gt;not &lt;/STRONG&gt;true for unique. &lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;What does this really mean?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Consider a &lt;STRONG&gt;TabA&lt;/STRONG&gt; with a clustered index on &lt;STRONG&gt;col1&lt;/STRONG&gt;.&amp;nbsp; These two index definitions are identical: 
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;CREATE INDEX nc_index ON TabA(col2) &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;CREATE INDEX nc_index ON TabA(col2, col1)&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;For a nonunique, nonclustered index (which the above index is), the clustered key &lt;STRONG&gt;col1&lt;/STRONG&gt; is automatically included at the leaf, and at all other levels, just as if we explicitly declared &lt;STRONG&gt;col1&lt;/STRONG&gt; to be part of the nonclustered index key. 
&lt;P&gt;However, these two index definitions are not exactly the same: 
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;CREATE UNIQUE INDEX nc_index ON TabA(col2) &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;CREATE UNIQUE INDEX nc_index ON TabA(col2, col1)&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;The first index definition will create a nonclustered index on &lt;STRONG&gt;col2&lt;/STRONG&gt;, and add &lt;STRONG&gt;col1&lt;/STRONG&gt; only at the leaf. The second index definition will&amp;nbsp; also create an nonclustered index on &lt;STRONG&gt;col2&lt;/STRONG&gt;, but it will add &lt;STRONG&gt;col1&lt;/STRONG&gt; as a true key column and it will be stored at all index levels.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Here's a specific example&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Let's look at a specific example. I will be demonstrating the contents of the index rows using DBCC PAGE, and determine the index pages I am interested in by using DBCC IND. For more information about these two undocumented commands, one of the best places to look is at a couple of blog posts by Paul Randal:&lt;/P&gt;
&lt;P&gt;&lt;A title=http://www.sqlskills.com/blogs/paul/2007/10/01/InsideTheStorageEngineUsingDBCCPAGEAndDBCCINDToFindOutIfPageSplitsEverRollBack.aspx href="http://www.sqlskills.com/blogs/paul/2007/10/01/InsideTheStorageEngineUsingDBCCPAGEAndDBCCINDToFindOutIfPageSplitsEverRollBack.aspx"&gt;http://www.sqlskills.com/blogs/paul/2007/10/01/InsideTheStorageEngineUsingDBCCPAGEAndDBCCINDToFindOutIfPageSplitsEverRollBack.aspx&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&lt;A title=http://blogs.msdn.com/sqlserverstorageengine/archive/2006/12/13/More-undocumented-fun_3A00_-DBCC-IND_2C00_-DBCC-PAGE_2C00_-and-off_2D00_row-columns.aspx href="http://blogs.msdn.com/sqlserverstorageengine/archive/2006/12/13/More-undocumented-fun_3A00_-DBCC-IND_2C00_-DBCC-PAGE_2C00_-and-off_2D00_row-columns.aspx"&gt;http://blogs.msdn.com/sqlserverstorageengine/archive/2006/12/13/More-undocumented-fun_3A00_-DBCC-IND_2C00_-DBCC-PAGE_2C00_-and-off_2D00_row-columns.aspx&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;DBCC IND returns one row for every page of a table or index. The rows contain values indicating what type of page is listed (2 is an index page), what index the page belongs to, what level of an index the page is at (0 is the leaf), and then the file ID and page number. These last two values are then used in DBCC PAGE to get the contents of the page.&amp;nbsp; The output also includes the file ID and page number for the next and previous pages in logical order. A page with no previous page is the first page in order.&lt;/P&gt;
&lt;P&gt;When running DBCC IND on a large table, you can get lots of rows returned, and it can be tricky to find the exact rows you're interested in. For this reason, I usually take the output of DBCC IND and save it to a table, and then I can query the table. I can included the script to build a table called sp_index_info in the master database. Because of the sp_ prefix, this table can be accessed from any database.&lt;/P&gt;
&lt;P&gt;I'm going to use a table that I copy from the AdventureWorks database into a test database. You can use any database you like to run this code on your own server.&lt;/P&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;USE testdb&lt;BR&gt;GO&lt;BR&gt;IF EXISTS (SELECT name FROM sys.tables WHERE name = 'Sales')&lt;BR&gt;&amp;nbsp;&amp;nbsp; DROP TABLE Sales;&lt;BR&gt;GO&lt;BR&gt;SELECT&amp;nbsp; * INTO Sales &lt;BR&gt;FROM AdventureWorks.Sales.SalesOrderDetail;&lt;BR&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Now build a clustered index on SalesOrderID and three similar nonclustered indexes on SalesOrderDetailID, which is unique.&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;&lt;FONT face="courier new"&gt;CREATE CLUSTERED INDEX Sales_ID_Index ON Sales(SalesOrderID);&lt;BR&gt;GO&lt;BR&gt;-- The first index is not declared as a unique index&lt;BR&gt;CREATE INDEX Sales_DetailID_Index1 ON Sales(SalesOrderDetailID);&lt;BR&gt;GO&lt;BR&gt;-- The second index is declared as a unique index&lt;BR&gt;CREATE UNIQUE INDEX Sales_DetailID_Index2 ON Sales(SalesOrderDetailID);&lt;BR&gt;GO &lt;BR&gt;-- The third index is declared as a unique index and explicitly includes the clustered index key&lt;BR&gt;CREATE UNIQUE INDEX Sales_DetailID_Index3 ON Sales(SalesOrderDetailID, SalesOrderID);&lt;BR&gt;GO&lt;/FONT&gt; &lt;/P&gt;
&lt;P&gt;Now, populate the sp_index_info table with the results of DBCC IND&lt;/P&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;TRUNCATE TABLE sp_index_info&lt;BR&gt;INSERT INTO sp_index_info&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; EXEC ('DBCC IND ( testdb, Sales, -1)'&amp;nbsp; );&lt;BR&gt;GO&lt;BR&gt;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Find the first leaf page from each index, by looking for pages with no previous page, and with an IndexLevel value of 0. &lt;/P&gt;
&lt;P&gt;&lt;BR&gt;&lt;FONT face="courier new"&gt;SELECT PageFID, PagePID, IndexID&lt;BR&gt;FROM sp_index_info&lt;BR&gt;WHERE PageType = 2 AND IndexLevel = 0&lt;BR&gt;&amp;nbsp; AND PrevPagePID = 0&lt;BR&gt;ORDER BY IndexID;&lt;BR&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Here's my output:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;PageFID PagePID&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; IndexID&lt;BR&gt;------- ----------- -------&lt;BR&gt;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3416&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2&lt;BR&gt;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1248&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3&lt;BR&gt;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1680&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4&lt;/FONT&gt; 
&lt;P&gt;To look at the actual rows, we need to use DBCC PAGE. An option value of 3 is very useful for index pages, as it gives us tabular output showing each individual index row.&amp;nbsp; I'll take each of the file and page numbers and use them in DBCC PAGE, and of course you'll have to substitute whatever page numbers you get.&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;&lt;FONT face="courier new"&gt;DBCC TRACEON (3604);&lt;BR&gt;DBCC PAGE(testdb, 1, 3416, 3);&lt;BR&gt;DBCC PAGE(testdb, 1, 1248, 3); &lt;BR&gt;DBCC PAGE(testdb, 1, 1680, 3);&lt;/FONT&gt; &lt;/P&gt;
&lt;P&gt;Here are my results:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityNonclusteredIndexKeys_9D3F/image_4.png"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=127 alt=image src="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityNonclusteredIndexKeys_9D3F/image_thumb_1.png" width=244 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;The results shown are for the leaf level. Note that because my &lt;STRONG&gt;clustered&lt;/STRONG&gt; index is not unique, it includes a &lt;EM&gt;uniqueifier&lt;/EM&gt; for every row. The &lt;EM&gt;uniqueifier&lt;/EM&gt; is never visible using SELECT statements, but is a hidden column stored within the row to make sure each row in the clustered index is unique. This &lt;EM&gt;uniqueifier&lt;/EM&gt; column is always considered part of the clustered key internally, so every nonunique clustered index is really a composite index. &lt;/P&gt;
&lt;P&gt;There is the exact same data in the leaf for all three nonclustered indexes (except for page numbers of course; all three indexes have their own pages). The same index keys are in each row of the first leaf level index page. However, you might note a couple of differences int the column headers. Just as the nonunique clustered index includes a uniqueifier, SQL Server has to have some way to make sure each nonclustered index row is unique. The column headers actually index which columns of my nonclustered indexes make up the key. For the nonunique index, SQL Server has to consider the entire clustered key as part of the nonclustered key. It knows the clustered key is unique, so adding it to the nonclustered guarantees uniqueness. You can see that all 3 columns are labeled as (key): SalesOrderDetailID, SalesOrderID (the clustered key) and the clustered index uniqueifier.&lt;/P&gt;
&lt;P&gt;For the second index, which was declared as unique, but did not explicitly include the clustered key, the only key the nonclustered index needs is the key the index was built on. Because that single key was declared as unique, no further information is needed to be part of the key. However, the clustered key stored in the leaf level rows, because SQL Server needs that to be able to find the row in the table data.&lt;/P&gt;
&lt;P&gt;The third index explicitly declared the clustered key to be part of the nonclustered index key. So the column labels indicate that both the column SalesOrderDetailID and SalesOrderID are key columns. Since the index was declared to be unique, only the explicitly declared key columns are part of the key. The uniqueifier is there, because as part of the clustered key is must be in the leaf level pages, but it is not marked as part of the nonclustered key.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;So how are these indexes different?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The indexes differ in the upper levels. The table sp_index_info still contains the output of DBCC IND, so we can find an upper level page for each index.&amp;nbsp; &lt;/P&gt;
&lt;P&gt;At the level above the leaf the IndexLevel is 1. I don't want to see the rows for the upper level pages of the clustered index, so I filter them out.&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;&lt;FONT face="courier new"&gt;SELECT PageFID, PagePID, IndexID&lt;BR&gt;FROM sp_index_info&lt;BR&gt;WHERE PageType = 2 AND IndexLevel &amp;gt; 0 and IndexID &amp;gt; 1&lt;BR&gt;&amp;nbsp; AND PrevPagePID = 0&lt;BR&gt;ORDER BY IndexID;&lt;/FONT&gt; &lt;/P&gt;
&lt;P&gt;Here are my results:&amp;nbsp; 
&lt;P&gt;&lt;FONT face="courier new"&gt;PageFID PagePID&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; IndexID&lt;BR&gt;------- ----------- -------&lt;BR&gt;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3520&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2&lt;BR&gt;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1360&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3&lt;BR&gt;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1744&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4&lt;/FONT&gt; 
&lt;P&gt;And then I use those results to look at the pages:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;DBCC TRACEON (3604);&lt;BR&gt;DBCC PAGE(testdb, 1, 3520, 3);&lt;BR&gt;DBCC PAGE(testdb, 1, 1360, 3); &lt;BR&gt;DBCC PAGE(testdb, 1, 1744, 3);&lt;/FONT&gt; 
&lt;P&gt;Here are my results:&lt;/P&gt;
&lt;P&gt;&lt;A href="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityNonclusteredIndexKeys_9D3F/image_6.png"&gt;&lt;IMG style="BORDER-RIGHT:0px;BORDER-TOP:0px;BORDER-LEFT:0px;BORDER-BOTTOM:0px;" height=133 alt=image src="http://sqlblog.com/blogs/kalen_delaney/WindowsLiveWriter/GeekCityNonclusteredIndexKeys_9D3F/image_thumb_2.png" width=244 border=0&gt;&lt;/A&gt; &lt;/P&gt;
&lt;P&gt;Now you should see something different. The first index, nonunique, still has all three columns in the upper level page: the nonclustered key SalesOrderDetailID, and the two columns of the clustered key: SalesOrderID and the uniqueifier.&lt;/P&gt;
&lt;P&gt;The second index is the unique nonclustered on a single column, and only has SalesOrderDetailID in the upper level page.&lt;/P&gt;
&lt;P&gt;The third index is unique and composite, so both of the declared keys are in the upper level. &lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;What other possibilities are there?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;One additional question you might have is what happens if the clustered index is also unique. How do the nonclustered index rows look different? I think with the tools and examples presented here, you should be able to figure that out for yourself!&lt;/P&gt;
&lt;P&gt;Have fun!&lt;/P&gt;
&lt;P&gt;&lt;FONT color=#ff00ff size=4&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="http://dvd.kalendelaney.com/"&gt;http://DVD.KalenDelaney.com&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="http://www.sqlcommunity.com/"&gt;www.SQLCommunity.com&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;------------------------------Script to create a table to hold DBCC IND output--------------------------------------------------------&lt;/P&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;-- Create a table to hold the output of DBCC IND&lt;BR&gt;USE master&lt;BR&gt;GO&lt;BR&gt;IF EXISTS (SELECT name FROM sys.tables&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE name = 'sp_index_info')&lt;BR&gt;&amp;nbsp;&amp;nbsp; DROP TABLE sp_index_info; &lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="courier new"&gt;GO&lt;BR&gt;CREATE TABLE sp_index_info&lt;BR&gt;(PageFID&amp;nbsp; tinyint, &lt;BR&gt;&amp;nbsp; PagePID int,&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp; IAMFID&amp;nbsp;&amp;nbsp; tinyint, &lt;BR&gt;&amp;nbsp; IAMPID&amp;nbsp; int, &lt;BR&gt;&amp;nbsp; ObjectID&amp;nbsp; int,&lt;BR&gt;&amp;nbsp; IndexID&amp;nbsp; tinyint,&lt;BR&gt;&amp;nbsp; PartitionNumber tinyint,&lt;BR&gt;&amp;nbsp; PartitionID bigint,&lt;BR&gt;&amp;nbsp; iam_chain_type&amp;nbsp; varchar(30),&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp; PageType&amp;nbsp; tinyint, &lt;BR&gt;&amp;nbsp; IndexLevel&amp;nbsp; tinyint,&lt;BR&gt;&amp;nbsp; NextPageFID&amp;nbsp; tinyint,&lt;BR&gt;&amp;nbsp; NextPagePID&amp;nbsp; int,&lt;BR&gt;&amp;nbsp; PrevPageFID&amp;nbsp; tinyint,&lt;BR&gt;&amp;nbsp; PrevPagePID int, &lt;BR&gt;&amp;nbsp; Primary Key (PageFID, PagePID));&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=5618" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/DBCC/default.aspx">DBCC</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/internals/default.aspx">internals</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/pages/default.aspx">pages</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/storage/default.aspx">storage</category></item></channel></rss>