Quick Answer
Missing indexes occur when SQL Server's query optimizer cannot find suitable indexes to support query predicates, joins, or sorting operations, forcing expensive table scans or inefficient index seeks. The Missing Index DMVs capture these opportunities automatically when queries execute. This scenario indicates suboptimal query performance but rarely causes immediate system failure.
Root Cause Analysis
When SQL Server's query optimizer generates execution plans, it evaluates available indexes against query predicates, join conditions, and ORDER BY clauses. If no suitable index exists, the optimizer chooses table scans, clustered index scans, or key lookups. During plan compilation, SQL Server records these missed opportunities in the Missing Index DMVs (sys.dm_db_missing_index_details, sys.dm_db_missing_index_groups, sys.dm_db_missing_index_group_stats).
The query optimizer calculates an estimated improvement cost for each missing index based on the query's execution frequency and resource consumption. This calculation considers CPU cycles, I/O operations, and memory usage that could be reduced with the proposed index. The optimizer only suggests covering indexes when the estimated benefit exceeds the maintenance overhead.
Buffer pool pressure increases when missing indexes force large table scans that flush useful pages from memory. The storage engine performs excessive I/O operations reading unnecessary data pages. Lock escalation becomes more likely during large scan operations, potentially blocking concurrent transactions.
SQL Server 2016 introduced Query Store, which provides additional context about missing index impact by correlating plan performance with index suggestions. SQL Server 2019 added automatic plan correction capabilities that can identify missing index scenarios more accurately.
AutoDBA checks Missing index detection, impact analysis, and automated index recommendation scoring across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.
Diagnostic Queries
-- Identify highest impact missing indexes with execution statistics
SELECT
migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) AS improvement_measure,
'CREATE INDEX IX_' + OBJECT_NAME(mid.object_id) + '_' + REPLACE(REPLACE(REPLACE(ISNULL(mid.equality_columns,''),', ','_'),'[',''),']','') +
CASE WHEN mid.inequality_columns IS NOT NULL THEN '_' + REPLACE(REPLACE(REPLACE(mid.inequality_columns,', ','_'),'[',''),']','') ELSE '' END +
CASE WHEN mid.included_columns IS NOT NULL THEN '_COVERING' ELSE '' END AS index_name,
'CREATE INDEX IX_' + OBJECT_NAME(mid.object_id) + '_' + REPLACE(REPLACE(REPLACE(ISNULL(mid.equality_columns,''),', ','_'),'[',''),']','') +
CASE WHEN mid.inequality_columns IS NOT NULL THEN '_' + REPLACE(REPLACE(REPLACE(mid.inequality_columns,', ','_'),'[',''),']','') ELSE '' END +
CASE WHEN mid.included_columns IS NOT NULL THEN '_COVERING' ELSE '' END +
' ON ' + mid.statement + ' (' + ISNULL(mid.equality_columns,'') +
CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL THEN ',' ELSE '' END +
CASE WHEN mid.inequality_columns IS NOT NULL THEN mid.inequality_columns ELSE '' END + ')' +
CASE WHEN mid.included_columns IS NOT NULL THEN ' INCLUDE (' + mid.included_columns + ')' ELSE '' END AS create_index_statement,
migs.user_seeks,
migs.user_scans,
migs.avg_total_user_cost,
migs.avg_user_impact
FROM sys.dm_db_missing_index_groups mig
INNER JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
WHERE migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) > 10
ORDER BY improvement_measure DESC;
-- Find queries with high logical reads that could benefit from missing indexes
SELECT TOP 20
qs.sql_handle,
qs.plan_handle,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.total_logical_writes / qs.execution_count AS avg_logical_writes,
qs.execution_count,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) + 1) AS statement_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE qs.total_logical_reads / qs.execution_count > 1000
ORDER BY avg_logical_reads DESC;
-- Analyze table scan operations that indicate missing index opportunities
SELECT
p.object_id,
OBJECT_NAME(p.object_id) AS table_name,
ios.leaf_allocation_count,
ios.leaf_page_merge_count,
ios.nonleaf_allocation_count,
ios.range_scan_count,
ios.singleton_lookup_count
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ios
INNER JOIN sys.partitions p ON ios.object_id = p.object_id AND ios.index_id = p.index_id
WHERE ios.range_scan_count > ios.singleton_lookup_count * 5
AND ios.range_scan_count > 100
ORDER BY ios.range_scan_count DESC;
-- Check for clustered index scans on large tables
SELECT
OBJECT_NAME(ios.object_id) AS table_name,
i.name AS index_name,
ios.range_scan_count,
ios.singleton_lookup_count,
p.rows AS table_rows,
ios.range_scan_count * 1.0 / (ios.range_scan_count + ios.singleton_lookup_count) AS scan_ratio
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ios
INNER JOIN sys.indexes i ON ios.object_id = i.object_id AND ios.index_id = i.index_id
INNER JOIN sys.partitions p ON ios.object_id = p.object_id AND ios.index_id = p.index_id
WHERE i.index_id <= 1 -- Clustered index or heap
AND p.rows > 10000
AND ios.range_scan_count > 50
AND ios.range_scan_count > ios.singleton_lookup_count
ORDER BY ios.range_scan_count DESC;
-- Identify specific queries causing missing index suggestions
SELECT TOP 10
migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) AS improvement_measure,
mid.statement,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns,
migs.user_seeks,
migs.avg_user_impact,
migs.last_user_seek
FROM sys.dm_db_missing_index_groups mig
INNER JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
ORDER BY improvement_measure DESC;
Fix Scripts
Create High-Impact Missing Indexes
This script creates indexes for the most beneficial missing index opportunities. Always test index creation during maintenance windows on large tables.
-- Generate and optionally execute missing index creation statements
-- WARNING: Test on development environment first. Large tables may require ONLINE=ON option.
DECLARE @sql NVARCHAR(MAX);
DECLARE @improvement_threshold FLOAT = 1000; -- Adjust based on your workload
DECLARE index_cursor CURSOR FOR
SELECT 'CREATE INDEX IX_' + REPLACE(OBJECT_NAME(mid.object_id), ' ', '_') + '_' +
FORMAT(ROW_NUMBER() OVER (ORDER BY migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) DESC), '000') +
' ON ' + mid.statement + ' (' +
ISNULL(mid.equality_columns, '') +
CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL THEN ', ' ELSE '' END +
ISNULL(mid.inequality_columns, '') + ')' +
CASE WHEN mid.included_columns IS NOT NULL THEN ' INCLUDE (' + mid.included_columns + ')' ELSE '' END +
' WITH (ONLINE = ON, FILLFACTOR = 90);' AS create_statement
FROM sys.dm_db_missing_index_groups mig
INNER JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
WHERE migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) > @improvement_threshold
ORDER BY migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) DESC;
OPEN index_cursor;
FETCH NEXT FROM index_cursor INTO @sql;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT @sql; -- Review before executing
-- EXEC sp_executesql @sql; -- Uncomment to execute
FETCH NEXT FROM index_cursor INTO @sql;
END;
CLOSE index_cursor;
DEALLOCATE index_cursor;
Batch Index Creation for Large Tables
Use this approach for creating indexes on tables larger than 1GB during maintenance windows.
-- Create indexes with progress monitoring for large tables
-- Monitors index creation progress and estimates completion time
DECLARE @object_name NVARCHAR(255) = 'YourLargeTableName';
DECLARE @index_sql NVARCHAR(MAX);
-- Replace with your specific missing index recommendation
SET @index_sql = N'CREATE INDEX IX_LargeTable_CustomIndex ON dbo.' + @object_name + ' (column1, column2) INCLUDE (column3, column4) WITH (ONLINE=ON, MAXDOP=4, SORT_IN_TEMPDB=ON)';
-- Monitor progress during execution
SELECT
command,
percent_complete,
start_time,
status,
DATEADD(ms, estimated_completion_time, GETDATE()) AS estimated_completion,
session_id
FROM sys.dm_exec_requests
WHERE command LIKE 'CREATE INDEX%';
PRINT 'Executing: ' + @index_sql;
EXEC sp_executesql @index_sql;
Clear Missing Index DMV Data
Reset missing index statistics after creating recommended indexes to get fresh recommendations.
-- Clear missing index DMV cache to reset statistics
-- Run this after implementing missing index recommendations
-- This clears all cached missing index data
DBCC FREEPROCCACHE; -- Clears plan cache and missing index stats
-- Or use more targeted approach:
-- ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; -- SQL Server 2016+
Create Filtered Indexes for Selective Queries
When missing index suggestions involve WHERE clauses with selective predicates, create filtered indexes instead of full table indexes.
-- Create filtered indexes for selective queries based on missing index analysis
-- More efficient than full table indexes when queries filter on specific values
-- Example: Create filtered index for active records only
-- Adjust filter predicate based on your missing index analysis
CREATE INDEX IX_Orders_Active_CustomerDate
ON dbo.Orders (customer_id, order_date)
INCLUDE (total_amount, status)
WHERE status = 'Active' AND order_date >= '2023-01-01'
WITH (ONLINE = ON, FILLFACTOR = 95);
-- Monitor filtered index usage
SELECT
OBJECT_NAME(ius.object_id) AS table_name,
i.name AS index_name,
i.filter_definition,
ius.user_seeks,
ius.user_scans,
ius.user_lookups
FROM sys.dm_db_index_usage_stats ius
INNER JOIN sys.indexes i ON ius.object_id = i.object_id AND ius.index_id = i.index_id
WHERE i.has_filter = 1
AND ius.database_id = DB_ID()
ORDER BY ius.user_seeks + ius.user_scans + ius.user_lookups DESC;
AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.
Prevention
Configure missing index monitoring alerts using SQL Server Agent jobs that query the Missing Index DMVs daily. Set thresholds based on improvement_measure calculations to identify indexes with potential impact exceeding 500 cost units. Export recommendations to a tracking table for historical analysis.
Implement automated query performance baselines using Query Store or custom DMV snapshots. Monitor avg_logical_reads per execution and table scan ratios weekly. Query Store automatic tuning in SQL Server 2017+ can identify regression patterns related to missing indexes.
Establish index creation approval workflows that require impact analysis before implementation. Create indexes during maintenance windows using ONLINE=ON for tables exceeding 1GB. Use MAXDOP settings that align with your server's NUMA topology to avoid resource contention during index builds.
Design table schemas with indexing strategies from the initial deployment. Include primary keys, foreign key indexes, and covering indexes for known query patterns. Use filtered indexes for selective WHERE clauses instead of full table indexes when predicates filter more than 90% of rows.
Configure database-scoped configurations like LEGACY_CARDINALITY_ESTIMATION and PARAMETER_SNIFFING based on workload characteristics. SQL Server 2019's adaptive query processing reduces some missing index scenarios through memory grant feedback and interleaved execution improvements.
Monitor index fragmentation levels monthly and rebuild indexes with fragmentation exceeding 30%. Fragmented indexes contribute to missing index false positives when the optimizer chooses table scans over fragmented index seeks. Maintain statistics currency with AUTO_UPDATE_STATISTICS enabled and consider asynchronous updates for write-heavy workloads.
Need hands-on help?
Dealing with persistent missing indexes issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.
Related Pages
Index Fragmentation
Index Maintenance
sys.dm_db_index_operational_stats
Index Analysis
sys.dm_db_index_physical_stats
Index Analysis
sys.dm_db_index_usage_stats
Index Analysis
sys.dm_db_missing_index_details
Index Analysis
sys.dm_db_missing_index_group_stats
Index Analysis