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+ 树的层级可能会变高,影响读取性能。
我见过有人把全表二十多列都塞进一个覆盖索引里。结果索引体积比表还大,性能比全表扫描还差。
嗯,适度就好。
4.4 如何选型:实战决策指南
好了,三种索引都搞清楚了。那在实际项目中怎么选?
我一般按这么几步走:
- 看查询模式。先收集业务方最频繁的查询。审计日志通常是按用户查、按时间段查、操作类型筛选。把这些 SQL 列出来。
- 看聚簇键。主键选递增的。雪花 ID 或自增 ID 都行。别用 UUID 或随机字符串。
- 建非聚集索引。等值查询列放左边,范围查询列放右边。复合索引,别搞单列索引乱炖。
- 检查覆盖性。高频查询要不要回表?如果查询列正好被索引覆盖,那就是覆盖索引。收益很大。
- 监控和维护。索引碎片、使用率、缺失索引。这些运维工具要跑起来。别建完就跑路。
我个人经验:审计日志场景,最推荐的做法是——主键用递增 ID 做聚集索引,然后建一个覆盖常用查询的复合非聚集索引。简单、高效、好维护。
不信你试试。查一次百万级分页,对比一下带覆盖和不带的性能差。你会回来感谢我的。