第2章:数据库索引优化实战
各位同学,今天我们来聊聊数据库索引优化。说实话,我在ERP行业摸爬滚打这么多年,见过太多系统因为索引设计不合理而崩溃的案例。有一次,某家制造企业的订单查询页面,每次打开要等30秒——嗯,就是那种让人想砸电脑的体验。后来一查,全表扫描,索引完全没起作用。
所以这一章,我打算把B+树索引原理、复合索引设计、以及EXPLAIN分析执行计划这三个核心点,一次性讲透。你想想看,搞懂了这些,ERP系统里那些高频查询——订单查询、库存查询——基本就能做到毫秒级响应。
2.1 B+树索引原理:为什么它最适合ERP?
先问个问题:为什么MySQL的InnoDB引擎选择B+树,而不是二叉树、哈希表?
我个人习惯从磁盘I/O的角度来理解。ERP系统的数据量动不动就上千万行,内存装不下,大部分数据在磁盘上。磁盘读写一次,大概要10毫秒——这比内存慢了几万倍。所以,减少磁盘I/O次数,就是优化的核心。
B+树有几个关键特性:
- 非叶子节点只存索引,不存数据——这样每个节点能存更多索引,树的高度就低。比如一个3层的B+树,能存上千万条记录。查询任何一条数据,最多3次磁盘I/O。
- 叶子节点形成有序链表——范围查询特别快。ERP里经常要查「本月所有订单」,B+树直接顺着链表扫就行,不用回表。
- 数据只在叶子节点——每次查询的路径长度都一样,性能稳定。不会像二叉搜索树那样,最坏情况退化成链表。
核心结论:B+树用「空间换时间」,通过增加索引存储空间,换来了极致的查询效率。对于ERP这种读写比例大约8:2的系统,B+树是最优解。
我在项目中遇到过一件事:有个同事给订单表建了哈希索引,觉得查询快。结果一做范围查询「订单时间在2024-01-01到2024-01-31」,直接全表扫描。哈希索引只支持等值查询,不支持范围查询——这就是典型的选错数据结构。
2.2 复合索引设计:ERP高频查询的实战案例
复合索引,说白了就是把多个字段拼成一个索引。但怎么拼,顺序怎么排,这里头门道很多。
先看一个ERP里最常见的查询:
-- 查询某个客户在某个时间段的订单
SELECT * FROM orders
WHERE customer_id = 1001
AND order_date BETWEEN '2024-01-01' AND '2024-01-31'
AND status = '已完成';
如果只建单列索引,MySQL只能用到其中一个索引,其他字段还得回表过滤。效率很低。这时候就需要复合索引。
设计复合索引的核心原则:
- 等值条件放前面——比如
customer_id = 1001这种精确匹配的字段,放在最左边。 - 范围条件放后面——比如
order_date BETWEEN这种范围查询,放在等值条件后面。 - 区分度高的字段优先——比如
customer_id能过滤掉90%的数据,就比status更适合放前面。
所以,上面这个查询,我建议的复合索引是:
CREATE INDEX idx_customer_date_status ON orders(customer_id, order_date, status);
你可能会问:为什么 status 放最后?因为 status 的区分度低(只有几个值),放前面会导致索引树的前缀区分度不够,扫描的行数还是很多。
避坑指南:我曾经犯过一个错误——给订单表建了 (status, customer_id, order_date) 的索引。结果查询时,MySQL先按 status 过滤,发现匹配了50%的数据,然后才用 customer_id 过滤。效率还不如全表扫描。后来改成 (customer_id, order_date, status),查询时间从2秒降到了20毫秒。
再来看库存查询的典型场景:
-- 查询某个仓库中,库存数量低于安全库存的商品
SELECT * FROM inventory
WHERE warehouse_id = 10
AND quantity < safety_stock;
这个查询里,warehouse_id 是等值条件,quantity < safety_stock 是范围条件。但注意,safety_stock 是字段,不是常量。所以索引设计要稍微调整:
CREATE INDEX idx_warehouse_qty ON inventory(warehouse_id, quantity);
这样,MySQL可以先通过 warehouse_id 定位到具体仓库,然后按 quantity 顺序扫描,找到所有低于安全库存的记录。因为B+树的叶子节点是有序的,所以 quantity < safety_stock 这个范围查询,只需要扫描一小段数据。
2.3 利用EXPLAIN分析执行计划
索引建得好不好,不能靠猜。得用 EXPLAIN 来看MySQL到底是怎么执行的。
我每次优化SQL,第一步就是跑 EXPLAIN。它就像医生的CT扫描,能告诉你:
- 用了哪个索引?
- 扫描了多少行?
- 有没有回表?
- 有没有文件排序?
来看一个实际例子:
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1001
AND order_date BETWEEN '2024-01-01' AND '2024-01-31';
输出结果的关键列:
| 列名 | 值 | 说明 |
|---|---|---|
| type | ref | 使用了非唯一索引的等值查找,不错 |
| key | idx_customer_date | 实际使用的索引名 |
| rows | 500 | 预估扫描的行数,越小越好 |
| Extra | Using index condition | 使用了索引下推,减少了回表次数 |
重点看 type 列:
const:主键或唯一索引等值查询,最快ref:普通索引等值查询,很快range:范围查询,还行index:扫描了整个索引树,有点慢ALL:全表扫描,最慢——看到这个就要警惕了
注意:如果 Extra 列出现 Using filesort,说明MySQL在内存或磁盘上做了排序。对于ERP里的分页查询(比如 ORDER BY order_date LIMIT 20),如果索引没有包含排序字段,就会触发文件排序。我曾经优化过一个报表查询,加上排序字段到复合索引后,查询时间从8秒降到了0.3秒。
还有一个容易被忽略的点:possible_keys 和 key 不一致时,说明MySQL没有选对索引。这时候可以用 FORCE INDEX 强制指定索引,但更好的做法是调整索引设计,让MySQL自己选对。
2.4 实战总结:索引优化的三个层次
做了这么多年优化,我总结出三个层次:
- 第一层:建对索引——根据查询条件,设计合适的复合索引,遵循等值在前、范围在后、区分度优先的原则。
- 第二层:用对索引——通过EXPLAIN验证,确保查询走了正确的索引,没有全表扫描、没有文件排序。
- 第三层:维护索引——定期检查索引碎片,删除无用索引。我见过一个ERP系统,因为历史原因积累了20多个冗余索引,插入性能下降了30%。
最后说一句:索引不是越多越好。每个索引都会增加写入开销。对于ERP这种高频写入的系统(订单创建、库存更新),索引数量控制在5-8个比较合理。超过10个,就要考虑是否有冗余了。
好了,这一章就到这里。下一章我们聊聊SQL语句本身的优化——有时候,改一行SQL比加三个索引还管用。