1. MySQL索引优化实战解析
作为一名长期奋战在数据库性能优化一线的工程师,我处理过数百个MySQL索引优化案例。今天要分享的这个实战项目,源于一个日均访问量200万次的电商平台订单系统。该系统在促销活动期间频繁出现查询超时,通过分析发现核心问题出在索引设计上。
这个案例的特殊性在于:表面上所有常用查询字段都已建立索引,但实际执行效率却远低于预期。经过深度排查,我们发现问题的根源在于复合索引的字段顺序错误、索引冗余以及未能充分利用覆盖索引的特性。下面我将详细拆解整个优化过程,包括问题定位、解决方案和实测效果。
2. 问题定位与SQL分析
2.1 慢查询日志分析
首先通过MySQL的慢查询日志(slow_query_log)收集执行时间超过2秒的SQL语句。关键配置如下:
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
分析日志后发现,以下类型的查询出现频率最高且性能最差:
sql复制SELECT order_id, user_id, product_id, status, create_time
FROM orders
WHERE user_id = 12345
AND status IN (1,2,3)
ORDER BY create_time DESC
LIMIT 20;
2.2 执行计划解读
使用EXPLAIN分析上述查询,发现虽然使用了索引,但出现了"Using filesort"和"Using temporary"这两个危险信号:
code复制+----+-------------+-------+------+---------------+---------+---------+-------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------+--------+-----------------------------+
| 1 | SIMPLE | orders| ref | idx_user | idx_user| 4 | const | 124578 | Using where; Using filesort |
+----+-------------+-------+------+---------------+---------+---------+-------+--------+-----------------------------+
2.3 现有索引结构分析
检查表结构发现当前索引设计存在三个主要问题:
- 单列索引过多:为user_id、status、create_time分别建立了独立索引
- 复合索引顺序不当:存在一个(idx_status_user)的复合索引,但查询条件中user_id的选择性更高
- 缺少覆盖索引:查询需要回表获取全部字段
3. 索引优化方案设计
3.1 复合索引设计原则
基于业务查询模式,我们遵循以下原则重新设计索引:
- 高选择性字段优先:user_id的选择性(0.85)远高于status(0.15)
- 等值查询字段在前:WHERE user_id = xxx是等值查询
- 范围查询字段在后:status IN (1,2,3)属于范围查询
- 排序字段包含在索引中:避免filesort
3.2 新索引方案
删除原有的单列索引和不当复合索引,建立以下两个核心索引:
sql复制ALTER TABLE orders
DROP INDEX idx_user,
DROP INDEX idx_status,
DROP INDEX idx_status_user,
ADD INDEX idx_user_status_ctime(user_id, status, create_time),
ADD INDEX idx_cover(user_id, status, create_time, product_id);
其中idx_cover是专门为这个高频查询设计的覆盖索引,包含查询所需的所有字段。
3.3 索引选择策略优化
通过FORCE INDEX引导优化器使用更合适的索引:
sql复制SELECT order_id, user_id, product_id, status, create_time
FROM orders FORCE INDEX(idx_cover)
WHERE user_id = 12345
AND status IN (1,2,3)
ORDER BY create_time DESC
LIMIT 20;
4. 优化效果验证
4.1 执行计划对比
优化后的EXPLAIN结果:
code复制+----+-------------+-------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-------------+---------+------+------+-------------+
| 1 | SIMPLE | orders| range | idx_cover | idx_cover | 9 | NULL | 32 | Using where |
+----+-------------+-------+-------+---------------+-------------+---------+------+------+-------------+
关键改进:
- 扫描行数从124578降到32
- 消除了filesort和temporary
- 使用覆盖索引避免了回表
4.2 性能测试数据
使用sysbench进行压测对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| QPS | 128 | 2100 | 1540% |
| 平均延迟(ms) | 1560 | 23 | -98.5% |
| 95分位延迟 | 3200 | 45 | -98.6% |
5. 深度优化技巧
5.1 索引跳跃扫描优化
对于status这种低选择性的字段,MySQL 8.0+支持索引跳跃扫描:
sql复制SELECT * FROM orders
WHERE status = 2
ORDER BY create_time DESC
LIMIT 10;
可以通过优化器开关控制:
sql复制SET @@optimizer_switch='skip_scan=on';
5.2 自适应哈希索引
对于特别热点的数据,可以启用自适应哈希索引:
sql复制SET GLOBAL innodb_adaptive_hash_index = ON;
5.3 索引条件下推(ICP)
充分利用ICP特性减少回表次数:
sql复制SET optimizer_switch='index_condition_pushdown=on';
6. 常见问题排查
6.1 索引失效场景
- 隐式类型转换:WHERE user_id = '12345'(user_id是INT)
- 使用函数:WHERE DATE(create_time) = '2023-01-01'
- 前导模糊查询:WHERE product_name LIKE '%手机%'
6.2 索引选择异常
当优化器选错索引时,可以:
- 使用ANALYZE TABLE更新统计信息
- 使用FORCE INDEX强制指定
- 调整optimizer_index_cost_config参数
6.3 索引维护建议
- 定期使用OPTIMIZE TABLE重整索引
- 监控索引碎片率:SHOW TABLE STATUS LIKE 'orders'
- 删除3个月未使用的索引
7. 监控与持续优化
建立索引使用监控体系:
sql复制-- 查看索引使用频率
SELECT * FROM sys.schema_index_statistics
WHERE table_schema = 'your_db';
-- 监控冗余索引
SELECT * FROM sys.schema_redundant_indexes;
配置定期检查任务:
bash复制# 每周分析索引使用情况
0 3 * * 1 mysql -e "SELECT * FROM sys.schema_unused_indexes" >> /var/log/mysql/index_monitor.log
这个案例给我的深刻启示是:索引优化不是一劳永逸的工作,需要随着业务发展持续调整。特别是在业务快速增长期,原先高效的索引设计可能很快就不再适用。我现在养成了每月全面检查一次核心表索引使用情况的习惯,这帮助我避免了很多潜在的性能问题。