<?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 : pages</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/pages/default.aspx</link><description>Tags: pages</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><item><title>Geek City: What's Worse Than a Table Scan?</title><link>http://www2.sqlblog.com/blogs/kalen_delaney/archive/2008/05/25/whats-worse-than-a-table-scan.aspx</link><pubDate>Sun, 25 May 2008 20:59:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:6970</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>10</slash:comments><comments>http://www2.sqlblog.com/blogs/kalen_delaney/comments/6970.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=6970</wfw:commentRss><description>&lt;P&gt;I have frequently heard SQL Server developers and DBAs gasp when a query plan is indicating that SQL Server is performing a table scan, thinking that is the worst thing that could ever happen to a query. The truth is, it's far from the worst thing and in addition, not all table scans are created equal. &lt;/P&gt;
&lt;P&gt;One thing that is far worse that a table scan is to execute a query plan that uses a nonclustered index, and having that plan look up every single row in a table! Although that is a horrible thing to behold, it is not the topic of this post.&lt;/P&gt;
&lt;P&gt;Today, I'm going to show you that two different table scans on the same data in a heap can give very different performance.&lt;/P&gt;
&lt;P&gt;The behavior has to do with a technique that SQL Server uses when a row in a heap is increased in size so it no longer fits in the original page. This usually occurs when a variable length column is updated to take more space.&amp;nbsp; If SQL Server just moved the row to another page, any nonclustered indexes would have to be updated to indicate the new page address.&amp;nbsp; (Remember, if the underlying table is a heap, nonclustered indexes point to the data row using a actual address.) Since there can be up to 249 nonclustered indexes on a single table, that could potentially be a LOT of work. So instead, when a row in a heap has to move, SQL Server leaves behind a forwarding pointer in place of the row that has moved. The nonclustered indexes continue to point to the old location, and then SQL Server just needs one more page lookup to find the new location. For just a few lookups, this expense is minimal and more than made up for my the savings of not having to update all the nonclustered indexes every time a row moves. &lt;/P&gt;
&lt;P&gt;However, what happens when there are LOTS of forwarding pointers?&lt;/P&gt;
&lt;P&gt;The metadata function &lt;EM&gt;sys.dm_db_index_physical_stats&lt;/EM&gt; has a column that indicates how many forwarded records are in any table. For tables with clustered indexes, this will always be 0. &lt;/P&gt;
&lt;P&gt;Let's look at an example. I'll make a copy of the &lt;EM&gt;Person.Address&lt;/EM&gt; table in the &lt;EM&gt;AdventureWorks&lt;/EM&gt; database, and add a new varchar column to it. Initially, the column takes no space.&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;USE AdventureWorks;&lt;BR&gt;GO&lt;BR&gt;IF EXISTS (SELECT 1 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 = 'Address2' AND schema_id =1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; DROP TABLE dbo.Address2;&lt;BR&gt;GO&lt;BR&gt;SELECT *, convert (varchar(500), 'comments') AS comments &lt;BR&gt;&amp;nbsp;&amp;nbsp; INTO Address2 &lt;BR&gt;FROM Person.Address;&lt;BR&gt;GO&lt;BR&gt;-- note that the pages are almost full and there are no forwarded records&lt;BR&gt;SELECT index_type_desc, page_count, avg_page_space_used_in_percent,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; avg_record_size_in_bytes,forwarded_record_count&lt;BR&gt;FROM sys.dm_db_index_physical_stats(db_id('AdventureWorks'), &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/FONT&gt;&lt;FONT face="Courier New" size=2&gt;object_id('Address2'),null, null, 'detailed');&lt;BR&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Now I'll increase the length of all the new columns and check the physical stats again:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;UPDATE Address2&lt;BR&gt;SET comments = replicate('a', 500);&lt;BR&gt;GO&lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=2&gt;SELECT index_type_desc, page_count, avg_page_space_used_in_percent,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; avg_record_size_in_bytes,forwarded_record_count&lt;BR&gt;FROM sys.dm_db_index_physical_stats(db_id('AdventureWorks'), &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/FONT&gt;&lt;FONT face="Courier New" size=2&gt;object_id('Address2'),null, null, 'detailed');&lt;BR&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;The output shows me I have 1763 pages in the table and 15961 forwarded records.&lt;/P&gt;
&lt;P&gt;Let's see what happens when we read every row in the table:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;SET STATISTICS IO ON;&lt;BR&gt;SELECT * FROM Address2;&lt;BR&gt;SET STATISTICS IO OFF;&lt;/FONT&gt; 
&lt;P&gt;The logical I/O value tells us that instead of just reading through every page, for a total of 1763 reads, SQL Server jumps out of sequence and follows the forwarding pointer for every forwarded record. So the number of logical reads is the sum of the number of pages plus the number of forwarded records:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;1763&amp;nbsp; +&amp;nbsp; 15961= 17724&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;I was discussing this behavior with my friend and colleague Tibor Karaszi and he proposed an explanation for this behavior. He related it to the same behavior that Itzik Ben-Gan has described for why SQL Server will always follow page pointers when scanning a clustered index if consistent reads are desired. The alternative would be to just read the pages in disk order, or page number order, which can be determined by examining the IAM structures for the object. For clustered tables, we need to follow the page pointers instead of the IAMs&amp;nbsp; to make sure that if a row is moved due to an update while the scan is occurring, that we don't read the same row twice (if the row is moved to a higher page number) or skip the row altogether (if the row is moved to a lower page number.)&lt;/P&gt;
&lt;P&gt;But what about a heap? Are there potential problems scanning a heap while updates are occurring? Could we potentially read the same row twice or skip a row, since there is no 'ordered list' to read? Tibor suggested the following:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;I believe that forwarding pointers take care of just that. Because of forwarding pointers, the "root" location for a row is stable. So, even if the row moves during a scan, the "root location"(forwarding stub) is at the same position. We have concluded that the scan uses the forwarding pointers when reading the rows. This means that a scan is not sensitive to row movements during the scan. It cannot "skip" rows that are there, or read the same row twice. &lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;So a few forwarding pointers are not a bad thing, but having lots of them can increase the work done during scans or partial scans by a considerable amount. &lt;/P&gt;
&lt;P&gt;So how do you get rid of forwarding pointers? There are 3 ways:&lt;/P&gt;
&lt;P&gt;1. If the row is updated, so that its size decreases, AND if there is still room on the page where the row came from, it will be moved back. This is not dependable, so it isn't really recommended as a solution.&amp;nbsp; When I updated my Address2 table, many of the forwarded records were moved, but not all:&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" size=2&gt;UPDATE Address2&lt;BR&gt;SET comments = '';&lt;BR&gt;GO &lt;BR&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" size=2&gt;SELECT index_type_desc, page_count, avg_page_space_used_in_percent,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; avg_record_size_in_bytes,forwarded_record_count&lt;BR&gt;FROM sys.dm_db_index_physical_stats(db_id('AdventureWorks'), &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/FONT&gt;&lt;FONT face="Courier New" size=2&gt;object_id('Address2'),null, null, 'detailed');&lt;BR&gt;GO&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;My results showed that I am still left with 1080 forwarded records. This is a great improvement over 15961, but it's still more forwarded records than there are pages in the table.&lt;/P&gt;
&lt;P&gt;2. Forwarded records will be cleaned up when you shrink the data file. This is definitely NOT recommended as a solution; I am only mentioning it for completeness. SQL Server does so much moving of data and updating nonclustered index pointers when shrinking a file, that updating the forwarded records is not very much extra work at all. &lt;/P&gt;
&lt;P&gt;3. Since forwarded records only exist in heaps, the best solution is to make the table not a heap. Build a clustered index, and all the forwarded records will go away. If you really don't want the clustered index, you can then drop it.&lt;/P&gt;
&lt;P&gt;Hopefully, this information will be 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=6970" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/allocation+structures/default.aspx">allocation structures</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/forwarding+pointers/default.aspx">forwarding pointers</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/metadata/default.aspx">metadata</category><category domain="http://www2.sqlblog.com/blogs/kalen_delaney/archive/tags/pages/default.aspx">pages</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>