SQLServer 索引与 SQL 优化 —— 聚集索引、非聚集索引、覆盖索引、碎片维护与执行计划

11_SQLServer 索引与 SQL 优化 ------ 聚集索引、非聚集索引、覆盖索引、碎片维护与执行计划

系列定位 :《SQL Server 数据库全栈运维实战系列》第 11 篇,与《MySQL 数据库全栈运维实战系列》同构对标(对标 MySQL 11 对应篇目)。适用版本 :SQL Server 2019/2022(2017 及以下差异随文标注)。官方文档:Microsoft Learn SQL Server 2019/2022 文档、索引体系(Indexes)、重新组织与重新生成索引(Reorganize and Rebuild)章节。


一、索引原理全景(官方:Clustered Index Design)

SQL Server 索引和 InnoDB 一样是 B 树结构 ,但它明确区分两种组织方式:聚集索引 决定数据行物理怎么存,非聚集索引 是独立的 B 树挂在数据上。没有聚集索引的表叫堆(Heap),数据行无序存放。

图 1

概念/对象 说明 类比 MySQL
聚集索引 Clustered 叶子节点就是数据行,一张表只能一个 InnoDB 主键聚簇索引
非聚集索引 Nonclustered 独立 B 树,叶子存键值 InnoDB 二级索引
堆 Heap 无聚集索引的表 无聚簇表的堆组织
键查找 Key Lookup 非聚集索引命中后回表取列 InnoDB 回表
覆盖索引 索引含查询所需全部列 覆盖索引
INCLUDE 列 索引叶子附加的非键列 InnoDB 二级索引自动含主键
碎片 页填充不均、逻辑乱序 InnoDB 碎片 / OPTIMIZE

一张表只有一个聚集索引:因为数据行只能按一种顺序物理排列。建表时不指定主键,SQL Server 默认把它建成聚集索引的键(和 InnoDB 一样,主键即聚簇)。


二、聚集索引 vs 非聚集索引 vs 回表

复制代码
-- 解释:建聚集索引(通常就是主键)
CREATE CLUSTERED INDEX CX_order_id ON appsch.t_order(order_id);
GO

-- 解释:建非聚集索引
CREATE NONCLUSTERED INDEX IX_order_cust ON appsch.t_order(customer_id);
GO

图 2

索引类型 叶子节点存什么 回表代价
聚集索引 完整数据行 无需回表
非聚集索引(键查找) 聚集键值 需回表取其余列,贵
覆盖非聚集索引 查询所需全部列 无需回表,最快

回表是性能大敌 :非聚集索引命中后如果还缺列,优化器要做 Key Lookup,行数一大就从 Seek 退化成接近全表扫。解法是用 INCLUDE 把需要的列塞进索引叶子。


三、最左前缀与复合索引(官方:Index Design)

复制代码
-- 解释:复合索引(customer_id, create_time)------最左前缀原则
CREATE NONCLUSTERED INDEX IX_order_cust_time
    ON appsch.t_order(customer_id, create_time);
GO
-- 解释:能命中此索引的 WHERE:
--   WHERE customer_id = 88
--   WHERE customer_id = 88 AND create_time >= '2026-10-01'
-- 不能命中:WHERE create_time >= '2026-10-01'(跳过了最左列 customer_id)
查询条件 复合索引 (a,b,c) 是否命中
WHERE a = 1 命中
WHERE a = 1 AND b = 2 命中
WHERE a = 1 AND b = 2 AND c = 3 命中
WHERE b = 2 不命中(跳过 a)
WHERE a = 1 AND c = 3 只用到 a

索引键长度红线:非聚集索引键总宽 ≤ 1700 字节(旧版 900),聚集索引键 ≤ 16 键列、总宽 128 键值。超了建索引报错,宽列用 INCLUDE 而不是放进键。


四、覆盖索引与 INCLUDE 列

复制代码
-- 解释:把高频查询要返回的列 INCLUDE 进索引,彻底消除键查找
CREATE NONCLUSTERED INDEX IX_order_cust_cover
    ON appsch.t_order(customer_id)
    INCLUDE (create_time, pay_amount, order_status);
-- 解释:现在 SELECT customer_id, create_time, pay_amount, order_status
--       WHERE customer_id=88 走这个索引直接出结果,不用回表
GO
写法 说明 类比 MySQL
列放键里 ON (a,b) 参与排序与定位,最左前缀生效 复合索引列
列放 INCLUDE 只存叶子供返回,不参与定位 InnoDB 二级索引叶子自带主键

