一、索引基础:从查字典到B树

1.1 什么是索引

先说个最直观的理解。你想想看,一本几千页的技术手册,我想找到“B+树”这个词,怎么办?

最笨的办法——从第1页翻到最后一页。这叫全表扫描。

聪明点的办法——翻到书末尾的“索引页”,找到“B+树”对应的页码,直接翻过去。这就是索引干的活。

数据库索引,说白了就是一种以空间换时间的数据结构。它单独维护一份“目录”,让你不用遍历整张表就能定位到目标行。

索引的本质:快速定位,避免全表扫描。

我记得刚入行那会儿,接手过一个慢查询。一个简单的用户查询跑了15秒。业务方急得跳脚。我一看表结构——好家伙,30万行数据,连个索引都没建。加完索引,查询变成0.02秒。嗯,这就是索引最直接的威力。

1.2 索引的核心作用

索引不是万能的,但它能解决绝大多数查询性能问题。我把它总结成四点:

  • 加速数据检索:这是索引最根本的使命。通过索引树快速定位,复杂度从O(n)降到O(log n)。
  • 保证数据唯一性:唯一索引确保了某列或某组列的数据全局不重复。比如用户表的email字段。
  • 加速排序和分组:B树索引本身就有序,ORDER BY 或 GROUP BY 可以免去额外的排序步骤。
  • 表连接加速:JOIN操作时,被驱动表的连接列上有索引,能大幅减少扫描行数。

你看第四点——连接查询的索引优化,我在项目里踩过不少坑。有一次重构报表系统,两张千万级的表做JOIN,没加索引跑了半小时没出结果。加完索引,3秒搞定。

索引不是越多越好。每个索引都会占用磁盘空间,并且拖慢INSERT/UPDATE/DELETE的写入速度。因为每次写数据,索引也得跟着维护。

1.3 B树索引:最常用的索引结构

面试时经常有人问我:MySQL为什么用B+树,而不是二叉树或哈希表?

原因很简单——磁盘I/O特性决定了树要矮、要宽。

B树是一种多路平衡查找树。它的核心特点是:

  1. 每个结点可以存储多个键值:不像二叉树一个结点只存一个值,B树的结点可以存几百个键值。
  2. 结点大小对齐磁盘页:InnoDB里一页是16KB。一个B树结点刚好占一页。一次I/O读取一整个结点。
  3. 树高度很低:对于百万级数据,B+树的高度通常只有3-4层。也就是说,找任何一条数据,最多做3-4次磁盘I/O。
B树和B+树不完全一样。B+树的非叶结点只存索引键值,不存数据;数据全存在叶子结点。叶子结点之间还有链表指针,方便范围扫描。MySQL InnoDB用的就是B+树。

我做性能调优时,经常查看B+树的层数。怎么查?看SHOW TABLE STATUS里的Index_length,再算算页数。

举个例子:

-- 查看表的索引信息
SHOW TABLE STATUS LIKE 'orders'\G

-- 假设 Index_length = 4194304(即4096KB)
-- 一页16KB,所以有 4096 / 16 = 256 页
-- 如果该B+树扇出是500,层数 = log_500(256) ≈ 0.9层
-- 嗯,这几乎就是一层,说明索引效率非常高

你想想看,为什么B+树特别适合范围查询?因为叶子结点有序排列,并且用双向链表连起来了。查“大于100且小于200”的数据,只需要先找到100的位置,然后沿着链表往后扫就行。

特性 B+树 Hash索引
适用查询 等值、范围、排序、前缀匹配 仅等值查询
磁盘I/O次数 较稳定(树高3~4) 通常1次(无碰撞情况)
存储空间 较大(有内部结点和非叶结点) 较小
支持排序 天然有序 不支持

1.4 聚簇索引与非聚簇索引

很多人分不清这两者。我直接说人话:

  • 聚簇索引:数据行和索引键存在一起。InnoDB的主键索引就是聚簇索引。找到索引,就等于找到了整行数据。
  • 非聚簇索引:索引单独存,数据在另一个地方。找到索引后,还需要回表查一次完整数据。也叫二级索引。

为什么InnoDB一定要有聚簇索引?

因为数据物理存储顺序就是主键顺序。换句话说,表数据本身就是一颗B+树——以主键为键值,叶子结点存的就是完整的行数据。

对于InnoDB表,建议使用自增整型主键。因为数据按主键顺序插入,不会频繁引起页分裂。如果是UUID作为主键,插入时是随机的,会导致大量页分裂和碎片。

我做过一个对比测试:

  • 自增主键 + 无碎片:单表插入性能稳定在每秒5万行
  • UUID主键 + 频繁页分裂:同一台机器,每秒只能插入2万行不到

差别就在这里。页分裂不仅影响写入,还浪费存储空间。

非聚簇索引的覆盖索引是调优利器。如果查询的字段全部在非聚簇索引里,就不需要回表,直接走索引就能拿到结果。这叫索引覆盖,能大大减少磁盘I/O。

举个例子:

-- 表 user 有字段 id, name, age, email
-- 建一个复合索引
CREATE INDEX idx_name_age ON user(name, age);

-- 下面这个查询就只需要扫描索引,不需要回表
SELECT name, age FROM user WHERE name = '张三';

1.5 B树索引结构图

下面这张图把B+树的整体结构串起来了。我建议你只看这张图就能复述出B+树的核心逻辑。

B+树索引结构示意 根结点 (非叶) 非叶结点 (10 | 30 | 50) 非叶结点 (70 | 90 | 110) 非叶结点 (130 | 150) 叶: 1,3,5,8 叶: 12,15,20 叶: 32,40,45 叶: 56,62,68 叶: 75,81,88 叶: 92,99,105 叶: 112,120,128 叶: 135,142,150 B+树关键特点 非叶结点只存键值,不存数据 → 结点更窄,扇出更大 → 树更矮 → I/O更少

1.6 评估索引好坏的几个指标

你怎么知道一个索引建得好不好?我一般看这几点:

  1. 选择性:索引列中不同值的比例。选择性越接近1,索引越有效。比如性别列只有男女两种值,选择性太低,建索引意义不大。
  2. 索引高度:前面说了,B+树的层数越少越好。一般超过4层就要考虑优化了。
  3. 回表比例:走了非聚簇索引之后,还要回主键索引查多少次。如果回表次数太多,不如直接走全表扫描。
  4. 索引碎片率:频繁插入和删除会导致页分裂,产生碎片。碎片率高了,查询性能会下降。可以用OPTIMIZE TABLE重建。

举个例子,我曾经优化过一个电商订单表:

-- 查看索引选择性
SELECT 
    COUNT(DISTINCT order_id) / COUNT(*) AS order_selectivity,
    COUNT(DISTINCT user_id) / COUNT(*) AS user_selectivity,
    COUNT(DISTINCT status) / COUNT(*) AS status_selectivity
FROM orders;

-- 查出结果
-- order_selectivity: 1.0000  (非常好)
-- user_selectivity: 0.0002  (差)
-- status_selectivity: 0.00001 (极差)

所以,在order_id上建索引效果最好,在status上建索引基本是浪费。

嗯,这就是索引基础部分最核心的内容。你把这些概念踩实了,后面不管是调优还是压缩策略,都会顺手很多。