MySQL EXPLAIN 深度解析与 SQL 优化实战指南
适用版本:MySQL 5.7 / 8.0
标签:
MySQLEXPLAINSQL优化JOIN子查询索引性能调优
文章目录
- [MySQL EXPLAIN 深度解析与 SQL 优化实战指南](#MySQL EXPLAIN 深度解析与 SQL 优化实战指南)
-
- 表与数据准备
- 一、EXPLA
- 二、各字段深度解析
-
- [2.1 id --- 查询序号](#2.1 id — 查询序号)
- [2.2 select_type --- 查询类型](#2.2 select_type — 查询类型)
- [2.3 table --- 当前行对应的表](#2.3 table — 当前行对应的表)
- [2.4 partitions --- 分区信息](#2.4 partitions — 分区信息)
- [2.5 type --- 访问类型(最重要)](#2.5 type — 访问类型(最重要))
-
- system
- [const (最优之一)](#const (最优之一))
- [eq_ref (最优之一)](#eq_ref (最优之一))
- ref
- ref_or_null
- index_merge
- range
- index
- [ALL (必须优化)](#ALL (必须优化))
- [2.6 possible_keys --- 候选索引](#2.6 possible_keys — 候选索引)
- [2.7 key --- 实际使用的索引](#2.7 key — 实际使用的索引)
- [2.8 key_len --- 索引使用的字节数](#2.8 key_len — 索引使用的字节数)
- [2.9 ref --- 索引匹配的值来源](#2.9 ref — 索引匹配的值来源)
- [2.10 rows --- 预估扫描行数](#2.10 rows — 预估扫描行数)
- [2.11 filtered --- 条件过滤百分比](#2.11 filtered — 条件过滤百分比)
- [2.12 Extra --- 额外执行信息(重要)](#2.12 Extra — 额外执行信息(重要))
-
- [Using index (最优)](#Using index (最优))
- [Using where](#Using where)
- [Using index condition (索引下推 ICP)](#Using index condition (索引下推 ICP))
- [Using filesort (需关注)](#Using filesort (需关注))
- [Using temporary (需优化)](#Using temporary (需优化))
- [Using join buffer (Block Nested Loop) (需优化)](#Using join buffer (Block Nested Loop) (需优化))
- [Select tables optimized away (最优)](#Select tables optimized away (最优))
- [Impossible WHERE](#Impossible WHERE)
- [三、JOIN 优化原理与实战](#三、JOIN 优化原理与实战)
-
- [3.1 驱动表与被驱动表的选择原理](#3.1 驱动表与被驱动表的选择原理)
- [3.2 INNER JOIN 优化实战](#3.2 INNER JOIN 优化实战)
- [3.3 被驱动表无索引的问题与优化](#3.3 被驱动表无索引的问题与优化)
- [3.4 LEFT JOIN 的特殊注意事项](#3.4 LEFT JOIN 的特殊注意事项)
- [3.5 覆盖索引消除回表](#3.5 覆盖索引消除回表)
- [3.6 多表 JOIN 的执行顺序分析](#3.6 多表 JOIN 的执行顺序分析)
- [四、UNION 优化原理与实战](#四、UNION 优化原理与实战)
-
- [4.1 UNION vs UNION ALL 底层原理](#4.1 UNION vs UNION ALL 底层原理)
- [4.2 UNION 临时表的内存控制](#4.2 UNION 临时表的内存控制)
- [4.3 UNION 结合 ORDER BY 的正确写法](#4.3 UNION 结合 ORDER BY 的正确写法)
- 五、子查询优化原理与实战
-
- [5.1 IN 子查询的物化优化](#5.1 IN 子查询的物化优化)
- [5.2 相关子查询(DEPENDENT SUBQUERY)优化](#5.2 相关子查询(DEPENDENT SUBQUERY)优化)
- [5.3 EXISTS vs IN 的选择](#5.3 EXISTS vs IN 的选择)
- [5.4 派生表(FROM 子查询)的物化与合并](#5.4 派生表(FROM 子查询)的物化与合并)
- [5.5 嵌套子查询改写实战](#5.5 嵌套子查询改写实战)
- [六、MySQL 参数调优](#六、MySQL 参数调优)
-
- [6.1 join_buffer_size --- JOIN 缓冲区](#6.1 join_buffer_size — JOIN 缓冲区)
- [6.2 sort_buffer_size --- 排序缓冲区](#6.2 sort_buffer_size — 排序缓冲区)
- [6.3 tmp_table_size / max_heap_table_size --- 临时表上限](#6.3 tmp_table_size / max_heap_table_size — 临时表上限)
- [6.4 innodb_buffer_pool_size --- 缓冲池大小](#6.4 innodb_buffer_pool_size — 缓冲池大小)
- [6.5 innodb_buffer_pool_instances --- 缓冲池实例数](#6.5 innodb_buffer_pool_instances — 缓冲池实例数)
- [6.6 read_rnd_buffer_size --- MRR 缓冲区](#6.6 read_rnd_buffer_size — MRR 缓冲区)
- [6.7 innodb_stats_persistent_sample_pages --- 统计信息采样页数](#6.7 innodb_stats_persistent_sample_pages — 统计信息采样页数)
- [6.8 参数调优总览](#6.8 参数调优总览)
- 七、优化检查清单
-
- [7.1 EXPLAIN 快速诊断流程](#7.1 EXPLAIN 快速诊断流程)
- [7.2 索引失效场景速查](#7.2 索引失效场景速查)
- [7.3 JOIN 优化原则](#7.3 JOIN 优化原则)
- [7.4 UNION 优化原则](#7.4 UNION 优化原则)
- [7.5 子查询优化原则](#7.5 子查询优化原则)
- [附录:常用诊断 SQL](#附录:常用诊断 SQL)
表与数据准备
sql
CREATE TABLE single_table (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
-- 创建第二张表用于 JOIN 演示
CREATE TABLE single_table2 (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2)
) ENGINE=InnoDB CHARSET=utf8;
-- 使用python脚本插入100万条数据
见下面python脚本代码
-- 插入部分数据(从 single_table 复制一些)
INSERT INTO single_table2 (key1, key2, common_field)
SELECT key1, key2, common_field FROM single_table LIMIT 10000;
python脚本插入数据
py
"""
Insert 100,000 random rows into single_table.
Requires: pip install pymysql
"""
import random
import string
import time
import pymysql
# ──────────────────────────────────────────
# Database connection config
# ──────────────────────────────────────────
DB_CONFIG = {
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "", # <- your password
"database": "", # <- your database name
"charset": "utf8mb4",
}
TOTAL_ROWS = 100_000 # total rows to insert
BATCH_SIZE = 1_000 # rows per batch commit
# ──────────────────────────────────────────
# Random data generators
# ──────────────────────────────────────────
def rand_str(min_len: int = 4, max_len: int = 20) -> str:
"""Generate a random lowercase string."""
length = random.randint(min_len, max_len)
return "".join(random.choices(string.ascii_lowercase, k=length))
def rand_str_or_none(none_prob: float = 0.1, **kwargs) -> str | None:
"""Return None with probability none_prob, otherwise a random string."""
return None if random.random() < none_prob else rand_str(**kwargs)
def rand_int_or_none(lo: int, hi: int, none_prob: float = 0.05) -> int | None:
"""Return None with probability none_prob, otherwise a random int."""
return None if random.random() < none_prob else random.randint(lo, hi)
def generate_batch(batch_size: int, used_key2: set) -> list[tuple]:
"""
Generate a batch of random rows.
key2 is a UNIQUE KEY, so duplicates are avoided via used_key2.
"""
rows = []
for _ in range(batch_size):
# Ensure key2 is unique across all inserted rows
while True:
k2 = rand_int_or_none(1, 10_000_000, none_prob=0.05)
if k2 is None or k2 not in used_key2:
if k2 is not None:
used_key2.add(k2)
break
rows.append((
rand_str_or_none(), # key1
k2, # key2 (unique)
rand_str_or_none(), # key3
rand_str_or_none(), # key_part1
rand_str_or_none(), # key_part2
rand_str_or_none(), # key_part3
rand_str(10, 50), # common_field (not null)
))
return rows
# ──────────────────────────────────────────
# Main
# ──────────────────────────────────────────
INSERT_SQL = """
INSERT INTO single_table
(key1, key2, key3, key_part1, key_part2, key_part3, common_field)
VALUES
(%s, %s, %s, %s, %s, %s, %s)
"""
def main():
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
print(f"[OK] Connected: {DB_CONFIG['host']}:{DB_CONFIG['port']} / {DB_CONFIG['database']}")
print(f"[>>] Inserting {TOTAL_ROWS:,} rows, batch size {BATCH_SIZE:,}...\n")
used_key2 = set()
inserted = 0
start_time = time.time()
try:
while inserted < TOTAL_ROWS:
current_batch = min(BATCH_SIZE, TOTAL_ROWS - inserted)
rows = generate_batch(current_batch, used_key2)
cursor.executemany(INSERT_SQL, rows)
conn.commit()
inserted += current_batch
elapsed = time.time() - start_time
speed = inserted / elapsed
remaining = (TOTAL_ROWS - inserted) / speed if speed > 0 else 0
print(
f" inserted: {inserted:>7,} / {TOTAL_ROWS:,}"
f" ({inserted / TOTAL_ROWS * 100:.1f}%)"
f" speed: {speed:,.0f} rows/s"
f" eta: {remaining:.1f}s"
)
except Exception as e:
conn.rollback()
print(f"\n[ERR] Rolled back: {e}")
raise
finally:
cursor.close()
conn.close()
total_time = time.time() - start_time
print(f"\n[DONE] {inserted:,} rows inserted in {total_time:.2f}s "
f"(avg {inserted / total_time:,.0f} rows/s)")
if __name__ == "__main__":
main()
一、EXPLA
IN 概述
EXPLAIN 是 MySQL 提供的查询执行计划分析工具,它能告诉你 MySQL 打算怎么执行这条 SQL,而不是实际去执行它。通过分析执行计划,可以发现:
- 是否走了索引,走了哪个索引
- 扫描了多少行数据
- 是否产生了临时表、文件排序
- JOIN 的驱动表顺序是否合理
sql
-- 基本用法
EXPLAIN SELECT * FROM single_table WHERE key1 = 'abc';
-- MySQL 8.0 支持 EXPLAIN ANALYZE(真实执行并返回实际耗时)
EXPLAIN ANALYZE SELECT * FROM single_table WHERE key1 = 'abc';
-- 格式化输出(树形结构,更直观)
EXPLAIN FORMAT=TREE SELECT * FROM single_table WHERE key1 = 'abc';
EXPLAIN 输出的完整列:
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
二、各字段深度解析
2.1 id --- 查询序号
id 标识每个独立的 SELECT 子句,是理解复杂查询执行顺序的关键。
规则:
- id 相同:从上到下顺序执行(同一层 JOIN)
- id 不同:id 越大越先执行(子查询先于外层)
- id 为 NULL:UNION 合并结果集的临时行
sql
-- 示例:子查询 id 不同
EXPLAIN
SELECT * FROM single_table s1
WHERE key2 = (
SELECT key2 FROM single_table s2 WHERE key1 = 'abc'
);
+----+-------------+-------+-------+---------------+----------+
| id | select_type | table | type | key | rows |
+----+-------------+-------+-------+---------------+----------+
| 1 | PRIMARY | s1 | const | idx_key2 | 1 | <- 后执行
| 2 | SUBQUERY | s2 | ref | idx_key1 | 1 | <- 先执行
+----+-------------+-------+-------+---------------+----------+
优化视角: 若子查询的 id 对应 rows 很大,说明子查询扫描量大,是优化重点。
2.2 select_type --- 查询类型
标识当前 SELECT 子句在整个查询中的角色。
| select_type | 说明 | 触发场景 |
|---|---|---|
SIMPLE |
简单查询 | 无子查询、无 UNION 的单表或 JOIN |
PRIMARY |
最外层查询 | 含子查询或 UNION 时的最外层 SELECT |
SUBQUERY |
不相关子查询 | SELECT/WHERE 中独立子查询 |
DEPENDENT SUBQUERY |
相关子查询 | 子查询引用了外层表的列 |
UNION |
UNION 第二个+ SELECT | SELECT ... UNION SELECT ... |
UNION RESULT |
UNION 去重临时表 | id 通常为 NULL |
DERIVED |
派生表 | FROM 子句中的子查询 |
MATERIALIZED |
物化子查询 | IN 子查询被优化为物化临时表(8.0) |
优化视角:
- 出现
DEPENDENT SUBQUERY说明子查询依赖外层,外层每行都要执行一次子查询,通常需要改写为 JOIN - 出现
DERIVED说明有派生表,考虑改写消除或确认有索引覆盖
2.3 table --- 当前行对应的表
通常是表名或别名,特殊情况如下:
| table 值 | 说明 |
|---|---|
表名/别名 |
正常的表 |
<unionM,N> |
id=M 和 id=N 的 UNION 结果集临时表 |
<derivedN> |
id=N 的派生表(FROM 子句子查询) |
<subqueryN> |
id=N 的物化子查询结果 |
2.4 partitions --- 分区信息
查询涉及的分区。非分区表此列为 NULL,分区表会显示实际访问的分区,用于验证分区裁剪是否生效。
sql
-- 分区表示例
EXPLAIN SELECT * FROM orders WHERE order_date = '2024-01-01';
-- partitions 列显示 p2024,说明只扫描了 2024 年的分区,其余分区被裁剪掉
2.5 type --- 访问类型(最重要)
type 是 EXPLAIN 中最核心的字段,直接反映查询性能优劣。
性能从优到差:
system > const > eq_ref > ref > ref_or_null > index_merge > range > index > ALL
system
表中只有一行数据(系统表的特殊情况),几乎不会出现在业务表中。
const (最优之一)
通过主键 或唯一索引进行等值查询,最多返回1行,MySQL 将其视为常量,只读一次磁盘。
sql
-- 主键等值查询
EXPLAIN SELECT * FROM single_table WHERE id = 1;
-- 唯一索引等值查询
EXPLAIN SELECT * FROM single_table WHERE key2 = 100;
| type | key | rows |
| const | PRIMARY | 1 |
原理: 优化器阶段直接将这个表的值替换成常量,后续查询不再访问该表。速度极快,可以认为是零代价查询。
eq_ref (最优之一)
多表 JOIN 时,被驱动表通过主键或唯一索引关联,每次只能匹配到1行。
sql
EXPLAIN
SELECT * FROM single_table s1
JOIN single_table s2 ON s1.id = s2.id
WHERE s1.key1 = 'abc';
| id | table | type | key | ref |
| 1 | s1 | ref | idx_key1| const |
| 1 | s2 | eq_ref | PRIMARY | s1.id | <- 每次精确匹配1行
ref
通过**普通索引(非唯一)**等值查询,可能匹配多行。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 = 'abc';
-- key1 是普通索引,可能有多条记录的 key1='abc'
| type | key | rows |
| ref | idx_key1 | 3 |
ref_or_null
在 ref 基础上,额外查找 NULL 值。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 = 'abc' OR key1 IS NULL;
index_merge
同时使用多个索引,将各自结果合并。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 = 'abc' OR key3 = 'xyz';
-- key1 走 idx_key1,key3 走 idx_key3,结果取并集
| type | key | Extra |
| index_merge | idx_key1,idx_key3 | Using union(idx_key1,idx_key3); Using where |
优化视角: index_merge 虽然用了多索引,但合并本身有开销。若该 SQL 执行频繁,考虑建联合索引替代两个单列索引。
range
索引范围扫描,扫描索引树的某个区间。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 > 'a' AND key1 < 'z';
EXPLAIN SELECT * FROM single_table WHERE key2 IN (1, 2, 3);
EXPLAIN SELECT * FROM single_table WHERE id BETWEEN 100 AND 200;
EXPLAIN SELECT * FROM single_table WHERE key1 LIKE 'abc%'; -- 前缀匹配
触发 range 的操作符:>、<、>=、<=、BETWEEN、IN()、LIKE 'xxx%'
index
全索引扫描,扫描整棵索引树,不回表。比 ALL 快,因为索引比完整数据行小很多。
sql
-- 查询的列恰好都在索引中(覆盖索引)
EXPLAIN SELECT key1 FROM single_table;
-- key1 在 idx_key1 上,只扫索引,不需要访问主键索引
| type | key | Extra |
| index | idx_key1 | Using index | <- Using index = 覆盖索引,不回表
ALL (必须优化)
全表扫描,从主键索引第一页读到最后一页,性能最差,百万数据表出现 ALL 必须优化。
sql
EXPLAIN SELECT * FROM single_table WHERE common_field = 'abc';
-- common_field 无索引,全表扫描
| type | key | rows | Extra |
| ALL | NULL | 980000 | Using where | <- 扫描近百万行!
2.6 possible_keys --- 候选索引
优化器考虑过但不一定最终使用的索引。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 = 'a' AND key2 = 100;
-- possible_keys 可能显示:idx_key1,idx_key2
-- key 只显示:idx_key2(优化器认为唯一索引代价更低)
优化视角:
possible_keys=NULL且type=ALL:完全没有可用索引,必须建索引possible_keys有值但key=NULL:有候选索引但优化器放弃了,通常因为数据量少或统计信息不准,可执行ANALYZE TABLE更新统计信息后再观察
2.7 key --- 实际使用的索引
优化器最终选择的索引。
sql
-- 强制使用指定索引(统计信息严重不准时临时使用)
SELECT * FROM single_table FORCE INDEX(idx_key1) WHERE key1 > 'a';
-- 忽略某个索引(让优化器排除某个错误选择)
SELECT * FROM single_table IGNORE INDEX(idx_key1) WHERE key1 > 'a';
-- 更新统计信息(优化器选错索引时的根本解决方案)
ANALYZE TABLE single_table;
2.8 key_len --- 索引使用的字节数
实际使用索引的字节数,用于判断联合索引用了几列。key_len 越小在相同效果下性能越好(用更少字节定位数据)。
计算规则:
| 字段类型 | 计算公式 | 说明 |
|---|---|---|
INT NOT NULL |
4 | 固定4字节 |
INT NULL |
5 | 额外1字节存 NULL 标志 |
BIGINT NOT NULL |
8 | 固定8字节 |
VARCHAR(N) utf8 NOT NULL |
N×3 + 2 | 2字节存变长长度 |
VARCHAR(N) utf8 NULL |
N×3 + 2 + 1 | 额外1字节存 NULL 标志 |
VARCHAR(N) utf8mb4 NOT NULL |
N×4 + 2 | utf8mb4 每字符最多4字节 |
CHAR(N) utf8 NOT NULL |
N×3 | 定长,无需存长度 |
DATETIME |
5 或 8 | MySQL 5.6.4+ 为5字节 |
sql
-- single_table 中:key_part1/2/3 均为 VARCHAR(100) utf8,允许 NULL
-- 每列 key_len = 100*3 + 2 + 1 = 303
-- 联合索引 idx_key_part(key_part1, key_part2, key_part3)
EXPLAIN SELECT * FROM single_table WHERE key_part1 = 'a';
-- key_len = 303 -> 只用了第1列
EXPLAIN SELECT * FROM single_table WHERE key_part1 = 'a' AND key_part2 = 'b';
-- key_len = 606 -> 用了前2列
EXPLAIN SELECT * FROM single_table
WHERE key_part1 = 'a' AND key_part2 = 'b' AND key_part3 = 'c';
-- key_len = 909 -> 全部3列都用上了
优化视角: 如果期望联合索引用3列,但 key_len 只对应1列的字节数,说明后续列的条件没有走索引(通常是违反了最左前缀原则)。
2.9 ref --- 索引匹配的值来源
| ref 值 | 说明 | 场景 |
|---|---|---|
const |
与常量比较 | WHERE key1 = 'abc' |
db.table.column |
与另一张表的列比较 | JOIN 关联条件 |
func |
与函数结果比较 | WHERE key1 = UPPER('abc') |
NULL |
范围查询或全表扫描 | range / ALL 类型 |
优化视角: ref 为 func 时,函数会导致索引无法直接定位,应将函数移到等号右边:
sql
-- 错误:对索引列使用函数,索引失效
SELECT * FROM single_table WHERE UPPER(key1) = 'ABC';
-- 正确:对常量侧使用函数,索引有效
SELECT * FROM single_table WHERE key1 = LOWER('ABC');
2.10 rows --- 预估扫描行数
优化器基于索引统计信息估算的扫描行数,不是精确值,误差可能达到 ±50%。
rows = 1 极好,精确定位
rows = 1000 良好,范围扫描
rows = 100万 全表扫描,必须优化
rows 在 JOIN 中的意义尤为重要:
JOIN 总扫描量 ≈ 驱动表 rows × 被驱动表单次查找代价
JOIN 优化的核心目标是:让驱动表的结果集(rows × filtered)尽量小。
2.11 filtered --- 条件过滤百分比
经过 WHERE 条件过滤后,剩余行占扫描行的百分比。
最终传给上层的估算行数 = rows × (filtered / 100)
sql
EXPLAIN SELECT * FROM single_table WHERE key1 > 'a' AND common_field = 'xyz';
-- rows = 50000, filtered = 2.00
-- 实际传给上层的行数估算 = 50000 * 2% = 1000 行
优化视角: filtered 越低说明过滤效果越好。在 JOIN 中,驱动表的 rows × filtered% 决定了被驱动表被访问的次数,应尽量减小这个值。
2.12 Extra --- 额外执行信息(重要)
Extra 是诊断性能问题最重要的补充字段,包含优化器对执行方式的关键说明。
Using index (最优)
覆盖索引,查询所需的列都在索引中,无需回表读取主键索引。
sql
-- key1 和 id 都可以从 idx_key1 中获取(叶子节点存主键值)
EXPLAIN SELECT id, key1 FROM single_table WHERE key1 = 'abc';
-- Extra: Using index
原理: InnoDB 二级索引的叶子节点存储的是 (索引键值, 主键值),如果查询字段都在这里,就不需要再根据主键回聚簇索引查完整行数据了,节省一次随机 IO。
Using where
Server 层在存储引擎返回数据后进行了额外的 WHERE 过滤,说明索引没有完全覆盖 WHERE 条件。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 = 'abc' AND common_field = 'xyz';
-- key1 用了索引,但 common_field 没有索引,需要 Server 层再过滤
-- Extra: Using where
Using index condition (索引下推 ICP)
MySQL 5.6+ 的索引下推优化(Index Condition Pushdown),将 WHERE 条件中能用索引判断的部分下推到存储引擎层,减少回表次数。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 > 'a' AND key1 LIKE '%bc';
-- key1 > 'a' 用索引范围扫描,key1 LIKE '%bc' 在索引层提前过滤
-- Extra: Using index condition
原理对比:
无 ICP(MySQL 5.5 及以前):
存储引擎:用 key1>'a' 找到所有行 -> 全部回表读完整数据
Server 层:对回表数据过滤 LIKE '%bc'
问题:大量不满足 LIKE 的行也做了无用的回表 IO
有 ICP(MySQL 5.6+):
存储引擎:用 key1>'a' 找到索引行 -> 在索引层检查 LIKE '%bc' -> 只满足的才回表
减少了大量不必要的回表,IO 次数大幅降低
Using filesort (需关注)
无法利用索引完成排序,需要额外的排序操作(在内存或磁盘)。
sql
EXPLAIN SELECT * FROM single_table WHERE key1 = 'a' ORDER BY key2;
-- key1 走了 idx_key1,但排序字段 key2 不在同一索引中
-- Extra: Using filesort
排序算法原理:
单路排序(默认,sort_buffer 足够时):
一次性将查询所需的所有列读入 sort_buffer
在 sort_buffer 中直接排序后输出
优点:只需一次 IO
缺点:sort_buffer 占用内存大
双路排序(sort_buffer 不足时):
先读排序列 + 行指针,排序后再回表读完整数据
优点:sort_buffer 占用内存小
缺点:需要两次 IO,回表代价高
优化方向: 建立覆盖排序字段的联合索引。
sql
-- 优化前:key1 过滤,key2 排序,Using filesort
EXPLAIN SELECT * FROM single_table WHERE key1 = 'a' ORDER BY key2;
-- 建联合索引
ALTER TABLE single_table ADD INDEX idx_key1_key2 (key1, key2);
-- 优化后:Using index(覆盖索引 + 索引排序,无需额外排序)
EXPLAIN SELECT id, key1, key2 FROM single_table WHERE key1 = 'a' ORDER BY key2;
Using temporary (需优化)
使用了内部临时表,通常出现在 GROUP BY、DISTINCT、UNION(非ALL)中。
sql
-- key1 有索引,利用索引分组,无临时表
EXPLAIN SELECT key1, COUNT(*) FROM single_table GROUP BY key1;
-- Extra: Using index
-- common_field 无索引,需要临时表
EXPLAIN SELECT common_field, COUNT(*) FROM single_table GROUP BY common_field;
-- Extra: Using temporary; Using filesort
原理: 无索引的 GROUP BY 需要先将数据放入临时表,再对临时表分组统计。临时表若超过 tmp_table_size 则转为磁盘临时表,性能骤降。
优化方向: 给 GROUP BY、ORDER BY 字段加索引。
Using join buffer (Block Nested Loop) (需优化)
JOIN 时被驱动表无索引,使用了 Join Buffer 内存块批量匹配。
sql
EXPLAIN SELECT * FROM single_table s1
JOIN single_table s2 ON s1.common_field = s2.common_field;
-- common_field 无索引
-- s2 行: Extra: Using where; Using join buffer (Block Nested Loop)
原理:
无 Join Buffer(逐行 Nested Loop):
for 驱动表每一行:
全表扫描被驱动表,逐行比对
总比较次数 = 驱动表行数 * 被驱动表行数
有 Join Buffer(Block Nested Loop):
批量读取驱动表数据放入 Join Buffer(一次读多行)
全表扫描被驱动表一次,与 Buffer 中所有行批量比对
IO 次数 = ceil(驱动表行数 / Join Buffer 能放的行数)
被驱动表全表扫描次数大幅减少
优化方向: 给 JOIN 条件的被驱动表字段加索引,彻底消除 Join Buffer 的使用。
Select tables optimized away (最优)
直接通过索引返回 MIN/MAX 聚合结果,无需扫描数据行。
sql
EXPLAIN SELECT MIN(key2), MAX(key2) FROM single_table;
-- key2 有索引,直接读索引树的最左/最右叶子节点
-- Extra: Select tables optimized away
Impossible WHERE
WHERE 条件永远为 false,不会扫描任何数据。
sql
EXPLAIN SELECT * FROM single_table WHERE id > 100 AND id < 1;
-- Extra: Impossible WHERE
三、JOIN 优化原理与实战
3.1 驱动表与被驱动表的选择原理
MySQL 执行 JOIN 的核心算法是 Nested Loop Join(嵌套循环连接):
for 驱动表中每一行 R:
在被驱动表中查找满足 JOIN 条件的行
拼接 R 与匹配行,输出结果
因此 JOIN 的总代价约为:驱动表行数 × 被驱动表单次查找代价
- 被驱动表有索引:单次查找代价 = O(log n),非常快
- 被驱动表无索引:单次查找代价 = O(n),全表扫描,极慢
驱动表选择规则:
INNER JOIN:MySQL 优化器自动选择,通常选结果集小的表作为驱动表LEFT JOIN:左表固定为驱动表,优化器不能调换顺序RIGHT JOIN:右表固定为驱动表
3.2 INNER JOIN 优化实战
sql
-- 两表关联,筛选出 key2=100 的数据与 key1 匹配
EXPLAIN SELECT s1.id, s1.key1, s2.common_field
FROM single_table s1
INNER JOIN single_table s2 ON s1.key1 = s2.key1
WHERE s1.key2 = 100;
最优执行计划:
+----+-------+--------+----------+---------+-----------+------+
| id | table | type | key | key_len | ref | rows |
+----+-------+--------+----------+---------+-----------+------+
| 1 | s1 | const | idx_key2 | 5 | const | 1 | <- 驱动,唯一索引精确定位
| 1 | s2 | ref | idx_key1 | 303 | s1.key1 | 1 | <- 被驱动,普通索引匹配
+----+-------+--------+----------+---------+-----------+------+
解读: s1 通过唯一索引 key2=100 精确定位1行,s2 用 s1.key1 关联,整体扫描量极小。
3.3 被驱动表无索引的问题与优化
sql
-- 问题:JOIN 条件列 common_field 无索引
EXPLAIN SELECT s1.id, s2.common_field
FROM single_table s1
JOIN single_table s2 ON s1.common_field = s2.common_field
WHERE s1.key1 = 'abc';
+----+-------+------+----------+--------+---------------------------------------------------+
| id | table | type | key | rows | Extra |
+----+-------+------+----------+--------+---------------------------------------------------+
| 1 | s1 | ref | idx_key1 | 1 | Using where |
| 1 | s2 | ALL | NULL | 980000 | Using where; Using join buffer(Block Nested Loop) |
+----+-------+------+----------+--------+---------------------------------------------------+
问题分析: s2 的 type=ALL,每次用 s1 的一行去全表扫 s2,s1 有1行结果,则 s2 被全表扫1次,扫98万行。
优化方案:
sql
-- 给 JOIN 条件的被驱动表字段加索引(最根本的解决方案)
ALTER TABLE single_table ADD INDEX idx_common (common_field);
-- 再次 EXPLAIN,s2 的 type 从 ALL 变为 ref,Using join buffer 消失
3.4 LEFT JOIN 的特殊注意事项
sql
-- LEFT JOIN:左表固定为驱动表,即使右表数据量更小
EXPLAIN SELECT s1.id, s1.key1, s2.key1 AS s2_key1
FROM single_table s1
LEFT JOIN single_table s2 ON s1.key2 = s2.key2
WHERE s1.key1 > 'a';
+----+-------+-------+----------+---------+---------+-------+------------------------+
| id | table | type | key | key_len | ref | rows | Extra |
+----+-------+-------+----------+---------+---------+-------+------------------------+
| 1 | s1 | range | idx_key1 | 303 | NULL | 50000 | Using index condition |
| 1 | s2 | eq_ref| idx_key2 | 5 | s1.key2 | 1 | Using where; Using index|
+----+-------+-------+----------+---------+---------+-------+------------------------+
LEFT JOIN 优化关键: 虽然驱动表不能换,但必须保证被驱动表(右表)的关联字段有索引,否则每次都要全表扫右表。
3.5 覆盖索引消除回表
sql
-- 优化前:查询包含 common_field,idx_key1 无法覆盖,需要回表
EXPLAIN SELECT id, key1, common_field FROM single_table WHERE key1 = 'abc';
-- Extra: NULL(需要回表读 common_field)
-- 优化后:建联合索引覆盖所有查询列
ALTER TABLE single_table ADD INDEX idx_key1_common (key1, common_field);
EXPLAIN SELECT id, key1, common_field FROM single_table WHERE key1 = 'abc';
-- Extra: Using index(覆盖索引,不回表)
原理: InnoDB 聚簇索引叶子节点存完整行数据,二级索引叶子节点存 (索引键, 主键值)。若查询字段都能从二级索引中获取,就无需根据主键再去聚簇索引读一次数据,节省一次随机 IO。对于高频查询,覆盖索引的收益非常显著。
3.6 多表 JOIN 的执行顺序分析
sql
-- 三表 JOIN,观察执行顺序
EXPLAIN SELECT s1.key1, s2.key1, s3.key1
FROM single_table s1
JOIN single_table s2 ON s1.key1 = s2.key1
JOIN single_table s3 ON s2.key2 = s3.key2
WHERE s1.key2 = 100;
+----+-------+--------+----------+---------+-----------+------+
| id | table | type | key | key_len | ref | rows |
+----+-------+--------+----------+---------+-----------+------+
| 1 | s1 | const | idx_key2 | 5 | const | 1 | <- 第1步,唯一索引定位
| 1 | s2 | ref | idx_key1 | 303 | s1.key1 | 1 | <- 第2步,用s1结果匹配
| 1 | s3 | eq_ref | idx_key2 | 5 | s2.key2 | 1 | <- 第3步,唯一索引关联
+----+-------+--------+----------+---------+-----------+------+
多表 JOIN 优化原则:
1. 第一个表(驱动表)的过滤条件要尽量精准,减少结果集
2. 每步的 rows 值应尽量小(理想情况每步都是1)
3. 每个 JOIN 条件的被驱动表字段必须有索引
4. 超过3张表的 JOIN 要谨慎,考虑分步查询后在应用层组合
四、UNION 优化原理与实战
4.1 UNION vs UNION ALL 底层原理
UNION 执行流程:
1. 执行第一个 SELECT,结果写入内部临时表(带唯一性约束)
2. 执行第二个 SELECT,逐行插入临时表(重复行自动忽略)
3. 扫描临时表,返回最终结果
开销:创建临时表 + 去重哈希比较 + 扫描临时表
UNION ALL 执行流程:
1. 执行第一个 SELECT,直接输出
2. 执行第二个 SELECT,追加输出
开销:仅两次 SELECT,无任何额外操作
sql
-- UNION:产生 UNION RESULT 行(内部临时表)
EXPLAIN
SELECT id, key1 FROM single_table WHERE key1 = 'a'
UNION
SELECT id, key1 FROM single_table WHERE key1 = 'b';
+------+--------------+-----------+------+----------+-----------------+
| id | select_type | table | type | key | Extra |
+------+--------------+-----------+------+----------+-----------------+
| 1 | PRIMARY | single... | ref | idx_key1 | NULL |
| 2 | UNION | single... | ref | idx_key1 | NULL |
| NULL | UNION RESULT | <union1,2>| ALL | NULL | Using temporary |
+------+--------------+-----------+------+----------+-----------------+
sql
-- UNION ALL:无 UNION RESULT 行,无临时表
EXPLAIN
SELECT id, key1 FROM single_table WHERE key1 = 'a'
UNION ALL
SELECT id, key1 FROM single_table WHERE key1 = 'b';
+----+-------------+-----------+------+----------+------+
| id | select_type | table | type | key | Extra|
+----+-------------+-----------+------+----------+------+
| 1 | PRIMARY | single... | ref | idx_key1 | NULL |
| 2 | UNION | single... | ref | idx_key1 | NULL |
+----+-------------+-----------+------+----------+------+
-- 没有第三行,没有 Using temporary
优化原则: 业务上确认无重复数据(或允许重复),优先用 UNION ALL,避免不必要的临时表开销。
4.2 UNION 临时表的内存控制
UNION 产生的临时表受以下参数控制:
sql
SHOW VARIABLES LIKE 'tmp_table_size'; -- 内存临时表上限(默认16MB)
SHOW VARIABLES LIKE 'max_heap_table_size'; -- 堆内存表上限(默认16MB)
-- 内存临时表大小 = MIN(tmp_table_size, max_heap_table_size)
-- 超出后转为磁盘临时表(MyISAM),性能急剧下降
4.3 UNION 结合 ORDER BY 的正确写法
sql
-- 错误写法:整体排序放在子查询内,MySQL 可能忽略
SELECT * FROM (
SELECT id, key1 FROM single_table WHERE key1 > 'a' ORDER BY key1
UNION ALL
SELECT id, key1 FROM single_table WHERE key3 > 'b' ORDER BY key1
) t;
-- 正确写法:ORDER BY 放在最外层
SELECT id, key1 FROM single_table WHERE key1 > 'a'
UNION ALL
SELECT id, key1 FROM single_table WHERE key3 > 'b'
ORDER BY key1
LIMIT 100;
五、子查询优化原理与实战
5.1 IN 子查询的物化优化
sql
-- IN 子查询
EXPLAIN SELECT * FROM single_table
WHERE key1 IN (
SELECT key1 FROM single_table WHERE key2 > 100 AND key2 < 200
);
MySQL 5.6+ 的物化优化(Materialization):
传统执行(低效):
对外层每一行,都执行一次子查询
子查询执行次数 = 外层表行数
总代价 = N 次子查询
物化优化(MySQL 5.6+):
1. 执行一次子查询,结果存入内部哈希临时表(物化表)
2. 外层查询与物化表做 JOIN 匹配
子查询只执行一次,效率大幅提升
EXPLAIN 中 select_type 会显示 MATERIALIZED:
+----+--------------+-------+-------+----------+
| id | select_type | table | type | key |
+----+--------------+-------+-------+----------+
| 1 | SIMPLE | s1 | ref | idx_key1 | <- 与物化表 JOIN
| 2 | MATERIALIZED | s2 | range | idx_key2 | <- 子查询被物化为临时表
+----+--------------+-------+-------+----------+
5.2 相关子查询(DEPENDENT SUBQUERY)优化
相关子查询是性能杀手,外层每返回一行,子查询就执行一次,N行就执行 N 次。
sql
-- 低效:相关子查询
EXPLAIN SELECT * FROM single_table s1
WHERE key2 > (
SELECT AVG(key2) FROM single_table s2 WHERE s2.key1 = s1.key1
);
+----+--------------------+-------+
| id | select_type | table |
+----+--------------------+-------+
| 1 | PRIMARY | s1 |
| 2 | DEPENDENT SUBQUERY | s2 | <- 随 s1 每行执行一次,极慢!
+----+--------------------+-------+
优化:将相关子查询改写为 JOIN
sql
-- 高效:用 JOIN 替代相关子查询
SELECT s1.* FROM single_table s1
JOIN (
SELECT key1, AVG(key2) AS avg_key2
FROM single_table
GROUP BY key1
) t ON s1.key1 = t.key1
WHERE s1.key2 > t.avg_key2;
改写后,内层聚合只执行一次,然后与 s1 做 JOIN,性能从 O(n²) 降为 O(n)。
5.3 EXISTS vs IN 的选择
sql
-- IN:先执行子查询得到结果集,再与外表匹配
-- 适合:子查询结果集小,外表大
SELECT * FROM single_table
WHERE key1 IN (SELECT key1 FROM small_table WHERE condition);
-- EXISTS:外表每行去子查询中检查是否存在
-- 适合:外表结果集小,被查询表大(且有索引)
SELECT * FROM single_table s1
WHERE EXISTS (
SELECT 1 FROM single_table s2 WHERE s2.key1 = s1.key1 AND s2.key2 > 1000
);
选择原则:
外表大,子查询结果集小 -> 用 IN(子查询结果集放内存,外表循环匹配)
外表小,子查询表大 -> 用 EXISTS(外表行数少,每次用子查询索引查找)
MySQL 8.0 优化器基本能自动处理 IN 和 EXISTS 的转换,但了解原理有助于在老版本和特殊场景下手动优化。
5.4 派生表(FROM 子查询)的物化与合并
sql
-- 派生表(FROM 中的子查询)
EXPLAIN SELECT t.key1, t.cnt
FROM (
SELECT key1, COUNT(*) AS cnt
FROM single_table
GROUP BY key1
) t
WHERE t.cnt > 10;
MySQL 5.7+ 的 Derived Merge 优化:
当条件允许时,MySQL 会将派生表与外层查询合并,避免创建中间临时表:
sql
-- MySQL 5.7+ 可能自动改写为:
SELECT key1, COUNT(*) AS cnt
FROM single_table
GROUP BY key1
HAVING COUNT(*) > 10;
-- 直接避免了派生表临时表的创建
查看是否启用派生表合并:
sql
SHOW VARIABLES LIKE 'optimizer_switch';
-- 找到 derived_merge=on 表示已启用(默认开启)
若派生表含 LIMIT、UNION、聚合等,无法合并,会产生物化临时表。
5.5 嵌套子查询改写实战
sql
-- 低效:三层嵌套子查询
SELECT * FROM single_table
WHERE key1 IN (
SELECT key1 FROM single_table
WHERE key2 IN (
SELECT key2 FROM single_table WHERE common_field = 'abc'
)
);
-- 高效:改写为两次 JOIN
SELECT DISTINCT s1.*
FROM single_table s1
JOIN single_table s2 ON s1.key1 = s2.key1
JOIN single_table s3 ON s2.key2 = s3.key2
WHERE s3.common_field = 'abc';
六、MySQL 参数调优
6.1 join_buffer_size --- JOIN 缓冲区
被驱动表无索引时,用于 Block Nested Loop JOIN 的缓冲区,一次性放入更多驱动表数据减少被驱动表扫描次数。
sql
-- 查看当前值
SHOW VARIABLES LIKE 'join_buffer_size';
-- 默认:262144(256KB)
-- 会话级临时调整(仅当前连接)
SET SESSION join_buffer_size = 8 * 1024 * 1024; -- 8MB
-- 全局调整
SET GLOBAL join_buffer_size = 4 * 1024 * 1024; -- 4MB
ini
# my.cnf
[mysqld]
join_buffer_size = 4M
调优建议:
- 默认 256KB 对大表 JOIN 不足,可调整至 4MB~8MB
- 不要设置过大:每个需要 Join Buffer 的查询都会独立分配,高并发时容易耗尽内存
- 根本优化是给 JOIN 字段加索引,消除 Join Buffer 的使用,而不是无限增大 join_buffer_size
6.2 sort_buffer_size --- 排序缓冲区
每个需要 ORDER BY / GROUP BY 排序的查询分配的内存缓冲区。
sql
SHOW VARIABLES LIKE 'sort_buffer_size';
-- 默认:262144(256KB)
ini
[mysqld]
sort_buffer_size = 2M # 建议 1MB~4MB,根据并发连接数调整
工作原理:
数据量 <= sort_buffer_size:
全部读入内存,快速排序,直接返回(最优)
数据量 > sort_buffer_size:
数据分批读入,每批排序后写入临时文件
最终对所有临时文件做归并排序(性能差)
临时文件数 = ceil(数据量 / sort_buffer_size)
监控排序溢出:
sql
SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
-- 值 > 0 说明发生了排序文件合并(sort_buffer 不足),考虑增大 sort_buffer_size
6.3 tmp_table_size / max_heap_table_size --- 临时表上限
影响 GROUP BY、UNION、子查询等操作中内存临时表的大小上限。
sql
SHOW VARIABLES LIKE 'tmp_table_size'; -- 默认 16MB
SHOW VARIABLES LIKE 'max_heap_table_size'; -- 默认 16MB
-- 实际内存临时表大小 = MIN(tmp_table_size, max_heap_table_size)
-- 超出后转为磁盘临时表(TempTable 引擎 / MyISAM),性能急剧下降
ini
[mysqld]
tmp_table_size = 64M
max_heap_table_size = 64M
监控磁盘临时表溢出率:
sql
SELECT
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Created_tmp_disk_tables') AS disk_tmp,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Created_tmp_tables') AS total_tmp;
-- 磁盘临时表比例 = disk_tmp / total_tmp
-- 理想值 < 5%,若比例过高,增大 tmp_table_size
6.4 innodb_buffer_pool_size --- 缓冲池大小
InnoDB 最重要的内存参数,缓存数据页和索引页,减少磁盘 IO。几乎所有查询性能都受此影响。
sql
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 默认 128MB,生产环境严重不足
ini
[mysqld]
# 建议设置为物理内存的 50%~75%
# 16GB 内存的服务器推荐配置:
innodb_buffer_pool_size = 10G
监控命中率:
sql
SELECT
ROUND(hit_rate / 10, 2) AS hit_rate_pct,
ROUND(pages_data * 16 / 1024, 0) AS used_mb,
ROUND(pages_free * 16 / 1024, 0) AS free_mb,
ROUND(pages_dirty * 16 / 1024, 0) AS dirty_mb
FROM information_schema.INNODB_BUFFER_POOL_STATS;
-- hit_rate_pct < 99% 时,考虑增大 innodb_buffer_pool_size
6.5 innodb_buffer_pool_instances --- 缓冲池实例数
将 Buffer Pool 分成多个独立实例,减少多线程并发访问时的 mutex 竞争。
ini
[mysqld]
innodb_buffer_pool_size = 10G
innodb_buffer_pool_instances = 8 # 每个实例约 1.25GB
# 建议:buffer_pool >= 1GB 时,instances = min(CPU核心数, 8)
6.6 read_rnd_buffer_size --- MRR 缓冲区
Multi-Range Read(MRR)优化使用的缓冲区,将二级索引的随机回表 IO 转换为顺序 IO。
sql
SHOW VARIABLES LIKE 'read_rnd_buffer_size';
-- 默认 256KB
ini
[mysqld]
read_rnd_buffer_size = 4M # 建议 4MB~8MB
MRR 原理:
无 MRR(传统回表):
按二级索引顺序逐行回表 -> 主键索引随机 IO,代价高
有 MRR:
批量收集二级索引中的主键值 -> 按主键排序 -> 顺序回表
随机 IO 转为顺序 IO,尤其对机械硬盘效果显著
验证 MRR 是否生效(Extra 中显示 Using MRR):
sql
SET optimizer_switch = 'mrr=on,mrr_cost_based=off';
EXPLAIN SELECT * FROM single_table WHERE key1 > 'a' AND key1 < 'z';
-- Extra: Using index condition; Using MRR
6.7 innodb_stats_persistent_sample_pages --- 统计信息采样页数
收集索引统计信息时采样的页数,影响优化器对 rows、filtered 的估算准确性。
sql
SHOW VARIABLES LIKE 'innodb_stats_persistent_sample_pages';
-- 默认 20
ini
[mysqld]
innodb_stats_persistent_sample_pages = 64 # 提高准确性,建议 32~200
当优化器选错索引时:
sql
-- 手动更新统计信息
ANALYZE TABLE single_table;
-- 查看某索引的统计信息
SELECT * FROM mysql.innodb_index_stats
WHERE database_name = 'your_db' AND table_name = 'single_table';
6.8 参数调优总览
| 参数 | 默认值 | 建议值 | 影响场景 |
|---|---|---|---|
join_buffer_size |
256KB | 4MB~8MB | 无索引 JOIN 的 Block Nested Loop |
sort_buffer_size |
256KB | 1MB~4MB | ORDER BY / GROUP BY 排序 |
tmp_table_size |
16MB | 64MB~256MB | UNION / GROUP BY 临时表 |
max_heap_table_size |
16MB | 64MB~256MB | 内存临时表大小上限 |
innodb_buffer_pool_size |
128MB | 物理内存的 60%~75% | 数据/索引页缓存 |
innodb_buffer_pool_instances |
1 | 8(缓冲池 >= 1GB 时) | 并发访问 Buffer Pool |
read_rnd_buffer_size |
256KB | 4MB~8MB | MRR 随机 IO 转顺序 IO |
innodb_stats_persistent_sample_pages |
20 | 64 | 统计信息准确性 |
七、优化检查清单
7.1 EXPLAIN 快速诊断流程
拿到一条慢 SQL,按以下顺序检查 EXPLAIN 结果
|
+-- Step 1: 看 type 列(每一行)
| ALL -> 必须优化,加索引或改写查询
| index -> 全索引扫描,确认是否真的需要
| range+ -> 基本合格
| ref/const/eq_ref -> 最优
|
+-- Step 2: 看 rows 列(每一行)
| rows 接近总行数 -> 索引过滤效果差,考虑换索引或优化条件
| JOIN 中看 rows 乘积 -> 驱动表 rows 越小越好
|
+-- Step 3: 看 Extra 列(每一行)
| Using filesort -> 建覆盖排序字段的联合索引
| Using temporary -> 给 GROUP BY 字段加索引
| Using join buffer-> 给 JOIN 条件字段加索引
| Using index -> 最优,覆盖索引无需回表
|
+-- Step 4: 看 key 和 possible_keys
| possible_keys 有值但 key=NULL -> ANALYZE TABLE 更新统计信息
| possible_keys=NULL -> 建索引
|
+-- Step 5: 多表 JOIN 验证执行顺序
id 相同的行从上到下是驱动顺序
确认驱动表结果集(rows * filtered%)是否最小
7.2 索引失效场景速查
sql
-- 1. 对索引列使用函数 -> 失效
WHERE YEAR(create_time) = 2024 -- 失效
WHERE create_time >= '2024-01-01' -- 有效,改为范围查询
-- 2. 对索引列做运算 -> 失效
WHERE key2 + 1 = 100 -- 失效
WHERE key2 = 99 -- 有效
-- 3. 隐式类型转换 -> 失效(VARCHAR 字段传数字)
WHERE key1 = 123 -- 失效(key1 是 VARCHAR)
WHERE key1 = '123' -- 有效
-- 4. LIKE 以通配符开头 -> 失效
WHERE key1 LIKE '%abc' -- 失效(无法利用 B+Tree 前缀)
WHERE key1 LIKE 'abc%' -- 有效(前缀匹配)
-- 5. 违反最左前缀原则 -> 联合索引失效
-- idx_key_part(key_part1, key_part2, key_part3)
WHERE key_part2 = 'b' -- 失效(跳过了 key_part1)
WHERE key_part1 = 'a' AND key_part2 = 'b' -- 有效
-- 6. OR 连接非索引列 -> 失效
WHERE key1 = 'a' OR common_field = 'b' -- 失效(common_field 无索引)
WHERE key1 = 'a' OR key3 = 'b' -- 有效(两列都有索引,走 index_merge)
-- 7. NOT IN / NOT EXISTS -> 通常无法走索引
WHERE key1 NOT IN ('a', 'b') -- 通常全表扫描
WHERE key1 IN ('a', 'b') -- 走 range
7.3 JOIN 优化原则
必须做:
被驱动表的 JOIN 条件字段必须有索引
WHERE 条件尽量在驱动表上(减少驱动表结果集大小)
只查需要的列(避免 SELECT *,减少回表和排序数据量)
建议做:
INNER JOIN 时让小结果集的表做驱动表(优化器通常会自动处理)
LEFT JOIN 时注意左表数据量,左表大时考虑能否改写为 INNER JOIN
超过3张表的 JOIN 考虑拆分为多次查询
避免:
JOIN 条件中对字段使用函数或运算
JOIN 条件两侧字段类型不一致(触发隐式转换)
笛卡尔积(忘写 ON 条件)
7.4 UNION 优化原则
优先 UNION ALL:确认无重复或不关心重复时,永远用 UNION ALL
ORDER BY 放外层:UNION 中每个子查询的 ORDER BY 可能被忽略
控制结果集大小:各子查询加 WHERE 条件后再 UNION,而不是先 UNION 再过滤
7.5 子查询优化原则
相关子查询必须改写:DEPENDENT SUBQUERY 是性能杀手,改为 JOIN
IN 子查询检查物化:MySQL 8.0 会自动物化,老版本手动改写为 JOIN 更稳妥
派生表尽量简化:利用 derived_merge 特性,避免产生物化临时表
嵌套层数控制:子查询嵌套不超过2层,更深的改写为 JOIN
附录:常用诊断 SQL
sql
-- 1. 查当前正在执行的慢查询
SELECT * FROM information_schema.PROCESSLIST
WHERE TIME > 5 AND COMMAND != 'Sleep'
ORDER BY TIME DESC;
-- 2. 查历史慢查询 TOP 10(按总耗时)
SELECT
DIGEST_TEXT AS sql_template,
COUNT_STAR AS exec_count,
ROUND(SUM_TIMER_WAIT / 1e12, 2) AS total_sec,
ROUND(AVG_TIMER_WAIT / 1e12, 3) AS avg_sec,
SUM_ROWS_EXAMINED AS rows_examined,
SUM_NO_INDEX_USED AS no_index_count
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'your_database'
ORDER BY total_sec DESC
LIMIT 10;
-- 3. 查未使用的索引(需开启 performance_schema)
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'your_database';
-- 4. 查冗余索引
SELECT * FROM sys.schema_redundant_indexes
WHERE table_schema = 'your_database';
-- 5. 更新统计信息(执行计划异常时)
ANALYZE TABLE single_table;
-- 6. 查 Buffer Pool 命中率
SELECT
ROUND(hit_rate / 10, 2) AS hit_rate_pct,
ROUND(pages_data * 16 / 1024, 0) AS used_mb,
ROUND(pages_free * 16 / 1024, 0) AS free_mb
FROM information_schema.INNODB_BUFFER_POOL_STATS;
-- 7. 查排序溢出次数
SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
-- 8. 查磁盘临时表比例
SELECT
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Created_tmp_disk_tables') AS disk_tmp_tables,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Created_tmp_tables') AS total_tmp_tables;