MySQL 索引原理与慢查询优化实战:从 B+ 树到执行计划调优

摘要:索引本质是「空间换时间」的有序结构,但日常慢 SQL 大多不是「没建索引」,而是「查询没真正命中或高效利用索引」。本文从 B+ 树结构讲起,串起聚簇/二级索引、回表、覆盖索引、最左前缀与索引失效机制,再用慢查询日志与 EXPLAIN 把代价量化,给出一份可被执行计划验证的优化清单。

导语

你一定遇到过:给字段加了索引,查询却依旧全表扫描、依旧慢。问题往往不在「有没有索引」,而在「查询是否真的命中并高效利用了它」。

回表、覆盖索引、最左前缀、索引下推(ICP,Index Condition Pushdown)------这些概念单看都懂,落到真实 SQL 上却不会设计索引。本文以「为什么加了索引还是慢」为主线,把索引底层结构与排查工具链串成一条可复现路径。

沿线的原理与官方参数均以 MySQL 8.0 文档为准。推荐先读这篇实战路径总结 MySQL 索引调优实战:从 B+ Tree 到慢查询排查的完整路径 建立整体印象,再回来看本文的细节对比。

一、B+ 树索引:MySQL 为什么选它

InnoDB 绝大多数索引(主键、唯一索引、普通索引、全文索引)都以 B-tree (B 树)存储,而非哈希。B+ 树 是一种「矮胖」的平衡树:三层结构即可承载千万到亿级数据,叶子节点之间通过链表相连,非常适合范围查询与排序。

哈希索引只能做等值匹配,无法支持范围、排序和前缀匹配,所以 InnoDB 主键选择 B+ 树。聚簇索引 即「索引组织表」:整张表的用户数据都存放在聚簇索引 B+ 树的叶子节点中,这也是 InnoDB 与堆表引擎(如 MyISAM)的根本差异。

下面用一张表对比二者选型上的取舍:

维度 B+ 树索引 哈希索引
等值查询 支持(O(log n)) 支持(接近 O(1))
范围 / 排序 原生支持(叶子链表) 不支持
前缀匹配 支持(最左前缀) 不支持
典型用途 InnoDB 主键、二级索引 Memory 引擎可选

二、聚簇索引、二级索引与回表

InnoDB 每张表有且只有一个聚簇索引:通常是主键;若无主键,则用第一个全 NOT NULL 唯一索引;再没有就会隐式生成一个 6 字节的隐藏行 ID(GEN_CLUST_INDEX)作为聚簇索引。

二级索引 的叶子节点不存整行数据,只存「索引列值 + 主键值」。这意味着当查询需要的列超出二级索引覆盖范围时,InnoDB 必须凭主键回到聚簇索引取完整行------这个过程叫 回表,会产生额外随机 IO。

sql 复制代码
-- 建表并创建一个联合二级索引
CREATE TABLE orders (
  id        BIGINT PRIMARY KEY,
  user_id   INT  NOT NULL,
  status    TINYINT NOT NULL,
  amount    DECIMAL(10,2) NOT NULL,
  KEY idx_user_status (user_id, status)
) ENGINE=InnoDB;

-- 回表:需要 amount,二级索引只覆盖 (user_id, status),须回聚簇索引取行
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 1\G
-- Extra 中无 Using index → 发生了回表

-- 覆盖:只取索引列,无需回表
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 100 AND status = 1\G
-- Extra 显示 Using index → 覆盖索引,无回表

主键设计上建议用「短主键」:二级索引叶子都带着主键副本,主键越长,所有二级索引越占空间。高频写入表尤其要避免过长的聚簇键。

三、覆盖索引与最左前缀原则

覆盖索引 指查询所需列全部包含在某一个索引里,引擎可直接从索引取数,无需回表,EXPLAIN 的 Extra 列会显示 Using index。把高频查询的 SELECT 列塞进二级索引,是消除回表最直接的手段。

