25 KiB
25 KiB
MySQL 索引优化
问题
背景:MySQL 索引是后端面试的必考题,也是实际工作中最常遇到的性能优化点。
问题:
- 聚簇索引和非聚簇索引的区别是什么?
- 什么是回表?如何通过覆盖索引避免回表?
- 什么是最左前缀原则?为什么
(a, b, c)联合索引无法支持WHERE b = 1? - 如何分析 SQL 是否使用了索引?有哪些常见的索引失效场景?
- 在实际项目中,你是如何设计索引的?有没有优化过慢 SQL?
标准答案
1. 聚簇索引 vs 非聚簇索引
存储引擎差异
InnoDB:
- 有聚簇索引
- 数据文件本身就是索引文件(.ibd)
- 一个表只能有一个聚簇索引(主键索引)
MyISAM:
- 没有聚簇索引
- 索引文件(.MYI) 和数据文件(.MYD) 分离
- 所有索引都是非聚簇索引
聚簇索引 (Clustered Index)
定义: 索引结构的叶子节点直接存储了整行数据。
特点:
- InnoDB 的主键索引就是聚簇索引
- 数据行的物理顺序与索引的逻辑顺序一致
- 一个表只能有一个聚簇索引(因为数据只能按一种顺序存储)
图解:
聚簇索引 B+ 树结构:
[主键 100]
/ \
[主键 50] [主键 150]
/ \ / \
[主键 25] [主键 75] [主键 125] [主键 175]
↓ ↓ ↓ ↓
整行数据 整行数据 整行数据 整行数据
(id=25) (id=75) (id=125) (id=175)
优点:
- 范围查询效率高(数据在物理上连续)
- 主键查询速度最快(一次索引查找即可)
缺点:
- 主键更新代价大(需要移动数据行)
- 插入速度依赖主键顺序(UUID 会导致大量页分裂)
非聚簇索引 (Secondary Index / 二级索引)
定义: 叶子节点存储的是主键值,而不是整行数据。
特点:
- InnoDB 的普通索引(非主键索引)都是非聚簇索引
- 需要回表才能获取完整数据
- 一个表可以有多个非聚簇索引
图解:
二级索引 B+ 树结构:
[name '张三']
/ \
[name '李四'] [name '王五']
/ \ / \
[name 'A'] [name 'C'] [name 'D'] [name 'E']
↓ ↓ ↓ ↓
主键: 10 主键: 30 主键: 50 主键: 70
(需要回表) (需要回表) (需要回表) (需要回表)
MyISAM 的索引结构
MyISAM 主键索引(非聚簇):
[主键 100]
/ \
[主键 50] [主键 150]
/ \ / \
[主键 25] [主键 75] [主键 125] [主键 175]
↓ ↓ ↓ ↓
数据文件地址指针 (指向 .MYD 文件的物理位置)
对比总结:
| 特性 | InnoDB 聚簇索引 | InnoDB 二级索引 | MyISAM 索引 |
|---|---|---|---|
| 叶子节点存储 | 整行数据 | 主键值 | 数据文件地址指针 |
| 回表 | 不需要 | 需要 | 不需要(直接指针) |
| 数量 | 1 个 | 多个 | 多个 |
| 主键查询 | 最快 | 需要回表 | 快 |
2. 回表与覆盖索引
什么是回表?
定义: 通过二级索引找到主键值后,再回到聚簇索引(主键索引)中查找完整数据的过程。
示例:
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY, -- 聚簇索引
name VARCHAR(50), -- 二级索引 idx_name
age INT,
email VARCHAR(100)
);
-- 二级索引:idx_name
CREATE INDEX idx_name ON users(name);
-- 查询语句
SELECT * FROM users WHERE name = '张三';
执行流程(有回表):
1. 在 idx_name 索引树中查找 name = '张三'
└─ 找到主键 id = 100
2. 在聚簇索引(主键索引)中查找 id = 100
└─ 获取整行数据(id, name, age, email)
3. 返回结果
两次索引查找 = 回表
性能问题:
- 每次回表都是一次额外的磁盘 I/O
- 大量回表会导致性能下降
什么是覆盖索引?
定义: 一个索引包含了查询所需的所有字段,无需回表即可返回结果。
示例:
-- 使用覆盖索引优化
SELECT id, name FROM users WHERE name = '张三';
执行流程(无回表):
1. 在 idx_name 索引树中查找 name = '张三'
└─ 找到主键 id = 100 和 name = '张三'
2. 直接返回 id 和 name
一次索引查找 = 无回表 ✅
原因:
idx_name索引已经包含了name和id(InnoDB 二级索引自动包含主键)- 查询只需要
id和name,无需回表
联合索引实现覆盖索引
-- 创建联合索引
CREATE INDEX idx_name_age ON users(name, age);
-- 查询 1:使用覆盖索引(无回表)
SELECT id, name, age FROM users WHERE name = '张三' AND age = 25;
-- 查询 2:无法使用覆盖索引(需要回表)
SELECT id, name, age, email FROM users WHERE name = '张三' AND age = 25;
-- email 不在 idx_name_age 索引中,需要回表
联合索引结构:
idx_name_age 索引 B+ 树:
[('张三', 25)]
/ \
[('李四', 20)] [('王五', 30)]
/ \ / \
[('A', 18)] [('C', 22)] [('D', 28)] [('E', 35)]
↓ ↓ ↓ ↓
主键: 10 主键: 30 主键: 50 主键: 70
(name, age) 都在索引中
覆盖索引的优势
- 避免回表:减少磁盘 I/O
- 减少随机 I/O:索引扫描是顺序 I/O,回表是随机 I/O
- 提升性能:通常能提升 50% - 90% 的查询性能
EXPLAIN 验证:
EXPLAIN SELECT id, name FROM users WHERE name = '张三';
输出:
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+
| 1 | SIMPLE | users | ref | idx_name | idx_name | 153 | const | 1 | Using index |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+
↑
Extra: Using index = 使用了覆盖索引
3. 最左前缀原则
定义
对于联合索引 (a, b, c),查询必须从最左侧的列开始,才能使用索引。
示例索引:
CREATE INDEX idx_abc ON users(a, b, c);
支持索引的查询
-- ✅ 使用完整索引
WHERE a = 1 AND b = 2 AND c = 3
-- ✅ 使用索引前两列
WHERE a = 1 AND b = 2
-- ✅ 使用索引第一列
WHERE a = 1
-- ✅ 使用索引第一列 + 范围查询
WHERE a = 1 AND b > 2
-- ✅ 范围查询第一列
WHERE a > 1 AND a < 10
-- ✅ 只使用第一列排序
ORDER BY a
-- ✅ 使用前两列排序
ORDER BY a, b
-- ✅ 覆盖索引优化
SELECT a, b, c FROM users WHERE a = 1;
无法使用索引的查询
-- ❌ 跳过第一列,直接查询第二列
WHERE b = 2;
-- ❌ 跳过前两列,直接查询第三列
WHERE c = 3;
-- ❌ 跳过第二列
WHERE a = 1 AND c = 3;
-- ❌ 无法使用索引排序
ORDER BY b, c;
-- ❌ 范围查询后无法使用后续列
WHERE a = 1 AND b > 2 AND c = 3;
-- 只有 a 和 b 能使用索引,c 无法使用
为什么无法支持 WHERE b = 1?
联合索引的排序方式:
索引 idx_abc 按照 (a, b, c) 的顺序排序:
a=1: (1,1,1), (1,1,2), (1,2,1), (1,2,2), (1,3,1)
a=2: (2,1,1), (2,1,2), (2,2,1), (2,2,2), (2,3,1)
a=3: (3,1,1), (3,1,2), (3,2,1), (3,2,2), (3,3,1)
查询 WHERE b = 1:
- b 的值是乱序的(1,1,2,2,3,1,1,2,2,3...)
- 无法利用索引的有序性进行二分查找
- 只能全表扫描
图解:
idx_abc 索引:
Row 1: a=1, b=1, c=1
Row 2: a=1, b=1, c=2
Row 3: a=1, b=2, c=1 ← b 跳变
Row 4: a=1, b=2, c=2
Row 5: a=1, b=3, c=1 ← b 跳变
Row 6: a=2, b=1, c=1 ← a 跳变,b 回到 1
Row 7: a=2, b=1, c=2
...
查询 WHERE b = 1:
- 无法定位起始位置(b 的值不连续)
- 必须扫描所有行
最左前缀原则的底层原理
B+ 树的比较逻辑:
-- 索引 idx_abc 的比较顺序
(a1, b1, c1) < (a2, b2, c2)
等价于:
a1 < a2 OR (a1 = a2 AND b1 < b2) OR (a1 = a2 AND b1 = b2 AND c1 < c2)
查询 WHERE b = 2:
- 第一列 a 没有提供,无法确定索引的起始位置
- MySQL 优化器无法利用索引
查询 WHERE a = 1 AND c = 3:
- 可以利用 a = 1 定位到索引的起始位置
- 但 c 无法使用索引(因为 b 被跳过)
实际优化案例
表结构:
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
status INT,
amount DECIMAL(10,2),
created_at DATETIME,
KEY idx_user_status_time (user_id, status, created_at)
);
查询优化:
-- ❌ 无法使用索引
SELECT * FROM orders WHERE status = 1 AND created_at > '2024-01-01';
-- ✅ 使用索引
SELECT * FROM orders WHERE user_id = 100 AND status = 1;
-- ✅ 部分使用索引(user_id)
SELECT * FROM orders WHERE user_id = 100 AND created_at > '2024-01-01';
索引设计建议:
- 将区分度高的列放在前面
- 将常用于查询条件的列放在前面
- 将范围查询列放在最后
4. SQL 索引分析
EXPLAIN 命令
基本用法:
EXPLAIN SELECT * FROM users WHERE name = '张三';
输出字段详解:
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+
核心字段:
| 字段 | 说明 | 好的值 |
|---|---|---|
| type | 访问类型 | const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引 | 显示索引名称 |
| key_len | 使用的索引长度 | 越长越好(使用更多列) |
| rows | 预估扫描行数 | 越少越好 |
| Extra | 额外信息 | Using index(覆盖索引) |
type 字段详解(从好到坏)
-- 1. const:主键或唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE id = 100;
-- type = const(最优,最多返回一行)
-- 2. eq_ref:连接时使用主键或唯一索引
EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id;
-- type = eq_ref(每个表只读取一行)
-- 3. ref:非唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- type = ref(使用二级索引)
-- 4. range:范围查询
EXPLAIN SELECT * FROM users WHERE id > 100 AND id < 200;
-- type = range(索引范围扫描)
-- 5. index:索引全扫描
EXPLAIN SELECT id FROM users;
-- type = index(遍历索引树)
-- 6. ALL:全表扫描(最差)
EXPLAIN SELECT * FROM users WHERE age = 25;
-- type = ALL(没有使用索引,需要优化)
性能对比:
const > eq_ref > ref > range > index > ALL
↑ ↑
最快 最慢(需要优化)
Extra 字段详解
| Extra 值 | 说明 | 建议 |
|---|---|---|
| Using index | 使用覆盖索引,无需回表 | ✅ 优秀 |
| Using where | 使用 WHERE 过滤 | ⚠️ 可能需要优化 |
| Using filesort | 文件排序(内存或磁盘) | ❌ 需要优化 |
| Using temporary | 使用临时表 | ❌ 需要优化 |
| Using index condition | 索引条件下推 | ✅ 较好 |
| Using join buffer | 使用连接缓冲 | ⚠️ 可能需要优化 |
常见索引失效场景
场景 1:使用函数
-- ❌ 索引失效
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- ✅ 使用索引
SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
原因:
- 对索引列使用函数后,索引存储的值被改变
- 无法利用索引的有序性
场景 2:隐式类型转换
-- 表结构:phone VARCHAR(20)
-- 索引:idx_phone
-- ❌ 索引失效(字符串字段使用数字查询)
SELECT * FROM users WHERE phone = 13800138000;
-- ✅ 使用索引
SELECT * FROM users WHERE phone = '13800138000';
原因:
- MySQL 隐式将 phone 转换为数字(相当于使用函数)
- 索引失效
场景 3:前缀模糊查询
-- ❌ 索引失效
SELECT * FROM users WHERE name LIKE '%张三';
-- ❌ 索引失效
SELECT * FROM users WHERE name LIKE '%张三%';
-- ✅ 使用索引
SELECT * FROM users WHERE name LIKE '张三%';
原因:
%在开头无法利用索引的有序性- 只能全表扫描
优化方案:
- 使用全文索引(FULLTEXT)
- 使用 Elasticsearch
场景 4:OR 连接非索引列
-- ❌ 索引失效(age 没有索引)
SELECT * FROM users WHERE name = '张三' OR age = 25;
-- ✅ 使用索引(两个条件都有索引)
SELECT * FROM users WHERE name = '张三' OR email = 'test@example.com';
-- ✅ 改写为 UNION
SELECT * FROM users WHERE name = '张三'
UNION
SELECT * FROM users WHERE age = 25;
场景 5:不等于(!= 或 <>)
-- ❌ 索引失效
SELECT * FROM users WHERE status != 1;
-- ✅ 使用索引
SELECT * FROM users WHERE status IN (2, 3, 4);
原因:
- 不等于无法利用索引的有序性
- 范围太广,优化器选择全表扫描
场景 6:IS NULL 或 IS NOT NULL
-- ❌ 索引可能失效(取决于 MySQL 版本和数据分布)
SELECT * FROM users WHERE name IS NULL;
-- ✅ 解决方案:设置默认值
ALTER TABLE users MODIFY name VARCHAR(50) NOT NULL DEFAULT '';
SELECT * FROM users WHERE name = '';
场景 7:负向查询
-- ❌ 索引失效
SELECT * FROM users WHERE NOT (status = 1);
-- ✅ 使用索引
SELECT * FROM users WHERE status IN (2, 3, 4);
场景 8:排序优化
-- 索引:idx_name_age (name, age)
-- ✅ 使用索引排序
SELECT * FROM users WHERE name = '张三' ORDER BY age;
-- ❌ 索引失效 + 文件排序
SELECT * FROM users WHERE age > 25 ORDER BY name;
-- 原因:跳过了 name,无法使用索引排序
-- ❌ 索引失效 + 文件排序
SELECT * FROM users WHERE name = '张三' ORDER BY age, email;
-- 原因:email 不在索引中
慢查询分析工具
1. 慢查询日志
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2; -- 超过 2 秒记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未使用索引的查询
-- 查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';
-- 输出:/var/lib/mysql/mysql-slow.log
日志示例:
# Time: 2024-02-28T10:30:00.123456Z
# User@Host: app[app] @ [192.168.1.100]
# Query_time: 5.234567 Lock_time: 0.000123 Rows_sent: 10 Rows_examined: 1000000
SET timestamp=1709100600;
SELECT * FROM orders WHERE user_id = 100;
2. mysqldumpslow 分析工具
# 查看最慢的 10 条 SQL
mysqldumpslow -s t -t 10 /var/lib/mysql/mysql-slow.log
# 查看访问次数最多的 10 条 SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/mysql-slow.log
# 查看返回行数最多的 10 条 SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/mysql-slow.log
输出示例:
Count: 150 Time=5.23s (784s) Lock=0.00s (0s) Rows=1000.0 (150000), app[app]@2hosts
SELECT * FROM orders WHERE user_id = N
5. 索引设计与实战优化
索引设计原则
1. 选择合适的列建立索引
-- ✅ 适合建立索引的列:
-- 1. 经常用于 WHERE、JOIN、ORDER BY 的列
-- 2. 区分度高的列(基数大)
CREATE INDEX idx_email ON users(email); -- 区分度高,适合索引
CREATE INDEX idx_gender ON users(gender); -- 区分度低(只有男/女),不适合索引
-- 3. 索引的选择性(唯一值数 / 总行数)
-- 计算选择性:
SELECT COUNT(DISTINCT email) / COUNT(*) FROM users; -- 接近 1,适合索引
SELECT COUNT(DISTINCT gender) / COUNT(*) FROM users; -- 接近 0.5,不适合索引
2. 联合索引的列顺序
原则:
- 区分度高的列放在前面
- 常用于查询条件的列放在前面
- 范围查询列放在最后
示例:
-- 表结构
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
status INT, -- 1:待支付, 2:已支付, 3:已完成
amount DECIMAL(10,2),
created_at DATETIME
);
-- 分析选择性
SELECT
COUNT(DISTINCT user_id) / COUNT(*) AS user_id_selectivity,
COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
COUNT(DISTINCT DATE(created_at)) / COUNT(*) AS date_selectivity
FROM orders;
-- 输出:
-- user_id_selectivity: 0.9(高)
-- status_selectivity: 0.01(低)
-- date_selectivity: 0.3(中)
-- 索引设计
CREATE INDEX idx_user_status_date ON orders(user_id, status, created_at);
-- ↑ ↑ ↑
-- 高 低 中
-- user_id 优先,status 次之,date 最后
常见查询优化:
-- 查询 1:用户待支付订单
-- ✅ 使用索引
SELECT * FROM orders WHERE user_id = 100 AND status = 1;
-- 查询 2:用户某时间段订单
-- ✅ 部分使用索引(user_id)
SELECT * FROM orders WHERE user_id = 100 AND created_at > '2024-01-01';
3. 覆盖索引优化
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
email VARCHAR(100),
created_at DATETIME
);
-- 需求:频繁查询用户列表(只需要 id, name, age)
-- ❌ 原始 SQL(需要回表)
SELECT id, name, age FROM users WHERE age > 18;
-- ✅ 创建覆盖索引
CREATE INDEX idx_age_name ON users(age, name);
-- 索引已包含 age, name, id(id 自动包含)
-- 无需回表,性能提升 50% - 90%
4. 前缀索引优化
场景:
- 对长字符串(VARCHAR、TEXT)建立索引
- 索引占用空间过大,影响性能
示例:
-- 表结构
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
created_at DATETIME
);
-- ❌ 全长索引(占用空间大)
CREATE INDEX idx_title ON articles(title);
-- ✅ 前缀索引(只索引前 20 个字符)
CREATE INDEX idx_title_prefix ON articles(title(20));
-- 计算合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(title, 10)) / COUNT(*) AS prefix_10,
COUNT(DISTINCT LEFT(title, 20)) / COUNT(*) AS prefix_20,
COUNT(DISTINCT LEFT(title, 30)) / COUNT(*) AS prefix_30
FROM articles;
-- 输出:
-- prefix_10: 0.75
-- prefix_20: 0.95 ← 足够高,选择 20
-- prefix_30: 0.98
注意事项:
- 前缀索引无法用于 ORDER BY 和 GROUP BY
- 前缀索引无法用于覆盖索引
5. 避免冗余索引
-- ❌ 冗余索引
CREATE INDEX idx_a ON users(a);
CREATE INDEX idx_a_b ON users(a, b);
-- idx_a 是冗余的(idx_a_b 已经包含 a)
-- ✅ 优化后
CREATE INDEX idx_a_b ON users(a, b);
-- 删除 idx_a
DROP INDEX idx_a ON users;
-- ❌ 冗余索引
CREATE INDEX idx_a ON users(a);
CREATE INDEX idx_a_id ON users(a, id);
-- InnoDB 二级索引自动包含主键,idx_a_id 是冗余的
实战优化案例
案例 1:分页查询优化
问题 SQL:
-- 查询第 100 万页的数据
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- 执行时间:30 秒
问题分析:
- MySQL 需要扫描 1000000 + 20 行,然后抛弃前 1000000 行
- 大量无效扫描
优化方案 1:延迟关联
-- ✅ 优化后(0.5 秒)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) tmp
ON o.id = tmp.id;
-- 原理:先在覆盖索引中快速定位 ID,再回表查询完整数据
优化方案 2:使用上次最大 ID
-- ✅ 优化后(0.01 秒)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
-- 原理:利用主键索引直接定位起始位置
案例 2:JOIN 优化
问题 SQL:
-- 执行时间:10 秒
EXPLAIN SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 1;
EXPLAIN 输出:
type: ALL(全表扫描)
rows: 1000000
优化方案:
-- 1. 在 join 列上建立索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 2. 在过滤列上建立索引
CREATE INDEX idx_status ON orders(status);
-- 3. 优化后(0.2 秒)
EXPLAIN SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 1;
-- 输出:
-- type: ref
-- key: idx_user_id, idx_status
-- rows: 100
案例 3:COUNT 优化
问题 SQL:
-- 执行时间:5 秒
SELECT COUNT(*) FROM orders WHERE status = 1;
优化方案 1:使用索引
-- ✅ 创建索引(0.05 秒)
CREATE INDEX idx_status ON orders(status);
SELECT COUNT(*) FROM orders WHERE status = 1;
优化方案 2:使用覆盖索引
-- ✅ 创建覆盖索引(0.02 秒)
CREATE INDEX idx_status_id ON orders(status, id);
SELECT COUNT(*) FROM orders WHERE status = 1;
-- 只需要扫描索引,无需回表
优化方案 3:使用计数表
-- ✅ 使用计数表(0.001 秒)
CREATE TABLE order_stats (
id INT PRIMARY KEY,
status INT,
count INT,
updated_at DATETIME
);
-- 定时更新计数
INSERT INTO order_stats (id, status, count, updated_at)
VALUES (1, 1, (SELECT COUNT(*) FROM orders WHERE status = 1), NOW())
ON DUPLICATE KEY UPDATE count = VALUES(count), updated_at = NOW();
-- 查询时直接读取计数
SELECT count FROM order_stats WHERE status = 1;
案例 4:索引下推优化(ICP)
MySQL 5.6+ 的索引条件下推优化
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
address VARCHAR(200),
KEY idx_name_age (name, age)
);
-- 查询
SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
无 ICP(MySQL 5.6 之前):
1. 使用索引定位 name LIKE '张%' 的所有记录
2. 回表查询完整数据
3. 过滤 age > 25
有 ICP(MySQL 5.6+):
1. 使用索引定位 name LIKE '张%'
2. 在索引中直接过滤 age > 25(无需回表)
3. 只对符合条件的记录回表
验证 ICP:
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
-- 输出:
-- Extra: Using index condition
-- ↑ 表示使用了索引条件下推
索引维护
1. 分析索引使用情况
-- 查看未使用的索引(MySQL 5.7+)
SELECT
object_schema AS database_name,
object_name AS table_name,
index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema = 'your_database'
ORDER BY object_schema, object_name;
-- 查看索引基数(选择性)
SHOW INDEX FROM users;
-- Cardinality 列:唯一值数量,越高越好
2. 删除冗余索引
-- 查看重复索引
SELECT
a.table_schema,
a.table_name,
a.index_name AS index1,
b.index_name AS index2,
a.column_name
FROM information_schema.statistics a
JOIN information_schema.statistics b
ON a.table_schema = b.table_schema
AND a.table_name = b.table_name
AND a.column_name = b.column_name
AND a.index_name != b.index_name
WHERE a.table_schema = 'your_database';
-- 删除冗余索引
DROP INDEX idx_user_id ON orders;
3. 定期 ANALYZE TABLE
-- 更新表的统计信息,帮助优化器选择正确的索引
ANALYZE TABLE users;
ANALYZE TABLE orders;
6. 总结
索引优化检查清单
- 是否创建了必要的索引(WHERE、JOIN、ORDER BY 列)
- 联合索引是否遵循最左前缀原则
- 是否使用了覆盖索引避免回表
- 索引的选择性是否足够高(> 0.1)
- 是否存在冗余索引
- 是否有索引失效的场景(函数、类型转换、%前导)
- 慢查询是否都经过 EXPLAIN 分析
- 是否定期 ANALYZE TABLE 更新统计信息
EXPLAIN 判断标准
✅ 优秀
- type: const, eq_ref, ref
- Extra: Using index
- rows: < 1000
⚠️ 需要关注
- type: range, index
- rows: 1000 - 10000
❌ 需要优化
- type: ALL
- Extra: Using filesort, Using temporary
- rows: > 10000
7. 阿里 P7 加分项
深度理解:
- 理解 B+ 树的底层实现(页分裂、页合并)
- 理解索引统计信息的更新机制
- 了解 MySQL 优化器的成本计算模型
实战经验:
- 有将慢 SQL 从秒级优化到毫秒级的案例
- 有处理千万级数据索引设计的经验
- 有索引重构和数据迁移的经验
架构能力:
- 能设计分库分表后的索引策略
- 有读写分离的索引同步经验
- 能设计索引变更的灰度方案
监控和工具:
- 搭建慢查询监控和告警系统
- 编写自动化索引分析工具
- 有 pt-index-usage、pt-duplicate-key-checker 等工具使用经验