Missing Index Recommendations (optimize-index)

The most complex and powerful tool in the suite. A multi-stage consolidation pipeline that takes SQL Server's raw DMV missing index suggestions — often hundreds of noisy, overlapping, context-free recommendations — and distills them into a smaller set of actionable, non-overlapping index operations with smart size estimation and enterprise-aware script generation.

Why This Tool Exists

SQL Server's sys.dm_db_missing_index_* DMVs are the starting point for most index tuning. But they have significant limitations:

This tool solves all of these. In production testing across 50+ servers with 4,000+ databases, a typical fleet generates ~2,000 raw DMV recommendations that consolidate down to ~1,600 actionable operations — an 18-20% reduction in index count, with each surviving recommendation being higher quality than any individual DMV suggestion.

Pipeline Architecture

The tool runs as a seven-stage pipeline, each stage feeding the next through staging tables in the indexing schema:

sp_CreateIndexOptimizationRecommendations(@DatabaseName, @RunnableUnder)
  |
  Stage 1: sp_LoadDataFromServer
  |   Collects raw DMV missing index data + current index inventory
  |   → indexing.IndexRecommendations, indexing.CurrentIndexes
  |
  Stage 2: sp_ConsolidateRecommendations
  |   Basic consolidation: group by identical key columns, merge includes
  |   → indexing.ConsolidatedRecommendations
  |
  Stage 3: sp_ApplyPrefixConsolidation
  |   Prefix + cross-coverage: merge where keys are leftmost prefixes
  |   → indexing.PrefixConsolidatedGroups
  |
  Stage 4: sp_ApplySuperConsolidation
  |   Super-consolidation: merge groups with same first key + compatible includes
  |   → indexing.SuperConsolidatedGroups
  |
  Stage 5: sp_RemoveRedundantAfterSuperConsolidation
  |   Remove groups made fully redundant by super-consolidated indexes
  |   → indexing.CleanedConsolidatedGroups
  |
  Stage 6: sp_AnalyzeIndexStrategies
  |   Match against existing indexes: CREATE, UPDATE, EXTEND, or REBUILD
  |   → indexing.AnalysisResults
  |
  Stage 7: sp_GenerateOptimizationScript
      Generate ready-to-run SQL with size estimation + enterprise clauses
      → indexing.GeneratedScripts, indexing.MissingIndexResults

Typical reduction: In a real fleet of 50 servers, the pipeline reduced 1,993 raw DMV recommendations to 1,621 final operations — 372 fewer indexes to create (18.7% reduction), with every surviving recommendation being a higher-quality, consolidated index.

StageExample CountReductionWhat Happens
Raw Recommendations1,993—DMV data collected from all databases
Stage 2: Basic Consolidation1,850-143 (7.2%)Group by (server, db, table, keys), merge includes
Stage 3: Prefix Consolidation1,793-57 (3.1%)Detect leftmost prefix relationships, cross-coverage
Stage 4: Super-Consolidation1,654-139 (7.8%)Same first key + compatible includes → single super-index
Stage 5: Redundancy Removal1,621-33 (2.0%)Remove groups fully covered by super-consolidated groups
Final1,621-372 (18.7%)372 fewer indexes to create

Consolidation Deep Dive

Stage 2: Basic Consolidation

Groups recommendations with identical key columns on the same table and merges their include columns. The first recommendation's column order is preserved; additional includes from other recommendations are appended in first-appearance order.

Example: Three raw recommendations for the same table, all with key [status]:

InputKeyIncludesAdvantage
Rec 1[status][date]100
Rec 2[status][user_id]200
Rec 3[status][site_id]300

Output: One consolidated group: [status] INCLUDE [date], [user_id], [site_id] — covers 3 recommendations, combined advantage 600.

Stage 3: Prefix Consolidation

Detects two patterns across groups on the same table:

Pattern A — Leftmost Prefix: If Group A's keys are a leftmost prefix of Group B's keys, Group B already covers Group A's seeks. Merge A's includes into B.

InputKeysIncludes
Group A[status][date]
Group B[status], [site_id][user_id]

[status] is a leftmost prefix of [status], [site_id]. Output: [status], [site_id] INCLUDE [user_id], [date] — one index covers both.

