<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://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 : included columns</title><link>http://sqlblog.com/blogs/kalen_delaney/archive/tags/included+columns/default.aspx</link><description>Tags: included columns</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><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://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://sqlblog.com/blogs/kalen_delaney/comments/10065.aspx</comments><wfw:commentRss>http://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://sqlblog.com/aggbug.aspx?PostID=10065" width="1" height="1"&gt;</description><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/sort/default.aspx">sort</category><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/included+columns/default.aspx">included columns</category><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/query+tuning/default.aspx">query tuning</category></item><item><title>Geek City: Included Columns</title><link>http://sqlblog.com/blogs/kalen_delaney/archive/2007/12/31/included-columns.aspx</link><pubDate>Mon, 31 Dec 2007 19:53:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:4295</guid><dc:creator>Kalen Delaney</dc:creator><slash:comments>3</slash:comments><comments>http://sqlblog.com/blogs/kalen_delaney/comments/4295.aspx</comments><wfw:commentRss>http://sqlblog.com/blogs/kalen_delaney/commentrss.aspx?PostID=4295</wfw:commentRss><description>&lt;P&gt;When an issue comes up more than once in a short period of time, I figure it is a message from the Universe. I might even assume it is a gift of blog fodder. So I won't turn down the gift, even if it means another juicy post before the end of year, much to Denis' disappointment. &lt;/P&gt;
&lt;P&gt;The issue is SQL Server 2005 "Included Columns". There was a post on the public newsgroups about them a couple of days ago, and just yesterday, there was a similar post on the SQL Server MVP private newsgroup.&amp;nbsp; Simply put, the questions were expressing concern that there were no built-in tools in SQL Server 2005 to list which columns were included columns. The information is of course available in the metadata views, but it can require a join of at least three of them to get a simple list of which columns are "included columns" in a given index.&amp;nbsp; The poster on the MVP newsgroup wanted to know if someone had already written such a query and could share it, to save him some work. &lt;/P&gt;
&lt;P&gt;There actually was a third post about included columns that I came across,&amp;nbsp; that was asking about the reasons for using included columns. I will address that question first, and then provide some scripts to return information about included columns.&lt;/P&gt;
&lt;P&gt;You need to be aware of two 'features' of indexes to fully appreciate included columns. &lt;/P&gt;
&lt;P&gt;First: all indexes have a limit of no more than 16 key columns, with a combined total of no more than 900 bytes.&lt;/P&gt;
&lt;P&gt;Second: a 'covering' index, which is a nonclustered index that contains all the columns referenced in a query from one table, can provide an incredible performance advantage. If all the information a query needs is contained in the index keys, SQL Server will never need to actually access the table data, and not having to do this table lookup can be a very good thing. (Disclaimer: covering indexes are not the solution to ALL query performance problems, and I don't have time today to provide a full discussion of covering indexes.) &lt;/P&gt;
&lt;P&gt;SQL Server 2005 allows you to get around the 16 column and 900 byte limit and add additional column to a nonclustered index to provide greater opportunity for covering indexes. The syntax would look something like this:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE INDEX bigindex on mybigrowtable(keycolumn) INCLUDE (bigcolumn1, bigcolumn2)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;If all the columns you would like to have in your index fit within the limits, there is no technical reason for having included columns; all your columns could just be regular key columns. However, there are some other issues to consider. &lt;/P&gt;
&lt;P&gt;1) Space requirements: Normal key columns, as part of the key, are propagated up through all levels of your indexes. So if you have some very large columns, even if they fit in the 900 byte limit, you can save space by defining them as included columns.&amp;nbsp; Here is an example, using the AdventureWorks database:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;USE AdventureWorks &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;-- set the database to bulk_logged recovery &lt;BR&gt;--&amp;nbsp; prior to copying tables &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;ALTER DATABASE AdventureWorks &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET RECOVERY bulk_logged&lt;BR&gt;GO&lt;BR&gt;&lt;/STRONG&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;---------------------&lt;BR&gt;IF ( OBJECT_ID('Person.Address1') is not null)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; DROP TABLE Person.Address1&lt;BR&gt;GO&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;IF ( OBJECT_ID('Person.Address2') is not null)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; DROP TABLE Person.Address2 &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;-------------------------------------- &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;-- Create two copies of the Person.Address table and enlarge one of the columns &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;SELECT * INTO Person.Address1 &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Person.Address&lt;BR&gt;GO&lt;BR&gt;ALTER TABLE Person.Address1 &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ALTER COLUMN AddressLine2 nchar(350)&lt;BR&gt;GO &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;-- Create an index with two included columns &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;CREATE INDEX IX_Address_City &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; on [Person].[Address1] (City, StateProvinceID)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; INCLUDE(AddressLine1, AddressLine2)&lt;BR&gt;GO&lt;BR&gt;&lt;/STRONG&gt;&lt;/FONT&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;------------------- &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;SELECT * INTO Person.Address2 &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM Person.Address&lt;BR&gt;GO&lt;BR&gt;ALTER TABLE Person.Address2 &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ALTER COLUMN AddressLine2 nchar(350)&lt;BR&gt;GO&lt;BR&gt;-- Create a 'regular' index with no included columns;&lt;BR&gt;-- All four columns are keys, and included at all index levels&lt;BR&gt;CREATE INDEX IX_Address_City &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; on [Person].[Address2] &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (City, StateProvinceID, AddressLine1, AddressLine2)&lt;BR&gt;GO&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;-- Note that the Address2 index uses about 1.5 MB &lt;BR&gt;-- more than&amp;nbsp; the index on Address1, because it contains&lt;BR&gt;-- all 4 keys in all levels of the index. &lt;/STRONG&gt;&lt;/FONT&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;EXEC sp_spaceused 'Person.Address1'&lt;BR&gt;EXEC sp_spaceused 'Person.Address2'&lt;BR&gt;GO&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;2) Maintenance costs: Normal key columns are maintained in sorted order. An index on (lastname, state, city) would have 3 sort columns and every new row would have to be put in the proper location based on all 3 columns. But an index on lastname, with (state, city) as included columns, would not have to maintain sort order on state and city and that could improve the performance of data modification operations. I haven't done any tests yet to determine how much savings you might realize, but it will be faster with included columns. &lt;/P&gt;
&lt;P&gt;The downside of included columns not being sorted, is that those columns will not be useful for searching if those columns are in the WHERE clause, in particular if those columns are using in an inequality expression. &lt;/P&gt;
&lt;P&gt;So now, how can you retrieve information about which columns are included and which are key columns?&amp;nbsp; Sp_helpindex does not display this information, in fact, it makes no mention of included columns. The information is available in a catalog view called sys.index_columns, in a column called 'is_included_column'. The code below creates a view that you can use to return information about all the columns in your indexes.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT size=2&gt;&lt;FONT face="Courier New"&gt;&lt;FONT color=#0000a0&gt;&lt;STRONG&gt;CREATE VIEW index_column_info&lt;BR&gt;AS&lt;BR&gt;&amp;nbsp; SELECT object_name = object_name(ic.object_id),&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; index_name = i.name,&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 'column' = c.name,&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 'column usage' = CASE ic.is_included_column&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHEN 0 then 'KEY'&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ELSE 'INCLUDED'&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; END&lt;BR&gt;&amp;nbsp;&amp;nbsp; FROM sys.index_columns ic JOIN sys.columns c&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;&amp;nbsp; ON ic.object_id = c.object_id&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;&amp;nbsp; AND ic.column_id = c.column_id&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; JOIN sys.indexes i&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;&amp;nbsp; ON i.object_id = ic.object_id&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;&amp;nbsp; AND i.index_id = ic.index_id&lt;/STRONG&gt; &lt;/FONT&gt;&lt;/FONT&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;You can select from this view and supply a table name and/or an index name or id in a WHERE clause: 
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;SELECT * FROM index_column_info&lt;BR&gt;WHERE object_name = 'Address1'&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;A student in one of my classes earlier this year wanted a way to get input back that looked like the sp_helpindex output. I am attaching a script to build a stored procedure called new_helpindex which will do that. However, the new procedure is used a bit differently than sp_helpindex. sp_helpindex requires a schema name and new_helpindex does not. So to get information about my new Address1 table created above, the two procedures would be called as shown:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;FONT face="Courier New" color=#0000a0 size=2&gt;&lt;STRONG&gt;EXEC new_helpindex 'address1'&lt;BR&gt;EXEC sp_helpindex 'person.address1'&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;If you want the new_helpindex procedure to accept schema names, and to return exactly the same details as sp_helpindex (plus the included columns), it is certainly possible, and I leave that as an exercise to my readers. 
&lt;P&gt;Have fun! 
&lt;P&gt;&lt;FONT color=#ff00ff&gt;~Kalen&lt;/FONT&gt;&lt;/P&gt;&lt;img src="http://sqlblog.com/aggbug.aspx?PostID=4295" width="1" height="1"&gt;</description><enclosure url="http://sqlblog.com/blogs/kalen_delaney/attachment/4295.ashx" length="2955" type="text/plain" /><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/Tools/default.aspx">Tools</category><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/included+columns/default.aspx">included columns</category><category domain="http://sqlblog.com/blogs/kalen_delaney/archive/tags/indexes/default.aspx">indexes</category></item></channel></rss>