联合索引遵循 最左前缀原则 :索引 (a, b, c) 可支持 (a)、(a, b)、(a, b, c) 三种前缀组合查找,但单独查 (b) 或跳过前列通常走不上索引。

sql 复制代码
-- 联合索引 (user_id, status, amount)
-- 命中:满足最左前缀
EXPLAIN SELECT id FROM orders WHERE user_id = 100 AND status = 1\G

-- 失效:跳过 user_id,无法利用 (user_id, status, amount)
EXPLAIN SELECT id FROM orders WHERE status = 1 AND amount > 10\G
-- key = NULL,type = ALL,说明没用上索引

-- 覆盖索引改造:把 amount 纳入索引,避免回表
ALTER TABLE orders DROP INDEX idx_user_status;
ALTER TABLE orders ADD INDEX idx_user_status_amt (user_id, status, amount);
EXPLAIN SELECT user_id, status, amount FROM orders WHERE user_id = 100\G
-- Extra: Using index(覆盖)

设计经验:高区分度列靠前,范围查询列(>、BETWEEN、LIKE 'x%')放最后------因为范围之后的列会失效。同时尽量 SELECT 具体列而非 SELECT *,既减少回表也缩小索引体积。

四、索引失效的六大场景(及执行计划判断法)

索引失效不是靠死记场景,而是用 EXPLAIN 看 type / key / rows / Extra 四列判断。常见失效点有六类:

  1. 索引列做函数/表达式运算 :WHERE YEAR(create_time)=2023 会让索引失效,应改写成范围查询。
  2. 隐式类型转换 :字符串列用数字比较 WHERE phone = 13800000000 触发自动转换,索引失效。
  3. LIKE 左模糊 :LIKE '%abc' 无法用最左前缀,LIKE 'abc%' 可走索引。
  4. 违反最左前缀 :如上一节单独查 (b)。
  5. 范围条件后的列失效 :(a, b, c) 中 a 用范围后,b、c 无法用于索引过滤。
  6. OR 连接无索引列 :WHERE a=1 OR b=2 中 b 无索引时整句可能放弃索引。

此外,优化器也可能主动选全表扫描 :表很小、回表比例过高、或统计信息过期(需 ANALYZE TABLE)时,它认为扫全表更划算。

sql 复制代码
-- 反例1:索引列套函数 → 失效
EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2023\G
-- 正确改写:用范围查询保留索引列「纯净」
EXPLAIN SELECT * FROM orders
  WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01'\G

-- 反例2:隐式类型转换(phone 为 VARCHAR)
EXPLAIN SELECT * FROM user WHERE phone = 13800000000\G   -- 失效
EXPLAIN SELECT * FROM user WHERE phone = '13800000000'\G  -- 命中

-- 反例3:左模糊
EXPLAIN SELECT * FROM user WHERE name LIKE '%明'\G          -- 失效
EXPLAIN SELECT * FROM user WHERE name LIKE '张%'\G         -- 命中

EXPLAIN 的 type 从优到劣大致为:system > const > eq_ref > ref > range > index > ALL。看到 ALL 且 key = NULL,基本可判定索引没生效。

五、慢查询定位:慢查询日志与 EXPLAIN 执行计划解读

定位慢 SQL 的第一步是打开 慢查询日志 (Slow Query Log)。它由一组系统变量控制:slow_query_log(总开关)、slow_query_log_file(日志路径)、long_query_time(阈值,默认 10 秒)、log_queries_not_using_indexes(记录全表扫描)、min_examined_row_limit(最小检查行数,默认 0)。

ini 复制代码
# my.cnf 持久化配置(SET GLOBAL 重启后会失效)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
min_examined_row_limit = 100
sql 复制代码
-- 运行时临时开启(重启失效)
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 1;

-- 触发一条慢 SQL 后查看日志
SELECT SLEEP(2);  -- 超过 long_query_time,会写入慢日志
-- 日志内容示例:# Query_time: 2.000123  Lock_time: 0.000000 Rows_sent: 1  Rows_examined: 1

