当前位置: 首页 > news >正文

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+树:

  1. 主键索引(PRIMARY KEY)B+树: 以id为排序依据。这棵树不光记录id,还完整地保存着id, k, name这条记录的所有信息!它是户口本的原件,按照身份证号id顺序装订好。

  2. 普通索引(INDEX(k))B+树: 以k(家庭人口数)为排序依据。但这棵树上只记录两样东西:k的值和对应的主键值id。它相当于一个按家庭人口数编排的索引目录,告诉你某个家庭人口数(k)对应的是哪个身份证号(id)。想找详细信息?对不起,请拿着id去主键索引树查原件(回表)。

(二)深入江湖:B+树秘境的探幽

为什么是B+树?不是二叉树、不是哈希表、不是B树?且听我慢慢道来(敲黑板,技术重点!):

  1. B+树的“人设”:

    • 多路平衡搜索树: 一个节点(江湖人称“页”,默认16KB)可以有很多孩子(指针),不像二叉树只有俩。这大大降低了树的高度(层数),减少了磁盘I/O(想象翻户口本翻的次数少)。平衡意味着不会出现“瘸腿”树,查询稳定。

    • 叶子节点才存数据(主键索引): 在主键索引树里,只有最底层的叶子节点才存放完整的行数据(id, k, name)。非叶子节点只存储索引键(id)和指向下一层节点的指针。这就好比目录页只写章节标题和页码,具体内容在正文页。

    • 叶子节点链表串联(核心!): 所有叶子节点按索引键大小顺序(id从小到大)用双向链表连接起来!这个设计太妙了,做范围查询(找id在200到500之间的人)和全表扫描效率极高,顺着链表撸就行。

    • 数据都在叶子节点(普通索引): 在普通索引树(index(k))里,叶子节点存储的是索引键(k)和对应的主键值(id)。同样,非叶子节点只存k和指针。

  2. 图解江湖:两棵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+树的关系了吧?让我们系统总结下区别:

  1. 存储内容:

    • 主键索引: 叶子节点存储整行数据。它是数据的“家”,是户口本的原件。

    • 普通索引: 叶子节点存储索引列的值 + 主键值。它是数据的“快捷方式”或“目录页”。

  2. 查询方式 (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 = ?):

  3. 唯一性:

    • 主键索引: 天然具有唯一性约束。确保id列的值绝对不重复。户口本号能重复吗?不能!

    • 普通索引: 没有唯一性保证(除非你显式创建UNIQUE INDEX)。允许多个不同的id对应相同的k值。比如,可以有好几个“五口之家”(k=5),但他们的id(身份证号)肯定不同。

  4. 数据组织:

    • 主键索引: 决定了表中数据的物理存储顺序(在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索引树里只有kid,没有name。所以,Using index只表示WHERE部分高效,最终取数据还是要回表!回表操作在EXPLAIN的输出中不会明确标出“Using where for lookup”之类的(在某些版本/场景下可能有提示,但通常不明显),我们需要根据typeref/rangeExtraUsing index但查询列不在索引中这个组合来推断发生了回表。

重点:Using indexExtra出现,仅表示查询的列被索引覆盖(Covering Index)时才意味着避免了回表。如果SELECT的列不全在索引中(如这里的SELECT *),即使ExtraUsing index,也只是指WHERE条件用索引快速定位了,回表依然会发生!

(五)避坑指南与性能优化:让查询飞起来

理解了“回表”这个性能刺客,我们就能有的放矢地优化:

  1. 能主键,不普通: 如果查询条件能用主键,优先用主键!这是最快的路径。就像相亲直接报身份证号。

  2. 覆盖索引 (Covering Index) - 避免回表的神器!

    • 原理: 如果一个普通索引包含了查询语句中所有需要返回的字段SELECT的列)以及满足WHERE条件的字段,那么查询只需要扫描这一棵普通索引树就能得到结果,完全不需要回表

    • 优化上面查询2: 如果我们只需要idk(这两个字段都在k索引树里):

