index-strategies

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Index Strategies

索引策略

Comprehensive guide to SQL Server index design and optimization.
SQL Server索引设计与优化综合指南。

Quick Reference

快速参考

Index Types

索引类型

TypeDescriptionBest For
ClusteredTable data orderPrimary access path, range scans
NonclusteredSeparate structureSpecific query patterns
ColumnstoreColumn-based storageAnalytics, aggregations
FilteredPartial indexWell-known subsets
CoveringAll columns neededAvoiding key lookups
类型描述最佳适用场景
Clustered表数据按该索引顺序存储主要访问路径、范围扫描
Nonclustered独立于表的索引结构特定查询模式
Columnstore基于列的存储结构分析查询、聚合计算
Filtered仅包含表中部分数据的索引针对已知数据子集的查询
Covering包含查询所需全部列的索引避免键查找

Clustered Index Guidelines

Clustered Index 设计准则

Ideal Clustered Key:
  • Narrow (small data type)
  • Unique or mostly unique
  • Ever-increasing (identity, sequential GUID)
  • Static (rarely updated)
sql
-- Good: Identity column
CREATE CLUSTERED INDEX CIX_Orders ON Orders(OrderID);

-- Good: Sequential GUID
CREATE TABLE Orders (
    OrderID UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY CLUSTERED
);

-- Avoid: Wide composite keys, frequently updated columns, GUIDs (NEWID)
理想的Clustered Key:
  • 窄键(数据类型占用空间小)
  • 唯一或基本唯一
  • 持续递增(比如identity列、顺序GUID)
  • 静态(极少更新)
sql
-- Good: Identity column
CREATE CLUSTERED INDEX CIX_Orders ON Orders(OrderID);

-- Good: Sequential GUID
CREATE TABLE Orders (
    OrderID UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY CLUSTERED
);

-- Avoid: Wide composite keys, frequently updated columns, GUIDs (NEWID)

Nonclustered Index Design

Nonclustered Index 设计

sql
-- Basic index
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders(CustomerID);

-- Covering index (avoids key lookup)
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_Cover
ON Orders(CustomerID)
INCLUDE (OrderDate, TotalAmount, Status);

-- Filtered index (partial)
CREATE NONCLUSTERED INDEX IX_Orders_Active
ON Orders(CustomerID, OrderDate)
WHERE Status = 'Active';

-- Descending order
CREATE NONCLUSTERED INDEX IX_Orders_DateDesc
ON Orders(OrderDate DESC, OrderID DESC);
sql
-- Basic index
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders(CustomerID);

-- Covering index (avoids key lookup)
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_Cover
ON Orders(CustomerID)
INCLUDE (OrderDate, TotalAmount, Status);

-- Filtered index (partial)
CREATE NONCLUSTERED INDEX IX_Orders_Active
ON Orders(CustomerID, OrderDate)
WHERE Status = 'Active';

-- Descending order
CREATE NONCLUSTERED INDEX IX_Orders_DateDesc
ON Orders(OrderDate DESC, OrderID DESC);

Index Selection Guide

索引选择指南

By Query Pattern

按查询模式选择

PatternRecommended Index
WHERE Col = value
Nonclustered on Col
WHERE Col = v1 AND Col2 = v2
Nonclustered on (Col, Col2)
WHERE Col = v ORDER BY Col2
Nonclustered on (Col, Col2)
WHERE Col BETWEEN x AND y
Col as leftmost key
SELECT * WHERE Col = v
Clustered or covering NC
Large aggregationsColumnstore
Specific subset queriesFiltered index
查询模式推荐索引
WHERE Col = value
Nonclustered Index(基于Col列)
WHERE Col = v1 AND Col2 = v2
Nonclustered Index(基于Col、Col2列)
WHERE Col = v ORDER BY Col2
Nonclustered Index(基于Col、Col2列)
WHERE Col BETWEEN x AND y
将Col作为索引最左键
SELECT * WHERE Col = v
Clustered Index 或包含所有列的Nonclustered Index
大型聚合查询Columnstore Index
针对特定子集的查询Filtered Index

Column Order in Composite Keys

复合索引中的列顺序

sql
-- Order matters! Left-to-right matching
CREATE INDEX IX_Example ON Table(A, B, C);

-- These queries CAN use the index:
WHERE A = 1
WHERE A = 1 AND B = 2
WHERE A = 1 AND B = 2 AND C = 3
WHERE A = 1 AND B > 5 ORDER BY B

