Files
interview/questions/mysql-index-optimization.md
2026-02-28 21:07:32 +08:00

25 KiB
Raw Blame History

MySQL 索引优化

问题

背景MySQL 索引是后端面试的必考题,也是实际工作中最常遇到的性能优化点。

问题

  1. 聚簇索引和非聚簇索引的区别是什么?
  2. 什么是回表?如何通过覆盖索引避免回表?
  3. 什么是最左前缀原则?为什么 (a, b, c) 联合索引无法支持 WHERE b = 1
  4. 如何分析 SQL 是否使用了索引?有哪些常见的索引失效场景?
  5. 在实际项目中,你是如何设计索引的?有没有优化过慢 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 索引已经包含了 nameidInnoDB 二级索引自动包含主键)
  • 查询只需要 idname,无需回表

联合索引实现覆盖索引

-- 创建联合索引
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) 都在索引中

覆盖索引的优势

  1. 避免回表:减少磁盘 I/O
  2. 减少随机 I/O:索引扫描是顺序 I/O回表是随机 I/O
  3. 提升性能:通常能提升 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';

索引设计建议

  1. 将区分度高的列放在前面
  2. 将常用于查询条件的列放在前面
  3. 将范围查询列放在最后

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

场景 4OR 连接非索引列
-- ❌ 索引失效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);

原因

  • 不等于无法利用索引的有序性
  • 范围太广,优化器选择全表扫描

场景 6IS 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. 联合索引的列顺序

原则

  1. 区分度高的列放在前面
  2. 常用于查询条件的列放在前面
  3. 范围查询列放在最后

示例

-- 表结构
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, idid 自动包含)
-- 无需回表,性能提升 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;
-- 原理:利用主键索引直接定位起始位置

案例 2JOIN 优化

问题 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

案例 3COUNT 优化

问题 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;

无 ICPMySQL 5.6 之前)

1. 使用索引定位 name LIKE '张%' 的所有记录
2. 回表查询完整数据
3. 过滤 age > 25

有 ICPMySQL 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 等工具使用经验