MySQL性能优化:深入理解索引原理与查询优化实战

作为一名后端开发,MySQL是绕不开的必修课。在日常工作中,慢查询往往是系统性能的头号杀手,而索引则是解决这一问题的核心利器。本文将带你从索引的本质出发,深入B+树原理,结合Explain工具分析慢SQL,并总结一套可落地的查询优化方法论。

一、索引的本质:为什么数据量一大就慢?

没有索引时,MySQL只能进行全表扫描 ,复杂度O(n)。以一张1000万行的用户表为例,查找某条记录平均需要扫描500万行,耗时可能达到秒级。索引的本质是用空间换时间,通过维护一种有序的数据结构(B+树),将查找复杂度降低到O(log n)。

1.1 常见的索引数据结构对比

数据结构 磁盘I/O次数 适用场景 MySQL为何不用?
哈希表 O(1) 等值查询 不支持范围查询
二叉树 O(log n) 通用 易退化成链表
AVL/红黑树 O(log n) 通用 树高过高,I/O次数多
B树 O(log_m n) 范围查询 非叶子节点也存数据,空间浪费
B+树 O(log_m n) 范围查询+扫库 叶子节点形成链表,完美适配磁盘预读

结论 :InnoDB采用B+树作为索引结构,关键在于其矮胖 (扇出系数高)和叶子节点有序链表的设计。

二、B+树索引是如何工作的?

2.1 一张图看懂B+树结构

text

复制代码
[根节点] (页20)
   |----> [内节点] (页10)  (存储键值+指针)
   |----> [内节点] (页11)
            |----> [叶子节点] (页1) (1,2,3,4,5) -> next指针 -> 
            |----> [叶子节点] (页2) (6,7,8,9,10) -> next指针 ->
            |----> [叶子节点] (页3) (11,12,13,14,15)
  • 非叶子节点:只存索引键 + 子页指针,不存真实数据行

  • 叶子节点:存储完整的索引键 + 行数据(或主键值,取决于聚簇/二级索引)

2.2 聚簇索引 vs 二级索引

聚簇索引:叶子节点直接存储整行数据。InnoDB表中,主键就是聚簇索引。如果没有显式主键,则会选择第一个NOT NULL UNIQUE列,否则自动生成隐藏的ROW_ID。

二级索引(辅助索引) :叶子节点存储索引列 + 主键值 。因此,通过二级索引查询数据需要回表(先查到主键,再回聚簇索引查完整行)。

sql

复制代码
-- 示例:假设 user 表有主键 id 和二级索引 name
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(32),
    age INT,
    INDEX idx_name (name)
);

-- 以下查询只需扫描二级索引(覆盖索引)
SELECT id, name FROM user WHERE name = 'Tom';

-- 以下查询需要回表:二级索引查到 id,再回聚簇索引取 age
SELECT age FROM user WHERE name = 'Tom';

小贴士 :尽量让查询只走二级索引就拿到所有要的字段,这就是覆盖索引优化。

三、索引使用的最佳实践

3.1 最左前缀原则

复合索引 (a, b, c) 相当于创建了 (a)、(a,b)、(a,b,c) 三个索引。查询条件必须从索引最左列开始,不能跳过中间的列。

sql

复制代码
-- 能用到索引 idx_abc 的情况:
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3  -- 只用到 a,c 部分用不到

-- 用不到索引的情况:
WHERE b = 2
WHERE c = 3
WHERE a > 1 AND b = 2  -- 范围之后失效(a用了范围,b就失效)

3.2 索引失效的场景(切记!)

写法 是否失效 原因
WHERE name LIKE '%张' ✅ 失效 通配符在前,无法比较索引树
WHERE age + 1 = 20 ✅ 失效 对索引列做了计算/函数
WHERE LEFT(name,2) = '张三' ✅ 失效 函数破坏了索引
WHERE a = 1 OR b = 2 ⚠️ 部分失效 除非两个列都有索引,否则全表扫描
WHERE id IN (1,2,3) ❌ 有效 IN 在MySQL5.7+会被优化成多个等值
WHERE name IS NULL ❌ 有效 IS NULL 也能走索引

3.3 索引选择性

选择性 = 不同值数量 / 总行数,比值越接近1,索引效果越好。比如性别的选择性只有0.5,而身份证号接近1。

sql

复制代码
-- 查看列选择性
SELECT 
    COUNT(DISTINCT gender)/COUNT(*) AS gender_sel,
    COUNT(DISTINCT email)/COUNT(*) AS email_sel
FROM user;

当选择性低于20%时,优化器可能认为全表扫描更划算(因为大量回表成本高)。

四、定位慢查询:Explain 你真的会用吗?

4.1 开启慢查询日志

sql

复制代码
-- 查看当前设置
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';

-- 临时开启(生产谨慎)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;   -- 超过1秒记录

4.2 读懂Explain输出

拿一条实际SQL来分析:

sql