-- These queries CANNOT use index seek:
WHERE B = 2                    -- A not specified
WHERE B = 2 AND C = 3          -- A not specified
WHERE A = 1 AND C = 3          -- B skipped (partial match only)
sql
-- 顺序很重要!从左到右匹配查询条件
CREATE INDEX IX_Example ON Table(A, B, C);

-- 以下查询可以使用该索引:
WHERE A = 1
WHERE A = 1 AND B = 2
WHERE A = 1 AND B = 2 AND C = 3
WHERE A = 1 AND B > 5 ORDER BY B

-- 以下查询无法使用索引查找:
WHERE B = 2                    -- 未指定A列
WHERE B = 2 AND C = 3          -- 未指定A列
WHERE A = 1 AND C = 3          -- 跳过了B列(仅部分匹配)

Columnstore Indexes

Columnstore Indexes

Clustered Columnstore

Clustered Columnstore

sql
-- Best for data warehousing
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales
ON FactSales;

-- Ordered columnstore (SQL 2022+)
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales
ON FactSales
ORDER (DateKey, ProductKey);
sql
-- 最适用于数据仓库场景
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales
ON FactSales;

-- 有序列存储索引(SQL 2022+)
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales
ON FactSales
ORDER (DateKey, ProductKey);

Nonclustered Columnstore

Nonclustered Columnstore

sql
-- Hybrid OLTP/OLAP
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Orders_Analysis
ON Orders(OrderDate, ProductID, Quantity, Amount)
WHERE Status = 'Completed';
sql
-- 混合OLTP/OLAP场景适用
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Orders_Analysis
ON Orders(OrderDate, ProductID, Quantity, Amount)
WHERE Status = 'Completed';

Columnstore Best Practices

Columnstore Index 最佳实践

  1. Load batches >= 102,400 rows - Creates compressed segments
  2. Order data by filtered columns - Better segment elimination
  3. Use REORGANIZE, not REBUILD - More efficient maintenance
  4. Avoid frequent small updates - Causes deltastore fragmentation
  5. Partition by date - Enables partition elimination
sql
-- Maintenance
ALTER INDEX CCI_FactSales ON FactSales REORGANIZE;

-- Check fragmentation
SELECT
    object_name(object_id) AS TableName,
    index_id,
    avg_fragmentation_in_percent,
    fragment_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED');
  1. 批量加载≥102400行 - 可创建压缩段
  2. 按筛选列排序数据 - 提升段消除效率
  3. 使用REORGANIZE而非REBUILD - 维护更高效
  4. 避免频繁的小批量更新 - 会导致增量存储碎片
  5. 按日期分区 - 支持分区消除
sql
-- 维护操作
ALTER INDEX CCI_FactSales ON FactSales REORGANIZE;

-- 检查碎片情况
SELECT
    object_name(object_id) AS TableName,
    index_id,
    avg_fragmentation_in_percent,
    fragment_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED');

Filtered Indexes

Filtered Indexes

sql
-- Index active orders only
CREATE NONCLUSTERED INDEX IX_Orders_Active
ON Orders(CustomerID, OrderDate)
WHERE Status = 'Active';

-- Index non-NULL values
CREATE UNIQUE INDEX IX_Users_Email
ON Users(Email)
WHERE Email IS NOT NULL;

-- Constraints:
-- - Cannot use variable in filter
-- - Query WHERE must match or be subset of filter WHERE
-- - May cause parameter sniffing issues
sql
-- 仅为活跃订单创建索引
CREATE NONCLUSTERED INDEX IX_Orders_Active
ON Orders(CustomerID, OrderDate)
WHERE Status = 'Active';

-- 仅为非NULL值创建唯一索引
CREATE UNIQUE INDEX IX_Users_Email
ON Users(Email)
WHERE Email IS NOT NULL;

-- 约束条件:
-- - 筛选条件中不能使用变量
-- - 查询的WHERE子句必须与筛选条件匹配或为其子集
-- - 可能会导致参数嗅探问题

Covering Indexes

Covering Indexes

sql
-- Eliminate key lookups
-- Original: Index on CustomerID, query selects OrderDate, Amount
-- Execution plan shows Key Lookup

-- Solution: Covering index
CREATE INDEX IX_Orders_CustomerID_Cover
ON Orders(CustomerID)
INCLUDE (OrderDate, Amount, Status);

