【架构实战】MySQL索引优化:从慢查询到毫秒响应
一、一页订单列表,查了30秒
去年接手一个电商后台项目,运营同学反馈订单列表页面打开要等30秒。我一查慢查询日志,一条SQL跑了28秒。
sql
SELECT * FROM orders
WHERE status = 'PAID'
AND create_time >= '2025-06-01'
ORDER BY create_time DESC
LIMIT 20;
orders表有2000万行数据,status字段上建了索引,但执行计划显示全表扫描。原因是 status = 'PAID' 过滤后还有800万行,再按 create_time 排序,MySQL选择放弃索引直接全扫。
这就是典型的"建了索引但不生效"问题。
二、索引失效的七大经典场景
2.1 索引列上做函数运算
sql
-- 索引失效
SELECT * FROM orders WHERE DATE(create_time) = '2025-06-01';
-- 正确写法
SELECT * FROM orders WHERE create_time >= '2025-06-01' AND create_time < '2025-06-02';
原理:对索引列使用函数后,MySQL无法使用索引的有序性,只能全表扫描。
2.2 隐式类型转换
sql
-- phone字段是VARCHAR类型
-- 索引失效:传入数字
SELECT * FROM users WHERE phone = 13800138000;
-- 正确:传入字符串
SELECT * FROM users WHERE phone = '13800138000';
原理 :MySQL自动将字符串转为数字,等价于 WHERE CAST(phone AS SIGNED) = 13800138000,索引列被函数包裹。
2.3 LIKE前置通配符
sql
-- 索引失效
SELECT * FROM products WHERE name LIKE '%手机%';
-- 索引有效(只后置通配符)
SELECT * FROM products WHERE name LIKE '华为%';
原理:B+树索引按前缀排序,"%手机%"无法确定从哪个位置开始扫描。
2.4 联合索引最左前缀失效
sql
-- 联合索引 idx_a_b_c (a, b, c)
-- 索引生效:a在最左
SELECT * FROM t WHERE a = 1 AND b = 2;
-- 索引不生效:跳过a
SELECT * FROM t WHERE b = 2 AND c = 3;
-- 部分生效:只用到a
SELECT * FROM t WHERE a = 1 AND c = 3;
原理:联合索引按a->b->c的顺序构建B+树,跳过a无法定位索引入口。
2.5 OR条件的索引失效
sql
-- name有索引,status无索引 → 全表扫描
SELECT * FROM products WHERE name = 'iPhone' OR status = 'ON_SALE';
-- 改造:UNION ALL
SELECT * FROM products WHERE name = 'iPhone'
UNION ALL
SELECT * FROM products WHERE status = 'ON_SALE' AND name != 'iPhone';
2.6 范围查询阻断联合索引
sql
-- 联合索引 idx_a_b_c
-- 索引失效:a做范围查询后,后面的b、c无法使用
SELECT * FROM t WHERE a > 10 AND b = 2 AND c = 3;
-- 优化:精确匹配放前面,范围放最后
-- 调整索引为 idx_b_c_a (b, c, a)
SELECT * FROM t WHERE b = 2 AND c = 3 AND a > 10;
2.7 查询条件使用 != 或 NOT IN
sql
-- 索引失效概率高
SELECT * FROM orders WHERE status != 'CANCELLED';
-- 优化:正向枚举
SELECT * FROM orders WHERE status IN ('PENDING', 'PAID', 'SHIPPED', 'COMPLETED');
三、索引优化实战:让慢查询飞起来
3.1 覆盖索引:避免回表
原始查询每次都要回表查 amount 和 user_id:
sql
SELECT order_id, amount, user_id
FROM orders
WHERE user_id = 12345 AND status = 'PAID';
-- 创建覆盖索引,包含所有查询字段
CREATE INDEX idx_user_status_cover ON orders(user_id, status, amount, order_id);
覆盖索引让所有数据从索引中直接获取,省去回表的随机IO。
3.2 索引下推(ICP):让存储引擎干活
sql
-- MySQL 5.6+ 支持的索引条件下推
-- 联合索引 idx_name_age (name, age)
-- 无ICP:存储引擎只按name过滤,age交给Server层过滤
-- 有ICP:存储引擎同时按name和age过滤,减少回表
SELECT * FROM users WHERE name LIKE '张%' AND age = 25;
3.3 前缀索引:节省空间
sql
-- 长字符串字段建立前缀索引
ALTER TABLE articles ADD INDEX idx_title_prefix(title(20));
-- 评估前缀选择性
SELECT COUNT(DISTINCT LEFT(title, 20)) / COUNT(*) AS selectivity FROM articles;
-- 如果大于0.9,说明前缀区分度足够
3.4 实战:订单慢查询完整优化
回到开头的30秒查询,给出最终优化方案:
sql
-- Step 1: 分析执行计划
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND create_time >= '2025-06-01'
ORDER BY create_time DESC LIMIT 20;
-- 发现:rows = 800万, Extra: Using filesort
-- Step 2: 创建联合索引(精准索引)
CREATE INDEX idx_status_time ON orders(status, create_time);
-- Step 3: 验证
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND create_time >= '2025-06-01'
ORDER BY create_time DESC LIMIT 20;
-- 结果:rows = 5000, Extra: Using index condition
-- 查询时间:28秒 → 12ms
四、索引设计的黄金法则
| 原则 | 说明 | 反例 |
|---|---|---|
| 高选择性优先 | WHERE过滤后数据量小于5%才建索引 | 性别字段建索引(选择性50%) |
| 最左前缀 | 联合索引把等值查询放前面,范围放最后 | (age, name) 优于 (name, age) |
| 避免冗余 | (a,b) 已覆盖 (a),不要重复建 | 建了 idx_a_b 又建 idx_a |
| 控制数量 | 单表索引不超过5个 | 10个索引,写入性能下降80% |
| 定期清理 | DROP无用索引,用pt-duplicate-key-checker | 历史遗留索引从来不删 |
五、一条SQL的生命周期
客户端 → 连接器 → 分析器 → 优化器 → 执行器 → 存储引擎
│
选择索引的关键环节
┌─────────┬──────────┐
│ 成本模型 │ 统计信息 │
│ (Cost) │(采样估算) │
└─────────┴──────────┘
优化器基于统计信息估算每个索引的IO成本和CPU成本,选择成本最低的执行计划。但统计信息可能不准:
sql
-- 手动更新统计信息
ANALYZE TABLE orders;
-- 查看索引基数
SHOW INDEX FROM orders;
六、监控与工具
bash
# 1. 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒记录
# 2. 查看未使用索引
SELECT * FROM sys.schema_unused_indexes;
# 3. 查看冗余索引
SELECT * FROM sys.schema_redundant_indexes;
# 4. 用pt-query-digest分析慢日志
pt-query-digest /var/log/mysql/slow.log --limit 10
七、总结
索引优化是数据库性能优化的第一课,也是最容易见效的手段。
核心要点:
- 索引不是建了就生效,要理解索引失效的七种场景
- 覆盖索引是性能优化的大杀器,避免回表是关键
- 联合索引遵循最左前缀原则,等值在前、范围在后
- EXPLAIN是日常工具,每条SQL上线前必须过一遍
- 定期用
sys.schema_unused_indexes清理无用索引
血的教训 :线上加索引要在业务低峰期用 ALGORITHM=INPLACE, LOCK=NONE,否则DDL会锁表,当年因为这个事故导致订单系统挂了15分钟。
索引调优没有银弹,理解B+树原理 + 看懂执行计划 = 解决90%的慢查询问题。
个人观点,仅供参考