复制代码
EXPLAIN SELECT u.id, u.name, o.amount 
FROM user u 
INNER JOIN order o ON u.id = o.user_id 
WHERE u.age BETWEEN 20 AND 30
ORDER BY o.create_time DESC
LIMIT 10;

关键字段解读:

列名 值示例 含义
type ref/range/index/ALL 访问类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL
possible_keys idx_age,PRIMARY 可能用到的索引
key idx_age 实际使用的索引
key_len 5 索引使用字节数,可推断用了哪几列
rows 10000 估计扫描的行数(越少越好)
Extra Using index condition; Using filesort; Using temporary Using filesort 和Using temporary通常是优化的信号

关键点 :Using filesort 表示MySQL需要额外一次排序,而不是直接利用索引顺序。如果 order by 的列有索引,则不会出现这个提示。

五、实战:一个慢查询的优化全过程

5.1 问题场景

我们有一张订单表 order,记录数 2000万,业务方反馈一个后台查询页面打开极慢(>8秒)。

sql

复制代码
SELECT order_id, user_name, amount, status, create_time
FROM `order`
WHERE status = 1 
  AND create_time BETWEEN '2025-01-01' AND '2025-01-31'
ORDER BY create_time DESC
LIMIT 20;

5.2 分析步骤

Step 1: 查看表结构

sql

复制代码
SHOW CREATE TABLE `order`;
-- 发现只有主键索引 `PRIMARY KEY (order_id)`,没有其他索引

Step 2: Explain 分析

text

复制代码
+----+-------------+-------+------+---------------+------+---------+------+----------+-----------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows     | Extra                       |
+----+-------------+-------+------+---------------+------+---------+------+----------+-----------------------------+
|  1 | SIMPLE      | order | ALL  | NULL          | NULL | NULL    | NULL | 20,000,000| Using where; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+----------+-----------------------------+

type=ALL 全表扫描,rows=2000万,外加 Using filesort(结果集排序),不慢才怪。

Step 3: 尝试创建复合索引

根据 等值查询在前,范围查询在后 的原则,status = 1 是等值,create_time 是范围 + 排序,所以索引应为 (status, create_time)。

sql

复制代码
ALTER TABLE `order` ADD INDEX idx_status_ctime (status, create_time);

Step 4: 再次 Explain

text

复制代码
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
| id | select_type | table | type  | possible_keys    | key              | key_len | ref  | rows | Extra                 |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
|  1 | SIMPLE      | order | range | idx_status_ctime | idx_status_ctime | 11      | NULL | 8540 | Using index condition |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
  • type 变为 range,扫描行数从 2000万 降到 8540。

  • Extra 中不再有 Using filesort,因为 create_time 已经在索引中且排序方向一致(索引默认升序,我们ORDER BY DESC,InnoDB支持反向扫描,效率接近)。

Step 5: 验证性能

查询时间从 8秒 降到 18ms,完美解决。

六、索引维护与常见误区

6.1 索引不是越多越好

  • 每个索引都需要占用磁盘空间(一颗B+树)

  • 写操作(INSERT/UPDATE/DELETE)要同时维护所有索引,导致性能下降

  • 建议单表索引数量不超过 5~6 个

6.2 冗余索引与重复索引

sql

复制代码
-- 重复索引
INDEX (a) 和 PRIMARY KEY (a)  # 主键已经是唯一索引了

-- 冗余索引
INDEX (a,b) 和 INDEX (a)    # (a,b) 已经能覆盖 (a) 的查询

可以使用 sys.schema_redundant_indexes 视图来检查冗余索引(MySQL 5.7+)。

6.3 索引下推(ICP)

MySQL 5.6 引入了 Index Condition Pushdown,可以在索引遍历时就直接过滤掉不满足条件的记录,减少回表次数。

sql

复制代码
-- 例如复合索引 (name, age)
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;

没有ICP时:先通过 name 找到主键,再回表取 age 判断。

有ICP时:在索引树上同时判断 age = 20,满足条件的才回表,大大减少I/O。

七、总结:索引优化的核心心法

  1. 慢查询第一现场:开启慢查询日志,定期分析。

  2. Explain 是你的火眼金睛 :重点关注 type、key、rows、Extra。

  3. 复合索引遵循最左前缀:把区分度高的列放在左边,等值查询放前,范围查询放后。

  4. 避免索引失效 :不在索引列上做任何计算、函数、类型隐式转换;杜绝 % 开头的LIKE。

  5. 覆盖索引是王道:查询只走二级索引,避免回表。

  6. 写操作多的表,索引适度:不是所有WHERE列都要建索引,优先优化高频慢查询。

最后送大家一句话:索引犹如书的目录,设计得好,查找飞快;设计得不好,不如没有。 希望本文能帮你在MySQL优化的道路上少踩坑,多提效。

相关推荐
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓6 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1236 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)7 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
五彩小白1 天前
自回归和上下文
android