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 filesortUsing 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 是你的火眼金睛 :重点关注 typekeyrowsExtra

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

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

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

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

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

相关推荐
恋猫de小郭1 小时前
Flutter 凉了没?Flutter 2026 的未来行程和规划,一些有趣的变化
android·前端·flutter
帅次1 小时前
Android 高级工程师专题深挖:WebView、Context 与初始化链
android·binder·webview·zygote·web app·dalvik
y小花1 小时前
安卓音频低延时与AAudio
android·音视频
Jwest20211 小时前
佳维视工业安卓一体机在医生移动查房车中的应用
android
大龄程序员狗哥2 小时前
第49篇:TensorFlow Lite实战——将图像分类模型部署到安卓手机(项目实战)
android·分类·tensorflow
BetterNow.2 小时前
安卓内存Previous为什么可以算进freeRam
android·linux·安卓·安卓性能·安卓内存
码云数智-园园2 小时前
PHP 8.x 命名的参数与属性(Attribute):告别注释,构建真正的元数据
android·ide·android studio
0pen12 小时前
ZygiskNext 源码解析(三):zygiskd 的模块管理、memfd 与 companion
android·安全·开源
Android_xiong_st2 小时前
(原创)2026安卓面试复盘
android·面试·职场和发展