Clustered vs Non-Clustered Indexes
Selection Criteria
Read aloud is unavailable in this browser.
The Art and Science of Index Selection
You've learned what clustered and non-clustered indexes are, how they work internally, and why only one clustered index per table is possible. Now comes the practical challenge every database professional faces: When should you use each type?
Index selection is both art and science. The science involves understanding I/O patterns, analyzing query workloads, and calculating storage overhead. The art involves weighing competing requirements, anticipating future access patterns, and making judgment calls when data is incomplete.
This page synthesizes everything we've learned into actionable selection criteria. We'll examine specific scenarios, provide decision frameworks, and equip you with the knowledge to make confident indexing decisions for any database design challenge.
By the end of this page, you will have clear criteria for when to use clustered vs non-clustered indexes, understand the trade-offs in common scenarios, possess a systematic decision framework for index selection, and be able to evaluate indexing strategies for production database designs.
Core Selection Principles
Before diving into specific scenarios, let's establish the fundamental principles that guide index selection. These principles emerge from the physical characteristics we've studied.
Principle 1: Clustered for Range, Non-Clustered for Point
The clustered index's primary advantage is sequential I/O for range scans. Non-clustered indexes are efficient for point lookups (especially with covering). This leads to a fundamental guideline:
- Use clustered when the most critical queries scan ranges of the key
- Use non-clustered when the most critical queries look up individual rows by various keys
| Query Pattern | Clustered Efficiency | Non-Clustered Efficiency | Recommendation |
|---|---|---|---|
| Single row by key | ★★★★★ | ★★★★☆ (if covering) | Either works well |
| Range scan (1-5%) | ★★★★★ | ★★☆☆☆ (many lookups) | Clustered strongly preferred |
| Range scan (>10%) | ★★★★★ | ★☆☆☆☆ (scan likely) | Clustered or full scan |
| Multiple point lookups | ★★★★☆ | ★★★★★ (index union) | Non-clustered with covering |
| Full table scan | ★★★★☆ (ordered) | N/A | Neither helps; consider partitioning |
| ORDER BY match | ★★★★★ (free sort) | ★★★☆☆ (index scan) | Clustered if dominant pattern |
Principle 2: One Clustered, Many Non-Clustered
You get exactly one clustered index—use it for the most impactful access pattern. Use non-clustered indexes to support additional access patterns:
- Clustered: The 'primary' access pattern deserving physical optimization
- Non-clustered: Secondary access patterns supported by separate index structures
Principle 3: Balance Read and Write
Every index has maintenance cost. The trade-off:
- Read-heavy workloads: More indexes improve query performance
- Write-heavy workloads: Fewer indexes reduce INSERT/UPDATE/DELETE overhead
The clustered index is maintained on every write regardless. Non-clustered indexes add incremental write overhead proportional to their count and width.
For OLTP tables, aim for 5 or fewer indexes total (including clustered). Each additional index beyond 5 should require explicit justification. For OLAP/reporting tables where writes are rare, more indexes are acceptable. For staging/ETL tables, often zero non-clustered indexes is optimal.
Principle 4: Consider Index Width
Both clustered and non-clustered index width matters:
- Clustered key width impacts every non-clustered index (it's the row locator)
- Non-clustered key width impacts the specific index's efficiency
Narrower is almost always better. Wide keys reduce fanout, increase tree height, and consume more storage.
When to Choose a Clustered Index Key
The clustered index key should satisfy as many of these criteria as possible. No key will satisfy all perfectly—prioritize based on your workload.
- •Unique: Avoids uniqueifier overhead; guarantees efficient point lookups. If not naturally unique, consider a composite key or accept the uniqueifier cost.
- •Narrow: 4-8 bytes ideal; minimizes size of non-clustered indexes. Avoid VARCHAR, wide composites, or UUIDs unless necessary.
- •Static: Values rarely or never change. Key updates cause row movement and cascade to all non-clustered indexes.
- •Ever-Increasing: Sequential values (IDENTITY, timestamps) minimize page splits and fragmentation. Random values guarantee fragmentation.
- •Frequently Range-Scanned: The column(s) appearing in BETWEEN, >, <, or bulk reads. This is where clustered indexes provide the most value.
- •Aligned with ORDER BY: If queries frequently sort by this column, clustering eliminates sort operations.
Scoring Matrix Example:
Consider an Orders table with candidate keys:
| Criterion | Weight | OrderID (INT) | OrderDate | CustomerID | (CustomerID, OrderDate) |
|---|---|---|---|---|---|
| Unique | 2 | ✓✓ | ✗ | ✗ | ✓✓ |
| Narrow | 2 | ✓✓ | ✓ | ✓ | ✓ |
| Static | 3 | ✓✓✓ | ✓✓ | ✓ | ✓✓ |
| Ever-increasing | 2 | ✓✓ | ✓✓ | ✗ | ✗ |
| Range-scanned | 3 | ✗ | ✓✓✓ | ✓ | ✓✓ |
| ORDER BY match | 1 | ✗ | ✓ | ✓ | ✓ |
| Total Score | 11 | 12 | 7 | 11 |
In this example, OrderDate clusters well for date-range reporting; (CustomerID, OrderDate) works if customer+date queries dominate; OrderID is the safe default.
Never cluster on: (1) Frequently updated columns. (2) Random values like UUIDv4. (3) Wide TEXT/VARCHAR columns. (4) Low-cardinality columns (Status, Type). (5) Composite keys with 4+ columns. These cause excessive fragmentation, overhead, or poor selectivity.
When to Create Non-Clustered Indexes
Non-clustered indexes should be created strategically to support specific query patterns without creating excessive write overhead.
Create a Non-Clustered Index When:
- •Frequent WHERE Clause Columns: Columns regularly appearing in query filters, especially with high selectivity (< 5% of rows match)
- •JOIN Columns: Foreign keys and columns used in join conditions benefit from index-assisted joins
- •Covering Query Opportunities: When INCLUDE columns can eliminate bookmark lookups for critical queries
- •Unique Constraint Enforcement: UNIQUE constraints automatically create non-clustered indexes
- •Sort Key Different from Clustered: ORDER BY on non-clustered columns can use index if it avoids sort operations
- •Query Performance Bottlenecks: Execution plans showing table scans where seeks would help
Avoid Creating Non-Clustered Indexes When:
- •Low Selectivity: Columns with few distinct values (Status, Boolean flags) rarely benefit from indexes
- •Small Tables: Tables under ~1000 rows are faster to scan than to seek via index
- •Rarely Queried Columns: Don't index columns that appear in infrequent ad-hoc queries
- •High Write Ratio: Tables with 90%+ write operations should minimize indexes
- •Duplicate Indexes: An index on (A, B) makes a separate index on (A) redundant
- •Wide Result Sets: If queries return >10% of table, full scan may be faster than index + lookups
Non-Clustered Index Design Checklist:
| Question | Consideration |
|---|---|
| Which columns to index? | Start with WHERE clause columns in critical queries |
| Column order in composite? | Most selective column first; leftmost prefix rule applies |
| Include INCLUDEd columns? | Add columns to eliminate lookups for specific queries |
| Ascending or descending? | Match ORDER BY; affects backward scans |
| Filtered index? | WHERE clause on index for partial data (e.g., active records only) |
| Unique? | If data should be unique, enforce with UNIQUE for integrity + performance |
Scenario-Based Decision Guide
Let's examine common database scenarios and the recommended indexing strategies for each.
Scenario: High-Volume Transaction Processing
Characteristics:
- Many concurrent INSERT/UPDATE/DELETE operations
- Point lookups by primary key common
- Some filtered queries by business keys
- Write performance critical
Recommended Strategy:
-- Clustered on auto-increment for insert speed
CREATE TABLE Orders (
OrderID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
CustomerID INT NOT NULL,
OrderDate DATETIME NOT NULL,
Status VARCHAR(20) NOT NULL,
TotalAmount DECIMAL(18,2)
);
-- Minimal non-clustered for critical lookups
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders(CustomerID) INCLUDE (OrderDate, TotalAmount);
-- Foreign key index for join performance
CREATE NONCLUSTERED INDEX IX_Orders_Status
ON Orders(Status) WHERE Status = 'Pending';
Rationale:
- Auto-increment clustered minimizes page splits
- Covering index on CustomerID eliminates lookups for dashboard queries
- Filtered index on Status targets common workflow query
Trade-off Analysis Matrix
Every indexing decision involves trade-offs. This matrix helps you systematically evaluate options.
Storage vs Performance Trade-offs:
| Choice | Storage Cost | Read Performance | Write Performance |
|---|---|---|---|
| Heap (no clustered) | Minimal overhead | Poor (no ordering) | Good (no order maintenance) |
| Clustered only | Base storage | Excellent for key ranges | Good (one index maintained) |
| Clustered + 1-2 NC | Moderate (+10-20%) | Very good (multiple paths) | Good (limited overhead) |
| Clustered + 5+ NC | Significant (+50%+) | Excellent (many paths) | Poor (high maintenance) |
| Covering NC indexes | High (duplicate columns) | Excellent (no lookups) | Poor (wide index maintenance) |
| Indexed views | Very high (data copy) | Excellent (dedicated) | Very poor (view maintenance) |
Key Width Trade-offs:
| Key Width | Fanout | Tree Height | NC Index Impact | Best For |
|---|---|---|---|---|
| 4 bytes (INT) | ~500 entries/page | 2-3 levels | Minimal | Most OLTP tables |
| 8 bytes (BIGINT) | ~400 entries/page | 2-3 levels | Low | Large tables, millisecond timestamps |
| 16 bytes (GUID) | ~300 entries/page | 3-4 levels | Moderate | Distributed systems (use UUIDv7) |
| 50+ bytes (composite) | ~100 entries/page | 4-5 levels | High | Only if query patterns demand |
When making indexing decisions, quantify the trade-offs. Calculate expected storage sizes, estimate write overhead percentages, and measure actual query improvements. Decisions based on measurement outperform decisions based on intuition.
Feature vs Maintenance Trade-offs:
| Feature | Benefit | Maintenance Cost |
|---|---|---|
| INCLUDE columns | Eliminates lookups | Updated on any included column change |
| Filtered indexes | Smaller, focused | Requires careful predicate design |
| Computed columns | Index expressions | Column maintenance on source changes |
| Unique constraints | Data integrity | Validation on every insert/update |
| Partitioning | Isolation, aging | Partition management complexity |
Index Lifecycle Management
Indexes aren't 'set and forget.' Effective index management requires ongoing monitoring, evaluation, and adjustment.
The Index Lifecycle:
- •Design Phase: Analyze workload, identify access patterns, design initial indexes based on expected queries
- •Deployment Phase: Create indexes, measure baseline performance, document design rationale
- •Monitoring Phase: Track index usage statistics, fragmentation levels, and query performance
- •Optimization Phase: Add missing indexes, tune existing indexes, remove unused indexes
- •Maintenance Phase: Regular rebuilds/reorganizes, statistics updates, storage management
- •Review Phase: Periodic comprehensive review as workload evolves; repeat cycle
Key Monitoring Metrics:
123456789101112131415161718192021222324252627282930
-- SQL Server: Find unused indexesSELECT i.name AS IndexName, OBJECT_NAME(i.object_id) AS TableName, ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updatesFROM sys.indexes iJOIN sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_idWHERE ius.database_id = DB_ID() AND (ius.user_seeks + ius.user_scans + ius.user_lookups) < ius.user_updatesORDER BY (ius.user_updates - ius.user_seeks - ius.user_scans - ius.user_lookups) DESC; -- SQL Server: Find missing indexesSELECT mig.index_group_handle, mid.statement AS TableName, mid.equality_columns, mid.inequality_columns, mid.included_columns, migs.user_seeks, migs.avg_user_impactFROM sys.dm_db_missing_index_groups migJOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handleJOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handleORDER BY migs.avg_user_impact * migs.user_seeks DESC;If an index has zero seeks and zero scans over several weeks (across all typical workload periods), it's likely a candidate for removal. But verify: some indexes may be used only monthly (e.g., month-end reports). Capture usage over a full business cycle before dropping.
Common Mistakes and How to Avoid Them
Years of database consulting reveal recurring indexing mistakes. Understanding these helps you avoid the same pitfalls.
- •Mistake 1: Indexing Every Column — Creates massive write overhead; most indexes go unused. FIX: Index based on actual queries, not speculation.
- •Mistake 2: UUID/GUID as Clustered Key — Guarantees maximum fragmentation; bloats all non-clustered indexes. FIX: Use IDENTITY or sequential UUIDs (v7).
- •Mistake 3: Ignoring Index Order — Creating index on (A, B) doesn't help WHERE B = ?. FIX: Understand leftmost prefix rule; order matters.
- •Mistake 4: Never Dropping Indexes — Old indexes accumulate, slowing writes. FIX: Regular usage audits; drop unused indexes.
- •Mistake 5: Missing Foreign Key Indexes — JOINs and DELETE cascades become scans. FIX: Always index foreign key columns.
- •Mistake 6: Over-Relying on Missing Index Suggestions — DMV suggestions are greedy; may suggest overlapping indexes. FIX: Consolidate suggestions into optimal covering indexes.
- •Mistake 7: Forgetting Maintenance — Fragmented indexes degrade performance. FIX: Schedule regular REBUILD/REORGANIZE based on fragmentation levels.
- •Mistake 8: Same Indexes in Dev and Prod — Dev has tiny data; indexes seem fine. FIX: Test with production-scale data volumes.
The Consolidation Principle:
Instead of creating separate indexes for each query:
-- DON'T: Separate indexes
CREATE INDEX IX1 ON Orders(CustomerID);
CREATE INDEX IX2 ON Orders(CustomerID, OrderDate);
CREATE INDEX IX3 ON Orders(CustomerID) INCLUDE (TotalAmount);
-- DO: Consolidated covering index
CREATE INDEX IX_Orders_Customer ON Orders(CustomerID, OrderDate)
INCLUDE (TotalAmount, Status);
The consolidated index:
- Supports WHERE CustomerID = ? (leftmost prefix)
- Supports WHERE CustomerID = ? AND OrderDate = ?
- Covers SELECT TotalAmount, Status queries
- One index instead of three
Always test index changes with realistic data volumes. An index that seems to help on 10,000 rows may behave differently on 10 million rows. Use Query Store, execution plan comparisons, and A/B testing to validate improvements before production deployment.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.