4. 数据库索引类型:聚集、非聚集与覆盖索引

索引这事儿,说起来就三种核心类型。但你别看只有三种,选错了就是天壤之别。

我记得刚入行那会儿,接手了一个审计日志查询慢的问题。业务方天天催,说查某天的日志要等几十秒。我一查执行计划,全表扫描。加了索引,秒级返回。哪有什么神秘招数?就是把索引类型搞对了。

4.1 聚集索引(Clustered Index)

聚集索引,说白了就是物理排序。数据行在磁盘上怎么排,就按这个索引来。你想想看,一个表只能有一个聚集索引,因为物理顺序只能有一种。

在 MySQL InnoDB 里,如果你不指定主键,它也会给你弄个隐藏的聚集索引。但我不建议你依赖这个默认行为。主动设计,才是正路。

核心特征:

  • 数据行物理上按索引键排序
  • 叶子节点就是实际数据行
  • 一个表只有一个聚集索引
  • 查询范围数据时效率极高

我在项目中遇到过一个问题:审计日志表的主键用 UUID。

为什么?因为 UUID 无序,每次插入数据都要在 B+ 树中间找个位置插进去,导致页分裂频繁。性能一塌糊涂。后来我改成自增雪花 ID,写入速度直接翻倍。

说白了,聚集索引的键最好是单调递增。范围查询、排序操作,天生适合聚集索引。

请注意:如果频繁更新聚集索引键所在的列,会导致大量物理数据移动。审计日志通常不更新,这点倒不用担心。但其他业务表就要谨慎了。

4.2 非聚集索引(Non-Clustered Index)

非聚集索引,你可以理解成书的目录。索引结构和数据是分开的。索引页里存的是索引键 + 指向数据行的指针(在 InnoDB 里是主键值)。

一个表最多可以有 999 个非聚集索引(SQL Server 限制)。但别手抖创建太多。索引不是免费的,写操作要维护所有索引。

我见过最典型的问题:有人给审计日志表建了十几个单列索引。每个都只覆盖一列。

查询是快了?不,慢得很。因为查询优化器要判断用哪个索引,还可能做索引合并。而且插入一条日志,十几个索引都得更新一遍。写入性能直接崩了。

我个人的习惯:非聚集索引尽量建复合索引,把等值查询的列放左边,范围查询的列放右边。这样能用上最左前缀原则。

场景 索引设计 说明
查询用户某天的日志 (user_id, event_time) user_id 等值,event_time 范围
查询某段时间的日志 (event_time) 单列范围查询
按操作类型统计 (action_type, event_time) action_type 过滤,event_time 排序

一个小技巧:非聚集索引的列不要太多。3 到 5 列就够了。列多了,索引页会变大,IO 量也跟涨。

4.3 覆盖索引(Covering Index)

覆盖索引,实际上是索引设计的一种优化策略,不是独立的索引类型。

它的原理很简单:索引里包含了查询需要的所有列,不需要回表。你看,非聚集索引查询时,先找到主键值,再回表找数据行。覆盖索引省掉这步回表,性能自然就上来了。

我曾经优化过一个审计日志导出功能。用户要导出某段时间的日志,查询列是 user_id, action_type, event_time, content。

最早的写法:

SELECT user_id, action_type, event_time, content
FROM audit_log
WHERE event_time BETWEEN '2024-01-01' AND '2024-01-31';

这个查询走的是 event_time 上的非聚集索引。找到主键后,还得回表拿 user_id, action_type, content 的数据。每次回表都是一次随机 IO。

于是我改成了覆盖索引:

CREATE INDEX idx_event_time_covering
ON audit_log (event_time)
INCLUDE (user_id, action_type, content);

注意,SQL Server 支持 INCLUDE 语法。MySQL 不支持,得把覆盖的列全放进索引键里:

CREATE INDEX idx_event_time_covering
ON audit_log (event_time, user_id, action_type, content);

改完之后,执行计划显示“Index Only Scan”。没有回表。导出时间从 8 秒降到 0.5 秒。业务方直呼神奇。

覆盖索引的收益:

  • 减少回表带来的随机 IO
  • 降低查询延时,提升吞吐
  • 适用于高频查询的固定列组合
  • 注意索引体积会增加,权衡好

你想想看,是不是这个道理?如果查询经常固定那几列,不如让索引自己把数据都包了。

避坑指南:覆盖索引不是万能的。索引列越多,写入代价越大。而且 B+ 树的层级可能会变高,影响读取性能。

我见过有人把全表二十多列都塞进一个覆盖索引里。结果索引体积比表还大,性能比全表扫描还差。

嗯,适度就好。

索引类型结构对比图 聚集索引 数据行 = 索引叶子节点 根节点:索引键范围 内节点 键 1-100 内节点 键 101-200 内节点 键 201-300 叶子节点 (数据行1-100) 叶子节点 (数据行101-200) 叶子节点 (数据行201-300) 非聚集索引 索引与数据分离 索引根节点 索引内节点 键+指针 索引内节点 键+指针 索引叶子节点 (键+主键值) 索引叶子节点 (键+主键值) 回表取数据 回表取数据 覆盖索引 不需要回表 索引根节点 内节点 键+数据 内节点 键+数据 叶子节点 直接包含所需列 叶子节点 直接包含所需列 说明: 聚集索引:数据行就是叶节点,无需回表。 非聚集索引:叶节点存主键值,需回表取数据。覆盖索引:叶节点直接包含全部查询列,无需回表。

4.4 如何选型:实战决策指南

好了,三种索引都搞清楚了。那在实际项目中怎么选?

我一般按这么几步走:

  1. 看查询模式。先收集业务方最频繁的查询。审计日志通常是按用户查、按时间段查、操作类型筛选。把这些 SQL 列出来。
  2. 看聚簇键。主键选递增的。雪花 ID 或自增 ID 都行。别用 UUID 或随机字符串。
  3. 建非聚集索引。等值查询列放左边,范围查询列放右边。复合索引,别搞单列索引乱炖。
  4. 检查覆盖性。高频查询要不要回表?如果查询列正好被索引覆盖,那就是覆盖索引。收益很大。
  5. 监控和维护。索引碎片、使用率、缺失索引。这些运维工具要跑起来。别建完就跑路。

我个人经验:审计日志场景,最推荐的做法是——主键用递增 ID 做聚集索引,然后建一个覆盖常用查询的复合非聚集索引。简单、高效、好维护。

不信你试试。查一次百万级分页,对比一下带覆盖和不带的性能差。你会回来感谢我的。