上周排查一个线上慢SQL,情况很典型:一张两千多万行的订单表,查询条件的列上明明建了索引,EXPLAIN 却显示全表扫描,单次查询跑了四秒多。我把整个索引存储结构从头到尾翻了一遍,最终定位到根因——查询条件里发生了隐式类型转换,优化器在 B+ 树里按原键值对不上号,干脆放弃索引。
这件事让我一直想认真写一篇关于索引存储数据结构的文章。很多人把索引理解成一本“书的目录”,这个类比能讲清楚用途,却讲不清楚设计。索引的本质,是一组精心选择的数据结构,加上一套和物理存储深度绑定的布局方案。只有真正理解数据在磁盘和内存里怎么组织、键值怎么排序、页与页之间怎么关联,才能回答下面这一系列问题:索引为什么快?为什么会在某些场景下失效?为什么联合索引有最左前缀原则?为什么有些列建了索引反而没用?
这篇文章从磁盘 IO 约束出发,把 B+ 树、哈希索引、聚簇索引、联合索引、索引下推这些概念串成一条线,配合可复现的排查思路和设计建议。适合刚接触数据库索引原理的后端开发者,也适合那些已经能写 SQL、但 EXPLAIN 出了问题不知道从哪里下手的同学。
1. 索引存储的底层数据结构全景
1.1 索引不是“目录”,而是一棵能二分查找的树
教科书喜欢把索引比作字典目录,这个类比有误导性。字典目录是一个独立的线性列表,页码对应的内容在另一个位置;数据库索引则和数据本身存储在一起,在 InnoDB 里,表数据就是按主键顺序存放在 B+ 树叶子节点上的。索引不是一个“额外的文件”,它本身就是数据的组织形态。
以主键索引为例,InnoDB 会以主键为键值构建一棵 B+ 树。这棵树从根节点往下,每一层都是有序排列的键值区间,查询时从根节点出发,通过比较键值不断下探,最终在叶子节点找到目标记录。整棵树的高度通常只有 3 到 4 层,意味着哪怕表里有几千万条数据,一次查询也只需 3 到 4 次磁盘 IO 就能定位到目标行。这个数字很关键,后面讲磁盘 IO 约束时还会提到。
树结构和“目录”最本质的区别在于:目录告诉你“第 250 页”,你还得翻到那一页去读;B+树把数据直接放在叶子节点上(聚簇索引),你找到叶子节点的同时就找到了整行数据。这个“数据随索引一起存储”的设计,才是 InnoDB 表和普通 CSV 文件在查询性能上拉开差距的根本原因。
1.2 数据页与索引页:磁盘 IO 才是索引设计的终极约束
为什么索引要用树,不用数组、链表、哈希表?最核心的约束是磁盘 IO。机械硬盘随机读取一次大约 10ms,SSD 虽然快一些,但随机读取依然比顺序读取慢一到两个数量级。而数据库表的最小存储单位是页(Page),InnoDB 默认页大小是 16KB。也就是说,每次从磁盘读取数据,至少搬一整页回来,哪怕你只需要其中的一行。
这引出了一个重要的设计思路:磁盘 IO 次数等于树的高度,要让树尽量矮,每层能覆盖的键值区间尽量多。B+树每个节点就是一个页,一个 16KB 的页能存几百上千个键值。假设每页可存 500 个键,三层树就能覆盖 500×500×500,也就是 1.25 亿条数据。对比一下二叉树,同样的数据量树高得有 27 层,也就是 27 次磁盘 IO——查询性能差一个数量级。
页内部也不是简单的一串记录从头排到尾。InnoDB 在页内维护了一个目录(Page Directory),记录按主键顺序排列,同时每隔若干条记录在目录中保存一个“槽位”,指向页内的记录偏移量。查找页内记录时,先在目录里二分定位到槽位,再在槽位对应的小范围内顺序查找。也就是说,索引的查找是“整棵树二分 + 页内二分”的双层结构,每一层都在尽可能减少读盘次数。
1.3 聚簇索引与非聚簇索引:存储结构决定了一切
InnoDB 的主键索引是聚簇索引(Clustered Index),叶子节点直接存放整行数据,表数据本身就是主键索引。二级索引(非聚簇索引)则不同,它的叶子节点存放的是索引键值加主键值。用二级索引查询时,先在二级索引的 B+ 树里找到主键值,再拿着主键值回到聚簇索引里查一次完整数据行,这个过程叫回表(Back to Table)。也因此,回表意味着一次查询要有两次 B+ 树查找。
MyISAM 存储引擎的做法相反,表数据和索引完全分离,索引叶子节点存的是数据行的磁盘地址指针。查询时先通过索引找到指针,再根据指针去数据文件里读那一行。这个设计与 InnoDB 的一个显著差异是:MyISAM 的主键索引和二级索引在结构上完全对称,都只存指针;而 InnoDB 的二级索引必须携带主键值。
这两种设计带来的连锁反应值得展开说:InnoDB 要求表必须有主键,如果没有显式定义,会选第一个非空唯一索引,实在没有就自动生成一个隐藏的 6 字节 rowid 作为主键。二级索引因为存了主键值,主键越长,每个二级索引的存储空间越大。所以 InnoDB 表设计里“用自增整数做主键、不要用 UUID 这种长随机值”,不只是为了写性能,对索引存储空间的节约同样关键。聚簇索引还有个特性是物理顺序和主键顺序一致,范围查询时磁盘预读
