Column Access Pattern Analysis

The discovery tool in the suite. While the other tools react to what SQL Server tells you — missing index DMVs, unused indexes, expensive scans — this tool proactively analyzes the workload to find what indexes should exist based on how the data is actually accessed.

Why this matters: SQL Server's missing index DMVs only fire when a single query triggers the optimizer's threshold. A column filtered in 50 different queries — each too small to trigger a DMV suggestion individually — is a stronger index candidate than one appearing in 1 expensive query. This tool finds those hidden patterns by aggregating across the entire plan cache.

How It Differs from Missing Index DMVs

AspectMissing Index DMVsColumn Access Pattern Analysis
ScopePer-query: one suggestion per queryWorkload-level: aggregates across all queries
TriggerOptimizer decides a query would benefitStatistical analysis of actual column usage
Composite indexesSuggests per-query, often overlappingDiscovers columns that frequently appear together across many queries
Low-impact patternsMissed (below optimizer threshold)Found (aggregate impact across 50+ queries can be huge)
Existing index awarenessNoneChecks every column against existing indexes before recommending

Data Flow

sp_ColumnAccessPatternRecommendations(@RunAnalysis = 1)
  |
  +--> sp_ColumnAccessPatternAnalysis
  |      1. Collect top N plans by total reads from plan cache
  |      2. Extract predicate columns from scan/seek ops via XQuery
  |      3. Extract columns from missing index hints (equality/inequality/include)
  |      4. Filter to target databases
  |      5. Aggregate by database + table + column + usage type
  |         - Count distinct queries referencing each column
  |         - Sum total reads/executions
  |      6. Check existing index coverage per column (per-database loop)
  |         - LEADING_KEY, KEY, INCLUDED, or NULL (not indexed)
  |      7. Set suggested action based on coverage gap
  |      -> writes indexing.ColumnAccessPatternResults
  |
  +--> Build consolidated per-table recommendations:
  |      - Aggregate key columns (EQUALITY/INEQUALITY/SEEK/SCAN predicates)
  |      - Aggregate include columns
  |      - Check existing indexes for near-matches (EXTEND vs CREATE)
  |      - Generate CREATE INDEX or DROP_EXISTING for EXTEND
  |      - Estimate size via sp_GetHybridIndexSizeEstimate
  |
  +--> Generate ScriptText output grouped by database and table

Column Classification

The analysis extracts column references from the plan cache XML using XQuery, categorizing each by how the query uses it:

Usage TypeSource in Plan XMLIndex Implication
EQUALITYMissing index hint: ColumnGroup[@Usage="EQUALITY"]Strong key column candidate (leftmost position)
INEQUALITYMissing index hint: ColumnGroup[@Usage="INEQUALITY"]Key column candidate (after equality columns)
SEEK_PREDICATEIndex seek operation: SeekPredicates/SeekKeysAlready used as seek key — confirms importance
SCAN_PREDICATEScan/seek residual predicate: Predicate/ScalarOperatorFiltered during scan — would benefit from being a key
INCLUDEMissing index hint: ColumnGroup[@Usage="INCLUDE"]Include column candidate (output only, not filtered)

Columns used as EQUALITY or SEEK_PREDICATE across many queries become key columns. Columns used as INCLUDE stay as includes. The tool orders keys by frequency — the most commonly filtered column becomes the leading key.

Existing Index Coverage Check

Before recommending anything, each discovered column is checked against the server's existing indexes:

Coverage LevelMeaningAction
LEADING_KEYColumn is the first key in an existing indexNo action needed (well-indexed)
KEYColumn is a non-leading key in an existing indexUsually sufficient, but may benefit from reordering
INCLUDEDColumn is in an INCLUDE listConsider promoting to key if used in WHERE/JOIN
NULLColumn is not in any indexStrong candidate for a new or extended index

By default, columns with LEADING_KEY coverage are hidden from output. Use @ShowCovered = 1 to see the full picture.

Near-Match Detection & Consolidation

When multiple columns on the same table need indexing, the tool consolidates them into a single recommendation rather than suggesting separate indexes for each column. It then checks existing indexes for near-matches:

This prevents index proliferation — instead of 5 new indexes on one table, you might get 1 new index and 2 extensions of existing ones.

Results Table

indexing.ColumnAccessPatternResults — one row per database + table + column + usage type:

ColumnDescription
DatabaseName, SchemaName, TableNameThe table being analyzed
ColumnNameThe column being accessed
UsageTypeEQUALITY, INEQUALITY, SEEK_PREDICATE, SCAN_PREDICATE, INCLUDE
QueryCountDistinct queries referencing this column in this way
TotalExecutionsAggregate execution count across all referencing queries
TotalLogicalReadsAggregate logical reads across all referencing queries
ExistingIndexCoverageLEADING_KEY, KEY, INCLUDED, or NULL
SuggestedActionRecommended action based on coverage gap
EstimatedSizeGBEstimated index size if a new index is recommended
CreateStatementReady-to-run CREATE INDEX statement
ResolvedDateSet when the recommendation is reviewed/applied

Parameters

sp_ColumnAccessPatternRecommendations (reporting)

ParameterDefaultDescription
@RunAnalysis0When 1, runs sp_ColumnAccessPatternAnalysis first to refresh data
@DatabaseNameNULLNULL = all user databases. Supports CSV list and LIKE wildcards
@TableNameNULLFilter to one table. NULL = all
@RunnableUnderNULLNULL = all uncommented. '2' = 2GB both. '1,5e' = 1GB Std / 5GB Ent
@TopN200Max plans to analyze from the plan cache. Higher = more thorough but slower
@MinQueryCount3Minimum distinct queries referencing a column to include it. Filters one-off patterns
@ShowCovered0When 1, includes columns already well-indexed (LEADING_KEY)

sp_ColumnAccessPatternAnalysis (engine)

ParameterDefaultDescription
@DatabaseNameNULLTarget database(s). NULL = all user databases
@TopN200Top plans by total logical reads to analyze
@MinQueryCount3Column must appear in at least this many distinct queries

Script Output Features

Usage Examples

-- Default: analyze top 200 plans, show columns in 3+ queries
EXEC indexing.sp_ColumnAccessPatternRecommendations @RunAnalysis = 1;

-- Broader analysis: top 1000 plans across all databases
EXEC indexing.sp_ColumnAccessPatternRecommendations
    @RunAnalysis = 1,
    @DatabaseName = 'MyApp_%',
    @RunnableUnder = '1,5e',
    @TopN = 1000;

-- Stricter: only columns in 10+ queries (high-confidence patterns)
EXEC indexing.sp_ColumnAccessPatternRecommendations
    @RunAnalysis = 1,
    @MinQueryCount = 10;

-- Show everything including well-indexed columns (audit view)
EXEC indexing.sp_ColumnAccessPatternRecommendations
    @RunAnalysis = 1,
    @ShowCovered = 1;

-- Focus on a specific table
EXEC indexing.sp_ColumnAccessPatternRecommendations
    @TableName = 'audit_trail';

-- View raw analysis data
SELECT DatabaseName, TableName, ColumnName, UsageType,
       QueryCount, TotalLogicalReads, ExistingIndexCoverage
FROM indexing.ColumnAccessPatternResults
WHERE ResolvedDate IS NULL
ORDER BY TotalLogicalReads DESC;