.claude/skills/index-design-strategy/SKILL.md
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
npx skillsauth add klod68/littlerae index-design-strategyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The KB's only indexing guidance is "add indexes for every foreign key column" — a single bullet in ef-core.md. This is dangerously incomplete. Missing indexes cause table scans on million-row tables; redundant indexes waste storage and slow writes; wrong clustered key choices fragment the entire table. Indexing is the single highest-impact performance lever in any SQL Server database.
The clustered index determines the physical order of data on disk. Choose carefully — you get exactly one per table.
The NUSE rule — a good clustered key is:
Decision matrix:
| Key Type | Narrow | Unique | Static | Ever-increasing | Verdict |
|---|---|---|---|---|---|
| INT IDENTITY | ✅ 4 bytes | ✅ | ✅ | ✅ | Best default |
| BIGINT IDENTITY | ✅ 8 bytes | ✅ | ✅ | ✅ | Good for >2B rows |
| UNIQUEIDENTIFIER (NEWID()) | ❌ 16 bytes | ✅ | ✅ | ❌ Random | Worst — page splits, fragmentation |
| UNIQUEIDENTIFIER (NEWSEQUENTIALID()) | ❌ 16 bytes | ✅ | ✅ | ✅ | Acceptable if GUID required |
| VARCHAR(50) natural key | ❌ Variable | Maybe | ❌ May change | ❌ Random | Avoid — use as non-clustered UNIQUE |
| Composite (A, B, C) | ❌ Wide | Depends | Depends | ❌ Usually not | Rarely appropriate for clustered |
Show a ❌ bad vs ✅ good example:
-- ❌ Bad: GUID as clustered key — 16 bytes, random, causes page splits
CREATE TABLE dbo.OrderGuidBad (
OrderId UNIQUEIDENTIFIER PRIMARY KEY CLUSTERED DEFAULT NEWID(),
Title NVARCHAR(200)
);
-- ✅ Good: INT IDENTITY clustered + GUID as non-clustered unique
CREATE TABLE dbo.OrderGood (
OrderId INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
PublicId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
Title NVARCHAR(200),
INDEX IX_Order_PublicId UNIQUE NONCLUSTERED (PublicId)
);
A covering index contains ALL columns needed by a query — the optimizer never touches the base table (no key lookup).
-- Query: frequently executed
SELECT OrderId, Title, Status FROM dbo.[Order] WHERE CustomerId = @CustomerId AND Status = 'Active';
-- ❌ Non-covering: index on CustomerId alone → key lookup for Title and Status
CREATE NONCLUSTERED INDEX IX_Order_CustomerId ON dbo.[Order] (CustomerId);
-- ✅ Covering: INCLUDE columns satisfy the SELECT list without key lookup
CREATE NONCLUSTERED INDEX IX_Order_CustomerId_Status ON dbo.[Order] (CustomerId, Status)
INCLUDE (Title);
Column placement rules:
Cover only a subset of rows — smaller, faster to maintain, ideal for high-selectivity queries.
-- Only 5% of orders are Active, but 90% of queries filter on Active
CREATE NONCLUSTERED INDEX IX_Order_Active
ON dbo.[Order] (CustomerId, CreatedAt)
INCLUDE (Title, Status)
WHERE Status = 'Active';
Requirements and limitations:
OPTION (RECOMPILE) or literal valuesColumn order in composite indexes matters enormously.
The rule: Most selective column first → range/inequality column last → included columns in INCLUDE.
-- Query: WHERE Status = 'Active' AND CreatedAt >= @StartDate ORDER BY CreatedAt
-- Status has 5 distinct values (high selectivity for equality)
-- CreatedAt is a range predicate
-- ✅ Correct order: equality predicate first, range predicate last
CREATE NONCLUSTERED INDEX IX_Order_Status_CreatedAt
ON dbo.[Order] (Status, CreatedAt)
INCLUDE (Title, CustomerId);
-- ❌ Wrong order: range predicate first limits usefulness for Status filter
CREATE NONCLUSTERED INDEX IX_Order_CreatedAt_Status
ON dbo.[Order] (CreatedAt, Status)
INCLUDE (Title, CustomerId);
Provide these essential DMV queries:
Missing indexes (what SQL Server wishes it had):
SELECT
OBJECT_NAME(mid.object_id) AS TableName,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns,
migs.avg_user_impact,
migs.user_seeks,
migs.last_user_seek
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id = DB_ID()
ORDER BY migs.avg_user_impact * migs.user_seeks DESC;
Unused indexes (wasting write overhead):
SELECT
OBJECT_NAME(i.object_id) AS TableName,
i.name AS IndexName,
i.type_desc,
ius.user_seeks,
ius.user_scans,
ius.user_lookups,
ius.user_updates -- writes to maintain this index
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats ius
ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = DB_ID()
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
AND i.type_desc = 'NONCLUSTERED'
AND ISNULL(ius.user_seeks, 0) + ISNULL(ius.user_scans, 0) + ISNULL(ius.user_lookups, 0) = 0
ORDER BY ius.user_updates DESC;
Duplicate / overlapping indexes:
;WITH IndexColumns AS (
SELECT
i.object_id, i.index_id, i.name,
STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) AS KeyColumns
FROM sys.indexes i
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE ic.is_included_column = 0
GROUP BY i.object_id, i.index_id, i.name
)
SELECT
OBJECT_NAME(a.object_id) AS TableName,
a.name AS Index1, a.KeyColumns,
b.name AS Index2, b.KeyColumns AS OverlappingKeys
FROM IndexColumns a
JOIN IndexColumns b ON a.object_id = b.object_id AND a.index_id < b.index_id
WHERE a.KeyColumns = b.KeyColumns OR b.KeyColumns LIKE a.KeyColumns + ',%';
| Fragmentation % | Action | Command |
|---|---|---|
| < 5% | Do nothing | — |
| 5–30% | Reorganize (online, lightweight) | ALTER INDEX IX_Name ON dbo.Table REORGANIZE; |
| > 30% | Rebuild (heavier, can be online in Enterprise) | ALTER INDEX IX_Name ON dbo.Table REBUILD WITH (ONLINE = ON); |
UPDATE STATISTICS dbo.Table;sys.dm_db_index_physical_stats (use LIMITED mode for quick checks)| Anti-Pattern | Impact | Fix |
|---|---|---|
| GUID (NEWID()) as clustered key | Massive page splits, fragmentation, wide key copied to every NC index | Use INT IDENTITY; GUID as NC unique |
| Indexing every column individually | Write amplification, wasted storage, optimizer confusion | Design indexes for query patterns, not columns |
| Wide clustered key (>16 bytes) | Every non-clustered index carries a copy of the clustered key | Keep clustered key narrow (4–8 bytes) |
| Missing INCLUDE columns | Key lookups on every row — negates index benefit | Add SELECT-list columns as INCLUDE |
| Overlapping/duplicate indexes | Double write cost, wasted storage, no benefit | Audit with DMV query above; consolidate |
| Never rebuilding or reorganizing | Fragmentation degrades read performance over time | Schedule threshold-based maintenance |
| Filtered index on parameterized query | Optimizer cannot match parameter to filter predicate | Use OPTION (RECOMPILE) or literal values |
| Index on low-selectivity column alone | Full scan is faster than bookmark lookups on most rows | Combine with selective column or use filtered index |
sys.dm_db_missing_index_* views are cleared when SQL Server restarts or the plan cache is cleared. Collect data over weeks, not hours.REORGANIZE (always online).UPDATE STATISTICS manually after bulk inserts to prevent plan regressions.tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.
tools
Use when adding database persistence to a Truenorth.Components.* library, migrating legacy persistence code to the standard pattern, or adding new entities to an existing component following the stored-procedure-only architecture with EXECUTE-only permissions. Covers the 8-artifact pattern: embedded settings, language constants, static repository, component factory, DbMappings, Insert/Update/Delete commands, SelectAll/SelectById/Exists queries, and a CrudPersistenceHelper-based persistence service with IList/IListCollectable/ITableCollectable output variants. Domain: Data Access / Persistence. Level: Advanced. Tags: persistence, stored-procedure, CRUD, ADO.NET, provider-agnostic.