🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/


引言
在生产环境中,慢SQL是导致系统响应缓慢、CPU飙升、甚至服务宕机的头号元凶。一个糟糕的SQL查询可能会:
- 导致数据库CPU使用率飙升至100%
- 占用大量连接资源,引发连接池耗尽
- 锁表或锁行,阻塞其他正常查询
- 拖慢整个应用集群的响应速度
然而,很多开发者对索引的理解停留在"加索引就能变快"的层面,实际应用中却经常出现索引失效、索引冗余、索引设计不合理等问题。
本文将从慢SQL的定位、索引原理、优化实战到治理体系,为你提供一套完整的索引优化解决方案。
一、慢SQL定位与分析
1.1 开启慢查询日志
MySQL配置:
ini
[mysqld]
# 开启慢查询日志
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
# 设置慢查询阈值(秒)
long_query_time = 1
# 记录未使用索引的查询
log_queries_not_using_indexes = ON
动态开启:
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 设置阈值
SET GLOBAL long_query_time = 1;
1.2 使用pt-query-digest分析
bash
# 安装pt-query-digest
apt-get install percona-toolkit
# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt
# 分析最近1小时的慢查询
pt-query-digest --since=1h /var/log/mysql/slow.log
# 分析指定用户的慢查询
pt-query-digest --filter '$event->{User} =~ m/^app_user/' /var/log/mysql/slow.log
1.3 实时慢SQL监控
sql
-- 查看当前正在执行的查询
SHOW FULL PROCESSLIST;
-- 查看长时间运行的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;
-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
1.4 使用EXPLAIN分析执行计划
sql
EXPLAIN SELECT * FROM orders
WHERE user_id = 123 AND status = 'PAID'
ORDER BY create_time DESC
LIMIT 20;
关键字段解读:
| 字段 | 说明 | 理想值 |
|---|---|---|
| type | 访问类型 | system > const > eq_ref > ref > range > index > ALL |
| possible_keys | 可能使用的索引 | 不为空 |
| key | 实际使用的索引 | 不为空 |
| key_len | 索引使用长度 | 越短越好 |
| rows | 预估扫描行数 | 越少越好 |
| Extra | 额外信息 | 避免出现Using filesort、Using temporary |
二、索引核心原理
2.1 B+树索引结构
[根节点]
/ \
[内部节点] [内部节点]
/ | \ | \
[叶子] [叶子] [叶子] [叶子] [叶子]
| | | | |
数据 数据 数据 数据 数据
B+树的特点:
- 所有数据都在叶子节点:非叶子节点只存储索引值
- 叶子节点形成链表:便于范围查询
- 树高通常为3-4层:一次查询只需3-4次磁盘IO
- 自平衡:插入删除自动调整
2.2 聚簇索引 vs 二级索引
聚簇索引(Clustered Index):
叶子节点直接存储行数据
┌────────────────────────────────┐
│ 索引值 │ 行数据(所有列) │
├────────────────────────────────┤
│ 1 │ (1, 'Alice', 25, ...) │
│ 2 │ (2, 'Bob', 30, ...) │
└────────────────────────────────┘
- InnoDB的主键就是聚簇索引
- 一张表只能有一个聚簇索引
- 叶子节点存储完整的行数据
二级索引(Secondary Index):
叶子节点存储主键值
┌────────────────────────┐
│ 索引值 │ 主键值 │
├────────────────────────┤
│ Alice │ 1 │
│ Bob │ 2 │
└────────────────────────┘
- 可以有多个二级索引
- 叶子节点存储主键值,需要回表查询
- 回表:先查二级索引获取主键,再查聚簇索引获取行数据
2.3 回表与覆盖索引
回表查询:
sql
-- 假设name有二级索引
SELECT * FROM user WHERE name = 'Alice';
-- 1. 查name索引,得到主键id=1
-- 2. 用id=1查聚簇索引,获取完整行数据(回表)
覆盖索引:
sql
-- 假设(name, age)有联合索引
SELECT id, name, age FROM user WHERE name = 'Alice';
-- 直接通过索引返回结果,无需回表
覆盖索引是索引优化的重要手段,可以显著减少IO。
三、索引优化实战
3.1 最左前缀原则
联合索引 (a, b, c) 的生效规则:
sql
-- 生效
SELECT * FROM table WHERE a = 1; -- 使用a
SELECT * FROM table WHERE a = 1 AND b = 2; -- 使用a, b
SELECT * FROM table WHERE a = 1 AND b = 2 AND c = 3; -- 使用a, b, c
SELECT * FROM table WHERE a = 1 AND c = 3; -- 使用a,c不生效
-- 不生效
SELECT * FROM table WHERE b = 2; -- 无a,不生效
SELECT * FROM table WHERE b = 2 AND c = 3; -- 无a,不生效
SELECT * FROM table WHERE c = 3; -- 无a,不生效
实战案例:
sql
-- 原始索引
CREATE INDEX idx_user ON user(name, age, city);
-- 场景1:按城市查询(索引失效)
SELECT * FROM user WHERE city = 'Beijing'; -- 全表扫描
-- 优化:调整索引顺序
CREATE INDEX idx_city_name ON user(city, name);
3.2 索引失效的常见场景
场景1:函数操作导致索引失效
sql
-- 索引失效
SELECT * FROM user WHERE YEAR(create_time) = 2024;
-- 优化:使用范围查询
SELECT * FROM user
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
场景2:隐式类型转换导致索引失效
sql
-- 假设phone是varchar类型
-- 索引失效(数值比较导致隐式转换)
SELECT * FROM user WHERE phone = 13800138000;
-- 优化:使用字符串
SELECT * FROM user WHERE phone = '13800138000';
场景3:LIKE前缀通配符导致索引失效
sql
-- 索引失效
SELECT * FROM user WHERE name LIKE '%张%';
-- 索引生效(前缀匹配)
SELECT * FROM user WHERE name LIKE '张%';
-- 优化:使用全文索引
ALTER TABLE user ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM user WHERE MATCH(name) AGAINST('张');
场景4:OR条件导致索引失效
sql
-- 可能索引失效(取决于优化器判断)
SELECT * FROM user WHERE name = 'Alice' OR age = 25;
-- 优化:拆分为UNION
SELECT * FROM user WHERE name = 'Alice'
UNION
SELECT * FROM user WHERE age = 25;
场景5:NOT、!=、<>导致索引失效
sql
-- 索引失效
SELECT * FROM user WHERE status != 'deleted';
-- 优化:改为IN
SELECT * FROM user WHERE status IN ('active', 'pending');
3.3 索引设计最佳实践
实践1:选择区分度高的列
sql
-- 区分度 = 不同值数量 / 总行数
-- 性别列区分度低(只有男女),不适合单独建索引
SELECT COUNT(DISTINCT gender) / COUNT(*) FROM user; -- 约0.5
-- 手机号区分度高,适合建索引
SELECT COUNT(DISTINCT phone) / COUNT(*) FROM user; -- 接近1.0
实践2:控制索引数量
sql
-- 查询表的所有索引
SHOW INDEX FROM user;
-- 删除未使用的索引
DROP INDEX idx_unused ON user;
实践3:选择合适的索引类型
sql
-- 普通索引
CREATE INDEX idx_name ON user(name);
-- 唯一索引
CREATE UNIQUE INDEX uk_email ON user(email);
-- 联合索引
CREATE INDEX idx_name_age ON user(name, age);
-- 前缀索引(适用于长字符串)
CREATE INDEX idx_name_prefix ON user(name(10));
-- 全文索引
ALTER TABLE article ADD FULLTEXT INDEX ft_content(content);
实践4:联合索引列的顺序
sql
-- 原则:等值查询列在前,范围查询列在后
-- 场景:WHERE status = ? AND create_time > ? ORDER BY create_time
-- 索引设计
CREATE INDEX idx_status_time ON user(status, create_time);
3.4 覆盖索引优化
案例:分页查询优化
sql
-- 优化前:回表查询大量数据
SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 100000, 20;
-- 需要回表100020行,然后丢弃前100000行
-- 优化后:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 100000, 20
) t ON o.id = t.id;
-- 子查询只通过索引获取id,再回表20行
3.5 索引条件下推(ICP)
MySQL 5.6引入的优化,可以在存储引擎层过滤数据。
sql
-- 联合索引 (name, age)
SELECT * FROM user WHERE name = '张' AND age > 25;
-- ICP优化:在索引层就过滤掉age <= 25的数据
-- 减少回表次数
四、典型慢SQL优化案例
4.1 案例一:深分页优化
问题SQL:
sql
SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
LIMIT 1000000, 20;
-- 执行时间:5.2秒
执行计划:
+----+-------------+--------+------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+---------+-----------------------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 5000000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+---------+-----------------------------+
优化方案:
sql
-- 方案1:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
LIMIT 1000000, 20
) t ON o.id = t.id;
-- 需要添加索引
CREATE INDEX idx_status_time ON orders(status, create_time);
-- 方案2:记录上一页最后一条
-- 前端传递上一页最后的create_time和id
SELECT * FROM orders
WHERE status = 'PAID' AND create_time < '2024-01-01 12:00:00'
ORDER BY create_time DESC
LIMIT 20;
优化效果:5.2秒 → 0.05秒
4.2 案例二:GROUP BY优化
问题SQL:
sql
SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders
WHERE create_time >= '2024-01-01'
GROUP BY user_id
ORDER BY total_amount DESC
LIMIT 100;
-- 执行时间:8.5秒
优化方案:
sql
-- 添加覆盖索引
CREATE INDEX idx_time_user_amount ON orders(create_time, user_id, amount);
-- 如果数据量巨大,考虑预聚合
CREATE TABLE order_stats_daily (
stat_date DATE,
user_id BIGINT,
order_count INT,
total_amount DECIMAL(10,2),
PRIMARY KEY (stat_date, user_id)
);
-- 定时任务每日聚合
INSERT INTO order_stats_daily
SELECT DATE(create_time), user_id, COUNT(*), SUM(amount)
FROM orders
WHERE create_time >= CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(create_time), user_id;
-- 查询改为
SELECT user_id, SUM(order_count), SUM(total_amount)
FROM order_stats_daily
WHERE stat_date >= '2024-01-01'
GROUP BY user_id
ORDER BY SUM(total_amount) DESC
LIMIT 100;
优化效果:8.5秒 → 0.2秒
4.3 案例三:JOIN优化
问题SQL:
sql
SELECT u.*, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
-- 执行时间:12秒
问题分析:
- LEFT JOIN导致user表作为驱动表
- WHERE条件中的o.create_time使LEFT JOIN变成了INNER JOIN
- 缺少必要的索引
优化方案:
sql
-- 改为INNER JOIN(语义不变)
SELECT u.*, o.order_no, o.amount
FROM orders o
INNER JOIN user u ON u.id = o.user_id
WHERE u.status = 'active'
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
-- 添加索引
CREATE INDEX idx_user_status ON user(status, id);
CREATE INDEX idx_orders_time_user ON orders(create_time, user_id);
优化效果:12秒 → 0.3秒
4.4 案例四:子查询优化
问题SQL:
sql
SELECT * FROM user
WHERE id IN (
SELECT user_id FROM orders
WHERE amount > 1000
);
-- 执行时间:6秒
优化方案:
sql
-- 改为JOIN
SELECT DISTINCT u.*
FROM user u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;
-- 或者使用EXISTS(MySQL 5.6+已优化)
SELECT * FROM user u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.amount > 1000
);
-- 添加索引
CREATE INDEX idx_amount_user ON orders(amount, user_id);
五、索引治理体系
5.1 索引规范
创建规范:
- 表必须建立主键,推荐使用自增ID或雪花算法ID
- 唯一键使用
uk_前缀,普通索引使用idx_前缀 - 联合索引列不超过5个
- 字符串字段建议建立前缀索引,前缀长度根据区分度确定
命名规范:
sql
-- 主键
PRIMARY KEY (id)
-- 唯一索引
UNIQUE KEY uk_email (email)
-- 普通索引
KEY idx_name (name)
-- 联合索引
KEY idx_status_time (status, create_time)
5.2 索引评审流程
需求开发 → SQL编写 → EXPLAIN分析 → 索引设计 → 代码评审 → 测试验证 → 上线
评审检查项:
- 是否进行了EXPLAIN分析
- 是否有全表扫描(type=ALL)
- 是否使用了filesort或temporary
- 索引是否符合最左前缀原则
- 是否存在索引冗余
5.3 定期索引清理
查询未使用的索引:
sql
-- MySQL 5.7+
SELECT
table_schema,
table_name,
index_name,
seq_in_index,
column_name
FROM information_schema.STATISTICS
WHERE table_schema = 'your_database'
AND index_name NOT IN ('PRIMARY')
AND index_name NOT IN (
SELECT DISTINCT INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE INDEX_NAME IS NOT NULL
AND OBJECT_SCHEMA = 'your_database'
);
删除冗余索引:
sql
-- 安装pt-duplicate-key-checker
pt-duplicate-key-checker -u root -p password -d your_database
5.4 监控告警
慢SQL告警规则:
yaml
# Prometheus告警规则
groups:
- name: slow_sql_alerts
rules:
- alert: SlowSQLDetected
expr: rate(mysql_slow_queries[5m]) > 10
for: 5m
annotations:
summary: "慢查询数量异常"
description: "5分钟内慢查询数量超过10个"
- alert: FullTableScan
expr: mysql_handler_read_rnd_next_total > 1000000
for: 2m
annotations:
summary: "检测到全表扫描"
六、高级优化技巧
6.1 索引下推优化
sql
-- 联合索引 (name, age, city)
SELECT * FROM user
WHERE name LIKE '张%' AND age > 25 AND city = 'Beijing';
-- ICP会在索引层过滤age和city,减少回表
6.2 MRR(Multi-Range Read)优化
sql
-- 开启MRR
SET optimizer_switch = 'mrr=on,mrr_cost_based=off';
-- MRR可以将随机IO转换为顺序IO
SELECT * FROM user
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
6.3 BKA(Batched Key Access)优化
sql
-- 开启BKA
SET optimizer_switch = 'mrr=on,batched_key_access=on';
-- BKA优化JOIN时的索引查找
SELECT u.*, o.*
FROM user u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active';
6.4 虚拟列与函数索引
MySQL 5.7+支持生成列,MySQL 8.0+支持函数索引:
sql
-- MySQL 8.0 函数索引
CREATE INDEX idx_year ON user((YEAR(create_time)));
-- MySQL 5.7 生成列
ALTER TABLE user ADD COLUMN create_year INT
GENERATED ALWAYS AS (YEAR(create_time)) STORED;
CREATE INDEX idx_create_year ON user(create_year);
七、总结
索引优化是慢SQL治理的核心,需要系统性地掌握以下要点:
- 理解原理:掌握B+树结构、聚簇索引、二级索引、回表等核心概念
- 熟练工具:善用EXPLAIN、pt-query-digest等工具定位问题
- 避免陷阱:熟悉索引失效的各种场景,避开常见坑点
- 规范先行:建立索引设计规范,从源头避免问题
- 持续治理:定期清理无用索引,监控慢SQL
优化心法:
- 少即是多:不是索引越多越好,冗余索引反而影响写入性能
- 精准打击:针对具体SQL设计索引,不要盲目加索引
- 覆盖为王:尽量使用覆盖索引,避免回表
- 数据说话:优化前后一定要用EXPLAIN和执行时间验证效果
索引优化没有银弹,只有深入理解原理,结合实际场景,才能真正做到"对症下药"。希望本指南能帮助你在慢SQL治理的道路上少走弯路。