EXPLAIN SELECT id, k FROM dbbro WHERE k = 5; -- 只需要k索引树里的数据
    • type: ref

    • key: k

    • rows: 1

    • Extra: Using index (关键!) :这次是真的“覆盖索引”!查询所需数据 (idk) 在 k 索引树的叶子节点上全都有,引擎根本不需要去主键树查。性能瞬间飙升到接近主键查询!

    • 结果解读:

    • 设计技巧: 根据高频查询的SELECTWHERE子句,精心设计包含必要字段的组合索引。但要注意索引字段的顺序(最左前缀原则)和索引维护的代价(增删改慢一点)。

  1. 唯一索引 (UNIQUE INDEX): 如果业务上某列的值确实是唯一的(比如身份证号、邮箱),创建UNIQUE INDEX而不是普通索引。除了保证唯一性,UNIQUE INDEX在等值查询 (WHERE unique_col = ?) 时效率等同于主键索引 (const),因为它也能保证最多只找到一条记录。但它仍然是二级索引,如果查询列不全在索引中,依然可能回表(虽然只回一次,很快)。

  2. 索引下推 (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=5name LIKE '%临门%'的条目对应的id才需要回表。大大减少了不必要的回表!查看是否启用ICP,看EXPLAINExtra是否有Using index condition

(六)芝士就是力量:花絮与冷知识

  1. 为什么是B+树不是B树? B树的非叶子节点也会存储数据。这导致非叶子节点能存放的键值对数量减少,树变高(I/O增多)。B+树所有数据都在叶子节点,非叶子节点纯目录,能放更多键值,树更矮胖。且叶子链表对范围查询太友好了!

  2. 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层就足以支撑海量数据的高效查询。

  3. 没有主键怎么办? 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语句)​​​​​​​
http://www.dtcms.com/a/263057.html

相关文章:

  • 【软考高项论文】论信息系统项目的干系人管理
  • ACT-R 7.28
  • pbootcms程序运行异常: Modulo by zero,位置:/www/wwwroot/****/core/function/helper.php
  • 链表题解——设计链表【LeetCode】
  • langchain从入门到精通(二十四)——RAG优化策略(二)多查询结果融合策略及 RRF
  • [特殊字符]️ Hyperf框架的数据库查询(ORM组件)
  • iOS 接口频繁请求导致流量激增?抓包分析定位与修复全流程
  • Reactor重试操作
  • 十大排序算法汇总
  • 2025年06月30日Github流行趋势
  • 创客匠人解析强 IP 时代创始人 IP 打造的底层逻辑与破局之道
  • Java开发新变革!飞算JavaAI深度剖析与实战指南
  • 一文讲清楚React中类组件与函数组件的区别与联系
  • 手机屏暗点缺陷修复及相关液晶线路激光修复原理
  • 类图+案例+代码详解:软件设计模式----生成器模式(建造者模式)
  • Franka机器人赋能RoboCulture研究,打造生物实验室智能解决方案
  • Vue防抖节流
  • 最新版 JT/T808 终端模拟器,协议功能验证、平台对接测试、数据交互调试
  • Spring Cloud Bus 和 Spring Cloud Stream
  • HarmonyOS NEXT仓颉开发语言实战案例:外卖App
  • NAT 类型及 P2P 穿透
  • 人工智能和云计算对金融未来的影响
  • Docker 入门教程(九):容器网络与通信机制
  • Qt 前端开发
  • (3)pytest的setup/teardown
  • 文心大模型 4.5 系列开源首发:技术深度解析与应用指南
  • Python 数据分析与可视化 Day 12 - 建模前准备与数据集拆分
  • 【C语言 | 字符串处理】sscanf 用法(星号*、集合%[]等)详细介绍、使用例子源码
  • 嵌入式SoC多线程架构迁移多进程架构开发技巧
  • C++ std::list详解:深入理解双向链表容器