【数据库】MySQL 实战精通系列 · 第4篇:索引原理与执行计划实战

MySQL 实战精通系列 · 第4篇:索引原理与执行计划实战

本篇目标:搞懂索引为什么快、什么时候失效、怎么设计,并能用 EXPLAIN 独立分析慢 SQL。


文章目录

  • [MySQL 实战精通系列 · 第4篇:索引原理与执行计划实战](#MySQL 实战精通系列 · 第4篇:索引原理与执行计划实战)
    • 一、为什么需要索引?
      • [1.1 没有索引会怎样?](#1.1 没有索引会怎样?)
      • [1.2 有索引会怎样?](#1.2 有索引会怎样?)
    • 二、B+树索引原理
      • [2.1 为什么是 B+树?](#2.1 为什么是 B+树?)
      • [2.2 B+树特点](#2.2 B+树特点)
      • [2.3 一棵 B+树能存多少行?](#2.3 一棵 B+树能存多少行?)
    • [三、聚簇索引 vs 二级索引](#三、聚簇索引 vs 二级索引)
      • [3.1 聚簇索引(Clustered Index)](#3.1 聚簇索引(Clustered Index))
      • [3.2 二级索引(Secondary Index)](#3.2 二级索引(Secondary Index))
      • [3.3 回表](#3.3 回表)
    • 四、联合索引与最左前缀
      • [4.1 联合索引结构](#4.1 联合索引结构)
      • [4.2 最左前缀原则](#4.2 最左前缀原则)
      • [4.3 实战:设计联合索引](#4.3 实战:设计联合索引)
    • 五、覆盖索引、回表、索引下推
      • [5.1 覆盖索引](#5.1 覆盖索引)
      • [5.2 索引下推(ICP)](#5.2 索引下推(ICP))
      • [5.3 三种扫描方式对比](#5.3 三种扫描方式对比)
    • [六、EXPLAIN 实战](#六、EXPLAIN 实战)
      • [6.1 EXPLAIN 输出字段](#6.1 EXPLAIN 输出字段)
      • [6.2 type 访问类型(从好到差)](#6.2 type 访问类型(从好到差))
      • [6.3 Extra 常见值](#6.3 Extra 常见值)
      • [6.4 实战分析](#6.4 实战分析)
    • [七、索引失效的 7 种场景](#七、索引失效的 7 种场景)
      • [7.1 函数操作](#7.1 函数操作)
      • [7.2 隐式类型转换](#7.2 隐式类型转换)
      • [7.3 LIKE 以 % 开头](#7.3 LIKE 以 % 开头)
      • [7.4 OR 使用不当](#7.4 OR 使用不当)
      • [7.5 联合索引不满足最左前缀](#7.5 联合索引不满足最左前缀)
      • [7.6 范围查询后的列](#7.6 范围查询后的列)
      • [7.7 不等于、NOT IN、IS NOT NULL](#7.7 不等于、NOT IN、IS NOT NULL)
      • [7.8 索引失效速查表](#7.8 索引失效速查表)
    • 八、索引设计原则
      • [8.1 什么时候该加索引?](#8.1 什么时候该加索引?)
      • [8.2 联合索引字段顺序](#8.2 联合索引字段顺序)
      • [8.3 索引设计检查清单](#8.3 索引设计检查清单)
    • [九、实战:100 万数据压测](#九、实战:100 万数据压测)
      • [9.1 生成 100 万订单数据](#9.1 生成 100 万订单数据)
      • [9.2 对比有无索引](#9.2 对比有无索引)
      • [9.3 对比联合索引](#9.3 对比联合索引)
    • 十、本篇实战任务
    • 十一、本篇小结

一、为什么需要索引?

1.1 没有索引会怎样?

假设 orders 表有 100 万行,执行:

sql 复制代码
SELECT * FROM orders WHERE user_id = 12345;

没有索引时,MySQL 只能全表扫描:

复制代码
磁盘上的数据页
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│ 1  │ │ 2  │ │ 3  │ │... │ │100w│
└────┘ └────┘ └────┘ └────┘ └────┘
  ↑
逐个检查 user_id 是否等于 12345

100 万行 → 可能要读几千个数据页 → 几秒甚至几十秒。

1.2 有索引会怎样?

复制代码
                ┌─────────┐
                │  根节点  │
                └────┬────┘
                     │
          ┌──────────┼──────────┐
          ↓          ↓          ↓
      ┌───────┐  ┌───────┐  ┌───────┐
      │ 中间节点│  │ 中间节点│  │ 中间节点│
      └───┬───┘  └───┬───┘  └───┬───┘
          │          │          │
       ┌──┴──┐    ┌──┴──┐    ┌──┴──┐
       ↓     ↓    ↓     ↓    ↓     ↓
     ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐
     │叶 │ │叶 │ │叶 │ │叶 │ │叶 │ │叶 │
     └───┘ └───┘ └───┘ └───┘ └───┘ └───┘

B+树高度通常 3~4 层,100 万行只需 3~4 次磁盘 IO,毫秒级返回。

索引的本质:用空间换时间,把 O(n) 变成 O(log n)。


二、B+树索引原理

2.1 为什么是 B+树?

数据结构 范围查询 磁盘 IO 是否适合
哈希表 不支持 1 次 等值查询快,范围查询差
二叉搜索树 支持 高(树太高) 不适合磁盘
红黑树 支持 高 不适合磁盘
B 树 支持 较低 数据分散在各层
B+树 支持 低 最适合磁盘

2.2 B+树特点

复制代码
B+树结构
│
├── 非叶子节点只存索引键,不存数据
├── 数据全部存在叶子节点
├── 叶子节点之间用链表连接
└── 树高度低(3~4层),磁盘 IO 少

        ┌─────────────────────┐
        │   非叶子节点(只存键) │
        │   10 | 20 | 30       │
        └──┬──────┬──────┬────┘
           │      │      │
     ┌─────┘      │      └─────┐
     ↓            ↓            ↓
┌─────────┐  ┌─────────┐  ┌─────────┐
│ 叶子节点  │→ │ 叶子节点  │→ │ 叶子节点  │
│ 5,8,10  │  │ 15,18,20│  │ 25,28,30│
│ 数据指针  │  │ 数据指针  │  │ 数据指针  │
└─────────┘  └─────────┘  └─────────┘
     ↑ 链表连接,支持范围查询

2.3 一棵 B+树能存多少行?

假设:

  • 主键 BIGINT:8 字节
  • 页大小 16KB
  • 非叶子节点存 键+指针 ≈ 8+6 = 14 字节
  • 叶子节点存 一行数据 ≈ 1KB

计算:

复制代码
非叶子节点:16KB / 14B ≈ 1170 个键
叶子节点:16KB / 1KB ≈ 16 行

3 层 B+树:
1170 × 1170 × 16 ≈ 2190 万行

结论:3 层 B+树能存约 2000 万行,只需 3 次磁盘 IO。


三、聚簇索引 vs 二级索引

3.1 聚簇索引(Clustered Index)

InnoDB 的主键索引就是聚簇索引,叶子节点存整行数据。

复制代码
聚簇索引(主键 id)
        ┌─────────────┐
        │  根节点      │
        │  10 | 20    │
        └──┬───────┬──┘
           ↓       ↓
     ┌─────────┐ ┌─────────┐
     │ 叶子节点  │ │ 叶子节点  │
     │ id=10   │ │ id=20   │
     │ 整行数据  │ │ 整行数据  │
     └─────────┘ └─────────┘

特点:

  • 一张表只有一个聚簇索引
  • 按主键顺序存储
  • 主键查询最快

3.2 二级索引(Secondary Index)

非主键索引都是二级索引,叶子节点存主键值。

复制代码
二级索引(user_id)
        ┌─────────────┐
        │  根节点      │
        │ 100 | 200   │
        └──┬───────┬──┘
           ↓       ↓
     ┌─────────┐ ┌─────────┐
     │ 叶子节点  │ │ 叶子节点  │
     │user_id=100│ │user_id=200│
     │ 主键id=5  │ │ 主键id=8  │
     └─────────┘ └─────────┘

3.3 回表

用二级索引查询时,需要再回聚簇索引查整行,这叫回表。

sql 复制代码
SELECT * FROM orders WHERE user_id = 100;

执行过程:

复制代码
① 在 user_id 二级索引找到 user_id=100 的记录
   → 拿到主键 id = 5

② 回聚簇索引,用 id = 5 查整行数据
   → 这就是回表

二级索引                聚簇索引
┌─────────┐            ┌─────────┐
│user_id=100│          │ id=5    │
│  id=5    │──────────>│ 整行数据  │
└─────────┘   回表      └─────────┘

回表次数越多,性能越差。


四、联合索引与最左前缀

4.1 联合索引结构

sql 复制代码
CREATE INDEX idx_user_status_created ON orders (user_id, status, created_at);

联合索引按字段顺序排序:

复制代码
先按 user_id 排序
  user_id 相同,再按 status 排序
    status 相同,再按 created_at 排序

┌─────────────────────────────────────┐
│ user_id │ status │ created_at       │
├─────────┼────────┼──────────────────┤
│ 1       │ 0      │ 2024-01-01       │
│ 1       │ 0      │ 2024-01-02       │
│ 1       │ 1      │ 2024-01-01       │
│ 2       │ 0      │ 2024-01-01       │
│ 2       │ 1      │ 2024-01-03       │
└─────────────────────────────────────┘

4.2 最左前缀原则

复制代码
索引 (user_id, status, created_at)

能用上索引的查询:
  WHERE user_id = 1                          ✅
  WHERE user_id = 1 AND status = 1           ✅
  WHERE user_id = 1 AND status = 1 AND created_at > '2024-01-01'  ✅
  WHERE user_id = 1 AND created_at > '2024-01-01'  ⚠️ 只用 user_id
  WHERE status = 1                           ❌ 用不上
  WHERE created_at > '2024-01-01'            ❌ 用不上

记忆口诀:从左到右,遇到范围就停。

复制代码
(user_id, status, created_at)
   ①        ②        ③

① 等值 → 继续
② 等值 → 继续
③ 范围 → 停止,后面的列用不上

4.3 实战:设计联合索引

sql 复制代码
-- 查询:某用户已支付订单,按时间倒序
SELECT * FROM orders
WHERE user_id = 1 AND status = 1
ORDER BY created_at DESC;

-- 推荐索引
CREATE INDEX idx_user_status_created ON orders (user_id, status, created_at);

五、覆盖索引、回表、索引下推

5.1 覆盖索引

查询的列全在索引里,不需要回表。

sql 复制代码
-- 需要回表
SELECT * FROM orders WHERE user_id = 1;

-- 覆盖索引,不用回表
SELECT user_id, status FROM orders WHERE user_id = 1;
-- 如果索引是 (user_id, status)

EXPLAIN 中显示 Using index 就是覆盖索引。

5.2 索引下推(ICP)

MySQL 5.6+ 支持,在索引遍历时就过滤,减少回表。

sql 复制代码
SELECT * FROM orders
WHERE user_id = 1 AND status = 1;
复制代码
没有 ICP:
  ① 用 user_id = 1 从索引找到所有记录
  ② 回表查整行
  ③ 再过滤 status = 1

有 ICP:
  ① 用 user_id = 1 从索引找到记录
  ② 在索引里直接过滤 status = 1
  ③ 只对满足条件的回表

EXPLAIN 中显示 Using index condition。

5.3 三种扫描方式对比

方式 EXPLAIN Extra 是否回表 性能
全表扫描 Using where 否 最差
索引 + 回表 Using index condition 是 一般
覆盖索引 Using index 否 最好

六、EXPLAIN 实战

6.1 EXPLAIN 输出字段

sql 复制代码
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
字段 含义 关注点
id 查询序号 越大越先执行
select_type 查询类型 SIMPLE / PRIMARY / SUBQUERY
table 表名 ---
type 访问类型 最重要
possible_keys 可能用的索引 ---
key 实际用的索引 是否用了预期索引
key_len 索引长度 用了联合索引几列
ref 索引比较的列 ---
rows 预估扫描行数 越小越好
filtered 过滤百分比 ---
Extra 额外信息 很重要

6.2 type 访问类型(从好到差)

复制代码
system  →  const  →  eq_ref  →  ref  →  range  →  index  →  ALL
  最好                                                      最差
type 含义 场景
system 系统表,只有一行 ---
const 主键或唯一索引等值查询 WHERE id = 1
eq_ref JOIN 时主键关联 JOIN ON a.id = b.id
ref 普通索引等值查询 WHERE user_id = 1
range 索引范围查询 WHERE id > 100
index 全索引扫描 扫描整个索引树
ALL 全表扫描 最差,必须优化

6.3 Extra 常见值

Extra 含义 好坏
Using index 覆盖索引 好
Using index condition 索引下推 好
Using where 需要过滤 一般
Using filesort 文件排序 差
Using temporary 临时表 差
Using join buffer JOIN 缓冲 一般

6.4 实战分析

sql 复制代码
-- 案例1:主键查询
EXPLAIN SELECT * FROM orders WHERE id = 1;
-- type: const, key: PRIMARY, rows: 1

-- 案例2:普通索引查询
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
-- type: ref, key: idx_user_created, rows: 预估

-- 案例3:范围查询
EXPLAIN SELECT * FROM orders WHERE id > 100;
-- type: range, key: PRIMARY

-- 案例4:全表扫描
EXPLAIN SELECT * FROM orders WHERE total_amount > 100;
-- type: ALL, key: NULL, rows: 1000000  ← 需要优化

-- 案例5:覆盖索引
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 1;
-- Extra: Using index

-- 案例6:文件排序
EXPLAIN SELECT * FROM orders ORDER BY total_amount;
-- Extra: Using filesort  ← 需要优化

七、索引失效的 7 种场景

7.1 函数操作

sql 复制代码
-- 失效
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';

-- 优化
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';

7.2 隐式类型转换

sql 复制代码
-- phone 是 VARCHAR,传数字会失效
SELECT * FROM users WHERE phone = 13800000001;

-- 正确
SELECT * FROM users WHERE phone = '13800000001';

7.3 LIKE 以 % 开头

sql 复制代码
-- 失效
SELECT * FROM products WHERE name LIKE '%iPhone%';

-- 有效
SELECT * FROM products WHERE name LIKE 'iPhone%';

7.4 OR 使用不当

sql 复制代码
-- 如果 status 没索引,整体失效
SELECT * FROM orders WHERE user_id = 1 OR status = 1;

-- 优化:两个字段都加索引,或用 UNION
SELECT * FROM orders WHERE user_id = 1
UNION
SELECT * FROM orders WHERE status = 1;

7.5 联合索引不满足最左前缀

sql 复制代码
-- 索引 (user_id, status, created_at)
SELECT * FROM orders WHERE status = 1;  -- 失效

7.6 范围查询后的列

sql 复制代码
-- 索引 (user_id, status, created_at)
SELECT * FROM orders
WHERE user_id = 1 AND status > 0 AND created_at = '2024-01-01';
-- created_at 用不上索引

7.7 不等于、NOT IN、IS NOT NULL

sql 复制代码
-- 可能失效
SELECT * FROM orders WHERE status != 1;
SELECT * FROM orders WHERE status NOT IN (1, 2);
SELECT * FROM orders WHERE total_amount IS NOT NULL;

7.8 索引失效速查表

场景 是否失效 优化方式
函数操作 失效 改写条件
隐式类型转换 失效 类型一致
LIKE '%x' 失效 改前缀匹配
OR 一边无索引 失效 UNION
不满足最左前缀 失效 调整索引
范围后列 失效 调整索引顺序
!= / NOT IN 可能失效 改写

八、索引设计原则

8.1 什么时候该加索引?

复制代码
该加索引
│
├── 频繁出现在 WHERE 的列
├── 频繁出现在 ORDER BY 的列
├── 频繁出现在 JOIN ON 的列
├── 区分度高的列
└── 覆盖索引能覆盖的列

不该加索引
│
├── 区分度低的列(性别、状态)
├── 频繁更新的列
├── 很少查询的列
├── 表数据量很小
└── 索引已经很多了

8.2 联合索引字段顺序

复制代码
原则:
① 等值查询字段放前面
② 范围查询字段放后面
③ 区分度高的放前面
④ 考虑覆盖索引

示例:
查询:WHERE user_id = ? AND status = ? ORDER BY created_at DESC
索引:(user_id, status, created_at)

查询:WHERE status = ? AND user_id = ?
索引:(user_id, status)  -- 等值查询顺序可互换

8.3 索引设计检查清单

复制代码
索引设计检查
│
├── 主键是否用 BIGINT UNSIGNED?
├── 外键字段是否加了索引?
├── 高频查询字段是否加了索引?
├── 联合索引顺序是否合理?
├── 是否有冗余索引?
├── 是否有未使用的索引?
└── 区分度是否足够?

九、实战:100 万数据压测

9.1 生成 100 万订单数据

sql 复制代码
DELIMITER $$

CREATE PROCEDURE gen_orders_big(IN cnt INT)
BEGIN
    DECLARE i INT DEFAULT 0;
    WHILE i < cnt DO
        INSERT INTO orders (user_id, total_amount, status, created_at)
        VALUES (
            FLOOR(RAND() * 100000) + 1,
            ROUND(RAND() * 10000, 2),
            FLOOR(RAND() * 5),
            DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;

CALL gen_orders_big(1000000);

9.2 对比有无索引

sql 复制代码
-- 无索引:全表扫描
EXPLAIN SELECT * FROM orders WHERE user_id = 50000;
-- type: ALL, rows: 1000000

-- 加索引
CREATE INDEX idx_user ON orders (user_id);

-- 有索引
EXPLAIN SELECT * FROM orders WHERE user_id = 50000;
-- type: ref, key: idx_user, rows: 约10

9.3 对比联合索引

sql 复制代码
-- 单列索引
CREATE INDEX idx_user ON orders (user_id);

-- 联合索引
CREATE INDEX idx_user_status_created ON orders (user_id, status, created_at);

-- 查询:某用户已支付订单,按时间倒序
EXPLAIN SELECT * FROM orders
WHERE user_id = 1 AND status = 1
ORDER BY created_at DESC;
-- 联合索引:type: ref, key: idx_user_status_created, Extra: Using index condition

十、本篇实战任务

任务清单

  • 生成 100 万订单数据
  • 用 EXPLAIN 分析 5 条 SQL
  • 复现 7 种索引失效场景
  • 设计 3 个联合索引并验证
  • 对比有无索引的查询耗时
  • 找出冗余索引

自检问题

  1. B+树为什么适合磁盘存储?
  2. 聚簇索引和二级索引的区别?
  3. 什么是回表?怎么避免?
  4. 最左前缀原则是什么?
  5. EXPLAIN 中 type 有哪些值?哪个最差?
  6. Using index、Using index condition、Using filesort 分别什么意思?
  7. 索引失效的常见场景有哪些?

十一、本篇小结

复制代码
第4篇 核心收获
│
├── 索引原理
│   ├── B+树:3层存2000万行
│   ├── 聚簇索引:叶子存整行
│   └── 二级索引:叶子存主键
│
├── 回表与覆盖索引
│   ├── 回表:二级索引 → 聚簇索引
│   └── 覆盖索引:Using index
│
├── 联合索引
│   ├── 最左前缀原则
│   └── 等值在前,范围在后
│
├── EXPLAIN
│   ├── type:system > const > eq_ref > ref > range > index > ALL
│   ├── key:实际用的索引
│   ├── rows:预估扫描行数
│   └── Extra:Using index / filesort / temporary
│
├── 索引失效
│   ├── 函数操作
│   ├── 隐式转换
│   ├── LIKE '%x'
│   ├── OR 无索引
│   ├── 不满足最左前缀
│   ├── 范围后列
│   └── != / NOT IN
│
└── 索引设计
    ├── 该加:WHERE / ORDER BY / JOIN
    ├── 不该加:低区分度、频繁更新
    └── 联合索引:等值在前,范围在后

下一篇:第5篇《事务、隔离级别与锁实战》

相关推荐
m0_547486661 小时前
《数据库原理、技术与应用:MySQL》全套PPT课件2026
数据库·mysql
mmsx1 小时前
Android UI 复用体系:泛型基类、骨架与插槽
android·kotlin
JosieBook1 小时前
【数据库】MySQL 实战精通系列 · 第1篇:MySQL 全局观与实战环境搭建
数据库·mysql·adb
霸道流氓气质1 小时前
LangGraph式图编排引擎Java实现:状态机模型、Checkpoint恢复与生产级落地实战
java·开发语言·数据库
数据狐(Datafox)1 小时前
淘宝商品详情API实战:多语言代购商城自动同步数据完整方案
开发语言·前端·数据库·爬虫·json
Leo.yuan1 小时前
帆软 Data Agent 技术实践白皮书:NL2BI 的可执行与可追溯分析链路
数据库·microsoft
Cici_ovo2 小时前
手机数据删除原理、运行内存、闪存、机械硬盘完整原理详解
android·智能手机
细嗅蔷薇@2 小时前
DDL常见操作汇总
数据库·sql·mysql
颜颜yan_2 小时前
从 InfluxDB 到 DolphinDB:一场国产时序数据库的替代与超越
数据库·时序数据库