设计口诀:高频过滤列放键(享受 Seek 与排序),高频返回列放 INCLUDE(消除回表)。这是覆盖索引的标准做法。


五、执行计划与 SARGable 查询

复制代码
-- 解释:SARGable(可走索引)写法:条件列上不要包函数
-- 好:范围直接比较,能 Seek
WHERE order_status = 1 AND create_time >= '2026-10-01'

-- 坏:列上套函数,索引失效退化为 Scan
WHERE YEAR(create_time) = 2026
WHERE SUBSTRING(order_no,1,4) = 'SO20'
WHERE pay_amount + 100 > 500
GO
反模式 为什么坏 改写
WHERE YEAR(create_time)=2026 列包函数,索引失效 create_time >= '2026-01-01' AND < '2027-01-01'
WHERE col LIKE '%abc' 前导通配,无法定位 全文索引或调整前缀
WHERE col = @p 参数嗅探 偶发计划劣化 加查询提示或查询存储纠正
隐式类型转换 列类型与参数不匹配,失效 显式转类型对齐

六、索引维护与碎片清理(官方:Reorganize and Rebuild)

复制代码
-- 解释:查某表索引碎片率(10%~30% 重组,30% 以上重建)
SELECT OBJECT_NAME(ips.object_id) AS table_name,
       i.name AS index_name,
       ips.avg_fragmentation_in_percent,
       ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID('AppDB'), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 10
ORDER BY avg_fragmentation_in_percent DESC;
GO

-- 解释:碎片 10%~30%:重组(在线,轻量,整理页逻辑顺序)
ALTER INDEX IX_order_cust ON appsch.t_order REORGANIZE;
GO

-- 解释:碎片 30% 以上:重建(重建整个 B 树,2019 企业版 ONLINE)
ALTER INDEX IX_order_cust ON appsch.t_order REBUILD WITH (ONLINE = ON);
GO

-- 解释:更新统计信息(统计信息过期会导致优化器选错计划)
UPDATE STATISTICS appsch.t_order IX_order_cust WITH FULLSCAN;
GO

图 3

操作 碎片区间 是否在线 类比 MySQL
REORGANIZE 10%~30% 总是在线 轻量整理
REBUILD >30% 企业版 ONLINE,标准版离线 OPTIMIZE TABLE
UPDATE STATISTICS 统计过期 在线 ANALYZE TABLE

七、缺失索引与索引使用 DMV

复制代码
-- 解释:缺失索引建议(优化器认为缺的索引,按收益排序)
SELECT TOP 10
       ROUND(s.avg_total_user_cost * s.avg_user_impact * (s.user_seeks + s.user_scans),0) AS 收益,
       mid.equality_columns, mid.inequality_columns, mid.included_columns
FROM sys.dm_db_missing_index_group_stats s
JOIN sys.dm_db_missing_index_groups g ON s.group_handle = g.index_group_handle
JOIN sys.dm_db_missing_index_details mid ON g.index_handle = mid.index_handle
ORDER BY 收益 DESC;
GO

-- 解释:现有索引使用情况(找出从没被用、占空间的索引)
SELECT OBJECT_NAME(i.object_id) AS table_name, i.name AS index_name,
       s.user_seeks, s.user_scans, s.user_updates
FROM sys.indexes i
JOIN sys.dm_db_index_usage_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id
WHERE s.user_seeks = 0 AND s.user_scans = 0
ORDER BY s.user_updates DESC;
-- 解释:user_updates 很高却零 seek 的索引,是纯写负担,考虑删掉
GO
DMV 作用
sys.dm_db_missing_index_details 优化器建议加的索引
sys.dm_db_index_usage_stats 索引实际被用了多少次
sys.dm_db_index_physical_stats 索引碎片与页数

别照单全收缺失索引 :缺失索引 DMV 是建议不是圣旨,要权衡写放大(user_updates)。一条高频写的表,加太多索引反而拖慢写入。


八、列存储索引(2019 数据仓库利器)

