MySQL 底层原理与索引命中手册

进阶整理 · 重索引命中机制与 SQL 实例分析 · 深入 InnoDB 存储引擎、日志、事务、锁

1. 架构总览

1.1 Server 层 vs 存储引擎层

MySQL 分为两层:Server 层负责连接、SQL 解析、优化、执行与日志;存储引擎层(InnoDB)负责数据的实际存取。

# SQL 执行流程 客户端 → 连接器(认证/权限) → 分析器(语法/词法) → 优化器(选索引/生成执行计划) → 执行器(调用存储引擎) → InnoDB(缓冲池 → 文件) ← redo log / binlog
重点:优化器是"索引命中"的关键。它根据统计信息(information_schema 里的行数、区分度、代价模型)决定是走索引还是全表扫描。

2. InnoDB 存储引擎

2.1 页(Page)与行

InnoDB 以页(16KB 默认)为最小 I/O 单元,磁盘与内存之间按页读写。数据按行存储,行格式:COMPACT / DYNAMIC / REDUNDANT。

# 聚簇索引页结构(简化) 页头: page_no, page_type ... ------------------------------------- Infimum(下界哨兵) [记录1][记录2][记录3] ... (按主键有序) ------------------------------------- Supremum(上界哨兵) 页目录: 槽位数组(指向每组的最大记录) 页尾: checksum

2.2 聚簇索引(主键索引)

InnoDB 必须有聚簇索引。数据本身就按 B+ 树组织在聚簇索引的叶子节点里。如果没有主键会怎样?

为什么推荐自增主键:B+ 树插入有序,避免页分裂;而 UUID 主键无序,会导致频繁页分裂、索引空间膨胀、随机 IO。

2.3 二级索引(辅助索引)

二级索引的叶子节点存的是主键值(不是完整行)。因此通过二级索引查数据需要"回表":先按二级索引定位主键,再回聚簇索引取整行。

# 例: 表 user(id PK, name, age),索引 idx_name(name) SELECT * FROM user WHERE name='张三'; 1. 走 idx_name:定位到叶子节点的主键 id=5 2. 回表:用 id=5 去聚簇索引取整行

2.4 表空间与文件

InnoDB 数据存储在 .ibd(表空间)中。区分段、区、页:

层级规模说明
段—数据段 / 索引段 / 回滚段
区≈ 1MB由连续 64 个页组成
页16KB最小读写单元

2.5 Buffer Pool(缓冲池)

内存中的缓存,用来加速读写。核心机制:

3. 索引机制(核心)

3.1 为什么用 B+ 树而非 B 树 / 哈希 / 红黑树

结构IO 次数原因
哈希索引快(一次)只支持等值查询,不支持范围、排序、最左前缀。内存索引如 Memory 引擎用它。
红黑树log2n 高树高,每层一次 IO,磁盘场景退化。
B 树较低节点既存键也存数据,非叶子占空间。
B+ 树最低非叶子只存键,扇出大、树矮;叶子链成链表方便范围扫描。
B+ 树三大优势:①扇出大→树矮(3层可存数百万行)→IO 少;②叶子用链表连接→范围查询只需顺序遍历;③非叶子只存键→能容纳更多索引项。

3.2 联合索引与最左前缀

联合索引 idx(a,b,c) 建立后,先按 a 排、再按 b、再按 c。因此命中规则是最左前缀。

# 表 t(a,b,c),索引 idx(a,b,c) WHERE a=1 → 命中 a WHERE a=1 AND b=2 → 命中 a,b WHERE a=1 AND b=2 AND c=3 → 命中 a,b,c WHERE b=2 → 不命中(跳过最左) WHERE a=1 AND c=3 → 只命中 a,c 用不上排序(可当过滤)

3.3 回表与覆盖索引

回表:二级索引找到主键后再回聚簇索引取整行。覆盖索引:查询所需字段都在索引里,无需回表。

# idx(name, age) SELECT name, age FROM user WHERE name='张三'; → 覆盖索引,不回表 SELECT * FROM user WHERE name='张三'; → 需 age 等,回表取整行

优化技巧:需要回表时尽量"用覆盖索引",把常见查询字段塞进联合索引(注意别塞太多导致索引过大)。

3.4 索引下推(ICP, Index Condition Pushdown)

MySQL 5.6+ 优化:把非索引能覆盖的过滤条件下推到存储引擎,在二级索引扫描时就先过滤,减少回表次数。