得到慢 SQL 后,用 EXPLAIN 看执行计划,关键字段含义:

字段 含义 关注点
type 访问类型 越靠前越优,ALL 最差
key 实际用的索引 NULL 即未用索引
rows 估计扫描行数 越小越好
Extra 额外信息 Using index 好;Using filesort/Using temporary 警惕

MySQL 8.0.18+ 还提供 EXPLAIN ANALYZE,以 TREE 格式输出实际 执行时间与行数,比纯估计更直观;想看优化器为什么这么选,可开启 OPTIMIZER_TRACE。

六、实战优化清单(含索引下推 ICP)

落地时优先做三件事:覆盖索引消除回表 、保持索引列「纯净」、用联合索引前缀覆盖高频查询。EXPLAIN ANALYZE(MySQL 8.0.18 引入,TREE 格式含实际执行时间与行数)能直接量化回表前后的代价差异。

索引下推(ICP,Index Condition Pushdown) 把 WHERE 中可用索引列判断的部分下推到存储引擎,减少回表次数;Extra 显示 Using index condition;InnoDB 仅用于二级索引,默认开启,可用 optimizer_switch 控制。

sql 复制代码
-- 基于 INDEX(zipcode, lastname, firstname) 的 ICP 演示
SET optimizer_switch = 'index_condition_pushdown=off';
EXPLAIN SELECT * FROM people
  WHERE zipcode = '95054' AND lastname LIKE '%etrunia%'\G
-- Extra 无 Using index condition → 更多回表

SET optimizer_switch = 'index_condition_pushdown=on';
EXPLAIN SELECT * FROM people
  WHERE zipcode = '95054' AND lastname LIKE '%etrunia%'\G
-- Extra: Using index condition → 引擎层先过滤,回表次数下降

其余可复用的优化项:

  • 深分页 :用 WHERE id > ? LIMIT n 替代 LIMIT 10000, 10,避免大偏移扫描。
  • JOIN :给被驱动表的连接列建索引,把 ALL 变成 ref。
  • 索引不是越多越好 :每次写入都要维护 B+ 树,高频更新表谨慎加索引;定期 ANALYZE TABLE 更新统计信息,避免优化器误判。

总结

调优不是玄学,而是可执行计划验证的工程方法。主线是:结构原理 → 失效机制 → 定位工具 → 优化清单。

行动清单:先开慢查询日志找瓶颈,再用 EXPLAIN 看 type/key/rows/Extra 逐项验证,优先覆盖索引、避开失效写法,最后用 EXPLAIN ANALYZE 复测代价。把「凭感觉加索引」换成「用执行计划量化回表代价」,慢查询自然压得下来。


参考资料

© 2024 | 转载请注明出处

结论:PASS

相关推荐
专业程序开发源1 小时前
flask家电故障预测系统92491-计算机课程设计/毕业设计
vscode·python·sql·算法·flask·课程设计
liulilittle1 小时前
Linux 下 select 测试函数
linux·服务器·网络·数据库·c++·select·c
DongQiShanRen1 小时前
玄龙(上):TICK 主循环——意识心跳怎么跳
linux·jvm·数据库·人工智能·数据挖掘·rust
数据库小学妹2 小时前
存算分离到底分离了什么?四条架构变化与选型判断
数据库·数据库架构·云原生数据库·缓存一致性·存算分离
FPGA小徐2 小时前
【一生一芯 / PA】异常响应机制:RISC-V 中 ecall → mtvec → mret 的完整代
开发语言·数据库·c#
m0_646429972 小时前
MySQL 初始化 SQL 中文乱码问题总结
数据库·sql·mysql
实战派K8S&DB2 小时前
TDSQL 核心模块与进程体系
运维·数据库·分布式·sql·mysql
FfHUCisI2 小时前
Go 性能分析工具 pprof:CPU、Heap 与 Goroutine 实战
数据库·golang
仍然.2 小时前
Redis---分布式锁
数据库·redis·分布式