一、索引基础:从查字典到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秒搞定。
1.3 B树索引:最常用的索引结构
面试时经常有人问我:MySQL为什么用B+树,而不是二叉树或哈希表?
原因很简单——磁盘I/O特性决定了树要矮、要宽。
B树是一种多路平衡查找树。它的核心特点是:
- 每个结点可以存储多个键值:不像二叉树一个结点只存一个值,B树的结点可以存几百个键值。
- 结点大小对齐磁盘页:InnoDB里一页是16KB。一个B树结点刚好占一页。一次I/O读取一整个结点。
- 树高度很低:对于百万级数据,B+树的高度通常只有3-4层。也就是说,找任何一条数据,最多做3-4次磁盘I/O。
我做性能调优时,经常查看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+树——以主键为键值,叶子结点存的就是完整的行数据。
我做过一个对比测试:
- 自增主键 + 无碎片:单表插入性能稳定在每秒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+树的核心逻辑。
1.6 评估索引好坏的几个指标
你怎么知道一个索引建得好不好?我一般看这几点:
- 选择性:索引列中不同值的比例。选择性越接近1,索引越有效。比如性别列只有男女两种值,选择性太低,建索引意义不大。
- 索引高度:前面说了,B+树的层数越少越好。一般超过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上建索引基本是浪费。
嗯,这就是索引基础部分最核心的内容。你把这些概念踩实了,后面不管是调优还是压缩策略,都会顺手很多。