从事数据库开发十年来,我处理过上百个MySQL性能瓶颈案例,其中80%的问题通过合理的索引优化都能获得显著改善。索引就像图书馆的目录系统——没有它,我们只能全表扫描逐个查找;有了它,查询效率能提升几个数量级。
MySQL默认使用B+树索引结构,这是理解所有优化策略的基础。与普通二叉树不同,B+树具有以下关键特性:
我曾通过EXPLAIN分析一个商品表的查询:
sql复制SELECT * FROM products WHERE category_id = 5 AND price > 100 ORDER BY create_time DESC;
发现即使有(category_id, price)的联合索引,执行计划仍显示"Using filesort"。这是因为B+树索引的最左前缀原则——当查询条件不包含联合索引的第一列时,索引可能失效。
根据实际业务场景设计联合索引时,我总结出三条经验:
比如用户订单查询场景:
sql复制SELECT order_id, status FROM orders
WHERE user_id = 10086 AND create_time > '2023-01-01'
ORDER BY amount DESC;
最优索引应该是(user_id, create_time, amount),同时包含查询条件和排序字段。
注意:不要盲目创建单列索引。我曾见过一个表有20个单列索引,实际执行时MySQL优化器只能选择其中一个,反而降低写入性能。
通过大量案例复盘,我整理出最常见的索引失效场景:
| 失效场景 | 示例 | 解决方案 |
|---|---|---|
| 隐式类型转换 | WHERE user_id = '10086' |
保持字段与条件类型一致 |
| 使用函数操作 | WHERE DATE(create_time)=... |
改用范围查询 |
| 前导模糊查询 | WHERE name LIKE '%张' |
考虑全文索引 |
| 不符合最左前缀 | 索引(a,b,c)但只查b,c | 调整查询条件或索引顺序 |
| 使用OR条件 | WHERE a=1 OR b=2 |
改用UNION ALL |
| 优化器判断全表扫描更快 | 表数据量很小 | 使用FORCE INDEX提示 |
排序操作是CPU密集型任务,通过EXPLAIN看到"Using filesort"时就需要警惕。我常用的优化手段包括:
案例:电商订单列表优化
原始查询:
sql复制SELECT * FROM orders
WHERE shop_id = 123
ORDER BY create_time DESC
LIMIT 1000;
问题分析:
优化方案:
(shop_id, create_time)联合索引sql复制SELECT * FROM orders
WHERE shop_id = 123 AND create_time >= '2023-01-01'
ORDER BY create_time DESC
LIMIT 1000;
分组操作本质上是先排序后合并,同样面临性能挑战。一个物流系统的统计查询案例:
原始SQL:
sql复制SELECT province, COUNT(*)
FROM waybills
WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31'
GROUP BY province;
优化步骤:
(create_time, province)索引sql复制SELECT province, COUNT(*)
FROM waybills
WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31'
GROUP BY province
ORDER BY NULL; -- 避免默认排序开销
深度分页是性能杀手,典型场景:
sql复制SELECT * FROM articles
ORDER BY view_count DESC
LIMIT 100000, 20;
我实践过的三种优化方案:
sql复制SELECT a.* FROM articles a
JOIN (
SELECT id FROM articles
ORDER BY view_count DESC
LIMIT 100000, 20
) AS t ON a.id = t.id;
sql复制SELECT * FROM articles
WHERE view_count < 上次最小view_count
ORDER BY view_count DESC
LIMIT 20;
掌握EXPLAIN输出是调优的基本功,关键字段解读:
| 字段 | 关键值 | 含义 |
|---|---|---|
| type | const > ref > range > index > ALL | 访问类型,性能依次降低 |
| possible_keys | 可能使用的索引 | |
| key | 实际使用的索引 | |
| rows | 预估检查行数 | |
| Extra | Using index | 覆盖索引 |
| Using temporary | 使用临时表 | |
| Using filesort | 需要额外排序 |
MySQL的索引统计信息可能不准确,导致优化器选择错误执行计划。曾处理过一个案例:
WHERE status=1却走了全表扫描解决方法:
sql复制ANALYZE TABLE orders; -- 更新统计信息
-- 或强制使用索引
SELECT * FROM orders FORCE INDEX(idx_status) WHERE status = 1;
我的标准排查流程:
sql复制SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 捕获执行超过1秒的查询
bash复制pt-query-digest /var/lib/mysql/mysql-slow.log > slow_report.txt
当数据库突然出现性能下降时,我的紧急检查清单:
sql复制SHOW PROCESSLIST;
sql复制SELECT * FROM performance_schema.events_waits_current;
sql复制SHOW ENGINE INNODB STATUS;
KILL [thread_id]SQL_NO_CACHE测试定期索引维护能保持查询性能稳定:
sql复制ALTER TABLE orders ENGINE=InnoDB; -- 重建表
OPTIMIZE TABLE orders; -- 优化表
sql复制SELECT * FROM sys.schema_redundant_indexes;
sql复制SELECT * FROM sys.schema_unused_indexes;
索引优化是个需要持续迭代的过程。我建议每个季度做一次全面的索引审查,特别是在业务查询模式发生变化后。实际工作中,要平衡查询性能与写入开销,避免陷入"过度索引"的陷阱。