【架构实战】MySQL索引优化:从慢查询到毫秒响应

【架构实战】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 覆盖索引:避免回表

原始查询每次都要回表查 amountuser_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

七、总结

索引优化是数据库性能优化的第一课,也是最容易见效的手段。

核心要点

  1. 索引不是建了就生效,要理解索引失效的七种场景
  2. 覆盖索引是性能优化的大杀器,避免回表是关键
  3. 联合索引遵循最左前缀原则,等值在前、范围在后
  4. EXPLAIN是日常工具,每条SQL上线前必须过一遍
  5. 定期用 sys.schema_unused_indexes 清理无用索引

血的教训 :线上加索引要在业务低峰期用 ALGORITHM=INPLACE, LOCK=NONE,否则DDL会锁表,当年因为这个事故导致订单系统挂了15分钟。

索引调优没有银弹,理解B+树原理 + 看懂执行计划 = 解决90%的慢查询问题。


个人观点,仅供参考

相关推荐
XUHUOJUN1 小时前
Azure Local GPU 部署与企业应用场景指南(下篇)
microsoft·架构·nvidia·azure local
爱勇宝2 小时前
3位工程师靠“删AI代码”创业,一周收费1万美元:以后最贵的能力,可能不是写代码
前端·后端·架构
吠品2 小时前
PG_GBK 与 PG_UTF8 编码冲突:一次数据导入踩坑复盘
数据库
lld9510272 小时前
我做了一个 AI Agent Skill 云:用 MCP 在 Claude Code、Codex、OpenCode 之间同步技能
数据库·人工智能
●VON2 小时前
HarmonyKit | 鸿蒙开发:项目目录结构与多模块架构最佳实践
华为·架构·单元测试·harmonyos·鸿蒙
没落英雄3 小时前
6. 从零搭建一个 AI Agent —— 构建 Web 前端,让用户能和 agent 实时交互
前端·人工智能·架构
ttwuai3 小时前
GoFrame 连接 MySQL 报错排查:parseTime、时区和 utf8mb4 怎么配
数据库·mysql·adb
辉灰笔记3 小时前
MySQL8 意外关机/误移data目录导致服务启动失败 PID报错/权限报错 修复文档
linux·数据库·mysql·centos
麦聪聊数据4 小时前
电商大促数据库管控(下):数据需求小时级交付与全链路安全管控
数据库
喜欢的名字被抢了4 小时前
MySQL核心机制:事务、锁与存储引擎
数据库·sql·mysql·教程