“给查询加索引”不是性能问题的万能答案。索引是一种用额外空间和写入成本,换取特定读取路径更短的数据结构。要正确设计索引,需要先知道数据库真正读取的是页,而不是抽象的一行。
问题背景:索引优化的对象是什么
数据库访问存储设备时,以页为基本单位读取和缓存数据。一次随机访问的成本远高于在内存中比较几个键。B+Tree 的目标是让一次查找只经过很少的页,并让相邻键尽量位于相邻叶子页,兼顾点查与范围查询。
索引是否有效取决于数据分布、过滤比例、返回列和排序要求。小表全表扫描可能比走索引再回表更便宜;返回表中大部分数据时也是如此。
数据页与 B+Tree
B+Tree 的非叶子节点保存分隔键和子页指针,叶子节点保存有序记录并通过链表相连。一个页能容纳大量键,因而分支因子很高,数百万行数据的树高通常仍然很低。
点查从根页按键选择子页,直到叶子页;范围查询定位起点后沿叶子链表向后扫描。相比二叉树,B+Tree 用宽而矮的结构减少页访问次数。
聚簇索引和二级索引
InnoDB 表的聚簇索引叶子页保存完整行,通常按主键组织。二级索引叶子页保存二级键和对应主键,因此通过二级索引查询非索引列时,要先找到主键,再回到聚簇索引读取整行,这一步称为回表。
主键过长会被复制到所有二级索引,增加空间和缓存压力。随机主键还可能让插入分散到不同叶子页,引发更多页分裂。选择主键要同时考虑唯一性、稳定性、长度和写入分布。
联合索引与最左前缀
联合索引 (tenant_id, status, created_at) 先按租户排序,再按状态,最后按创建时间。它天然支持以下查询前缀:
01SELECT id, created_at02FROM orders03WHERE tenant_id = 4204 AND status = 'paid'05 AND created_at >= '2026-07-01'06ORDER BY created_at DESC07LIMIT 50;
如果跳过 tenant_id 只按 status 查,整棵树的第一排序维度无法确定,通常不能高效定位连续范围。把低选择性列放第一位也不一定错误:多租户系统中,tenant_id 可能是访问边界;索引顺序应服务真实查询,而不是机械按选择性排列。
实践:读懂执行计划
先建立可重复数据和查询,再查看执行计划:
01EXPLAIN ANALYZE02SELECT id, created_at03FROM orders04WHERE tenant_id = 4205 AND status = 'paid'06ORDER BY created_at DESC07LIMIT 50;
关注实际扫描行数、返回行数、循环次数、排序和回表成本。估算行数与实际行数差异很大时,可能需要更新统计信息,也可能说明列之间存在相关性。
不要只看是否出现索引名称。一个“使用索引”的计划仍可能扫描大量索引记录;相反,小表的顺序扫描可能正是成本最低的选择。
回表、覆盖与常见误区
如果查询需要的列都包含在索引中,数据库可以直接从二级索引返回结果,称为覆盖索引。覆盖能减少回表,但把所有返回列都塞进索引会扩大页、降低缓存密度并增加写放大。
常见错误包括:
- 给每个字段分别建单列索引,却忽略组合查询与排序;
- 对索引列做函数运算,导致无法直接定位原始键值;
- 使用前导通配符
LIKE '%word'期待普通 B+Tree 高效搜索; - 建立大量重复或前缀相同的索引;
- 在没有数据规模和执行计划的情况下断言某条 SQL 必须走索引。
维护成本与总结
索引会占用磁盘与 Buffer Pool;插入、更新和删除还要维护所有相关树。上线索引前应估算空间和写入成本,在线创建时关注锁、日志量与复制延迟,并准备取消与回滚方式。
B+Tree 有效的根本原因是高分支因子、低树高和有序叶子页。索引设计则是在查询路径、回表次数、排序需求与写入成本之间取舍。下一步可以继续学习执行计划、MVCC、锁范围以及连接池容量。