MySQL索引原理-主键索引与普通索引
各位数据库世界的探险家们,欢迎来到“DBA的相声台”!今天咱不聊风花雪月,不侃国际局势,就唠唠咱MySQL里那点“查户口本”的事儿——MySQL索引原理和优化技巧-主键索引和普通索引的爱恨情仇、江湖地位以及它们是如何影响你“查户口”的速度和姿势的。准备好了吗?带上你的小板凳和瓜子,咱们开车了!
(一)开场段子:相亲角里的“索引”
想象一下,你是一位英俊潇洒(或者闭月羞花)的程序员,周末被老妈强行拉去人民公园相亲角。放眼望去,人头攒动,信息爆炸!
-
场景A(MySQL主键索引): 老妈精准定位:“闺女,看那边穿格子衫、秃…哦不,是发际线充满智慧光芒、ID叫
码农小张_007
的小伙!就是他!” 你嗖一下过去,目标明确,效率极高,牵手成功(也可能失败,但过程快!)。 -
场景B(MySQL普通索引): 老妈说:“闺女,给我找个程序员!”。好家伙,放眼望去,格子衫的海洋,秃…哦不,智慧的光芒此起彼伏。你得挨个问:“你是程序员吗?(在k索引树搜索)”。终于找到一个说“是”的(k=5),一问名字,叫
码农小张_007
(得到ID=500)。然后你又在茫茫人海中高喊:“码农小张_007
在哪儿?(回主键树搜索)”。等你找到他,可能人家孩子都会打酱油了。
看出区别了吗?主键索引就像精准的身份证号查询,直达目标。普通索引就像按职业(程序员)筛选,找到了职业符合的人,还得再拿他的身份证号(主键)去人海里精准定位一次,多跑一趟腿!这就是传说中的回表(Back to the Table Lookup),也是今天相声的主角之一。
而在MySQL的江湖,特别是咱InnoDB
这位大侠掌管的客栈里,所有的“户口本”(数据表),都是用一种叫做索引组织表(Index-Organized Table, IOT) 的方式存放的,并且用的是武林中赫赫有名的B+树数据结构!每个索引,都对应一棵B+树。我们的故事,就从一张叫做dbbro
的“户口本”开始。
-- 隆重介绍我们的户口本:dbbro表!
CREATE TABLE dbbro (id INT PRIMARY KEY auto_increment, -- 户主身份证号(主键),独一无二!k INT NOT NULL, -- 家庭人口数(k),上面有个索引name VARCHAR(16) -- 户主大名
) ENGINE=InnoDB; -- 由InnoDB大侠管理
-- 插入几条户口信息
INSERT INTO dbbro (id, k, name) VALUES(100, 1, '独行侠'),(200, 2, '二人世界'),(300, 3, '三口之家'),(500, 5, '五福临门'),(600, 6, '六六大顺');
现在,dbbro
这本户口册在InnoDB
大侠手里变成了两棵神奇的B+树:
-
主键索引(PRIMARY KEY)B+树: 以
id
为排序依据。这棵树不光记录id
,还完整地保存着id, k, name
这条记录的所有信息!它是户口本的原件,按照身份证号id
顺序装订好。 -
普通索引(INDEX(k))B+树: 以
k
(家庭人口数)为排序依据。但这棵树上只记录两样东西:k
的值和对应的主键值id
。它相当于一个按家庭人口数编排的索引目录,告诉你某个家庭人口数(k)对应的是哪个身份证号(id)。想找详细信息?对不起,请拿着id
去主键索引树查原件(回表)。
(二)深入江湖:B+树秘境的探幽
为什么是B+树?不是二叉树、不是哈希表、不是B树?且听我慢慢道来(敲黑板,技术重点!):
-
B+树的“人设”:
-
多路平衡搜索树: 一个节点(江湖人称“页”,默认16KB)可以有很多孩子(指针),不像二叉树只有俩。这大大降低了树的高度(层数),减少了磁盘I/O(想象翻户口本翻的次数少)。平衡意味着不会出现“瘸腿”树,查询稳定。
-
叶子节点才存数据(主键索引): 在主键索引树里,只有最底层的叶子节点才存放完整的行数据(
id, k, name
)。非叶子节点只存储索引键(id
)和指向下一层节点的指针。这就好比目录页只写章节标题和页码,具体内容在正文页。 -
叶子节点链表串联(核心!): 所有叶子节点按索引键大小顺序(
id
从小到大)用双向链表连接起来!这个设计太妙了,做范围查询(找id在200到500之间的人)和全表扫描效率极高,顺着链表撸就行。 -
数据都在叶子节点(普通索引): 在普通索引树(
index(k)
)里,叶子节点存储的是索引键(k
)和对应的主键值(id
)。同样,非叶子节点只存k
和指针。
-
-
图解江湖:两棵B+树长啥样?
假设我们的数据量小,树很矮。想象一下dbbro
的两棵树:
-
主键索引树 (id)
-
查询
id=500
:从根节点(300)出发,500>300,走右边,直接定位到数据页2的id:500
记录。一步到位!Done!
-
普通索引树 (k)
-
查询
k=5
:从根节点(3)出发,5>3,走右边,在索引页2找到k:5
对应的id:500
。但这还没完! 这只是一个目录,只告诉你k=5
对应的户主身份证号是500
。要查户主叫啥名(name
),还得拿着id=500
这个身份证号,再跑一趟主键索引树去查!这就是回表!两步走!
(三)核心对决:主键索引 vs 普通索引 - “查户口”的姿势与代价
现在明白相亲角和B+树的关系了吧?让我们系统总结下区别:
-
存储内容:
-
主键索引: 叶子节点存储整行数据。它是数据的“家”,是户口本的原件。
-
普通索引: 叶子节点存储索引列的值 + 主键值。它是数据的“快捷方式”或“目录页”。
-
-
查询方式 (SELECT * WHERE ...):
-
第一步:在普通索引(k)的B+树中搜索,找到满足
k=?
条件的叶子节点,获取对应的主键值(id)。 -
第二步:拿着这个主键值
id
,回到主键索引的B+树中再搜索一次(回表)。 -
第三步:在主键索引的叶子节点中找到该
id
对应的完整行数据。 -
效率: ⚡⚡ O(log N + log N) ≈ O(2 log N)。多了一次树搜索(回表)!如果普通索引查询需要返回的列不全在索引中(比如需要
name
),回表无法避免。这就是普通索引查询通常比主键查询慢一截的根本原因。
-
只需在主键索引的B+树中进行一次搜索(从根节点到叶子节点)。
-
在叶子节点直接找到目标记录的所有列数据。
-
效率: ⚡⚡⚡ O(log N),通常只需要几次磁盘I/O(取决于树高)。精准直达!
-
主键查询 (WHERE id = ?):
-
普通索引查询 (WHERE k = ?):
-
-
唯一性:
-
主键索引: 天然具有唯一性约束。确保
id
列的值绝对不重复。户口本号能重复吗?不能! -
普通索引: 没有唯一性保证(除非你显式创建
UNIQUE INDEX
)。允许多个不同的id
对应相同的k
值。比如,可以有好几个“五口之家”(k=5
),但他们的id
(身份证号)肯定不同。
-
-
数据组织:
-
主键索引: 决定了表中数据的物理存储顺序(在InnoDB的聚簇索引中)。户口本按
id
号顺序存放。相邻id
的记录在物理磁盘上也更可能相邻。 -
普通索引: 只影响自身B+树中
k
值的排序,不影响主表数据的物理顺序。按家庭人口数k
做的目录,不影响户口本原件按id
存放的顺序。
-
(四)实战演练:EXPLAIN 揭秘查询计划
光说不练假把式。让我们请出MySQL的“照妖镜”——EXPLAIN
,看看查询背后的故事。
-
查询1:精准定位户主 (主键查询)
EXPLAIN SELECT * FROM dbbro WHERE id = 500;
结果解读(关键字段):
-
type: const
(性能最优级别之一):通过主键或者唯一索引一次就找到了。如同精确输入身份证号查户口。 -
key: PRIMARY
:使用了主键索引。 -
rows: 1
:预估扫描1行。 -
Extra: NULL
:没有额外操作,完美。
-
查询2:寻找五口之家 (普通索引查询 + 回表)
EXPLAIN SELECT * FROM dbbro WHERE k = 5;
结果解读(关键字段):
-
type: ref
(常见于使用非唯一索引的等值查询):通过普通索引找到了匹配的行(可能是多行,虽然本例k=5只有一行)。 -
key: k
:使用了名为k
的普通索引。 -
rows: 1
:预估在k索引树扫描1行(找到k=5)。 -
Extra: Using index
:注意!这里容易误解! 它表示查询只使用索引树中的信息就完成了 WHERE 条件的筛选(即找到了k=5对应的id=500),但并不意味着它不需要回表!因为SELECT *
需要获取所有列数据,而k
索引树里只有k
和id
,没有name
。所以,Using index
只表示WHERE部分高效,最终取数据还是要回表!回表操作在EXPLAIN的输出中不会明确标出“Using where for lookup”之类的(在某些版本/场景下可能有提示,但通常不明显),我们需要根据type
是ref/range
且Extra
有Using index
但查询列不在索引中这个组合来推断发生了回表。
重点:Using index
在Extra
出现,仅表示查询的列被索引覆盖(Covering Index)时才意味着避免了回表。如果SELECT
的列不全在索引中(如这里的SELECT *
),即使Extra
有Using index
,也只是指WHERE条件用索引快速定位了,回表依然会发生!
(五)避坑指南与性能优化:让查询飞起来
理解了“回表”这个性能刺客,我们就能有的放矢地优化:
-
能主键,不普通: 如果查询条件能用主键,优先用主键!这是最快的路径。就像相亲直接报身份证号。
-
覆盖索引 (Covering Index) - 避免回表的神器!
-
原理: 如果一个普通索引包含了查询语句中所有需要返回的字段(
SELECT
的列)以及满足WHERE
条件的字段,那么查询只需要扫描这一棵普通索引树就能得到结果,完全不需要回表! -
优化上面查询2: 如果我们只需要
id
和k
(这两个字段都在k
索引树里):
-
EXPLAIN SELECT id, k FROM dbbro WHERE k = 5; -- 只需要k索引树里的数据
-
-
type: ref
-
key: k
-
rows: 1
-
Extra: Using index
(关键!) :这次是真的“覆盖索引”!查询所需数据 (id
,k
) 在k
索引树的叶子节点上全都有,引擎根本不需要去主键树查。性能瞬间飙升到接近主键查询!
-
结果解读:
-
设计技巧: 根据高频查询的
SELECT
和WHERE
子句,精心设计包含必要字段的组合索引。但要注意索引字段的顺序(最左前缀原则)和索引维护的代价(增删改慢一点)。
-
-
唯一索引 (UNIQUE INDEX): 如果业务上某列的值确实是唯一的(比如身份证号、邮箱),创建
UNIQUE INDEX
而不是普通索引。除了保证唯一性,UNIQUE INDEX
在等值查询 (WHERE unique_col = ?
) 时效率等同于主键索引 (const
),因为它也能保证最多只找到一条记录。但它仍然是二级索引,如果查询列不全在索引中,依然可能回表(虽然只回一次,很快)。 -
索引下推 (Index Condition Pushdown, ICP) - MySQL 5.6+ 福利:
-
解决的问题: 对于组合索引
INDEX(a, b)
,查询WHERE a = ? AND b LIKE '%xxx%'
。在旧版本,即使a
能用索引定位,引擎也会把a=?
对应的所有主键id找出来回表,拿到完整数据行后再在Server层过滤b LIKE '%xxx%'
。如果a=?
对应很多行,回表次数就很多。 -
ICP的作用: 支持ICP的存储引擎(如InnoDB),会把
WHERE
条件中索引包含的列(即使不能完全用于查找,如b LIKE '%xxx%'
这种范围/模糊)的过滤操作,“下推”到存储引擎层,在扫描索引时就进行过滤。这样,需要回表的主键id数量就大大减少了。 -
对
dbbro
的启发: 如果我们有INDEX(k, name)
,查询WHERE k=5 AND name LIKE '%临门%'
。在支持ICP的情况下,引擎在扫描k
索引树找到k=5
的条目后,会直接在索引条目上检查name LIKE '%临门%'
是否成立,如果不成立,这个条目对应的id
就不会加入回表列表。只有真正匹配k=5
且name LIKE '%临门%'
的条目对应的id
才需要回表。大大减少了不必要的回表!查看是否启用ICP,看EXPLAIN
的Extra
是否有Using index condition
。
-
(六)芝士就是力量:花絮与冷知识
-
为什么是B+树不是B树? B树的非叶子节点也会存储数据。这导致非叶子节点能存放的键值对数量减少,树变高(I/O增多)。B+树所有数据都在叶子节点,非叶子节点纯目录,能放更多键值,树更矮胖。且叶子链表对范围查询太友好了!
-
B+树能存多少数据? 做个简单估算:假设主键是
bigint(8字节)
,指针6字节(InnoDB实现)。一页16KB=16384字节。一个非叶子节点大约能存放16384 / (8 + 6) ≈ 1170
个键值对。假设一行数据1KB,一个叶子节点大约存放16384 / 1024 ≈ 16
条记录。-
一层树:根节点即叶子节点,最多16条记录。
-
两层树:根节点有1170个孩子(叶子节点),总记录数 ≈ 1170 * 16 ≈ 18, 720 条。
-
三层树:根节点有1170个孩子(非叶子节点),每个非叶子节点再有1170个孩子(叶子节点),总记录数 ≈ 1170 * 1170 * 16 ≈ 21, 902, 400 条 (两千多万!)。
-
四层树:≈ 1170^3 * 16 ≈ 25, 625, 808, 000 条 (两百五十亿!)。 这就是B+树恐怖的地方!通常3-4层就足以支撑海量数据的高效查询。
-
-
没有主键怎么办? InnoDB会非常捉急!它会:
-
首先看有没有定义了
UNIQUE
且所有列都是NOT NULL
的索引,如果有,就用它做主键(称为“隐式主键”)。 -
如果还没有,InnoDB会在表创建时偷偷地生成一个6字节的隐藏列
DB_ROW_ID
作为主键。用户看不到这个列,但它真实存在并维护着主键索引树。强烈建议显式定义主键! 让隐藏主键干活,会增加额外开销且不可控。
-
谢谢大家的关注、点赞、分享、收藏!
如有疑问,可以留言,DB哥看到后会及时回复,也可以加DB哥微信交流
🕶️ 加入「DB哥数据库帮」
DB哥微信:dbelder
🎁 DB哥数据库帮专属福利
▸ 授人以渔
关注DB哥微信公众号「DB哥」免费学DBA级MySQL视频课程【149课时】
▸ 技术辅助
🔧 10年数据库救火队老炮 | 用实战教你少熬三年夜
💥 亲手调优3000+故障库 | 企业级数据库架构
🚀 库崩了?锁死了?SQL慢如🐌?CPU100%
🔥 别慌,DB哥专治数据库各种“不调”!
▸ 背锅侠租赁
临时工小张随时待命:
UPDATE salary SET bonus =0;-- 小张干的!
帮规:
1.不准在生产环境执行UPDATE不带WHERE,否则罚用触控板代替鼠标一周
2.删库后不跑路,否则罚用Windows XP装 MySQL5.0(不兼容也要装)
3.必须用 JOIN 代替子查询,否则罚直播用子查询实现复杂报表(不许用JOIN!)
4.生产环境执行DDL必须测试,否则罚胸口碎大石(罪名:惊动监控告警)
5.不用SELECT * 横扫全表,否则罚罚抄《索引优化十诫》100遍(用毛笔写SQL语句)