Pattern B — Cross-Coverage: If Group A's include columns appear in Group B's keys (or vice versa), the groups can be merged by promoting shared columns to keys.

InputKeysIncludes
Group A[entity_id][status], [audit_type]
Group B[status][entity_id]

[status] appears in A's includes AND B's keys. Output: [entity_id], [status] INCLUDE [audit_type] — covers both groups.

Stage 4: Super-Consolidation

The most aggressive consolidation stage. Finds groups that share the same first key column with compatible include columns and merges them into a single "super-index."

A size threshold (100K) prevents creating excessively wide indexes. Only groups whose combined key+include width stays under the threshold are merged.

Example: Three groups sharing first key [status]:

InputKeysIncludesRecs Covered
Group A[status], [site_a][date]4
Group B[status], [site_b][date]3
Group C[status], [site_c][date]5

Output: [status], [site_a], [site_b], [site_c] INCLUDE [date] — one super-index covering 12 original recommendations from 3 groups.

Stage 5: Redundancy Removal

After super-consolidation, some original groups may be fully covered by the new super-indexes. This stage detects and removes those redundant groups using the fn_SuperGroupCoversIndividual function, which checks whether a super-group's keys and includes fully satisfy each individual group's needs.

Strategy Analysis

After consolidation, each surviving recommendation is matched against the server's existing indexes to determine the optimal action. The system uses a scoring algorithm to find the best match:

StrategyScoreWhen ChosenAction
EXACT1000Existing index has identical keys + includesREBUILD if disabled; otherwise skip (already covered)
KEY_MATCH / UPDATE900Same keys, different includesReplace index with expanded includes
PREFIX_MATCH / EXTEND900+Recommendation's keys are a leftmost prefix of existingExtend existing index with DROP_EXISTING=ON
SUPERSET_MATCH / EXTEND700+Recommendation's keys are a subset of existing (any position)Extend existing index with additional includes
CREATE0No existing index covers this recommendationCreate a brand new index
CREATE_CONSOLIDATED0New index, but it covers multiple consolidated recommendationsCreate new index (documents how many recs it covers)

Scoring example:

Recommendation needs: [access_category_id] INCLUDE [doc_status]
Existing index:       [info_card_id], [doc_status], [access_category_id]

Strategy: SUPERSET_MATCH (keys are a subset of existing)
  Base score:    700
  Position:      [access_category_id] at position 3 → +10
  Penalty:       3 existing keys × 5 = -15
  Total:         700 + 10 - 15 = 695 → EXTEND existing index

