七:EXPLAIN 深度解析与 SQL 优化实战指南

MySQL EXPLAIN 深度解析与 SQL 优化实战指南

适用版本:MySQL 5.7 / 8.0

标签:MySQL EXPLAIN SQL优化 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 的操作符:><>=<=BETWEENIN()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=NULLtype=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;

相关推荐
polaris06302 小时前
使用Dify访问数据库(mysql)
数据库·mysql
数据知道2 小时前
MongoDB分片集群组件详解:Config Server, Mongos, Shard角色与配置
数据库·mongodb
执笔画情ora2 小时前
pg数据库管理-PostgreSQL 的 COPY TO 和 COPY FROM 命令
数据库·postgresql
璞~2 小时前
DBeaver 连接达梦数据库(DM8)完整步骤
数据库·oracle
一只小bit2 小时前
JavaWeb 开发 —— 从 JDBC 到 Mybatis 数据库使用
数据库·maven·mybatis
爱吃牛肉的大老虎2 小时前
PostgreSQL基础之安装
数据库·postgresql
yttandb2 小时前
数据库的设计
java·数据库
Gauss松鼠会2 小时前
openGauss数据库源码解析系列文章——存储引擎源码解析(一)
数据库·oracle·性能优化·database·opengauss
y = xⁿ2 小时前
【黑马店铺二刷day02】将店铺查询信息添加到Redis中的业务操作
数据库·redis·缓存