复制代码
-- 解释:列存储索引------分析型大表聚合查询提速几十倍(2019 行存批处理)
CREATE CLUSTERED COLUMNSTORE INDEX CCI_order ON appsch.t_order_big;
-- 解释:OLTP 小表别用,列存适合数仓大表 GROUP BY 聚合
GO
索引类型 适用场景 类比 MySQL
行存储 B 树 OLTP 点查、小范围 InnoDB B+Tree
列存储 数仓大表聚合扫描 MySQL 无原生列存
过滤索引 只索引某子集(如未处理单) MySQL 部分索引

九、踩坑记录(实测汇总)

# 坑 现象 解决
1 索引列上套函数 查询全表扫 改写为范围比较,让条件 SARGable
2 只建在最右列的复合索引 跳过最左列不命中 高频过滤列放最左前缀
3 高频返回列没 INCLUDE 键查找回表慢 高频返回列加进 INCLUDE
4 索引键太宽超 1700 字节 建索引报错 宽列移到 INCLUDE
5 从不维护碎片 大表越用越慢 碎片 10%~30% 重组,30%+ 重建
6 统计信息过期 优化器选错计划 UPDATE STATISTICS FULLSCAN
7 盲信缺失索引 DMV 写放大拖慢更新 权衡 user_updates,删零使用索引
8 一张表建了二十个索引 写入巨慢、占空间 合并冗余索引,删零 seek 索引
9 隐式类型转换 索引失效 参数与列类型显式对齐
10 堆表主键没建聚集索引 非聚集索引都存宽 RID 更慢 给高频主键建聚集索引
11 参数嗅探计划劣化 偶发慢查询 靠 Query Store 自动计划校正

十、要点速记

  1. 一张表一个聚集索引,主键默认即聚簇;没建就是堆。
  2. 消除回表靠 INCLUDE:过滤列进键,返回列进叶子。
  3. 最左前缀:复合索引从最左列开始才生效。
  4. 碎片分级维护:10%~30% REORGANIZE,30%+ REBUILD。
  5. SARGable:条件列不包函数、不前置通配,索引才走 Seek。

十一、参考资料(官方正式文档)

主题 官方链接
聚集索引设计 https://learn.microsoft.com/sql/relational-databases/indexes/clustered-index-design-guidelines
非聚集索引设计 https://learn.microsoft.com/sql/relational-databases/indexes/nonclustered-index-design-guidelines
覆盖索引与 INCLUDE https://learn.microsoft.com/sql/relational-databases/indexes/create-indexes-with-included-columns
重组与重建索引 https://learn.microsoft.com/sql/relational-databases/indexes/reorganize-and-rebuild-indexes
索引碎片 https://learn.microsoft.com/sql/relational-databases/system-dynamic-management-views/sys-dm-db-index-physical-stats-transact-sql
统计信息 https://learn.microsoft.com/sql/relational-databases/statistics/statistics
列存储索引 https://learn.microsoft.com/sql/relational-databases/indexes/columnstore-indexes-overview

本文章属于《SQL Server 数据库全栈运维实战系列》性能调优篇。欢迎收藏、交流。

相关推荐
java_logo39 分钟前
Docker 部署 DeepSeek Harness(moelin):轻松搭建带登录的本地 AI Agent 平台
运维·docker·容器·ai agent·deepseek·harness·轩辕镜像
Dawson Zhu2 小时前
【分布式训练④】张量并行与序列并行:把单层参数切碎分到多张卡
人工智能·语言模型·架构·aigc·agi
笃行3502 小时前
【金仓数据库产品体验官】订单增量同步实战:MERGE、OUTPUT 与 IDENTITY 的兼容验证
数据库
码客-ygluu4 小时前
一个通用游戏数据服务器
c++·架构
星恒随风5 小时前
MySQL 从零入门(六):复合查询详解——多表查询、自连接、子查询与 UNION
linux·服务器·数据库·mysql
打工仔折腾 AI6 小时前
Changedetection.io 网页变动监控:Docker 部署、通知配置与公网暴露实践
运维·人工智能·后端·python·docker·容器·性能优化
无奈笑天下6 小时前
银河麒麟上lsblk命令教程详解
linux·运维·服务器·数据库
fengkai45456 小时前
Prometheus+K8s弹性伸缩综合项目
运维·kubernetes·k8s·prometheus·hpa·vpa
炫酷的大高个7 小时前
98% 连跑 20 次只剩 67%,我算了大模型重试账
人工智能·架构·开源