# idx(name, age) SELECT * FROM t WHERE name='李' AND age>20; 无 ICP:先用 name 定位主键回表,再在 Server 层过滤 age。 有 ICP:在二级索引扫描 name='李' 时,同时判断 age>20,过滤掉不匹配的,少回表。

3.5 前缀索引

对长字符串(如 email、content)可只索引前 N 个字符,减小索引体积。但会降低区分度(长尾冲突),需权衡。

ALTER TABLE t ADD INDEX idx_email(email(10));   -- 只用前10字符
-- 区分度校验
SELECT COUNT(DISTINCT email)/COUNT(*) AS full_sel,
       COUNT(DISTINCT LEFT(email,10))/COUNT(*) AS pre_sel FROM t;

3.6 函数索引 / 表达式索引

当查询对列做了函数运算(DATE()、LOWER()),普通索引失效,需用函数索引或生成列(MySQL 5.7+ / 8.0 支持)。

# 失效 WHERE DATE(create_time)='2026-01-01' → 索引失效(对列做了函数) # 解决:函数索引 CREATE INDEX idx_ct ON t((DATE(create_time))); WHERE DATE(create_time)='2026-01-01' → 命中

4. 索引命中与失效(SQL 实例)

核心:判断能否命中索引,本质是看优化器能否把 WHERE 条件转成对 B+ 树的范围扫描,且满足最左前缀、类型匹配、无函数包装。

4.1 EXPLAIN 解析

EXPLAIN SELECT * FROM t WHERE a=1 AND b=2;
-- 关注: type / key / rows / Extra
type含义效率
system仅一行(系统表)最优
const主键/唯一键等值,查询常量最优
eq_ref关联查询,被驱动表用唯一索引好
ref非唯一索引等值好
range索引范围扫描(BETWEEN、>、IN)较好
index全索引扫描(覆盖但遍历整个索引)一般
ALL全表扫描差,需警惕

4.2 索引失效的场景(对照 SQL)

下面以 user(id PK, name VARCHAR, age INT, email VARCHAR, idx(name,age), idx(email)) 为例。

① 对列做函数 / 运算 → 失效

-- 失效
SELECT * FROM user WHERE name = CONCAT('张','三');
SELECT * FROM user WHERE age + 1 = 30;
-- 命中(等价改写)
SELECT * FROM user WHERE name = '张三';
SELECT * FROM user WHERE age = 29;

② 隐式类型转换 → 失效

-- 如果 phone 是 VARCHAR 类型
SELECT * FROM user WHERE phone = 13800000000;  -- 隐式转成数字比较 → 索引失效
SELECT * FROM user WHERE phone = '13800000000'; -- 字符串匹配 → 命中

因为 '1'<'9' 的字符串排序与数字不同,MySQL 会把列做转换,导致索引失效。

③ LIKE 以 % 开头 → 失效

SELECT * FROM user WHERE name LIKE '%张%';   -- 失效(前缀不定)
SELECT * FROM user WHERE name LIKE '张%';    -- 可命中(前缀确定 → 范围)
SELECT * FROM user WHERE name LIKE '%张';    -- 失效

④ 最左前缀被破坏(联合索引)

SELECT * FROM user WHERE age = 30;            -- 跳过 name → 失效
SELECT * FROM user WHERE name='张三' AND age=30; -- 命中 name,age
SELECT * FROM user WHERE name='张三';          -- 只用 name → 部分命中

⑤ 使用 OR 且两侧条件未同列索引 → 失效

SELECT * FROM user WHERE name='张三' OR email='[email protected]';
-- 若 OR 两侧分别走不同索引,MySQL 常退化为 全表/index_merge。
-- name/email 有索引仍可能走 index_merge,但若一侧无索引则全表。

⑥ 对列隐式取部分(SUBSTRING / 连接)

SELECT * FROM user WHERE SUBSTRING(name,1,1)='张'; -- 失效

⑦ 范围条件之后的列(联合索引)

-- 假设 idx(name, age, email)
SELECT * FROM user WHERE name='张三' AND age>20 AND email='a@b';
-- age 是范围,email 无法用于排序/索引定位(只是过滤)→ 只命中 name,age

⑧ 索引列可空但查询用 IS NULL 等

MySQL 对 IS NULL/IS NOT NULL 的索引优化较复杂,通常仍可走索引但代价可能高于全表,视统计而定。