-- INCLUDE columns:
-- - Not in key (not sorted)
-- - Stored at leaf level only
-- - Don't contribute to 900-byte key limit
-- - Perfect for frequently selected columns
sql
-- 消除键查找
-- 原场景:基于CustomerID列的索引,查询需要返回OrderDate、Amount列
-- 执行计划中会显示Key Lookup(键查找)

-- 解决方案:使用Covering Index
CREATE INDEX IX_Orders_CustomerID_Cover
ON Orders(CustomerID)
INCLUDE (OrderDate, Amount, Status);

-- INCLUDE列特性:
-- - 不属于索引键(不参与排序)
-- - 仅存储在索引叶节点
-- - 不受900字节的索引键长度限制
-- - 非常适合频繁被查询的列

Index Maintenance

索引维护

Fragmentation Guidelines

碎片处理准则

Fragmentation %Action
< 5%None needed
5-30%REORGANIZE
> 30%REBUILD
sql
-- Reorganize (online, minimal locking)
ALTER INDEX IX_Orders_CustomerID ON Orders REORGANIZE;

-- Rebuild (offline by default, more thorough)
ALTER INDEX IX_Orders_CustomerID ON Orders REBUILD;

-- Online rebuild (Enterprise Edition)
ALTER INDEX IX_Orders_CustomerID ON Orders
REBUILD WITH (ONLINE = ON);

-- Resumable rebuild (SQL 2017+)
ALTER INDEX IX_Orders_CustomerID ON Orders
REBUILD WITH (ONLINE = ON, RESUMABLE = ON, MAX_DURATION = 60);

-- Resume interrupted rebuild
ALTER INDEX IX_Orders_CustomerID ON Orders RESUME;
碎片占比操作建议
< 5%无需处理
5-30%执行REORGANIZE
> 30%执行REBUILD
sql
-- 重新组织(在线操作,锁占用少)
ALTER INDEX IX_Orders_CustomerID ON Orders REORGANIZE;

-- 重建(默认离线操作,清理更彻底)
ALTER INDEX IX_Orders_CustomerID ON Orders REBUILD;

-- 在线重建(仅企业版支持)
ALTER INDEX IX_Orders_CustomerID ON Orders
REBUILD WITH (ONLINE = ON);

-- 恢复中断的重建操作(SQL 2017+)
ALTER INDEX IX_Orders_CustomerID ON Orders RESUME;

Statistics Update

统计信息更新

sql
-- Update after index changes
UPDATE STATISTICS Orders;

-- Full scan for accurate stats
UPDATE STATISTICS Orders WITH FULLSCAN;

-- Check last update
SELECT
    OBJECT_NAME(object_id) AS TableName,
    name AS StatsName,
    STATS_DATE(object_id, stats_id) AS LastUpdated
FROM sys.stats
WHERE object_id = OBJECT_ID('Orders');
sql
-- 索引变更后更新统计信息
UPDATE STATISTICS Orders;

-- 全扫描以获取准确的统计信息
UPDATE STATISTICS Orders WITH FULLSCAN;

-- 检查统计信息最后更新时间
SELECT
    OBJECT_NAME(object_id) AS TableName,
    name AS StatsName,
    STATS_DATE(object_id, stats_id) AS LastUpdated
FROM sys.stats
WHERE OBJECTPROPERTY(object_id, 'IsUserTable') = 1;

Performance Monitoring

性能监控

Index Usage Stats

索引使用统计

sql
SELECT
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    ius.user_seeks,
    ius.user_scans,
    ius.user_lookups,
    ius.user_updates
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
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY ius.user_seeks + ius.user_scans DESC;
sql
SELECT
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    ius.user_seeks,
    ius.user_scans,
    ius.user_lookups,
    ius.user_updates
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
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY ius.user_seeks + ius.user_scans DESC;

Missing Index Recommendations

缺失索引建议

sql
SELECT
    migs.avg_user_impact AS ImpactPercent,
    mid.statement AS TableName,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns
FROM sys.dm_db_missing_index_groups mig
JOIN sys.dm_db_missing_index_group_stats migs
    ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details mid
    ON mig.index_handle = mid.index_handle
ORDER BY migs.avg_user_impact DESC;
sql
SELECT
    migs.avg_user_impact AS ImpactPercent,
    mid.statement AS TableName,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns
FROM sys.dm_db_missing_index_groups mig
JOIN sys.dm_db_missing_index_group_stats migs
    ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details mid
    ON mig.index_handle = mid.index_handle
ORDER BY migs.avg_user_impact DESC;