As you may all know, having up to date
index statistics is the key to make sure the query optimizer can choose the
most optimal execution plan for any query.
Here is DMV query that you can use against a database to see
if there are statistics that have not been updated and have had good deal of
data modifications in the underlying tables.
SELECT
TOP 100 OBJ.NAME,
STAT.NAME,
SCHEMA_NAME(OBJ.SCHEMA_ID) SCHEMA_NAME,
OBJ.CREATE_DATE,
OBJ.MODIFY_DATE,
C.NAME AS COLUMN_NAME,
SP.*
FROM
SYS.OBJECTS AS OBJ
INNERJOIN SYS.STATS AS STAT ON STAT.OBJECT_ID = OBJ.OBJECT_ID
INNERJOIN SYS.STATS_COLUMNS AS SC ON STAT.OBJECT_ID = SC.OBJECT_ID
AND STAT.STATS_ID = SC.STATS_ID
INNERJOIN SYS.COLUMNS AS C ON SC.OBJECT_ID = C.OBJECT_ID
AND C.COLUMN_ID = SC.COLUMN_ID
CROSSAPPLY SYS.DM_DB_STATS_PROPERTIES(STAT.OBJECT_ID, STAT.STATS_ID) AS SP
Note: Now just because a statistics is out
of date does not mean it’s affecting the query performance. You may have statistics that are there but are
no longer being used by the optimizer, maybe because they were auto created in
the past or its part of an unused or duplicate index. Those stats can be candidates for cleanup.
For me, this particular code to generate a script for recreating indexes has its most practical use in a replication environment. For example, by default SQL Server replication often excludes non‑clustered indexes from subscribers, and later you may realize that some or all of those indexes are actually needed to maintain query performance on the subscriber databases. This script lets you quickly generate consistent index definitions across the publisher and subscribers without having to manually reverse‑engineer each index.
Also, generally speaking, index requirements for publisher and subscriber databases can be quite different. As a result, you may end up with different sets of indexes on the publisher and its subscribers.
Beyond replication, the script is also useful in several other common DBA scenarios. For instance, you can use it to recreate indexes after a schema‑only restore or when building dev/test environments from production schemas, ensuring that performance‑critical indexes are not accidentally omitted. It is also handy when migrating databases to new instances or clusters, or when documenting and auditing index layouts across multiple environments, giving you a repeatable, script‑based approach rather than relying on ad‑hoc hand‑written DDL.
/* ============================================================================ INDEX SCRIPT GENERATOR (SQL Server 2005+) ============================================================================ Purpose: Generates CREATE INDEX statements for all user‑defined indexes in the current database, including: - Clustered / nonclustered rowstore - Clustered / nonclustered columnstore - Filtered indexes - XML and spatial indexes Result: Outputs a column "CreateIndexStatement" containing full T‑SQL scripts. These can be pasted into a new window, run as‑is, or loaded into a temp table (e.g., #dba_index) for later use. Author: Updated: 2026‑03‑26 USAGE NOTES AND CAVEATS ---------------------------------------------------------------------------- 1. Database context: - This script runs in the context of the current database. - It only lists indexes in that database. 2. Index types handled: - CLUSTERED / NONCLUSTERED (rowstore) - CLUSTERED_COLUMNSTORE / NONCLUSTERED_COLUMNSTORE - XML / SPATIAL - Filtered indexes (via WHERE clause) 3. Indexes skipped: - Primary key / unique constraint indexes (is_primary_key = 1, is_unique_constraint = 1). - Hypothetical indexes (is_hypothetical = 1). - System objects (is_ms_shipped = 1). 4. Collation‑handling: - Uses COLLATE DATABASE_DEFAULT on s.name, t.name, i.name to avoid collation conflicts when concatenating strings. - This assumes the database’ default collation is acceptable for all generated scripts. 5. Generated statement behavior: - Wraps each CREATE INDEX in: IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE ...) to avoid duplicate index creation. - Uses OBJECT_ID with schema‑qualified names (e.g., [schema].[table]). - Includes key columns, ASC/DESC, included columns, and index options (PAD_INDEX, FILLFACTOR, ONLINE effects, etc.). 6. What it does NOT do: - It does not script: - PRIMARY KEY / UNIQUE constraints (only pure indexes). - Statistics objects not backed by an index. - Columnstore ORDER(), MAXDOP=, ONLINE=, or other advanced options beyond the common WITH options listed. - It does not: - Drop indexes. - Handle partitioning syntax beyond the ON <filegroup | partition scheme>. - It does not preserve: - Existing index names on constraint‑created indexes (they are skipped). - Any index‑specific hints or undocumented options not exposed in sys.indexes. 7. Safety and testing: - Always test output in a non‑production environment first. - In active OLTP systems, CREATE INDEX can block or slow down workloads; consider: - Using ONLINE = ON (if Enterprise Edition). - Creating indexes during maintenance windows. - Monitoring blocking and wait stats. - If you have collation‑specific comparisons elsewhere in your app, verify that the generated script’s collation semantics match. 8. Compatibility: - Uses views introduced in SQL Server 2005 (sys.indexes, sys.index_columns, sys.stats, sys.data_spaces), so it works on 2005+. - ORDER BY CASE i.type_desc LIKE 'CLUSTERED%'... is SQL Server 2005+. - Some WITH options (e.g., ONLINE, DATA_COMPRESSION) may require SQL Server 2008+ or 2012+ depending on index type. - Columnstore indexes require SQL Server 2012+ (Enterprise Edition for some features). 9. Performance: - For very large databases with many tables and indexes, this query can be moderately heavy on catalog metadata; run it during low‑activity windows if that is a concern. ============================================================================ */SETNOCOUNTON;
SETTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;
;WITHIncludedColsAS
(
SELECTic.object_id,
ic.index_id,
IncludedColumns=STUFF(
(
SELECT', '+QUOTENAME(c.name)
FROMsys.index_columnsic1
JOINsys.columnscONc.object_id=ic1.object_id
ANDc.column_id=ic1.column_id
WHEREic1.object_id=ic.object_id
ANDic1.index_id=ic.index_id
ANDic1.is_included_column=1ORDERBYc.column_id
FORXMLPATH(''),TYPE).value('.','nvarchar(max)')
,1,2,'')
FROMsys.index_columnsic
GROUPBYic.object_id,ic.index_id
),
KeyColsAS
(
SELECTic.object_id,
ic.index_id,
KeyColumns=STUFF(
(
SELECT', '+QUOTENAME(c.name)+CASEWHENic1.is_descending_key=1THEN' DESC'ELSE' ASC'ENDFROMsys.index_columnsic1
JOINsys.columnscONc.object_id=ic1.object_id
ANDc.column_id=ic1.column_id
WHEREic1.object_id=ic.object_id
ANDic1.index_id=ic.index_id
ANDic1.is_included_column=0ORDERBYic1.key_ordinal
FORXMLPATH(''),TYPE).value('.','nvarchar(max)')
,1,2,'')
FROMsys.index_columnsic
GROUPBYic.object_id,ic.index_id
)
SELECT'IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'''+QUOTENAME(s.nameCOLLATEDATABASE_DEFAULT)+'.'+QUOTENAME(t.nameCOLLATEDATABASE_DEFAULT)+''') AND name = N'''+i.nameCOLLATEDATABASE_DEFAULT+''')CREATE '+CASEWHENi.is_unique=1THEN'UNIQUE 'ELSE''END+CASEWHENi.type_descIN('CLUSTERED','NONCLUSTERED')
THENi.type_desc+' 'WHENi.type_descLIKE'%COLUMNSTORE%'THENi.type_desc+' 'WHENi.type_descIN('XML','SPATIAL')
THENi.type_desc+' 'ELSE''END+'INDEX '+QUOTENAME(i.nameCOLLATEDATABASE_DEFAULT)+' ON '+QUOTENAME(s.nameCOLLATEDATABASE_DEFAULT)+'.'+QUOTENAME(t.nameCOLLATEDATABASE_DEFAULT)+CASEWHENi.type_descLIKE'%COLUMNSTORE%'THENISNULL(' ('+kc.KeyColumns+')','')
ELSE' ('+kc.KeyColumns+')'END+ISNULL(' INCLUDE ('+ic.IncludedColumns+')','')+ISNULL(' WHERE '+i.filter_definition,'')+' WITH ('+'PAD_INDEX = '+CASEWHENi.is_padded=1THEN'ON'ELSE'OFF'END+', FILLFACTOR = '+CONVERT(varchar(5),CASEWHENi.fill_factor=0THEN100ELSEi.fill_factorEND)+', IGNORE_DUP_KEY = '+CASEWHENi.ignore_dup_key=1THEN'ON'ELSE'OFF'END+', STATISTICS_NORECOMPUTE = '+CASEWHENst.no_recompute=1THEN'ON'ELSE'OFF'END+', ALLOW_ROW_LOCKS = '+CASEWHENi.allow_row_locks=1THEN'ON'ELSE'OFF'END+', ALLOW_PAGE_LOCKS = '+CASEWHENi.allow_page_locks=1THEN'ON'ELSE'OFF'END+')'+' ON ['+ds.name+'];'ASCreateIndexStatement
FROMsys.indexesi
JOINsys.tablestONt.object_id=i.object_id
JOINsys.schemassONs.schema_id=t.schema_id
JOINsys.statsstONst.object_id=i.object_id
ANDst.stats_id=i.index_id
JOINsys.data_spacesdsONds.data_space_id=i.data_space_id
LEFTJOINIncludedColsicONic.object_id=i.object_id
ANDic.index_id=i.index_id
LEFTJOINKeyColskcONkc.object_id=i.object_id
ANDkc.index_id=i.index_id
WHEREi.is_hypothetical=0-- Exclude hypothetical indexes.ANDi.index_id>0-- Exclude heap row locators.ANDi.is_primary_key=0-- Skip primary key indexes.ANDi.is_unique_constraint=0-- Skip unique constraint indexes.ANDt.is_ms_shipped=0-- Skip system tables.ORDERBYs.name,
t.name,
CASEWHENi.type_descLIKE'CLUSTERED%'THEN0ELSE1END,-- Clustered first...i.name;-- then index name.
The script is thoroughly documented, so please review it carefully before running it, even in a test environment.
I welcome your comments, feedback, and suggestions for improvement.