4. 数据库索引优化:索引类型详解(B+树、哈希索引),如何为查询创建高效索引
索引这东西,说白了就是数据库的「目录」。没有索引,查询就是全表扫描——数据量小还好,一旦上了百万级,响应时间直接崩。我在项目中见过太多因为索引没建好导致的慢查询,有的甚至把数据库CPU打满。
这一节,我们重点聊两种最常见的索引类型:B+树索引和哈希索引。搞懂它们,你就能为查询选对索引。
4.1 B+树索引:最通用的索引结构
B+树是MySQL InnoDB的默认索引结构。它长什么样?你可以想象成一个倒过来的树,叶子节点存数据,非叶子节点只存「路标」。
为什么B+树这么流行?因为它支持范围查询。比如 WHERE age > 20 AND age < 30,B+树可以沿着叶子节点的链表顺序扫描,效率极高。
核心特点:
- 所有数据都在叶子节点,非叶子节点只存索引键
- 叶子节点之间通过双向链表连接,支持范围扫描
- 树的高度通常为2-4层,查询次数稳定
- 支持排序(ORDER BY)和分组(GROUP BY)
我个人习惯,只要查询涉及范围、排序、模糊匹配(前缀),优先选B+树索引。它几乎能覆盖80%的业务场景。
4.2 哈希索引:精确匹配的利器
哈希索引的原理很简单:对索引列计算哈希值,然后直接定位到数据行。你想想看,这就像查字典时直接翻到拼音索引,而不是一页页翻。
但哈希索引有个致命短板——不支持范围查询。为什么?因为哈希值是无序的,age > 20这种条件没法用哈希索引加速。
| 对比维度 | B+树索引 | 哈希索引 |
|---|---|---|
| 查询类型 | 等值、范围、排序、模糊匹配 | 仅等值匹配 |
| 查询速度 | 稳定(O(log n)) | 极快(O(1),无冲突时) |
| 存储引擎 | InnoDB、MyISAM等 | Memory、NDB(InnoDB自适应哈希) |
| 排序支持 | 支持 | 不支持 |
| 冲突处理 | 无冲突 | 链地址法,冲突多时退化为链表 |
嗯,这里要注意:InnoDB内部有一个自适应哈希索引,它会自动为频繁访问的热点数据建立哈希索引。但这是引擎自己决定的,我们无法手动创建。所以日常开发中,我们主要打交道的还是B+树。
4.3 如何为查询创建高效索引
光知道类型还不够,关键是怎么用。我总结了几个实战原则,都是踩过坑之后才明白的。
4.3.1 最左前缀原则
联合索引(多列索引)遵循最左前缀原则。比如你建了一个索引 (a, b, c),那么查询条件必须从a开始才能用到索引。
-- 能用到索引
SELECT * FROM t WHERE a = 1;
SELECT * FROM t WHERE a = 1 AND b = 2;
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
-- 用不到索引(跳过了a)
SELECT * FROM t WHERE b = 2;
SELECT * FROM t WHERE c = 3;
我曾经接手过一个慢查询,开发建了 (status, created_at) 索引,但查询条件是 WHERE created_at > '2024-01-01'——跳过了status,索引完全没用上。全表扫描跑了3秒多。
4.3.2 区分度高的列放前面
联合索引中,把区分度(cardinality)高的列放在前面。区分度越高,索引过滤掉的数据越多。
小技巧:可以用 SELECT COUNT(DISTINCT col) / COUNT(*) FROM t 计算区分度。比值越接近1,区分度越高。
举个例子:(gender, user_id) 和 (user_id, gender) 哪个好?gender只有男女两种值,区分度极低;user_id几乎唯一。所以应该把user_id放前面。
4.3.3 覆盖索引:避免回表
如果索引中已经包含了查询需要的所有列,MySQL就不需要回表查数据行。这叫覆盖索引,性能提升非常明显。
-- 假设有索引 (user_id, name)
-- 这个查询只需要 user_id 和 name,索引里都有,直接返回
SELECT user_id, name FROM t WHERE user_id = 100;
我在优化一个报表查询时,把 SELECT * 改成了只查索引包含的列,查询时间从800ms降到了50ms。你想想看,少了多少次磁盘IO?
4.3.4 避免在索引列上做函数操作
这个坑我踩过好几次。在索引列上使用函数,会让索引失效。
-- 索引失效
SELECT * FROM t WHERE DATE(created_at) = '2024-01-01';
-- 正确写法(范围查询)
SELECT * FROM t WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
避坑指南:我曾经在线上看到一条慢SQL,开发写了 WHERE LEFT(phone, 3) = '138',导致全表扫描。改成 WHERE phone LIKE '138%' 后,索引生效,查询从2秒降到10毫秒。
4.4 索引选择的核心逻辑
下面这张图,是我做索引设计时脑子里过的流程。你可以参考一下。
4.5 实战:一个慢查询的索引优化案例
最后分享一个真实案例。某订单查询接口,响应时间一直在1.5秒以上。
-- 原始查询
SELECT * FROM orders
WHERE status = 'PAID'
AND created_at > '2024-06-01'
AND created_at < '2024-07-01'
ORDER BY created_at DESC
LIMIT 20;
原索引是 (created_at)。问题在哪?status没有走索引,先按时间范围扫出大量数据,再过滤status。
优化方案:
- 新建联合索引
(status, created_at) status区分度不高,但作为等值条件放前面,能快速定位到目标数据created_at放后面,支持范围查询和排序
优化后,查询时间降到了30毫秒。效果立竿见影。
我的建议:每次建索引前,先问自己三个问题:
- 查询条件是等值还是范围?
- 哪些列需要排序或分组?
- 能不能用覆盖索引避免回表?
想清楚这三点,索引设计基本不会跑偏。