目录
[2.1 为什么不用 Hash 索引](#2.1 为什么不用 Hash 索引)
[2.2 B+Tree 结构](#2.2 B+Tree 结构)
[2.3 为什么不用 B-Tree](#2.3 为什么不用 B-Tree)
[3.1 按功能分类](#3.1 按功能分类)
[3.2 按存储方式分类(面试重点)](#3.2 按存储方式分类(面试重点))
[3.3 回表查询](#3.3 回表查询)
[6.1 函数操作导致失效](#6.1 函数操作导致失效)
[6.2 隐式类型转换导致失效](#6.2 隐式类型转换导致失效)
[6.3 LIKE 左模糊导致失效](#6.3 LIKE 左模糊导致失效)
[6.4 OR 条件导致失效](#6.4 OR 条件导致失效)
[6.5 NOT IN / != 导致索引失效](#6.5 NOT IN / != 导致索引失效)
[6.6 索引失效速查表](#6.6 索引失效速查表)
[七、索引下推(Index Condition Pushdown, ICP)](#七、索引下推(Index Condition Pushdown, ICP))
[MySQL 5.6 之后的优化](#MySQL 5.6 之后的优化)
[8.1 什么时候该建索引](#8.1 什么时候该建索引)
[8.2 选择性计算](#8.2 选择性计算)
[8.3 联合索引列顺序原则](#8.3 联合索引列顺序原则)
[九、EXPLAIN 分析(面试实战)](#九、EXPLAIN 分析(面试实战))
[type 详解](#type 详解)
[Extra 常见值](#Extra 常见值)
[Q1:为什么 InnoDB 推荐使用自增主键?](#Q1:为什么 InnoDB 推荐使用自增主键?)
[Q3:count(*) vs count(1) vs count(字段)](#Q3:count(*) vs count(1) vs count(字段))
[Q4:一条 SQL 是如何走索引的?](#Q4:一条 SQL 是如何走索引的?)
一、索引基础概念
什么是索引
索引是一种数据结构,用于快速定位和访问数据库表中的数据。类比书籍的目录,不用从头翻,直接定位到目标页。
为什么需要索引
索引的优缺点
没有索引:全表扫描(逐行比对)→ 数据量大时极慢
有索引: 通过索引树快速定位 → 从 O(n) 降到 O(log n)
| 优点 | 缺点 |
|---|---|
| 大幅提升查询速度 | 占用额外存储空间 |
| 加速排序和分组 | 增删改时需要维护索引,降低写入速度 |
| 保证数据唯一性(唯一索引) | 创建和维护索引需要时间 |
二、索引数据结构(重点中的重点)
2.1 为什么不用 Hash 索引
Hash 索引的原理是将 key 通过哈希函数映射到桶中:
hash("john") → bucket3
hash("jane") → bucket7
| 特性 | Hash 索引 | B+Tree 索引 |
|---|---|---|
| 精确匹配 | ✅ O(1),极快 | ✅ O(log n) |
| 范围查询 | ❌ 不支持 | ✅ 支持 |
| 排序 | ❌ 不支持 | ✅ 支持 |
| 最左前缀 | ❌ 不支持 | ✅ 支持 |
| Hash 冲突 | ⚠️ 性能下降 | 无此问题 |
结论:MySQL 选择 B+Tree 作为默认索引结构,因为实际业务中范围查询和排序太常见了。
2.2 B+Tree 结构
MySQL InnoDB 的 B+Tree 结构:
[15 | 30] ← 根节点(只有索引键值) / \ [10 | 12 | 15] [20 | 25 | 30] ← 非叶子节点(只有索引键值) / | \ / | \ [10] [12] [15] [20] [25] [30] ← 叶子节点(存完整数据/主键) ←———————— 链表连接 ————————→ ← 叶子节点双向链表
B+Tree 的核心特点:
| 特点 | 说明 |
|---|---|
| 所有数据在叶子节点 | 非叶子节点只存索引键,能容纳更多索引项,树更矮 |
| 叶子节点用链表连接 | 范围查询时,找到起始点后沿链表顺序扫描即可 |
| 树高度很低 | 三层 B+Tree 可存 2000 万+ 数据,每次查询只需 3 次磁盘 IO |
2.3 为什么不用 B-Tree
B-Tree:每个节点都存数据 → 节点大 → 每页存的索引少 → 树更高 → IO 更多
B+Tree:只有叶子节点存数据 → 节点小 → 每页存的索引多 → 树更矮 → IO 更少
sql
-- 查看 InnoDB 页大小(默认 16KB)
SHOW VARIABLES LIKE 'innodb_page_size';
-- 结果:16384 (16KB)
-- 假设一个索引项占 14 字节(bigint 8B + 指针 6B)
-- 16KB / 14B ≈ 1170 个索引项/页
-- 三层 B+Tree:1170 × 1170 × 16 ≈ 2000万条数据
三、索引分类
3.1 按功能分类
sql
-- 1. 主键索引(Primary Key):自动创建,不允许 NULL
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
-- 2. 唯一索引(Unique Index):值必须唯一,允许 NULL(但只能有一个 NULL)
CREATE UNIQUE INDEX idx_email ON users(email);
-- 3. 普通索引(Normal Index):最基本的索引,无唯一性约束
CREATE INDEX idx_name ON users(name);
-- 4. 联合索引(Composite Index):多个列组成一个索引
CREATE INDEX idx_name_age ON users(name, age);
-- 5. 全文索引(Fulltext Index):用于文本搜索
CREATE FULLTEXT INDEX idx_content ON articles(content);
3.2 按存储方式分类(面试重点)
这是面试最高频的问题之一:
| 类型 | 说明 | InnoDB | MyISAM |
|---|---|---|---|
| 聚簇索引(Clustered) | 叶子节点存完整行数据 | ✅ 有且仅有一个 | ❌ 没有 |
| 非聚簇索引(Secondary/Non-Clustered) | 叶子节点存主键值 | ✅ 可以有多个 | ✅ 叶子节点存数据地址 |
InnoDB 的聚簇索引选择规则:
如果有主键 → 主键就是聚簇索引
如果没有主键但有唯一索引 → 第一个非空唯一索引是聚簇索引
如果都没有 → InnoDB 会生成一个隐藏的 row_id 作为聚簇索引
3.3 回表查询
sql
假设表 users(id, name, age, email),id 是主键,name 上有普通索引
查询 SELECT * FROM users WHERE name = '张三'
过程:
1. 走 name 索引树 → 找到 name='张三' → 叶子节点存的是 id=100
2. 拿着 id=100 → 回到主键索引树 → 找到完整行数据
这就是【回表】:先查二级索引,再查主键索引,查了两棵树
-- 用 EXPLAIN 验证回表
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- type: ref
-- Extra: (无额外信息,说明回表了)
EXPLAIN SELECT id, name FROM users WHERE name = '张三';
-- 如果是覆盖索引,Extra 会显示 Using index
四、覆盖索引
什么是覆盖索引
查询的列全部包含在索引中,不需要回表。
sql
-- 联合索引:idx_name_age (name, age)
-- ❌ 回表(SELECT * 包含了 email,索引里没有 email)
SELECT * FROM users WHERE name = '张三';
-- ✅ 覆盖索引(查询的 id, name, age 都在索引里或主键里)
SELECT id, name, age FROM users WHERE name = '张三';
-- ✅ 覆盖索引
SELECT name, age FROM users WHERE name = '张三';
-- EXPLAIN 验证
EXPLAIN SELECT id, name, age FROM users WHERE name = '张三';
-- Extra: Using index ← 这就是覆盖索引
覆盖索引为什么快
回表:索引树 → 主键 → 主键索引树 → 完整数据(两次树查找)
覆盖索引:索引树 → 直接返回数据(一次树查找)
五、最左前缀原则
什么是联合索引
CREATE INDEX idx_a_b_c ON table1(a, b, c);
联合索引在 B+Tree 中的排序规则:先按 a 排序,a 相同按 b 排序,b 相同按 c 排序。
a=1, b=1, c=1
a=1, b=1, c=2
a=1, b=2, c=1
a=2, b=1, c=1
a=2, b=1, c=2
最左前缀规则
sql
-- 索引:idx_a_b_c(a, b, c)
-- ✅ 全部命中
WHERE a = 1 AND b = 2 AND c = 3
-- ✅ 命中 a, b
WHERE a = 1 AND b = 2
-- ✅ 命中 a
WHERE a = 1
-- ❌ 不命中(跳过了 a)
WHERE b = 2 AND c = 3
-- ❌ 不命中(跳过了 a, b)
WHERE c = 3
-- ⚠️ 命中 a(中间断了,b 用不上)
WHERE a = 1 AND c = 3
范围查询导致后续索引失效
sql
-- 索引:idx_a_b_c(a, b, c)
-- ✅ 全部命中
WHERE a = 1 AND b = 2 AND c = 3
-- ⚠️ 只命中 a, b(b 是范围查询,c 用不上)
WHERE a = 1 AND b > 2 AND c = 3
-- MySQL 5.6 之后有索引下推(ICP),可以部分优化
六、索引失效场景(面试必问)
6.1 函数操作导致失效
sql
-- 索引:idx_create_time(create_time)
-- ❌ 对索引列使用函数,索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2026;
-- ✅ 改成范围查询
SELECT * FROM orders
WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01';
6.2 隐式类型转换导致失效
sql
-- phone 字段是 VARCHAR 类型,索引:idx_phone(phone)
-- ❌ 传入数字,MySQL 会对 phone 做隐式转换 CAST(phone AS SIGNED),索引失效
SELECT * FROM users WHERE phone = 13800138000;
-- ✅ 传入字符串
SELECT * FROM users WHERE phone = '13800138000';
6.3 LIKE 左模糊导致失效
sql
-- 索引:idx_name(name)
-- ❌ 左模糊,索引失效
SELECT * FROM users WHERE name LIKE '%张';
-- ❌ 左右模糊,索引失效
SELECT * FROM users WHERE name LIKE '%张%';
-- ✅ 右模糊,索引有效
SELECT * FROM users WHERE name LIKE '张%';
6.4 OR 条件导致失效
sql
-- name 有索引,age 没有索引
-- ❌ age 没有索引,导致整个 OR 查询走全表扫描
SELECT * FROM users WHERE name = '张三' OR age = 25;
-- ✅ 给 age 也加索引
CREATE INDEX idx_age ON users(age);
SELECT * FROM users WHERE name = '张三' OR age = 25;
-- 现在会走索引合并 index_merge
-- ✅ 用 UNION 代替 OR
SELECT * FROM users WHERE name = '张三'
UNION
SELECT * FROM users WHERE age = 25;
6.5 NOT IN / != 导致索引失效
sql
-- 在数据量大时通常不走索引(优化器认为全表扫描更快)
-- ⚠️ 可能失效
SELECT * FROM users WHERE status != 1;
-- ⚠️ 可能失效
SELECT * FROM users WHERE id NOT IN (1, 2, 3);
-- 注意:是否失效取决于数据分布,优化器会自动判断
6.6 索引失效速查表
| 场景 | 是否失效 | 解决方案 |
|---|---|---|
| 对索引列使用函数 | ❌ 失效 | 改用范围查询 |
| 隐式类型转换 | ❌ 失效 | 保持类型一致 |
| LIKE 左模糊 | ❌ 失效 | 改用右模糊或全文索引 |
| OR 非索引列 | ❌ 失效 | 给所有条件列加索引或用 UNION |
| 联合索引不满足最左前缀 | ❌ 失效 | 调整查询条件顺序 |
| IS NOT NULL | ⚠️ 可能失效 | 视数据分布而定 |
| NOT IN | ⚠️ 可能失效 | 用 LEFT JOIN + IS NULL |
| 范围查询后的列 | ❌ 失效 | 调整索引列顺序 |
七、索引下推(Index Condition Pushdown, ICP)
MySQL 5.6 之后的优化
-- 联合索引:idx_name_age(name, age)
-- 查询:WHERE name LIKE '张%' AND age = 25
没有 ICP 时(MySQL 5.6 之前):
存储引擎根据 name LIKE '张%' 找到所有匹配的主键
回表取完整数据
Server 层过滤 age = 25
问题:回表了大量不必要的数据
有 ICP 时(MySQL 5.6 之后):
存储引擎根据 name LIKE '张%' 找到匹配的索引记录
在存储引擎层就判断 age = 25(不需要回表就先过滤)
只有同时满足条件的才回表
好处:减少了回表次数
-- 验证 ICPEXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age = 25;
-- Extra: Using index condition ← 表示使用了索引下推
八、索引设计原则(实战)
8.1 什么时候该建索引
-- ✅ 适合建索引
WHERE 条件中的列 -- 频繁出现在 WHERE 中
ORDER BY 的列 -- 避免额外排序
GROUP BY 的列 -- 避免额外分组
JOIN 的关联列 -- 外键字段
高选择性的列 -- 值分布广(如手机号、邮箱)
-- ❌ 不适合建索引
数据量小的表 -- 几百条数据,全表扫描更快
频繁更新的列 -- 维护索引成本高
选择性低的列 -- 如性别(只有男/女),加索引没意义
8.2 选择性计算
-- 选择性 = COUNT(DISTINCT column) / COUNT(*)
-- 越接近 1,选择性越高,越适合建索引
SELECT
COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity, -- 约 0.0001
COUNT(DISTINCT email) / COUNT(*) AS email_selectivity -- 约 1.0
FROM users;
-- email 选择性高 → 适合建索引
-- gender 选择性低 → 不适合单独建索引
8.3 联合索引列顺序原则
-- 原则:把选择性最高的列放在最前面
-- 但也要考虑实际查询场景
-- 场景1:大部分查询是 WHERE name = ? AND age = ?
CREATE INDEX idx_name_age ON users(name, age);
-- 场景2:name 等值查询多,age 经常排序
CREATE INDEX idx_name_age ON users(name, age);
-- 这样 ORDER BY age 可以利用索引排序,避免 filesort
九、EXPLAIN 分析(面试实战)
核心字段解读
sql
EXPLAIN SELECT * FROM users WHERE name = '张三' ORDER BY age;
| 字段 | 含义 | 关注点 |
|---|---|---|
| type | 访问类型 | 从好到坏:system > const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引 | NULL 表示没用索引 |
| rows | 预估扫描行数 | 越小越好 |
| Extra | 额外信息 | Using index(覆盖索引)、Using filesort(需要排序)、Using temporary(临时表) |
type 详解
-- const:主键或唯一索引等值查询(最快)
EXPLAIN SELECT * FROM users WHERE id = 1;
-- eq_ref:多表关联时,被驱动表使用主键或唯一索引
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- ref:非唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- range:索引范围查询
EXPLAIN SELECT * FROM users WHERE age > 18;
-- index:全索引扫描(比 ALL 好,但数据量大时也慢)
EXPLAIN SELECT name FROM users;
-- ALL:全表扫描(最差)
EXPLAIN SELECT * FROM users WHERE email IS NULL;
Extra 常见值
Using index → 覆盖索引,最优
Using index condition → 索引下推
Using where → Server 层过滤
Using filesort → 需要额外排序,考虑加索引优化
Using temporary → 使用临时表,常见于 GROUP BY
十、面试高频问题汇总
Q1:为什么 InnoDB 推荐使用自增主键?
-- 自增主键:插入时总是追加到最后,不会导致页分裂
-- UUID 主键:随机插入,频繁触发页分裂,性能差
-- ✅ 推荐
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
-- ❌ 不推荐(聚簇索引用 UUID 会导致严重的页分裂)
CREATE TABLE users (
id CHAR(36) PRIMARY KEY, -- UUID
name VARCHAR(50)
);
Q2:页分裂是什么?
B+Tree 叶子节点满了(16KB),需要分裂:
插入 id=7 到 1, 3, 5, 9 所在页
↓
页 1, 3, 5, 9 已满
↓
分裂为 1, 3, 5 和 7, 9
↓
产生新的索引项,需要更新上层节点
自增插入:总是在最后一页追加,几乎不分裂
随机插入:频繁触发页分裂,严重影响性能
Q3:count(*) vs count(1) vs count(字段)
-- InnoDB 中:
SELECT COUNT(*) FROM users; -- 推荐,MySQL 专门优化过
SELECT COUNT(1) FROM users; -- 和 COUNT(*) 性能基本一样
SELECT COUNT(email) FROM users; -- 会跳过 email 为 NULL 的行,且不走覆盖索引(除非 email 有索引)
Q4:一条 SQL 是如何走索引的?
SELECT * FROM users WHERE name = '张三' AND age = 25;
优化器分析:有哪些可用索引、数据分布如何
选择执行计划:决定走 idx_name_age 索引
存储引擎:在 B+Tree 中查找 name='张' 开头的记录
索引下推:在存储引擎层判断 age=25
回表:拿主键 ID 去主键索引取完整行数据
返回结果给 Server 层