MySQL 普通索引和唯一索引,到底怎么选?

本文整理自《MySQL 实战 45 讲》第 09 讲,梳理"普通索引 vs 唯一索引"的选型逻辑,并补充第 10 讲对思考题的最新解答。

一句话结论

查询几乎无差别,关键看更新——核心机制是 change buffer。redo log 省随机写 IO(转顺序写),change buffer 省随机读 IO。业务能保证唯一性时,优先普通索引。


一、问题背景

假设你在维护一个市民系统,每个人都有一个唯一的身份证号,而且业务代码已经保证了不会写入两个重复的身份证号。如果市民系统需要按照身份证号查姓名,就会执行类似这样的 SQL 语句:

select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';

所以,你一定会考虑在 id_card 字段上建索引。

⚠️ 由于身份证号字段比较大,不建议把身份证号当做主键——主键是聚簇索引,长主键会让所有二级索引膨胀、每页 key 数减少、插入易页分裂。优先用短整型自增主键,身份证号作二级索引。

那么现在有两个选择:要么给 id_card 字段创建唯一索引,要么创建一个普通索引。如果业务代码已经保证了不会写入重复的身份证号,这两个选择逻辑上都是正确的。

从性能的角度考虑,你选哪个?依据是什么?

下面从查询和更新两个过程来分析。


二、查询过程:性能几乎无差别

select id from T where k=5 为例。B+ 树从树根按层搜索到叶子节点(数据页),数据页内部通过二分法定位记录。

索引类型 命中后行为
普通索引 查到第一个满足条件的记录 (5,500) 后,还要找下一条记录,直到碰到第一个不满足 k=5 的记录
唯一索引 索引定义了唯一性,查到第一个满足条件的记录后立即停止

这个不同带来的性能差距有多少?答案是,微乎其微

为什么"多查一次"几乎免费?

关键在于 InnoDB 按数据页(默认 16KB)为单位读写

唯一略贵的情况(概率极低)

仅当 k=5 记录刚好是数据页的最后一条,取下一条需读下一个数据页(可能触发磁盘 IO)。但整型字段一个 16KB 页可放近千个 key,记录恰好是页末的概率极低,平均性能差异仍可忽略。

查询性能不是选型依据。


三、更新过程:change buffer 是关键

1. change buffer 是什么?

当需要更新一个数据页时:

关键性质:

2. merge:应用变更

把 change buffer 中的操作应用到原数据页、得到最新结果的过程称为 merge。触发时机:

  1. 访问该数据页时;
  2. 后台线程定期 merge;
  3. 数据库正常关闭(shutdown)时。

merge 前累积的变更多 → 收益越大(一次读盘应用多次变更)。

3. 谁能用 change buffer?

索引类型 能否用 change buffer 原因
普通索引 ✅ 能 无需判唯一性,不必读页
唯一索引 ❌ 不能 更新必须先读页判断是否违反唯一性约束;既然已读入内存,直接更新更快,change buffer 无意义

这是普通索引 vs 唯一索引更新性能差距的根因

4. 插入 (4,400) 的两种情况

情况一:目标页在内存中

情况二:目标页不在内存中

真实案例:某 DBA 把业务库的一个普通索引改成唯一索引后,内存命中率从 99% 暴跌到 75%,整个系统阻塞、更新语句全部堵住。原因:大量插入操作下,唯一索引被迫频繁读页判重,用不上 change buffer。


四、应用场景(重点)

change buffer 只对普通索引生效,且并非所有普通索引场景都收益。判断标准:merge 之前累积的变更多,收益才大。

✅ 场景一:写多读少(收益最大)

页面写完后马上被访问的概率小,change buffer 能长时间累积变更,merge 时一次读盘应用多次变更。

❌ 场景二:写后立即读(反而副作用)

写入之后马上查询该数据页 → 立即触发 merge,随机 IO 次数没减少,反而增加了 change buffer 的维护代价。

✅ 场景三:机械硬盘 + 历史数据归档库(收益显著)

机械硬盘随机 IO 代价远高于 SSD,change buffer 省随机读的收益被放大。

📌 选型决策表

业务情况 选型
业务能保证唯一性 + 写多读少 普通索引(借 change buffer 加速更新)
业务能保证唯一性 + 写后立即读 普通索引 + 关闭 change buffer
业务不能保证 / 要 DB 约束唯一 唯一索引(正确性优先,没得选)
机械硬盘归档库 普通索引 + 开大 change buffer

核心原则:业务正确性优先。本文讨论性能的前提是"业务代码已保证不写入重复数据"。若业务不能保证,必须唯一索引;这时本文的意义在于——遇到大量插入慢、内存命中率低时,多一个排查思路。


五、change buffer vs redo log(易混淆重点)

两者都为减少随机 IO,但机制不同:

机制 节省的 IO 本质
redo log 随机 IO(转顺序写) WAL:先写日志再改数据
change buffer 随机 IO 缓存对不在内存页的更新,推迟读页

更新流程(Page1 在内存、Page2 不在内存)

执行 insert into t(id,k) values(id1,k1),(id2,k2);,涉及四部分:内存、redo log(ib_logfileX)、数据表空间(t.ibd)、系统表空间(ibdata1)。

  1. Page 1 在内存中,直接更新内存;
  2. Page 2 不在内存,在 change buffer 区域记录"往 Page 2 插入一行";
  3. 上述两个动作都记入 redo log。

→ 事务完成。成本很低:写两处内存 + 一次顺序写盘。图中两个虚线箭头是后台操作,不影响响应时间。

读流程

→ 直到需要读 Page 2 时,该数据页才被读入内存。


六、思考题解答

题目:change buffer 一开始是写内存的,如果这个时候机器掉电重启,会不会导致 change buffer 丢失?change buffer 丢失的话,再从磁盘读入数据就没有 merge 过程,等于数据丢失了,会出现这种情况吗?

答案:不会丢失

以下解答来自第 10 讲「上期问题时间」的最新讲解。

虽然是只更新内存,但在事务提交的时候,change buffer 的操作也记录到 redo log 里了,所以崩溃恢复的时候,change buffer 也能找回来。

补充:merge 的执行流程(第 10 讲展开)

  1. 从磁盘读入数据页到内存;
  2. 从 change buffer 里找出这个数据页的 change buffer 记录(可能有多个),依次应用,得到新版数据页;
  3. 写 redo log。这个 redo log 包含了数据的变更change buffer 的变更

到这里 merge 过程结束。此时数据页和内存中 change buffer 对应的磁盘位置都还没有修改,属于脏页,之后各自刷回自己的物理数据,是另外一个过程。

关键点


七、总结

  1. 查询:普通索引和唯一索引性能几乎无差别(按 16KB 数据页读写,多查一次是内存内指针+计算);
  2. 更新:关键在 change buffer——唯一索引用不上(必须读页判唯一性),普通索引能用;
  3. 场景:写多读少收益最大,写后立即读反而副作用,机械硬盘归档库收益显著;
  4. 选型:业务能保证唯一性 → 优先普通索引;不能保证 → 必须唯一索引(正确性优先);
  5. redo log vs change buffer:前者省随机写,后者省随机读,互补;
  6. 掉电不丢:change buffer 操作记入 redo log,崩溃恢复可找回。

🔗 关联