4.3 查询优化实战(SQL 重写)

# 场景1:深分页很慢 SELECT * FROM t ORDER BY id LIMIT 1000000, 20; # 优化:延迟关联 / 覆盖索引定位主键 SELECT * FROM t JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000,20) tmp ON t.id=tmp.id;
# 场景2:`SELECT *` 导致回表 按需列,尽量覆盖索引,减少回表与网络传输。 # 场景3:IN 大量值 vs 连接 IN 过多会用不上索引用全表,可拆成临时表 JOIN。

排序(filesort)优化

-- 想避免 filesort,ORDER BY 的字段要满足索引的最左前缀且方向一致
SELECT * FROM user WHERE name='张三' ORDER BY age; -- 命中索引,无需 filesort

4.4 索引设计原则

5. 事务与隔离级别

5.1 ACID

特性实现
原子性 Aundo log(回滚)
一致性 C约束、日志共同保证
隔离性 I锁 + MVCC
持久性 Dredo log(WAL)

5.2 隔离级别

级别脏读不可重复读幻读
读未提交 RU✔ 可能✔ 可能✔ 可能
读已提交 RC否✔ 可能✔ 可能
可重复读 RR否否部分(靠锁/MVCC)
串行化 SERIALIZABLE否否否

InnoDB 默认 RR 级别。幻读在 RR 下可通过间隙锁或 SELECT ... FOR UPDATE 规避。

5.3 MVCC(多版本并发控制)

MVCC 通过隐藏列 + undo log 版本链 + Read View 实现非阻塞读(快照读)。

快照读 vs 当前读:普通 SELECT 是快照读(MVCC,不加锁);SELECT ... FOR UPDATE / FOR SHARE / 更新语句是当前读(读最新版本并加锁)。

6. 日志系统

6.1 redo log(重做日志)

实现 WAL(Write-Ahead Logging):先写 redo,再写内存,最后刷盘。崩溃后根据 redo 恢复已提交数据,保证持久性。

6.2 undo log(回滚日志)

用于回滚事务和 MVCC 多版本。记录反向操作,逻辑日志。回滚段在表空间内,也进行 MVCC 版本链构建。

6.3 binlog(归档日志)

Server 层日志,记录"逻辑操作",用于主从复制、数据恢复。格式:STATEMENT / ROW / MIXED。

# binlog vs redo log redo: 物理、InnoDB层、循环写、崩溃恢复 binlog: 逻辑、Server层、追加写、复制/恢复

6.4 两阶段提交(2PC)

为保证 redo log 与 binlog 一致,事务提交采用两阶段:

  1. 写 redo log 记录"prepare"状态。
  2. 写 binlog。
  3. 写 redo log 记录"commit"状态。

崩溃恢复时,若 binlog 有记录且 redo 为 prepare,则提交;否则回滚。

7. 锁机制

7.1 锁类型

层次类型
全局全局读锁(FTWRL),整个库只读
表表级锁、MDL 元数据锁
行记录锁 / 间隙锁 / 临键锁(Next-Key)

7.2 行锁细究

# 加锁示例 (RR 级别) SELECT * FROM user WHERE age BETWEEN 10 AND 20 FOR UPDATE; → 对命中记录加记录锁,对(10,20)间及边界加间隙锁,防止插入。 # RC 级别只加记录锁,不加间隙锁(幻读仍可能)

7.3 死锁

两个事务互相等待对方持有的锁。InnoDB 通过 innodb_deadlock_detect(默认 on) 主动检测并回滚代价小的事务。降低死锁:按固定顺序加锁、减少事务时间、缩小加锁范围。

8. 速查清单

主题一句话答案
为什么 B+ 树树矮 IO 少、叶子链表支持范围、非叶子只存键
聚簇 vs 二级索引聚簇存整行;二级索引叶子存主键、需回表
覆盖索引查询列全在索引内,免回表
索引下推 ICP过滤下推到引擎,减少回表
最左前缀联合索引按列顺序命中,跳列后续失效
索引失效原因函数包装、隐式转换、左模糊、破坏最左前缀、OR
MVCCundo 版本链 + Read View 实现快照读
redo vs binlogredo 崩溃恢复(物理) / binlog 复制备份(逻辑)
两阶段提交redo prepare → binlog → redit commit
幻读解决间隙锁 / Next-Key(RR)