
|
Overview
I often find myself wanting to know lots about the indexes for a table or for all tables in a database. This is why I wrote sp_indexinfo.
Versions etc.
This script was written for SQL Server 2005 sp2 or later (the procedure uses the OBJECT_SCHEMA_NAME() function which was introduced in SQL Server 2005 service pack 2).
Article written: 2008-07-07
Article last modified: 2009-02-18
Details
I can't count how many times I wanted to know "more" about physical attributes for a table, including index information.
So I end up running sp_helpindex, some SELECT over sys.indexes, some reports in SSMS, some other queries against other catalog and dynamic management views, etc.
I wrote this procedure for those cases. Here's my version of a procedure to give me information like:
- What indexes exists for a particular table or for each table in the database
- Clustered, non-clustered or heap
- Columns in the index
- Included columns in the index
- Unique or nonunique
- Number rows in the table
- Space usage
- How frequently the indexes has been used
- Any obvious indexes I should add?
For this, I use a number of sources, like sys.indexes, sys.columns, sys.partitions, sys.allocation_units, sys.data_spaces, sys.dm_db_index_usage_stats etc.
Tip
I frequently spend lots of time in one particular database, and I then would want to "post-process" the result from a procedure like this.
By "post-process" I mean things like sorting and grouping. I suggest that you for those cases take the first SELECT statement and create a view out of it.
You can now SELECT from this view with any ORDER BY, GROUP BY, aggregate function etc as you wish.
Start with the row starting with WITH and end before the "AND OBJECT_NAME(i.OBJECT_ID) LIKE @tblPat" row.
For instance, I created such a view (I named the view index_info) in one of my databases and below are some queries I ran against this view:
SELECT * FROM index_info
ORDER BY MB DESC
SELECT * FROM index_info
WHERE table_name = 'tblname'
SELECT table_name, SUM(MB) AS size
FROM index_info
GROUP BY table_name
ORDER BY size DESC
Considerations
- You should practice caution when interpreting the results. For instance, a missing index reported doesn't neccesarily mean that you should go and create this index.
Make sure you understand the information and where it is comming from (read the source code from the proc). I've only used documented sources - Books Online is your friend.
- In order for this to be treated as a regular system procedure we need to create it in the master database and also mark it as a system procedure.
Marking your own procedures as system procedures is not supported so do this at your own risk!!!
- If you want filter index specification, you need to uncomment that line of code. This is only available on 2008, so I decided to have this commented so I (still) can have
only one version of this proc. Just search the source for 2008 and you will quickly enough find the line of code to uncomment.
Parameters:
@tblPat sysname = '%'
Name of table. Default is all tables in current database.
@missing_ix tinyint = 0
Whether or not to include missing indexes information. Default is 0 (no).
Usage examples
USE Adventureworks
EXEC sp_indexinfo
EXEC sp_indexinfo 'Product'
EXEC sp_indexinfo 'Product', 0
EXEC sp_indexinfo 'Product%'
EXEC sp_indexinfo DEFAULT, 0
EXEC pubs..sp_indexinfo
Information returned
Below is brief documentation of what the procedure returns. Note that the procedure only uses documented sources (catalog views, functions etc.).
So for deeper documentation I suggest you check the SQL Server Books Online for sys.indexes, sys.partitions etc. (see the source code for the procedure).
First resultset, index information
| Column | Description |
| schema_name | Relational schema name. |
| table_name | Table name. |
| index_name | Index name. |
| type | Type of index: heap = datapages for heap table (this is not really an index) cl = clustered index nc = non-clustered index xml = xml index |
| is_unique | 1 if index is unique, else 0. |
| cnstr | Whether index is created through constraint no = no PK = yes, primary key constraint UQ = yes, unique constraint |
| key_columns | Columns in index key. NULL for type = heap. |
| included_columns | Included columns, if any. |
| filter_definition | Only available on 2008 and if you uncommended that line of code. Expression for the subset of rows included in the filtered index. NULL for heap or non-filtered index. |
| rows | Number of rows in the table. |
| pages | Number of pages used. One page = 8KB. |
| MB | Number of MB used. |
| user_seeks | Number of seeks by user queries since start of SQL Server. |
| user_scans | Number of scans by user queries since start of SQL Server. |
| user_lookups | Number of bookmark lookups (only for cl ix, lookup from nc index) by user queries since start of SQL Server. |
| user_updates | Number of updates by user queries since start of SQL Server. |
Second resultset, missing index information. This is recommended indexes according to the optimizer. When the optimizer work a query, it can notice "obvious" indexes to add.
I do not suggest you follow these blindly. They might, however, pick up things you missed.
This informationis picked up from the sys.dm_db_missing_index DMV's. See SQL Server Books Online for deeper information.
| Column | Description |
| schema_name | Relational schema name. |
| table_name | Table name. |
| ddl | CREATE INDEX statements, generated from the DMVs. |
| user_seeks | Number of seeks caused by user queries that the recommended index in the group could have been used for. |
| user_scans | Number of scans caused by user queries that the recommended index in the group could have been used for. |
| avg_user_impact | Average percentage benefit that user queries could experience if this missing index group was implemented.
The value means that the query cost would on average drop by this percentage if this missing index group was implemented. |
The code:
USE master
GO
IF OBJECT_ID('sp_indexinfo') IS NOT NULL DROP PROC sp_indexinfo
GO
CREATE PROCEDURE sp_indexinfo
@tblPat sysname = '%'
,@missing_ix tinyint = 0
AS
--Written by Tibor Karaszi 2008-07-07
--Last modified by Tibor Karaszi 2010-02-04
WITH key_columns AS
(
SELECT
c.OBJECT_ID
,c.name AS column_name
,ic.key_ordinal
,ic.is_included_column
,ic.index_id
,ic.is_descending_key
FROM sys.columns AS c
INNER JOIN sys.index_columns AS ic ON c.OBJECT_ID = ic.OBJECT_ID AND ic.column_id = c.column_id
)
, physical_info AS
(
SELECT p.OBJECT_ID, p.index_id, ds.name AS location, SUM(p.rows) AS rows, SUM(a.total_pages) AS pages
FROM sys.partitions AS p
INNER JOIN sys.allocation_units AS a ON p.hobt_id = a.container_id
INNER JOIN sys.data_spaces AS ds ON a.data_space_id = ds.data_space_id
GROUP BY OBJECT_ID, index_id, ds.name
)
SELECT
OBJECT_SCHEMA_NAME(i.OBJECT_ID) AS schema_name
,OBJECT_NAME(i.OBJECT_ID) AS table_name
,i.name AS index_name
,CASE i.type
WHEN 0 THEN 'heap'
WHEN 1 THEN 'cl'
WHEN 2 THEN 'nc'
WHEN 3 THEN 'xml'
ELSE CAST(i.type AS VARCHAR(2))
END
AS type
,i.is_unique
,CASE
WHEN is_primary_key = 0 AND is_unique_constraint = 0 THEN 'no'
WHEN is_primary_key = 1 AND is_unique_constraint = 0 THEN 'PK'
WHEN is_primary_key = 0 AND is_unique_constraint = 1 THEN 'UQ'
END
AS cnstr
,STUFF((SELECT CAST(', ' + kc.column_name + CASE kc.is_descending_key
WHEN 0 THEN ''
ELSE ' DESC'
END
AS VARCHAR(MAX))
AS [text()]
FROM key_columns AS kc
WHERE i.OBJECT_ID = kc.OBJECT_ID AND i.index_id = kc.index_id AND kc.is_included_column = 0
ORDER BY key_ordinal
FOR XML PATH('')
), 1, 2, '') AS key_columns
,STUFF((SELECT CAST(', ' + column_name AS VARCHAR(MAX)) AS [text()]
FROM key_columns AS kc
WHERE i.OBJECT_ID = kc.OBJECT_ID AND i.index_id = kc.index_id AND kc.is_included_column = 1
ORDER BY key_ordinal
FOR XML PATH('')
), 1, 2, '') AS included_columns
--,i.filter_definition -- 2008
,CASE WHEN i.is_disabled = 1 THEN '[DISABLED]' ELSE p.location END AS location
,p.rows
,p.pages
,CAST((p.pages * 8.00) / 1024 AS decimal(9,2)) AS MB
,s.user_seeks
,s.user_scans
,s.user_lookups
,s.user_updates
FROM sys.indexes AS i
LEFT OUTER JOIN physical_info AS p
ON i.OBJECT_ID = p.OBJECT_ID AND i.index_id = p.index_id
LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s
ON s.OBJECT_ID = i.OBJECT_ID AND s.index_id = i.index_id AND s.database_id = DB_ID()
WHERE OBJECTPROPERTY(i.OBJECT_ID, 'IsMsShipped') = 0
AND OBJECTPROPERTY(i.OBJECT_ID, 'IsTableFunction') = 0
AND OBJECT_NAME(i.OBJECT_ID) LIKE @tblPat
ORDER BY table_name, index_name
IF @missing_ix = 1
BEGIN
SELECT
OBJECT_SCHEMA_NAME(d.OBJECT_ID) AS schema_name
,OBJECT_NAME(d.OBJECT_ID) AS table_name
,'CREATE INDEX <IndexName> ON ' + OBJECT_SCHEMA_NAME(d.OBJECT_ID) + '.' + OBJECT_NAME(d.OBJECT_ID) + ' '
+ '(' + COALESCE(d.equality_columns + COALESCE(', ' + d.inequality_columns, ''), d.inequality_columns) + ')'
+ COALESCE(' INCLUDE(' + d.included_columns + ')', '')
AS ddl
,s.user_seeks
,s.user_scans
,s.avg_user_impact
FROM sys.dm_db_missing_index_details AS d
INNER JOIN sys.dm_db_missing_index_groups AS g
ON d.index_handle = g.index_handle
INNER JOIN sys.dm_db_missing_index_group_stats AS s
ON g.index_group_handle = s.group_handle
WHERE OBJECT_NAME(d.OBJECT_ID) LIKE @tblPat
AND d.database_id = DB_ID()
ORDER BY avg_user_impact DESC
END
GO
EXEC sp_MS_Marksystemobject sp_indexinfo
Change log
2008-07-08:
Added missing index information controlled by optional parameter.
Added schema name in first resultset.
2008-07-09:
Added 'DESC' for descending keys, tip from Alejandro Mesa.
Added COALESCE for included columns so the whole CREATE INDEX isn't NULL for missing index recommendation where no included columns are recommended. Caught by Aaron Bertrand.
Restricted missing index recommendation to current database. Caught by Aaron Bertrand.
Added parenthesis around key columns in the generated CREATE INDEX command, in the missing indexes resultset.
2008-07-13:
Fixed so that a comma is introduced in the generated CREATE TABLE if both equality as well as inequality columns are recommended. Thanks to Razvan Socol.
2009-02-18:
Made key_columns and included_columns a bit nicer by removing ending ', '.
Included XML indexes. I have not, however considered adding information for space usage for these "indexes". XML indexes are not regular b-tree indexes so space usage info
is found elsewhere. And I still want to keep this in one query (not procedural code). Perhaps some slow day I will look at adding spae usage info for XML indexes, but not today. :-)
Thanks to Dejan Sarka for above two tips.
2009-02-18:
Added brief documentation of what the procedure returns.
2009-02-19:
Added filtered index, as suggested by at my blog by Lidong (thanks!).
2010-02-04:
Added explicit text to indicate if index is disabled. Thanks to Henrik Staun Poulsen.
|