The strategy with the highest score wins. Ties are broken by fewest extra keys (prefer extending indexes that are closest to the recommendation's shape).

Size Estimation

Every CREATE and EXTEND operation includes an estimated index size. The estimation uses a hybrid approach:

Strategy 1: Smart Estimation (similarity-based)

If the table already has indexes with similar structure, the system uses those as a baseline. An exact key match with 3 existing includes that needs 2 more includes will estimate base_size + (2 × per_include_cost), where per_include_cost is derived from the existing index's actual size.

Strategy 2: Calculated Estimation (data sampling, v2.0)

When no similar indexes exist, the system falls back to calculation. Version 2.0 samples actual data using AVG(DATALENGTH(column)) on 10,000 rows per column instead of using max_length guesses. This is dramatically more accurate — the old approach could overestimate by 40x or more on tables with wide NVARCHAR columns that are mostly NULL or short.

Key rule: The estimated size is always ≥ the current index size. Adding columns to an index can never make it smaller. Both paths enforce a floor of current size + 5%.

Estimates are displayed in MB for consistency with the existing index size display.

Script Generation Features

Internal Objects

Tables (9 staging + 1 results)

TablePurpose
IndexRecommendationsRaw DMV missing index suggestions
CurrentIndexesCurrent index inventory from the server
ServerInfoServer edition metadata
ConsolidatedRecommendationsAfter basic consolidation (Stage 2)
PrefixConsolidatedGroupsAfter prefix consolidation (Stage 3)
SuperConsolidatedGroupsAfter super-consolidation (Stage 4)
CleanedConsolidatedGroupsAfter redundancy removal (Stage 5)
AnalysisResultsStrategy analysis output (Stage 6)
GeneratedScriptsFinal generated SQL scripts
MissingIndexResultsPer-finding results with ResolvedDate tracking

Functions (8)

FunctionPurpose
fn_ContainsAllColumnsChecks if column set A contains all columns in set B
fn_AreIncludesCompatibleChecks include column compatibility for merging
fn_CanSuperConsolidateGroupsDetermines if groups can be merged (with 100K threshold)
fn_SuperGroupCoversIndividualChecks if a super-group covers an individual recommendation
fn_KeyIncludeCoversGroupChecks key + include coverage
fn_IsLeftmostPrefixChecks if one key set is a leftmost prefix of another
fn_ContainsAllKeysChecks if all keys are present (any position)
fn_FindRedundantDisabledIndexesIdentifies disabled indexes made redundant by recommendations

Procedures (~13)

ProcedurePipeline Stage
sp_LoadDataFromServerStage 1: Collect DMV data + current indexes
sp_ConsolidateRecommendationsStage 2: Basic consolidation
sp_ApplyPrefixConsolidationStage 3: Prefix + cross-coverage
sp_ApplySuperConsolidationStage 4: Super-consolidation
sp_RemoveRedundantAfterSuperConsolidationStage 5: Redundancy removal
sp_AnalyzeIndexStrategiesStage 6: Strategy analysis
sp_GenerateOptimizationScriptStage 7: Script generation
sp_GetSmartTableSizeEstimateSize estimation (similarity-based)
sp_CalculateIndexSizeEstimateSize estimation (data sampling, v2.0)
sp_GetHybridIndexSizeEstimateSize estimation (orchestrator: smart → calculated)
sp_ExportGeneratedScriptExport scripts from GeneratedScripts table
sp_LoadTestDataFromCSVDevelopment: load test data from CSV files
sp_CreateIndexOptimizationRecommendationsEntry point wrapper (calls full pipeline)

Parameters

sp_CreateIndexOptimizationRecommendations (entry point)

ParameterDefaultDescription
@DatabaseNameNULLNULL = all user databases. Supports comma-delimited list
@MinIndexAdvantage2500.0Minimum DMV index advantage score to consider
@MinAvgUserImpact99.0Minimum average user impact percentage
@RunnableUnderNULLNULL = all uncommented. '2' = 2 GB both editions. '1,5e' = 1 GB Standard / 5 GB Enterprise
@Debug00 = silent. 1 = verbose output with step timing

sp_MissingIndexRecommendations (per-finding view)

ParameterDefaultDescription
@DatabaseNameNULLNULL = all user databases. Supports CSV list and LIKE wildcards
@TableNameNULLFilter to one table. NULL = all
@RunnableUnderNULLNULL = all uncommented. '1,5e' = edition-aware thresholds
@RunAnalysis0When 1, runs Load + Analyze + Generate before output
@MinIndexAdvantage2500.0Passed through to analysis when @RunAnalysis = 1
@MinAvgUserImpact99.0Passed through to analysis when @RunAnalysis = 1

Usage Examples

-- Full analysis, all databases
EXEC indexing.sp_CreateIndexOptimizationRecommendations;

-- Single database with debug output
EXEC indexing.sp_CreateIndexOptimizationRecommendations
    @DatabaseName = 'MyDatabase', @Debug = 1;

-- With edition-aware RunnableUnder threshold
EXEC indexing.sp_CreateIndexOptimizationRecommendations
    @DatabaseName = 'MyApp_%',
    @RunnableUnder = '1,5e';

-- Per-finding view (runs analysis first)
EXEC indexing.sp_MissingIndexRecommendations
    @RunAnalysis = 1,
    @DatabaseName = 'MyApp_%',
    @RunnableUnder = '1,5e';

-- View previously generated scripts
EXEC indexing.sp_ExportGeneratedScript @ServerName = @@SERVERNAME;

-- List all generated scripts
SELECT ScriptID, ServerName, GeneratedDate, LEN(ScriptText) AS SizeChars
FROM indexing.GeneratedScripts
ORDER BY ScriptID DESC;

Real-World Impact

On a fleet of 50 production SQL Servers: