🌲 索引原理
B+ 树 · 聚簇/非聚簇 · 回表 · 最左前缀 · 覆盖索引 · 索引失效场景
1. 为什么 MySQL 用 B+ 树做索引?不用哈希、二叉搜索树、B 树?
- 哈希索引:等值查询 O(1) 最快,但不支持范围查询和排序;仅 Memory 引擎支持
- 二叉搜索树(含 AVL/红黑树):树太高。数据量大时层数几十层,每层一次磁盘 IO,太慢
- B 树:多路平衡树,矮胖(三层可存千万级)。但所有节点都存数据,中间节点能存的"指针数"少,树更矮不了多少;且范围查询要反复回溯中序遍历
- B+ 树(选择它):
- 只有叶子节点存数据,非叶子节点只存键 → 每个节点能容纳更多键 → 树更矮(3 层 ≈ 2000 万行),磁盘 IO 次数更少
- 叶子节点链表相连(双向)→ 范围查询/排序直接顺序扫描链表,一次 IO 定位后连续读
- 查询性能稳定:任何查询都要走到叶子,IO 次数固定(≈树高)
🎯 面试要点
- 页(Page,默认 16KB)是磁盘 IO 的基本单位,节点 = 页
- 估算:非叶子节点一个键 + 指针 ≈ 16B,16KB/16B ≈ 1000 个键/页;3 层 = 1000 × 1000 × 16(每叶子约 16 行)≈ 1600 万行
- B+ 树叶子节点的双向链表是范围查询的关键设计
2. 聚簇索引和非聚簇索引(二级索引)的区别?什么是回表?
- 聚簇索引(主键索引):叶子节点存整行数据。InnoDB 中表数据就是按主键组织的(索引即数据)。主键缺失时选唯一非空索引,再没有则隐藏 rowid
- 二级索引(非聚簇):叶子节点存索引列值 + 主键值,不存整行
- 回表:通过二级索引找到主键 → 再到聚簇索引查整行。多一次 IO
- 覆盖索引:查询的列全部在二级索引中(含主键),无需回表——优化利器
覆盖索引示例
-- 表 user(id, name, age, email),索引 idx_name_age(name, age)
-- ① 回表:查 email 不在索引里,要回聚簇索引取整行
SELECT * FROM user WHERE name = '张三';
-- ② 覆盖索引:只用索引列 + 主键,无需回表,快
SELECT id, name, age FROM user WHERE name = '张三';
-- ③ 为什么 id 不用回表:二级索引叶子节点存了主键
🎯 面试要点
- 主键选型:自增整型优于 UUID(UUID 无序导致页分裂、索引膨胀)——这是"为什么主键要自增"的答案
- 二级索引要避免"select *"大字段:覆盖不了就回表,列宽还让索引页更大
- MyISAM 的索引都是非聚簇(叶子存行地址),InnoDB 主键才是聚簇
3. 最左前缀原则是什么?联合索引怎么建?
联合索引 (a, b, c) 底层是一棵 B+ 树,先按 a 排序,a 相同按 b 排序,b 相同按 c 排序。因此查询必须从最左列开始连续匹配才走索引:
- 走索引:a、a+b、a+b+c
- 不走索引:b、c、b+c(跳过了 a)
- 部分走:a+c —— a 走索引,c 无法用(a 相同区间内 c 无序)
联合索引设计原则:
- 区分度高的列放前面(性别 0/1 放前面会让索引几乎失效)
- 经常范围查询的列放后面(范围列之后的列无法用索引)
- 覆盖常用查询(尽量把 select 的列加进去)
- 可删除冗余索引:(a) 与 (a,b) 可合并为 (a,b)
🎯 面试要点
- 范围查询(>/</BETWEEN)之后的列不能用于缩小扫描区间:WHERE a > 1 AND b = 2 →
key_len只到 a。但 MySQL 5.6+ 默认开启 ICP(索引条件下推),b 会在存储引擎层作为索引条件过滤、减少回表(EXPLAIN 出现Using index condition) - LIKE 'xx%' 走索引,'%xx%' 不走(最左前缀同理)
- 索引下推(ICP):联合索引中,范围后的条件会下推到存储引擎层过滤,减少回表——MySQL 5.6+ 默认开启
4. 索引失效的常见场景(背全)?
- 对索引列做函数/运算:WHERE DATE(create_time) = '2026-01-01'、WHERE id + 1 = 5
- 隐式类型转换:varchar 列用数字查(WHERE phone = 13800000000)——MySQL 会做隐式 CAST,等同函数
- LIKE 以 % 开头:'%abc' 不走,'abc%' 走
- 违反最左前缀:跳过联合索引首列
- OR 连接非索引列:WHERE name = 'a' OR age > 20(age 无索引 → 全表扫;改为 UNION 或两边都建索引)
- 不等于/IS NOT NULL/NOT IN:!= 和 <> 通常不走索引(优化器评估后可能走)
- 字符集不一致:join 两表字段 collation 不同,隐式转换
- 优化器选择全表扫:数据量小/区分度低时,优化器认为全表更快(可 FORCE INDEX 但慎用)
🎯 面试要点
- 排查手段:EXPLAIN 看 type(const > eq_ref > ref > range > index > ALL)和 key
- 字符集统一 utf8mb4;字段上尽量避免函数(要优化就建函数索引 MySQL 8.0)
- 区分度高的意思:该列不同值的数量/总行数,接近 1 最好
5. 索引的优缺点?什么时候不该建索引?
优点:加速查询(等值、范围、排序、覆盖)
代价:
- 占用磁盘空间(每个索引一棵 B+ 树)
- 写入变慢:每次 INSERT/UPDATE/DELETE 都要维护所有索引树
- 随机 IO:页分裂、页合并
不该建索引的场景:数据量小的表;区分度低的列(性别/状态);频繁更新的列;查询基本不用的列;大文本字段(可前缀索引)。
🎯 面试要点
- 索引不是越多越好:一张表建议 ≤ 5 个左右,写入频繁的线上表更要克制
- 前缀索引:INDEX(name(10)) 用前 10 字符做索引,省空间,但无法覆盖排序