14 MySQL 索引特性

14 MySQL 索引特性
小米里的大麦MySQL 索引特性
1. 索引的概念
索引就是帮 MySQL 快速查数据 的一种 数据结构,就像书的目录一样,不需要翻整本书,只查目录定位页码直接跳到目标,没有索引就得 全表扫,一条一条比对特征值,慢得很(有索引 → 快,无索引 → 慢)。任何便捷都是有代价的,索引也一样:占用空间 + 增删改变慢。
1. 索引的价值
为了测试索引效果,我们需要先准备一张包含大量数据的表(海量数据),这里创建一个名为 index_test 的数据库,并在其中创建 emp 员工表,插入 800 万条记录。这里的先看效果,SQL 语句后面会讲到。
1 | # 1. 创建数据库 |
2. 验证索引价值
1. 无索引查询
查看表结构,确认当前没有索引:
1 | mysql> desc emp; |
查询指定工号,观察耗时:
1 | mysql> select * from emp where empno=100003; |
没有索引时,全表扫描,800 万数据大概需要数秒左右。
2. 创建索引
给 empno 字段创建索引:
1 | mysql> alter table emp add index(empno); |
数据量大,建索引需要一点时间。
3. 有索引查询
再次查询相同数据:
1 | mysql> select * from emp where empno=100003; |
耗时几乎为 0,这就是索引的价值。
2. 认识磁盘
数据库的根儿在磁盘。磁盘有机械动作(摇磁头、转盘片),比内存慢十万倍起步,MySQL(特别是索引)的所有折腾,本质就干一件事:尽量少碰磁盘,碰了也要按顺序碰。
- MySQL 给用户提供存储服务,存储的数据在磁盘这个外设当中。
- 磁盘是计算机中的一个机械设备,相比于计算机的其他电子元件,磁盘的效率是比较低的。
- 而如何提高效率是 MySQL 的一个重要话题,因此我们有必要了解一下磁盘的相关内容。
1. 磁盘的结构
从硬件到 MySQL,数据是一层层打包的:
- 扇区(Sector):磁盘物理读写的最小单元,传统大小为 512 字节,现代磁盘多为 4KB。
- 块(Block):Linux 操作系统读写的基本单位,常见 4KB。
- 页(Page):MySQL(InnoDB)与磁盘打交道的最小单位,默认 16KB。
重点:MySQL 哪怕只查 1 字节的数据,也会触发一次 I/O,把包含这字节的整整一页(16KB)全拉进内存。因为磁盘找位置太慢,既然找到了,“来都来了”,干脆多捞点,摊薄成本。
2. 访问方式:随机访问 vs 顺序访问
- 随机访问:磁头需要频繁移动和等待盘片旋转到目标扇区,涉及昂贵的 寻道时间 和 旋转延迟,单次耗时数毫秒。相比内存纳秒级的速度,磁盘随机访问慢 10 万倍以上。
- 顺序访问:读写连续扇区上的数据,磁头几乎不需要移动,可以持续高速传输,速度比随机访问快几十到几百倍。
所以: 索引设计的核心目标之一,就是 尽量减少随机磁盘 I/O 的次数,并尽可能将数据访问模式转化为顺序访问。
3. 文件系统:在磁盘上建立索引结构
操作系统通过文件系统管理磁盘空间,将物理扇区组织成文件和目录。现代文件系统多为 索引式文件系统,其设计对数据库性能有深远影响。
1. 索引式文件系统(以 Linux Ext 系列为例)
Ext 系列文件系统将分区划分为多个 块组,每个块组包含:
- 超级块:存储整个文件系统的全局信息(如大小、状态、块总数、inode 总数),并在多个块组中备份。
- 块组描述符表:描述每个块组中块位图、inode 位图、inode 表的位置。
- 块位图:用一个 bit 对应一个数据块,标识块组中数据块的使用情况。
- inode 位图:用一个 bit 对应一个 inode,标识 inode 的使用情况。
- inode 表:存放 inode(索引节点)的区域。每个文件或目录都有一个唯一的 inode,记录文件的元数据(权限、所有者、时间戳)以及 指向文件数据所在磁盘块的指针。
- 数据块:真正存放文件内容的地方。
工作原理:读取文件时,操作系统根据文件名找到其 inode,从 inode 中获取数据块指针,然后一次性找到所有数据块进行读取。这种结构使得文件的 所有数据块可以被高效地定位。
2. 非索引式文件系统(以老旧 FAT 为例)
FAT 文件系统没有 inode,而是通过一个“文件分配表”来记录数据块的链接关系。读取文件时,需要沿着数据块链逐个读取:读第一个块,从中找到下一个块的地址,再读下一个块……如果文件的数据块在磁盘上 碎片化严重(分散在不同位置),这种访问就会变成大量的随机 I/O,性能急剧下降。因此 FAT 文件系统需要定期进行 碎片整理,将同一文件的数据块重新排列成连续存储,将随机访问转化为顺序访问。
4. MySQL 怎么把性能拉满 —— I/O 路径优化
- B+ 树索引:直接干掉全表扫描。把可能成千上万次的磁盘 I/O(扫无数个页),用二分查找压缩到 3-4 次 I/O。
- 聚簇索引(主键索引):数据直接跟着主键按顺序存。你搞个范围查询(
BETWEEN 100 AND 200),MySQL 顺着页挨个拿,直接把随机读变成了贼快的 顺序读。 - 缓冲池(Buffer Pool):热数据直接缓存在内存里,彻底绕开磁盘。
- 预读机制:根据局部性原理,猜你马上要看后面的数据,提前帮你把后面的页读进内存。
一切都是空间换时间、内存换磁盘、顺序换随机!
5. 从磁盘到 MySQL:一次 I/O 的完整路径与索引的优化作用
现在我们将磁盘、文件系统和 MySQL 串联起来,看一次查询背后的 I/O 路径,以及索引如何优化这个过程。
1. MySQL 的一次 I/O 过程
- MySQL 发起 I/O 请求:需要读取一行数据时,MySQL 知道该行位于某个 页(16KB) 中。如果该页不在内存的缓冲池(Buffer Pool)里,MySQL 就向操作系统发起 I/O 请求,要求读取这个页。
- 文件系统介入:操作系统收到请求后,根据数据库文件的 inode 和页在文件中的偏移量,找到该页对应的文件系统 数据块(通常 4KB),并向磁盘驱动发起读取这些数据块的指令。
- 实际磁盘 I/O:磁盘控制器执行物理操作:寻道(移动磁头到正确磁道)、旋转(等待目标扇区转到磁头下)、读取数据。磁盘将扇区数据返回给操作系统。
- 数据返回:操作系统将扇区数据拼装成文件系统块,再返回给 MySQL。MySQL 将整个页放入缓冲池,然后从中解析出目标数据行。
2. 索引如何优化这一过程?
- 显著减少 I/O 次数:没有索引时,MySQL 可能需要进行 全表扫描,即从磁盘读取表的第一个页直到最后一个页,可能涉及成千上万次 I/O。有了索引(如 B+ 树索引),MySQL 可以在索引结构中快速二分查找,将需要访问的页数量从成千上万减少到三到四次。
- 引导顺序访问:索引(特别是 InnoDB 的 聚簇索引,即主键索引)本身有序,并且会将数据行按照主键顺序物理存储在页中。这使得 范围查询(如
WHERE id BETWEEN 100 AND 200)能够读取一系列连续的页,将大量的随机 I/O 转化为一次或少数几次顺序 I/O,极大提升速度。 - 避免数据块链遍历:索引直接指向记录所在的页,避免了像 FAT 文件系统那样为了找到一个文件的所有碎片而进行多次寻道的低效操作。
6. 小结
磁盘的物理特性决定了其访问速度远慢于内存,且随机访问比顺序访问慢得多。MySQL 的所有优化手段,本质上都是在 用空间换时间,用顺序换随机,用内存换磁盘。通过精心设计的页大小、预读机制、缓冲池以及最重要的——索引结构,MySQL 使得数据访问模式尽可能符合磁盘的物理特性,从而在保证海量数据存储的同时,实现高效的查询性能。
索引并非万能,它需要额外的存储和维护成本,但它是对磁盘 I/O 瓶颈最有效的应对策略之一。理解磁盘和文件系统的底层原理,有助于我们更合理地设计索引,让数据库真正“懂”硬件。
3. 索引的理解
由上文可知:
- 本质:索引就是帮你快速找到数据的“目录”,避免全表扫。
- 原理:数据库底层为表中数据建立特定的数据结构(一般是 B+ 树)。
- 好处:查询快、范围查询快。
- 代价:插入/删除/更新要维护索引,可能慢一点,且占空间。
1. 观察主键索引现象
底层其实就是 MySQL(InnoDB 引擎)拿主键建了一棵 B+ 树(这就是常说的聚簇索引)。
- 自动生成: 数据在硬盘上总得有个顺序存。设了主键,MySQL 就直接拿它当树的骨架,把整张表的数据全挂在这棵树上。
- 查得快: 查主键就是顺着 B+ 树往下找。这棵树很矮胖,通常只有 3 到 4 层。哪怕表里有几千万条数据,最多也就做 3、4 次判断就能定位,当然瞬间出结果。
- 自动维护唯一性: 往表里插新数据,等于往树里加新节点。新节点必须按大小找到对应的位置插进去,这个找位置的过程,顺手就把“有没有重复”给检查完了。
1 | # 1. 建表:设了主键 id |
2. 为什么 MySQL 与磁盘交互的基本单位是 Page
为了掩盖磁盘的慢,把“零售”变成“批发”。
- 磁盘最慢的动作是摇磁头找位置(寻道)。找位置花的时间,远比实际读数据花的时间长。所以读 1 个字节和读 16KB,其实耗时差不多。
- 计算机有个基本规律叫 局部性原理:指的是现在读了这条数据,一会大概率要读它挨着的数据。
- 所以 MySQL 定死规矩:只要去磁盘读写,最少按一页(默认 16KB)打包拿。哪怕只要 1 个字节,也把这 16KB 全拉进内存。这样下次要查附近的数据,直接在内存里就有,不用再让磁盘慢吞吞地找了。
1 | # 查看 innodb 的页大小,默认就是 16384 字节 (16kb) |
3. 推导主键索引结构构建
MySQL InnoDB 引擎的主键索引采用 B+ 树结构,其构建过程可分解为以下关键步骤,每一步都解决了数据管理中的一个实际问题,最终演化出高效的索引组织方式。
- 单个 Page:数据顺序存放。
- 单个 Page 内目录:记录页内的偏移,快速定位行。
- 多个 Page:数据多,跨页存储。
- Page 之上创建页目录:页目录指向每个数据 Page。
- 页目录之上再建页目录:层层索引 → B+ 树结构。
- B+ 树 Page 是否全部进 Buffer Pool:不需要全部,按需加载,减少内存占用。
1. 单个 Page:数据顺序存放
数据页(Page)是 InnoDB 磁盘管理的最小单位,默认 16KB。当数据量很小时,所有记录存放在一个页内,按主键顺序排列(逻辑顺序,物理上可能通过页内链表实现)。这是最基础的存储单元。
2. 单个 Page 内目录:记录页内的偏移,快速定位行
页内部维护一个“页目录”(Slot),用于快速二分查找。页内记录并不连续存储,而是通过“槽”指向每组记录的最大值(或记录本身)。这样在页内查找某条记录时,可以先通过目录二分定位到所在组,再遍历组内记录,避免了全页扫描。
3. 多个 Page:数据多,跨页存储
当单页存不下时,数据分散到多个页,页之间通过双向链表链接。此时若要在所有页中查找一条记录,需要从第一页开始顺序遍历所有页,效率低下。因此需要更高层的索引结构来快速定位到目标页。
4. Page 之上创建页目录:页目录指向每个数据 Page
新增一层“索引页”,专门存放每个数据页的边界值(主键最小值)及其页号。这个索引页相当于数据页的“目录”,每个条目指向一个数据页。查找时先读索引页,通过二分查找确定目标数据页,再进入该页查找,大幅减少 I/O。
5. 页目录之上再建页目录:层层索引 → B+ 树结构
当索引页也变多时,继续在其上建立更高层的索引页,形成多级树形结构。所有数据页都在最底层(叶子节点),上层均为索引页(非叶子节点),且每个节点(页)内都有“目录”结构。这就是典型的 B+ 树:叶子节点存放实际数据(或主键+行指针),非叶子节点存放索引键和子页指针,所有叶子节点通过双向链表连接,支持高效的范围扫描。
6. B+ 树 Page 是否全部进 Buffer Pool:不需要全部,按需加载,减少内存占用
Buffer Pool 只缓存经常访问的页,尤其是根节点和上层索引页常驻,而大量叶子页按需加载。B+ 树的高度通常很矮(2~4 层),查找一条记录只需访问根节点到叶子节点的路径上的少数页。因此无需将所有页加载到内存,而是通过 LRU 等算法管理缓冲池,既节省内存又保证高效访问。
4. 索引结构可以采用哪些数据结构
索引结构可以采用的数据结构(MySQL 实际用到的):
- Hash(内存表、Memory 引擎常用)
- B+树(InnoDB、MyISAM 的主流选择)
- R 树(空间索引)
- 倒排索引(全文索引)
链表、二叉搜索树、AVL、红黑树、Hash 为什么不适合做磁盘索引(或不作为主流):
| 数据结构 | 为什么不适合做磁盘上的主键/二级索引 | 缺点 |
|---|---|---|
| 链表 | 查找 O(n),必须顺序扫描 | 太慢 |
| 二叉搜索树 | 最坏退化成链表(O(n)),高度不可控 | 高度失衡、随机 IO 多 |
| AVL 树 | 严格平衡,每次插入/删除都要大量旋转,写性能极差 | 维护代价太大 |
| 红黑树 | 高度 ≈ 2logn,比 B+树高很多,同一层能放的 key 太少 | 树更高 → 更多随机 IO |
| Hash | 无法范围查询(>、<、between、order by)、无法利用索引排序 | 范围查询退化为全表扫描 |
所以:磁盘索引追求:树矮 + 每层 key 多 + 顺序读友好 + 支持范围查询,B+树几乎完美满足。
5. B 树 vs B+树
B+树比普通 B 树更适合 MySQL 磁盘索引,主要因为两点:
- B 树每个节点都既存键又存数据,受 Page 大小限制,非叶子节点能放的键数量少,导致树更高,查询时需要更多次随机磁盘 IO;B+树非叶子节点只存键和指针,能塞进更多键,树更矮,IO 次数显著减少。
- B 树叶子节点之间没有链接,范围查询(如 between、>、order by)需要多次随机跳转;B+树所有叶子节点用双向链表顺序连接,范围扫描几乎变成连续顺序读,效率远高于 B 树。
因此,MySQL(InnoDB/MyISAM)选择 B+树作为索引结构,最大化减少随机 IO 并优化范围查询性能。
6. 聚簇索引 VS 非聚簇索引
在 MySQL 中,索引的实现方式直接影响查询性能和数据存储。聚簇索引和非聚簇索引是两种最基本的索引类型,而不同存储引擎(InnoDB 与 MyISAM)对它们的实现又有显著差异。
- 聚簇索引(叶子节点 = 完整数据):索引即数据。找到了索引,就抓到了整行记录。
- 非聚簇索引(叶子节点 = 指针/主键):索引是索引,数据是数据。找到了索引,还得拿着“地址”或“主键 ID”去别处找数据(这叫 回表)。
1. 聚簇索引(Clustered Index)—— InnoDB 主键索引
特点:索引与数据行存储在一起,B+ 树的叶子节点直接保存完整数据行。InnoDB 中,主键索引 就是聚簇索引,数据按主键顺序物理存放。
- 优点:
- 主键查询极快:通过主键查找可直接定位到叶子节点,一次 I/O 即可获取整行。
- 范围查询高效:数据有序存储,范围扫描只需沿叶子节点链表顺序读取,磁盘预读友好。
- 排序与分组优化:按主键排序时无需额外排序操作。
- 缺点:
- 插入可能引发页分裂:在已满页中插入新记录需分裂页,影响写入性能。
- 更新主键代价大:主键变更会导致数据行移动,应避免。
- 文件存储:数据和索引同在一个
.ibd文件中。
2. 非聚簇索引(Secondary Index)
特点:索引与数据分开存放,叶子节点不存完整行,而是存指向数据的“引用”。MyISAM 的所有索引均为非聚簇;InnoDB 中除主键外的索引(普通索引、唯一索引)也是非聚簇。
1. MyISAM 索引结构(非聚簇)
- 主键索引:B+ 树叶子节点存放数据行的 物理地址(磁盘偏移量),指向
.MYD数据文件。索引文件独立为.MYI。 - 普通索引:结构同上,仅键值允许重复。所有索引都平等地指向同一份数据(
.MYD),无数据冗余。 - 查询流程:通过索引找到物理地址后,直接定位到数据文件中的行,无需回表(因为地址直接指向数据)。
2. InnoDB 普通索引结构(辅助索引)
- 回表:InnoDB 中通过辅助索引查找时,必须先拿到主键值,再回聚簇索引取整行。这是辅助索引的默认行为。
- 覆盖索引:若辅助索引的键已包含查询所需的所有列,则直接返回结果,无需回表。这是优化查询的重要手段。例如索引
(name, age)可覆盖查询SELECT age FROM t WHERE name = ...。
- 叶子节点存储主键值:B+ 树叶子节点中存放的是对应记录的主键值,而非物理地址。
- 查询流程(回表):先通过普通索引找到主键值,再到聚簇索引(主键索引)中查找完整行,这个过程称为 回表查询。这本质是 以时间换空间——多个普通索引不冗余数据,但增加了一次 I/O。
- 设计目的:节省空间。若每个普通索引叶子节点都存整行,会导致数据冗余;存主键值则保证了数据唯一性,且主键值不变时,即使数据行移动(如页分裂),普通索引也无需更新。
- 必有主键:InnoDB 表 必须有一个主键(用户未定义时,InnoDB 会自动创建隐式主键),因为完整数据行只存储在主键索引对应的 B+ 树中。
- 覆盖索引优化:如果查询所需的列全部包含在普通索引中(即索引覆盖),则无需回表,直接返回结果。
3. MyISAM VS InnoDB 非聚簇索引
| 对比项 | MyISAM(非聚簇) | InnoDB 普通索引(辅助索引) |
|---|---|---|
| 叶子节点内容 | 数据行的物理地址(指向 .MYD) | 对应记录的主键值 |
| 查询路径 | 索引 → 地址 → 数据文件(一次定位) | 索引 → 主键值 → 聚簇索引 → 完整行(回表) |
| 数据移动影响 | 数据移动(如插入)需更新所有索引地址 | 数据移动不影响辅助索引(因存主键值) |
| 空间占用 | 索引文件独立,体积较小 | 辅助索引体积小,但需额外存储主键值 |
3. 建议
- 聚簇索引(InnoDB 主键):适合主键查询、范围扫描、排序频繁的场景。建议选择自增主键,避免页分裂。
- 非聚簇索引(MyISAM):适合只读或读多写少的场景,所有索引平等且无需回表,但数据移动开销大,且不支持事务。
- InnoDB 辅助索引:需权衡回表代价,通过 覆盖索引 设计减少回表;多个索引不冗余数据,写入更稳定。




















