SQL性能调优:从执行计划看懂索引的使用与失效
-
- [📌 前言:调优的四个维度](#📌 前言:调优的四个维度)
- 一、先看效果:索引的"降维打击"实验
-
- [1.1 构建百万级测试数据](#1.1 构建百万级测试数据)
- [1.2 主键查询 vs 无索引列查询](#1.2 主键查询 vs 无索引列查询)
- [1.3 压测工具 `mysqlslap` 放大差距](#1.3 压测工具
mysqlslap放大差距)
- [二、调优核心利器:EXPLAIN 执行计划](#二、调优核心利器:EXPLAIN 执行计划)
-
- [2.1 执行计划完整字段列表](#2.1 执行计划完整字段列表)
- [2.2 `key_len` 精确计算法(高频面试计算题)](#2.2
key_len精确计算法(高频面试计算题)) - [2.3 `filtered` 与 `rows` 的联动计算](#2.3
filtered与rows的联动计算)
- [三、`type` 类型全解:从"火箭"到"拖拉机"](#三、
type类型全解:从“火箭”到“拖拉机”) -
- [3.1 `system`(极罕见)](#3.1
system(极罕见)) - [3.2 `const`(主键/唯一索引等值匹配)](#3.2
const(主键/唯一索引等值匹配)) - [3.3 `eq_ref`(多表连接之王)](#3.3
eq_ref(多表连接之王)) - [3.4 `ref` / `ref_or_null`](#3.4
ref/ref_or_null) - [3.5 `unique_subquery` / `index_subquery`](#3.5
unique_subquery/index_subquery) - [3.6 `range`(范围查询底线)](#3.6
range(范围查询底线)) - [3.7 `index`(全索引扫描)vs `ALL`(全表扫描)](#3.7
index(全索引扫描)vsALL(全表扫描))
- [3.1 `system`(极罕见)](#3.1
- [四、`Extra` 列深潜:那些决定生死的关键词](#四、
Extra列深潜:那些决定生死的关键词) -
- [4.1 `Using index`(索引覆盖,黄金标准)](#4.1
Using index(索引覆盖,黄金标准)) - [4.2 `Using where`(回表后过滤)](#4.2
Using where(回表后过滤)) - [4.3 `Using filesort`(外部排序,需优化)](#4.3
Using filesort(外部排序,需优化)) - [4.4 `Using temporary`(临时表,需优化)](#4.4
Using temporary(临时表,需优化)) - [4.5 `Using index condition`(索引下推 ICP)](#4.5
Using index condition(索引下推 ICP)) - [4.6 `Using MRR`(多范围读取)](#4.6
Using MRR(多范围读取))
- [4.1 `Using index`(索引覆盖,黄金标准)](#4.1
- [五、核心机制深度剖析:覆盖索引 / 回表 / 索引合并](#五、核心机制深度剖析:覆盖索引 / 回表 / 索引合并)
-
- [5.1 索引覆盖(Covering Index) vs 回表(Back to Table)](#5.1 索引覆盖(Covering Index) vs 回表(Back to Table))
- [5.2 索引合并(Index Merge)三种算法详解](#5.2 索引合并(Index Merge)三种算法详解)
- 六、优化SELECT的其他场景(MySQL优化器规则)
-
- [6.1 WHERE子句优化(MySQL自动执行)](#6.1 WHERE子句优化(MySQL自动执行))
- [6.2 范围优化(Range Access)的数学区间推导](#6.2 范围优化(Range Access)的数学区间推导)
- [6.3 外连接优化(LEFT JOIN / RIGHT JOIN)](#6.3 外连接优化(LEFT JOIN / RIGHT JOIN))
- [6.4 `IS NULL` 优化](#6.4
IS NULL优化) - [6.5 `ORDER BY` 优化](#6.5
ORDER BY优化) - [6.6 `GROUP BY` 与 `DISTINCT` 优化](#6.6
GROUP BY与DISTINCT优化) - [6.7 函数调用优化(确定性 vs 非确定性)](#6.7 函数调用优化(确定性 vs 非确定性))
- [七、💣 索引失效的 8 大经典场景](#七、💣 索引失效的 8 大经典场景)
- 八、索引设计实战黄金法则
- 九、综合调优实战案例
- [十、💡 面试高频问题速答](#十、💡 面试高频问题速答)
- [📝 总结](#📝 总结)
📌 前言:调优的四个维度
数据库性能优化是一个多层面的工作,绝不仅仅是加索引。在实际生产中,优化需从以下四个层次自上而下审视:
| 优化层次 | 具体内容 | 负责人 |
|---|---|---|
| 硬件层 | 机械硬盘换SSD、增大内存、万兆网卡 | DBA/运维 |
| 架构层 | 读写分离、分库分表、引入Redis缓存 | 架构师 |
| 配置层 | 调整innodb_buffer_pool_size(内存命中率)、sort_buffer_size(排序效率) |
DBA |
| SQL与设计层 | 表结构设计(数据类型)、索引设计、SQL语句编写质量 | 开发人员(重点) |
核心观点:对于开发人员,80%的性能瓶颈源于糟糕的SQL语句和缺失的索引设计。在InnoDB中,索引即数据组织方式,用好索引,事半功倍。
一、先看效果:索引的"降维打击"实验
1.1 构建百万级测试数据
为了方便演示,我们先构造一张包含100万条记录的表(包含主键id和普通索引class_id)。注意:运行以下存储过程耗时较长(约20-100分钟),建议在测试环境执行。
sql
-- 1. 修改SQL结束符
DELIMITER //
-- 2. 创建存储过程
CREATE PROCEDURE p_init_index_data ()
BEGIN
DECLARE id BIGINT DEFAULT 100000;
DECLARE age TINYINT DEFAULT 18;
DECLARE gender BIGINT DEFAULT 1;
DECLARE class_id BIGINT DEFAULT 1;
DECLARE count INT DEFAULT 0;
DROP TABLE IF EXISTS index_demo;
CREATE TABLE index_demo (
id BIGINT AUTO_INCREMENT,
sn VARCHAR(10) NOT NULL,
name VARCHAR(20) NOT NULL,
mail VARCHAR(20),
age TINYINT(1),
gender TINYINT(1),
password VARCHAR(36) NOT NULL,
class_id BIGINT NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
PRIMARY KEY (id),
INDEX (class_id)
);
INSERT INTO index_demo VALUES (100000, '100000', 'testUser', '100000@qq.com', 18, 1, UUID(), 1, NOW(), NOW());
WHILE count < 1000000 DO
SET id := id + 1;
IF count % 10 = 0 THEN SET age := age + 1; END IF;
IF age > 50 THEN SET age := 16; END IF;
IF count % 3 = 0 THEN SET gender := 0; ELSE SET gender := 1; END IF;
SET class_id := class_id + 1;
IF class_id > 10 THEN SET class_id := 1; END IF;
INSERT INTO index_demo VALUES (id, id, CONCAT('user_', id), CONCAT(id, '@qq.com'), age, gender, UUID(), class_id, NOW(), NOW());
SET count := count + 1;
END WHILE;
END //
DELIMITER ;
-- 调用存储过程
CALL p_init_index_data();
-- 验证数据
SELECT COUNT(*) FROM index_demo; -- 输出:1000001
1.2 主键查询 vs 无索引列查询
- 主键查询(聚簇索引) :
SELECT * FROM index_demo WHERE id = 1020000;耗时 0.00 sec - 无索引列查询(全表扫描) :
SELECT * FROM index_demo WHERE sn = '1020000';耗时 0.59 sec
同样的数据,性能相差近60倍。
1.3 压测工具 mysqlslap 放大差距
使用MySQL自带的mysqlslap模拟并发,结果触目惊心:
① 主键查询(100并发,10000次查询)
bash
mysqlslap -uroot -p123456 --concurrency=100 --iterations=100 \
--create-schema="topic01" --engine="innodb" --number-of-queries=10000 \
--query "select id, sn, name, mail, age, gender, class_id from topic01.index_demo where id = 1020000;"
结果 :Average number of seconds to run all queries: 0.203 seconds
② 无索引列查询(100并发,仅100次查询)
bash
mysqlslap -uroot -p123456 --concurrency=3 --iterations=1 \
--create-schema="topic01" --engine="innodb" --number-of-queries=10 \
--query "select id, sn, name, mail, age, gender, class_id from topic01.index_demo where sn = '1020000';"
结果 :Average number of seconds to run all queries: 1001.484 seconds(约16分钟!)
结论:无索引时,并发稍大即导致数据库连接池耗尽。
二、调优核心利器:EXPLAIN 执行计划
在SQL前加EXPLAIN关键字,即可查看MySQL优化器生成的执行计划。
sql
EXPLAIN SELECT id, sn, name FROM index_demo WHERE id = 1020000\G
2.1 执行计划完整字段列表
| 列名 | 核心解读 | 重点关注 |
|---|---|---|
| id | SELECT标识符,id越大越先执行,id相同则从上至下。 | 子查询/联合查询时看执行顺序 |
| select_type | 查询类型:SIMPLE(简单)、PRIMARY(外层)、SUBQUERY(子查询)、UNION/UNION RESULT。注意:INSERT/UPDATE/DELETE也会显示对应类型。 |
出现SUBQUERY时需留意能否优化 |
| table | 正在访问的表名。<unionM,N> 表示合并查询的临时表。 |
- |
| partitions | 使用了哪个分区(非分区表为NULL)。 | - |
| type | 连接类型(性能排序)。 | ⭐ 调优核心指标 |
| possible_keys | 可能选用的索引(理论)。 | - |
| key | 实际选用的索引(实践)。 | 是否为NULL |
| key_len | 使用的索引字节长度。 | ⭐ 推算使用了复合索引的多少列 |
| ref | 与索引比较的对象(常量const 或 某表的某列)。 |
- |
| rows | 预估扫描行数(InnoDB是估值)。 | 越小越好 |
| filtered | 存储引擎返回后,Server层过滤后剩余的比例。rows * filtered ≈ 实际行数。 |
越高越好(100%最佳) |
| Extra | 额外的执行策略信息。 | ⭐ 是否回表、是否排序 |
2.2 key_len 精确计算法(高频面试计算题)
key_len 显示的字节数,可倒推出SQL实际用到了复合索引的哪几列。
公式 :
key_len = 列定义长度 + (是否允许NULL ? 1 : 0) + (是否为变长类型VARCHAR ? 2 : 0)
举例 :复合索引 (name VARCHAR(20) NOT NULL, age TINYINT NULL),字符集utf8mb4。
name列:20 * 4 + 0 (NOT NULL) + 2 (VARCHAR) = 82字节。age列:1 (TINYINT) + 1 (允许NULL) + 0 = 2字节。- 如果执行计划
key_len = 82,说明只用了name。 - 如果
key_len = 84,说明name和age都用上了。
2.3 filtered 与 rows 的联动计算
如果 rows 显示 1000,filtered 显示 20.00,则最终返回给客户端的行数约为 1000 * 20% = 200 行。filtered 值越高,说明索引过滤性越好,回表浪费越少。
三、type 类型全解:从"火箭"到"拖拉机"
type 性能从高到低完整排序:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
3.1 system(极罕见)
- 含义 :表只有一行数据(仅MyISAM引擎支持显示
system,InnoDB即使只有一行也显示为ALL或const)。
sql
CREATE TABLE test_system_myisam (id INT PRIMARY KEY, name VARCHAR(255)) ENGINE=MyISAM;
INSERT INTO test_system_myisam VALUES (1, 'name');
EXPLAIN SELECT * FROM test_system_myisam; -- type: system
3.2 const(主键/唯一索引等值匹配)
- 含义:命中主键或唯一索引,且条件为常量等值,结果最多一行。
sql
EXPLAIN SELECT * FROM index_demo WHERE id = 1020000; -- type: const
3.3 eq_ref(多表连接之王)
- 含义 :多表JOIN中,被驱动表 的连接字段是主键 或唯一非空索引,且为等值匹配。
sql
-- 假设 account 表的 student_id 是唯一索引
EXPLAIN SELECT * FROM student s JOIN account a ON s.id = a.student_id;
-- 对 account 表的扫描 type 为 eq_ref
3.4 ref / ref_or_null
ref:普通索引(非唯一)等值匹配,返回多条。ref_or_null:相比ref,额外包含对IS NULL的检索。前提是列必须允许为NULL。
sql
ALTER TABLE index_demo MODIFY name VARCHAR(20) NULL;
EXPLAIN SELECT * FROM index_demo WHERE name = 'user' OR name IS NULL; -- type: ref_or_null
3.5 unique_subquery / index_subquery
针对
IN子查询的特殊优化,面试时常被遗漏。
unique_subquery:IN子查询返回的是主键/唯一键 ,用eq_ref方式替换子查询。index_subquery:IN子查询返回的是普通索引列 ,用ref方式替换子查询。
3.6 range(范围查询底线)
- 含义 :使用
>、<、BETWEEN、IN()、LIKE 'abc%'进行索引范围扫描。
sql
EXPLAIN SELECT * FROM index_demo WHERE id BETWEEN 110000 AND 200000; -- type: range
生产环境要求 :绝大多数查询必须达到
range及以上级别。
3.7 index(全索引扫描)vs ALL(全表扫描)
index:扫描了整个索引树(通常是因为查询列都在索引里,但没带WHERE条件)。ALL:全表扫描(灾难),必须优化。
四、Extra 列深潜:那些决定生死的关键词
4.1 Using index(索引覆盖,黄金标准)
- 含义 :查询所需的所有列都包含在某个索引树中,无需回表,直接返回索引数据。
sql
-- 复合索引 (mail, age, class_id)
EXPLAIN SELECT mail, age, class_id FROM index_demo WHERE mail = '1020000@qq.com';
-- Extra: Using index
4.2 Using where(回表后过滤)
- 含义:使用索引定位到主键后,需要回表读取完整行,再在Server层过滤。
sql
EXPLAIN SELECT * FROM index_demo WHERE id < 102000; -- Extra: Using where
4.3 Using filesort(外部排序,需优化)
- 含义 :
ORDER BY无法利用索引,MySQL在内存(sort_buffer_size)或磁盘上进行额外排序。 - 查看大小 :
SHOW VARIABLES LIKE 'sort_buffer_size';(默认256KB)。可调大至16M。
sql
EXPLAIN SELECT * FROM index_demo WHERE id < 1020000 ORDER BY age LIMIT 10;
-- Extra: Using filesort
4.4 Using temporary(临时表,需优化)
- 含义 :
GROUP BY或DISTINCT未走索引,需要创建内部临时表。
sql
EXPLAIN SELECT AVG(age) FROM index_demo GROUP BY gender; -- Extra: Using temporary
4.5 Using index condition(索引下推 ICP)
- 含义 :MySQL 5.6+引入,在存储引擎层利用复合索引中未用于范围查找的列提前过滤,大幅减少回表次数。
- 大白话:以前是"引擎查索引→回表→Server过滤",现在是"引擎查索引的同时,直接在索引上过滤→回表次数骤减"。
4.6 Using MRR(多范围读取)
- 含义:将回表时的随机I/O转换为顺序I/O,提升磁盘效率(常与ICP配合出现)。
五、核心机制深度剖析:覆盖索引 / 回表 / 索引合并
5.1 索引覆盖(Covering Index) vs 回表(Back to Table)
| 概念 | 解释 | 性能影响 | 如何实现 |
|---|---|---|---|
| 索引覆盖 | SELECT的列全部包含在索引中,数据直接从索引返回 | 极高(无需回表,减少随机I/O) | 创建复合索引包含所有查询列 |
| 回表查询 | 根据索引查到主键,再根据主键到聚簇索引取整行 | 较差(大量随机I/O) | 避免SELECT *,尽量只查索引列 |
5.2 索引合并(Index Merge)三种算法详解
索引合并是通过对一个表同时使用多个索引进行条件扫描,并将满足条件的多个主键集合取交集或并集后再进行回表。
| 算法 | Extra显示 |
适用场景 | 原理 |
|---|---|---|---|
| 交集算法 | Using intersect(...) |
AND条件连接多个不同索引 | 多个索引查出主键集合,取交集 |
| 并集算法 | Using union(...) |
OR条件连接多个不同索引(等值查询) | 多个索引查出主键集合,取并集 |
| 排序并集算法 | Using sort_union(...) |
OR条件连接多个不同索引(范围查询) | 先分别查出主键,排序后再去重合并 |
建议 :虽然索引合并能救急,但复合索引通常比索引合并更高效。
六、优化SELECT的其他场景(MySQL优化器规则)
6.1 WHERE子句优化(MySQL自动执行)
MySQL优化器在准备阶段会做以下等价变换(了解即可):
- 去除无效括号 :
((a AND b) AND c) OR ((a AND b) AND (c AND d))→(a AND b AND c) OR (a AND b AND c AND d)。 - 常量合并 :
(a<b AND b=c) AND a=5→b>5 AND b=c AND a=5。 - 常量条件去除 :
(b>=5 AND b=5) OR (b=6 AND 5=5)→b=5 OR b=6。 - 无效值剔除 :
TINYINT列c < 256直接优化为1(永真)。 - 常量表优先读取 :主键/唯一键等值匹配的表(
const表),在表连接中优先读取。
6.2 范围优化(Range Access)的数学区间推导
对于复合索引 (key_part1, key_part2, key_part3):
- 当使用
=、<=>、IS NULL时,精确锁定区间。 - 当遇到第一个
>、<、>=、<=、BETWEEN时,该列之后的所有列无法再用于缩小范围区间 ,但索引下推(ICP)仍可辅助过滤。
示例 :key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10
- 推导区间:
('foo', 10, -inf) < (key_part1, key_part2, key_part3) < ('foo', +inf, +inf) key_part3仅能用于ICP过滤,不能缩小范围边界。
6.3 外连接优化(LEFT JOIN / RIGHT JOIN)
- MySQL 不支持
FULL OUTER JOIN,RIGHT JOIN会被自动转换为LEFT JOIN。 - 核心优化点 :如果
WHERE条件中强过滤了右表(可空侧)的NOT NULL字段(如WHERE score IS NOT NULL或WHERE score > 80),则 外连接会被优化器自动降级为内连接(INNER JOIN),大幅提升优化空间。
6.4 IS NULL 优化
- 如果列允许NULL ,
IS NULL走索引(type为range或ref_or_null)。 - 如果列设置了
NOT NULL,写WHERE column IS NULL,执行计划会显示Impossible WHERE(直接跳过查表)。
6.5 ORDER BY 优化
可以使用索引排序的情况:
ORDER BY字段顺序与复合索引最左前缀完全一致。- 升降序(ASC/DESC)与索引方向一致(InnoDB支持反向扫描,但有额外开销)。
WHERE条件中引用的常量列,可以作为索引前缀的一部分。
无法使用索引排序的情况 (出现Using filesort):
- 对不同的索引列排序。
- 排序字段不在索引最左前缀。
- 排序字段有表达式或函数。
6.6 GROUP BY 与 DISTINCT 优化
GROUP BY 本质上会先排序或使用临时表。如果分组字段匹配索引最左前缀,则可直接利用索引顺序分组,避免Using temporary。
特例 :MIN() / MAX() 如果作用在索引的最后一列,且前面列是常量,性能极高。
sql
SELECT MAX(key_part2) FROM tbl WHERE key_part1 = 1; -- 走索引
6.7 函数调用优化(确定性 vs 非确定性)
- 确定性函数 (如
POW(1,2)):结果固定,可使用索引。 - 非确定性函数 (如
RAND()、UUID()):每次结果不同,会导致索引失效。
变通方案:将非确定性函数的结果先存入变量,再用于查询。
sql
-- 错误(不走索引)
SELECT * FROM t WHERE id = FLOOR(1 + RAND() * 49);
-- 正确(走索引)
SET @keyval = FLOOR(1 + RAND() * 49);
SELECT * FROM t WHERE id = @keyval;
七、💣 索引失效的 8 大经典场景
| 失效场景 | 错误SQL示例 | 底层原因 |
|---|---|---|
| 1. 违反最左匹配 | WHERE age = 20(索引是(name, age)) |
B+树索引先按name排序,跳过name无法定位 |
| 2. 隐式类型转换 | WHERE sn = 1020000(sn是VARCHAR) |
触发隐式函数,索引"失效" |
| 3. 索引列参与运算 | WHERE id + 1 = 1000020 |
索引列值被修改,无法直接查找 |
| 4. 索引列使用函数 | WHERE LENGTH(name) = 11 |
函数改变了原始值,索引无法匹配 |
| 5. LIKE 以 % 开头 | WHERE name LIKE '%user%' |
B+树无法从中间匹配 |
| 6. 使用 <> 或 NOT IN | WHERE name <> 'user' |
结果集过大,优化器放弃索引 |
| 7. OR 连接不全覆盖索引(致命陷阱) | WHERE id = 1 OR age = 20(age无索引) |
即使id有主键索引,OR两边必须都有索引,否则优化器大概率放弃索引走全表扫描 |
| 8. MySQL认为全表扫描更快 | 表中数据极少,或过滤条件区分度极低(如性别) | 优化器基于统计信息计算代价,认为扫表更快 |
八、索引设计实战黄金法则
- 每张表必须有主键 ,推荐
BIGINT AUTO_INCREMENT(自增主键保证B+树顺序插入,避免页分裂)。严禁使用UUID做聚簇索引。 - 为高频操作列建立索引 :
WHERE、JOIN、ORDER BY、GROUP BY、DISTINCT的列。 - 复合索引优先于多个单列索引 :
(a,b,c)相当于拥有了(a)、(a,b)、(a,b,c)三个索引(利用最左匹配)。 - 优先设计覆盖索引:避免回表,提升效率。
- 避免在高重复列上建索引:如性别,扫描二级索引再回表代价高于全表扫描。
- 控制索引总数:每张表建议不超过5-6个,维护索引有成本。
- 可以使用
USE INDEX/FORCE INDEX强制指定索引。 - 定期清理未使用或重复索引。
- 创建索引前确认无未提交的大事务,防止锁表。
SELECT COUNT(*)vsSELECT COUNT(1)的终极真相 :- InnoDB :两者完全等价 ,无性能差异。底层会遍历最小的可用二级索引(若无则扫主键)。
- MyISAM :无
WHERE时直接返回精确行数(不扫表)。 - 推荐写法 :统一使用
COUNT(*)。
九、综合调优实战案例
原始慢SQL:
sql
SELECT * FROM index_demo WHERE mail = '1020000@qq.com' ORDER BY age LIMIT 10;
- 假设
mail有普通索引,age无索引。 - 执行计划 :
type: ref,Extra: Using filesort。
优化第一步:建立复合覆盖索引
sql
CREATE INDEX idx_mail_age ON index_demo(mail, age);
- 优化后执行计划 :
type: ref,Extra: Using index(索引覆盖,且age利用索引有序,不再出现filesort)。
优化第二步(如果业务允许):避免SELECT *
sql
SELECT mail, age FROM index_demo WHERE mail = '1020000@qq.com' ORDER BY age LIMIT 10;
- 此时连回表都省了,直接走覆盖索引,达到极致性能。
十、💡 面试高频问题速答
-
Q:什么是索引下推(ICP)?
A :MySQL 5.6引入,在存储引擎层利用复合索引中未用于范围查找的列提前过滤,减少回表次数。
Extra显示Using index condition。 -
Q:为什么LIKE '%xxx' 会导致索引失效?
A:B+树索引按从左到右的顺序组织数据,无法跳过前缀直接定位到中间部分。
-
Q:复合索引 (a,b,c),查询条件 WHERE a=1 AND c=3 会走索引吗?
A :会走索引,但只会用到列 a(因为跳过了b,违反最左匹配,c无法用于缩小范围,但可能用于ICP过滤)。
-
Q:
SELECT *一定不好吗?A :是的。
SELECT *几乎无法利用覆盖索引,必然导致回表,且传输冗余数据增加网络IO。 -
Q:索引合并(Index Merge)是好事还是坏事?
A :是优化器的补救措施。能用复合索引解决的,尽量不要依赖索引合并,因为多个索引扫描再合并的开销通常大于单次复合索引扫描。
📝 总结
- 索引是查询的生命线,但需要科学设计,绝不是越多越好。
- Explain 是调优的"火眼金睛" ,务必对
type、key、Extra、key_len敏感。 - 覆盖索引 是性能之王,回表是万恶之源。
- 最左匹配、隐式转换、函数运算、OR乱用 是索引失效的四大天王,写SQL时务必警惕。
如果觉得有用,请点赞收藏,让更多小伙伴避开索引失效的深坑!有疑问欢迎评论区留言交流。👇