慢查询优化
文章目录
- 一、什么是慢查询
- 1.1 定义
- 1.2 慢查询的影响
- 1.3 性能基准
- 二、如何发现慢查询
- 2.1 方法1:慢查询日志(推荐)
- 开启慢查询日志
- 分析慢查询日志
- 2.2 方法2:performance_schema(MySQL 5.6+)
- 2.3 方法3:sys系统库(MySQL 5.7+)
- 2.4 方法4:SHOW PROCESSLIST
- 2.5 方法5:应用层监控
- 方式1:日志记录
- 方式2:APM工具
- 方式3:数据库中间件
- 三、慢查询的常见原因
- 3.1 索引问题(80%的慢查询原因)
- 1. 没有索引
- 2. 索引失效
- 3. 索引选择不当
- 3.2 SQL写法问题
- 1. SELECT *
- 2. 深度分页
- 3. 大批量操作
- 3.3 数据量问题
- 1. 单表数据量过大
- 2. 数据分布不均
- 3.4 锁和并发问题
- 1. 锁等待
- 2. 长事务
- 3.5 JOIN问题
- 1. 大表JOIN
- 2. JOIN列没有索引
- 3.6 服务器资源问题
- 1. CPU瓶颈
- 2. 内存不足
- 3. 磁盘IO瓶颈
- 四、系统的排查流程
- 4.1 五步排查法
- 4.2 详细排查步骤
- Step 1:获取慢SQL
- Step 2:EXPLAIN分析
- Step 3:定位问题
- Step 4:制定方案
- Step 5:验证效果
- 五、优化方法详解
- 5.1 索引优化
- 优化1:创建合适的索引
- 优化2:联合索引顺序
- 优化3:删除冗余索引
- 5.2 SQL改写优化
- 优化1:避免SELECT *
- 优化2:深度分页优化
- 优化3:IN优化
- 优化4:OR改写为UNION
- 优化5:子查询改写为JOIN
- 5.3 表结构优化
- 优化1:选择合适的数据类型
- 优化2:字段拆分
- 优化3:表分区
- 5.4 查询优化
- 优化1:避免全表扫描
- 优化2:使用LIMIT限制结果
- 优化3:批量操作
- 5.5 缓存优化
- 优化1:应用层缓存
- 优化2:查询缓存(Query Cache)
- 5.6 读写分离和分库分表
- 优化1:主从复制+读写分离
- 优化2:分库分表
- 六、实战优化案例
- 6.1 案例1:电商订单查询优化
- 问题描述
- 原始SQL
- 排查过程
- 6.2 案例2:报表统计优化
- 问题描述
- 原始SQL
- 排查过程
- 优化方案
- 6.3 案例3:JOIN查询优化
- 问题描述
- 原始SQL
- EXPLAIN分析
- 优化方案
- 七、监控与预防
- 7.1 慢查询监控
- 1. 数据库层监控
- 2. 应用层监控
- 3. 监控平台
- 7.2 SQL审核
- 上线前审核
- SQL审核工具
- 7.3 数据库规范
- 索引规范
- SQL规范
- 表设计规范
- 八、优化效果评估
- 8.1 性能指标
- 8.2 对比测试
- 九、常见问题FAQ
- Q1:为什么添加了索引还是慢?
- Q2:数据量不大为什么还慢?
- Q3:如何选择优化优先级?
- Q4:索引是不是越多越好?
- Q5:什么时候需要分表?
- 总结
- 慢查询优化核心思路
- 优化方法速查
- 最佳实践
一、什么是慢查询
1.1 定义
慢查询(Slow Query) 是指执行时间超过指定阈值的SQL语句。
判断标准:
-- 查看慢查询阈值(默认10秒)
SHOW VARIABLES LIKE 'long_query_time';-- 临时设置为1秒
SET GLOBAL long_query_time = 1;-- 永久配置(my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1 # 记录未使用索引的查询
1.2 慢查询的影响
影响维度 | 具体表现 |
---|---|
用户体验 | 页面响应慢,操作卡顿,超时报错 |
系统性能 | CPU飙升,内存占用高,磁盘IO繁忙 |
数据库压力 | 连接池耗尽,锁等待,主从延迟 |
业务影响 | 订单失败,支付超时,用户流失 |
成本增加 | 需要扩容硬件,增加服务器 |
1.3 性能基准
合理的查询时间:
⭐⭐⭐⭐⭐ < 10ms 极佳(简单主键查询)
⭐⭐⭐⭐ < 50ms 优秀(索引查询)
⭐⭐⭐ < 100ms 良好(复杂查询)
⭐⭐ < 500ms 可接受(报表查询)
⭐ < 1s 需优化
❌ > 1s 严重问题
二、如何发现慢查询
2.1 方法1:慢查询日志(推荐)
开启慢查询日志
-- 1. 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';-- 2. 临时开启(重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;-- 3. 永久配置(my.cnf或my.ini)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
min_examined_row_limit = 100 # 至少扫描100行才记录
分析慢查询日志
# 使用mysqldumpslow工具分析
# 按查询时间排序,显示前10条
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log# 按查询次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log# 按平均查询时间排序
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log# 按锁定时间排序
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log# 过滤特定数据库
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log | grep 'database_name'
慢查询日志示例:
# Time: 2024-01-15T10:30:45.123456Z
# User@Host: root[root] @ localhost [] Id: 12345
# Query_time: 5.234567 Lock_time: 0.000123 Rows_sent: 100 Rows_examined: 1000000
SET timestamp=1705318245;
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-15';
字段解读:
Query_time: 5.234567
- 查询耗时5.23秒Lock_time: 0.000123
- 锁等待0.12毫秒Rows_sent: 100
- 返回100行Rows_examined: 1000000
- 扫描100万行(关键指标!)
2.2 方法2:performance_schema(MySQL 5.6+)
-- 1. 开启performance_schema(需重启)
[mysqld]
performance_schema = ON-- 2. 查询最慢的SQL(按总执行时间)
SELECT DIGEST_TEXT AS query,COUNT_STAR AS exec_count,AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,SUM_TIMER_WAIT / 1000000000000 AS total_time_sec,MIN_TIMER_WAIT / 1000000000000 AS min_time_sec,MAX_TIMER_WAIT / 1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;-- 3. 查询平均时间最长的SQL
SELECT DIGEST_TEXT AS query,COUNT_STAR AS exec_count,AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;-- 4. 查询当前正在执行的SQL
SELECT t.PROCESSLIST_ID,t.PROCESSLIST_USER,t.PROCESSLIST_HOST,t.PROCESSLIST_DB,t.PROCESSLIST_COMMAND,t.PROCESSLIST_TIME,t.PROCESSLIST_STATE,t.PROCESSLIST_INFO
FROM performance_schema.threads t
WHERE t.PROCESSLIST_COMMAND != 'Sleep'
ORDER BY t.PROCESSLIST_TIME DESC;
2.3 方法3:sys系统库(MySQL 5.7+)
-- 1. 查询执行时间最长的SQL
SELECT * FROM sys.statement_analysis
ORDER BY avg_latency DESC
LIMIT 10;-- 2. 查询全表扫描的SQL
SELECT * FROM sys.statements_with_full_table_scans
ORDER BY total_latency DESC
LIMIT 10;-- 3. 查询临时表使用情况
SELECT * FROM sys.statements_with_temp_tables
ORDER BY total_latency DESC
LIMIT 10;-- 4. 查询排序操作
SELECT * FROM sys.statements_with_sorting
ORDER BY total_latency DESC
LIMIT 10;-- 5. 查询未使用索引的SQL
SELECT query,exec_count,sys.format_time(total_latency) AS total_latency,rows_sent,rows_examined,rows_sent_avg,rows_examined_avg
FROM sys.statements_with_full_table_scans
WHERE db = 'your_database'
ORDER BY total_latency DESC
LIMIT 10;
2.4 方法4:SHOW PROCESSLIST
-- 实时查看正在执行的查询
SHOW FULL PROCESSLIST;-- 查找执行时间超过5秒的查询
SELECT id,user,host,db,command,time,state,info
FROM information_schema.processlist
WHERE time > 5
AND command != 'Sleep'
ORDER BY time DESC;-- 杀掉慢查询
KILL QUERY 12345; -- 12345是进程ID
2.5 方法5:应用层监控
方式1:日志记录
// Java示例
long startTime = System.currentTimeMillis();
try {// 执行SQLList<User> users = userMapper.selectByAge(20);
} finally {long endTime = System.currentTimeMillis();long duration = endTime - startTime;if (duration > 1000) { // 超过1秒记录log.warn("慢查询警告: 耗时{}ms, SQL: {}", duration, sql);}
}
方式2:APM工具
- SkyWalking:链路追踪
- Pinpoint:性能监控
- Arthas:在线诊断
- CAT(美团):实时监控
方式3:数据库中间件
- ShardingSphere:SQL审计
- MyCAT:慢查询统计
- ProxySQL:查询路由和监控
三、慢查询的常见原因
3.1 索引问题(80%的慢查询原因)
1. 没有索引
-- ❌ 全表扫描
SELECT * FROM users WHERE email = 'test@example.com';-- ✅ 添加索引
CREATE INDEX idx_email ON users(email);
2. 索引失效
-- ❌ 函数导致索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;-- ✅ 改写SQL
SELECT * FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';-- 详见:数据库索引失效场景详解.md
3. 索引选择不当
-- 优化器选错索引
-- 使用 FORCE INDEX 强制指定
SELECT * FROM orders FORCE INDEX(idx_create_time)
WHERE create_time > '2024-01-01';
3.2 SQL写法问题
1. SELECT *
-- ❌ 查询所有列
SELECT * FROM orders WHERE user_id = 123;-- ✅ 只查询需要的列
SELECT id, order_no, amount FROM orders WHERE user_id = 123;
2. 深度分页
-- ❌ 偏移量过大
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;-- ✅ 使用子查询优化
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 1000000, 1)
LIMIT 10;-- ✅ 使用游标分页
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
3. 大批量操作
-- ❌ 一次插入10万条
INSERT INTO logs VALUES (1,...), (2,...), ... (100000,...);-- ✅ 分批插入(每次1000条)
INSERT INTO logs VALUES (1,...), ... (1000,...);
-- 循环100次
3.3 数据量问题
1. 单表数据量过大
建议:
- 单表 < 500万行:正常使用
- 500万 ~ 2000万:需优化索引和查询
- > 2000万:考虑分表
解决方案:
-- 水平分表(按时间)
CREATE TABLE orders_2024_01 LIKE orders;
CREATE TABLE orders_2024_02 LIKE orders;-- 水平分表(按用户ID)
CREATE TABLE orders_0 LIKE orders; -- user_id % 10 = 0
CREATE TABLE orders_1 LIKE orders; -- user_id % 10 = 1
2. 数据分布不均
-- 查询倾斜数据
SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status;-- 结果:
-- status=0: 1000万(95%)
-- status=1: 50万(5%)-- 查询status=0会很慢(即使有索引)
3.4 锁和并发问题
1. 锁等待
-- 查看锁等待
SELECT * FROM sys.innodb_lock_waits;-- 查看当前锁信息
SELECT * FROM performance_schema.data_locks;-- 查看锁等待超时时间
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 默认50秒
2. 长事务
-- 查找长事务(执行超过60秒)
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;-- 查看事务详情
SELECT trx_id,trx_state,trx_started,trx_requested_lock_id,trx_wait_started,trx_weight,trx_mysql_thread_id,trx_query
FROM information_schema.innodb_trx;
3.5 JOIN问题
1. 大表JOIN
-- ❌ 两个大表直接JOIN
SELECT *
FROM orders o -- 1000万
INNER JOIN order_details od ON o.id = od.order_id -- 5000万
WHERE o.user_id = 123;-- ✅ 先过滤再JOIN
SELECT *
FROM (SELECT * FROM orders WHERE user_id = 123) o
INNER JOIN order_details od ON o.id = od.order_id;
2. JOIN列没有索引
-- ❌ 关联列无索引
SELECT * FROM orders o
INNER JOIN users u ON o.user_email = u.email;-- ✅ 添加索引
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_user_email ON orders(user_email);
3.6 服务器资源问题
1. CPU瓶颈
# 查看CPU使用率
top
# 或
vmstat 1# 查看MySQL进程CPU占用
ps aux | grep mysql
2. 内存不足
-- 查看内存配置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- InnoDB缓冲池
SHOW VARIABLES LIKE 'key_buffer_size'; -- MyISAM缓冲-- 查看缓存命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
3. 磁盘IO瓶颈
# 查看磁盘IO
iostat -x 1# 关键指标:
# %util: 磁盘利用率(>80%表示繁忙)
# await: 平均等待时间(>10ms需关注)
四、系统的排查流程
4.1 五步排查法
Step 1: 发现慢查询↓
Step 2: 分析执行计划(EXPLAIN)↓
Step 3: 定位具体原因↓
Step 4: 制定优化方案↓
Step 5: 验证优化效果
4.2 详细排查步骤
Step 1:获取慢SQL
-- 从慢查询日志获取
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log-- 或从performance_schema获取
SELECT DIGEST_TEXT AS query,COUNT_STAR AS exec_count,AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,SUM_TIMER_WAIT / 1000000000000 AS total_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
Step 2:EXPLAIN分析
-- 执行EXPLAIN
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2024-01-15';-- 关键指标:
-- 1. type: ALL → 全表扫描(需优化)
-- 2. key: NULL → 未使用索引
-- 3. rows: 1000000 → 扫描100万行
-- 4. Extra: Using filesort → 需要排序优化
详细分析见:EXPLAIN执行计划详解.md
Step 3:定位问题
检查清单:
检查项 | 检查方法 | 问题表现 |
---|---|---|
索引 | SHOW INDEX FROM table | key=NULL, type=ALL |
表结构 | SHOW CREATE TABLE table | 字段类型、字符集 |
数据量 | SELECT COUNT(*) FROM table | 单表过大 |
数据分布 | SELECT col, COUNT(*) GROUP BY col | 数据倾斜 |
锁等待 | SHOW ENGINE INNODB STATUS | Lock wait timeout |
连接数 | SHOW PROCESSLIST | Too many connections |
服务器资源 | top , iostat , free | CPU/内存/IO瓶颈 |
Step 4:制定方案
决策树:
问题类型?
├─ 索引问题
│ ├─ 无索引 → 创建索引
│ ├─ 索引失效 → 改写SQL
│ └─ 索引选择错误 → 优化索引或强制索引
│
├─ SQL写法
│ ├─ SELECT * → 只查需要的列
│ ├─ 深度分页 → 改用游标分页
│ └─ 大批量操作 → 分批处理
│
├─ 数据量
│ ├─ 单表过大 → 分表
│ └─ 返回结果过多 → 添加LIMIT
│
├─ JOIN问题
│ ├─ 大表JOIN → 先过滤再JOIN
│ └─ JOIN列无索引 → 添加索引
│
└─ 服务器资源├─ CPU瓶颈 → 优化SQL或扩容├─ 内存不足 → 调整缓冲池或扩容└─ IO瓶颈 → 使用SSD或优化查询
Step 5:验证效果
-- 1. 执行优化后的SQL,记录耗时
SET profiling = 1;
SELECT ...; -- 优化后的SQL
SHOW PROFILES;-- 2. 对比EXPLAIN结果
EXPLAIN 优化前的SQL;
EXPLAIN 优化后的SQL;-- 3. 压力测试
-- 使用sysbench或mysqlslap测试-- 4. 监控线上效果
-- 观察慢查询日志、QPS、响应时间
五、优化方法详解
5.1 索引优化
优化1:创建合适的索引
-- 场景:WHERE条件列
CREATE INDEX idx_user_id ON orders(user_id);-- 场景:ORDER BY排序列
CREATE INDEX idx_create_time ON orders(create_time);-- 场景:JOIN关联列
CREATE INDEX idx_order_id ON order_details(order_id);-- 场景:GROUP BY分组列
CREATE INDEX idx_status ON orders(status);-- 场景:覆盖索引(包含查询的所有列)
CREATE INDEX idx_user_order_amount ON orders(user_id, order_no, amount);
优化2:联合索引顺序
-- 高频查询
SELECT * FROM orders
WHERE status = 1 AND user_id = 123 AND create_time > '2024-01-01';-- ❌ 错误顺序(范围查询在前)
CREATE INDEX idx_wrong ON orders(create_time, status, user_id);-- ✅ 正确顺序(等值查询在前,范围查询在后)
CREATE INDEX idx_right ON orders(status, user_id, create_time);
排序原则:
- 等值查询(=) > 范围查询(>、<、BETWEEN)
- 高频查询列 > 低频查询列
- 高区分度列 > 低区分度列
优化3:删除冗余索引
-- 查看表的所有索引
SHOW INDEX FROM orders;-- 冗余情况
CREATE INDEX idx_a ON orders(user_id);
CREATE INDEX idx_ab ON orders(user_id, status);
-- idx_a 是冗余的,可以删除-- 删除冗余索引
ALTER TABLE orders DROP INDEX idx_a;-- 使用sys库查找冗余索引(MySQL 5.7+)
SELECT * FROM sys.schema_redundant_indexes;
5.2 SQL改写优化
优化1:避免SELECT *
-- ❌ 查询所有列
SELECT * FROM orders WHERE user_id = 123;-- ✅ 只查询需要的列
SELECT id, order_no, amount, status FROM orders WHERE user_id = 123;-- 优势:
-- 1. 减少网络传输
-- 2. 可能使用覆盖索引
-- 3. 减少内存占用
优化2:深度分页优化
-- ❌ 偏移量过大(扫描+跳过100万行)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;-- ✅ 方案1:使用子查询(延迟关联)
SELECT o.*
FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) t ON o.id = t.id;-- ✅ 方案2:使用游标(记录上次位置)
SELECT * FROM orders
WHERE id > 1000000 -- 上次查询的最后一条记录ID
ORDER BY id
LIMIT 10;-- ✅ 方案3:使用ES等搜索引擎
-- 数据同步到Elasticsearch,使用scroll API分页
优化3:IN优化
-- ❌ IN中元素过多(>1000)
SELECT * FROM orders WHERE user_id IN (1, 2, 3, ..., 10000);-- ✅ 方案1:分批查询
SELECT * FROM orders WHERE user_id IN (1, 2, ..., 1000);
SELECT * FROM orders WHERE user_id IN (1001, 1002, ..., 2000);-- ✅ 方案2:使用临时表
CREATE TEMPORARY TABLE tmp_users (user_id INT);
INSERT INTO tmp_users VALUES (1), (2), ..., (10000);
SELECT o.* FROM orders o
INNER JOIN tmp_users t ON o.user_id = t.user_id;
优化4:OR改写为UNION
-- ❌ OR可能导致索引失效
SELECT * FROM orders WHERE status = 1 OR amount > 1000;-- ✅ 改写为UNION ALL
SELECT * FROM orders WHERE status = 1
UNION ALL
SELECT * FROM orders WHERE amount > 1000 AND status != 1;
优化5:子查询改写为JOIN
-- ❌ 关联子查询(N+1问题)
SELECT u.id,u.name,(SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_count
FROM users u;-- ✅ 改写为JOIN
SELECT u.id,u.name,COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
5.3 表结构优化
优化1:选择合适的数据类型
-- ❌ 使用过大的类型
CREATE TABLE users (id BIGINT, -- INT就够用age TINYINT UNSIGNED, -- ✅ 正确status VARCHAR(255), -- 只需要CHAR(1)amount DECIMAL(20,2) -- DECIMAL(10,2)就够
);-- ✅ 优化后
CREATE TABLE users (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,age TINYINT UNSIGNED,status CHAR(1),amount DECIMAL(10,2)
);-- 优势:
-- 1. 减少存储空间
-- 2. 提升索引效率
-- 3. 减少内存占用
优化2:字段拆分
-- ❌ 大字段和常用字段混在一起
CREATE TABLE articles (id INT PRIMARY KEY,title VARCHAR(200),author VARCHAR(50),content TEXT, -- 大字段,但不常查询view_count INT
);-- ✅ 垂直拆分
CREATE TABLE articles (id INT PRIMARY KEY,title VARCHAR(200),author VARCHAR(50),view_count INT
);CREATE TABLE article_contents (article_id INT PRIMARY KEY,content TEXT,FOREIGN KEY (article_id) REFERENCES articles(id)
);-- 优势:减少IO,提升常用查询性能
优化3:表分区
-- 按时间分区(适合历史数据查询)
CREATE TABLE orders (id INT,user_id INT,amount DECIMAL(10,2),create_time DATETIME
)
PARTITION BY RANGE (YEAR(create_time)) (PARTITION p2022 VALUES LESS THAN (2023),PARTITION p2023 VALUES LESS THAN (2024),PARTITION p2024 VALUES LESS THAN (2025),PARTITION p_future VALUES LESS THAN MAXVALUE
);-- 查询时只扫描特定分区
SELECT * FROM orders WHERE create_time >= '2024-01-01';
-- 只扫描p2024分区
5.4 查询优化
优化1:避免全表扫描
-- ❌ 没有WHERE条件
SELECT * FROM orders;-- ✅ 添加WHERE和LIMIT
SELECT * FROM orders WHERE status = 1 LIMIT 1000;
优化2:使用LIMIT限制结果
-- ❌ 返回全部结果
SELECT * FROM orders WHERE status = 1;-- ✅ 限制返回数量
SELECT * FROM orders WHERE status = 1 LIMIT 100;-- ✅ EXISTS只需判断存在性
SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = 123) AS has_order;
优化3:批量操作
-- ❌ 逐条插入
INSERT INTO logs (user_id, action) VALUES (1, 'login');
INSERT INTO logs (user_id, action) VALUES (2, 'logout');
-- 执行1000次...-- ✅ 批量插入
INSERT INTO logs (user_id, action) VALUES
(1, 'login'),
(2, 'logout'),
...
(1000, 'view');-- ✅ 批量更新
UPDATE orders SET status = 1 WHERE id IN (1, 2, 3, ..., 1000);
5.5 缓存优化
优化1:应用层缓存
// Redis缓存示例
public User getUserById(Long id) {// 1. 先查缓存String cacheKey = "user:" + id;User user = redisTemplate.opsForValue().get(cacheKey);if (user != null) {return user; // 缓存命中}// 2. 缓存未命中,查数据库user = userMapper.selectById(id);// 3. 写入缓存if (user != null) {redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);}return user;
}
优化2:查询缓存(Query Cache)
-- 注意:MySQL 8.0已移除查询缓存-- MySQL 5.7及以下
-- 开启查询缓存
SET GLOBAL query_cache_type = ON;
SET GLOBAL query_cache_size = 67108864; -- 64MB-- 查看缓存状态
SHOW VARIABLES LIKE 'query_cache%';
SHOW STATUS LIKE 'Qcache%';
5.6 读写分离和分库分表
优化1:主从复制+读写分离
// 使用动态数据源
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {String value() default "master";
}// 写操作走主库
@DataSource("master")
public void createOrder(Order order) {orderMapper.insert(order);
}// 读操作走从库
@DataSource("slave")
public Order getOrderById(Long id) {return orderMapper.selectById(id);
}
优化2:分库分表
// 使用ShardingSphere实现分库分表
// 按user_id取模分表
spring.shardingsphere.rules.sharding.tables.orders.actual-data-nodes=ds0.orders_$->{0..9}
spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-column=user_id
spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-algorithm-name=orders-inlinespring.shardingsphere.rules.sharding.sharding-algorithms.orders-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.orders-inline.props.algorithm-expression=orders_$->{user_id % 10}
六、实战优化案例
6.1 案例1:电商订单查询优化
问题描述
业务场景:用户查看订单列表
问题:订单表1000万数据,查询超时
SQL执行时间:8秒
原始SQL
SELECT * FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 10;
排查过程
Step 1:EXPLAIN分析
EXPLAIN SELECT * FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 10;
结果:
+----+-------------+--------+------+---------------+------+---------+------+----------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+----------+-----------------------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 10000000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+----------+-----------------------------+
问题分析:
- ❌
type = ALL
:全表扫描 - ❌
key = NULL
:未使用索引 - ❌
rows = 10000000
:扫描全表 - ❌
Extra = Using filesort
:需要排序
Step 2:优化方案
-- 1. 创建联合索引(user_id + create_time)
CREATE INDEX idx_user_create ON orders(user_id, create_time);-- 2. 优化SQL(只查询需要的列)
SELECT id, order_no, amount, status, create_time
FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 10;
Step 3:验证优化效果
EXPLAIN SELECT id, order_no, amount, status, create_time
FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 10;
优化后结果:
+----+-------------+--------+------+------------------+------------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+------------------+------------------+---------+-------+------+-------------+
| 1 | SIMPLE | orders | ref | idx_user_create | idx_user_create | 4 | const | 100 | Using where |
+----+-------------+--------+------+------------------+------------------+---------+-------+------+-------------+
优化效果:
- ✅
type = ref
:使用索引 - ✅
key = idx_user_create
:使用了新索引 - ✅
rows = 100
:只扫描100行(从1000万降到100) - ✅ 无
Using filesort
:利用索引排序 - ⏱️ 执行时间:从8秒降到0.01秒(提升800倍)
6.2 案例2:报表统计优化
问题描述
业务场景:后台订单统计
问题:统计查询超时
SQL执行时间:25秒
原始SQL
SELECT DATE(create_time) AS date,COUNT(*) AS order_count,SUM(amount) AS total_amount,AVG(amount) AS avg_amount
FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01'
GROUP BY DATE(create_time)
ORDER BY date;
排查过程
EXPLAIN分析:
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------------------------------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 10000000 | Using where; Using temporary; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------------------------------------+
问题:
- ❌
type = ALL
:全表扫描 - ❌
Extra = Using temporary
:使用临时表 - ❌
Extra = Using filesort
:需要排序 - ❌ GROUP BY使用了函数
DATE()
优化方案
方案1:添加冗余字段
-- 1. 添加日期字段
ALTER TABLE orders ADD COLUMN order_date DATE;-- 2. 创建索引
CREATE INDEX idx_order_date ON orders(order_date);-- 3. 更新历史数据
UPDATE orders SET order_date = DATE(create_time);-- 4. 应用层写入时自动填充
INSERT INTO orders (user_id, amount, create_time, order_date)
VALUES (123, 100.00, NOW(), CURDATE());-- 5. 优化后的SQL
SELECT order_date AS date,COUNT(*) AS order_count,SUM(amount) AS total_amount,AVG(amount) AS avg_amount
FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2024-02-01'
GROUP BY order_date
ORDER BY order_date;
方案2:汇总表(更优)
-- 1. 创建日统计表
CREATE TABLE order_daily_stats (stat_date DATE PRIMARY KEY,order_count INT,total_amount DECIMAL(15,2),avg_amount DECIMAL(10,2),update_time DATETIME
);-- 2. 定时任务汇总数据(凌晨执行)
INSERT INTO order_daily_stats (stat_date, order_count, total_amount, avg_amount, update_time)
SELECT DATE(create_time),COUNT(*),SUM(amount),AVG(amount),NOW()
FROM orders
WHERE DATE(create_time) = CURDATE() - INTERVAL 1 DAY
ON DUPLICATE KEY UPDATEorder_count = VALUES(order_count),total_amount = VALUES(total_amount),avg_amount = VALUES(avg_amount),update_time = VALUES(update_time);-- 3. 查询汇总表(毫秒级)
SELECT * FROM order_daily_stats
WHERE stat_date >= '2024-01-01' AND stat_date < '2024-02-01'
ORDER BY stat_date;
优化效果:
- ⏱️ 执行时间:从25秒降到0.005秒(提升5000倍)
- 💾 空间成本:增加一个汇总表(31行/月)
- 🎯 适用场景:历史数据统计、报表查询
6.3 案例3:JOIN查询优化
问题描述
业务场景:用户订单详情
问题:多表关联查询慢
SQL执行时间:12秒
原始SQL
SELECT u.name,o.order_no,o.amount,p.product_name,od.quantity
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_details od ON o.id = od.order_id
INNER JOIN products p ON od.product_id = p.id
WHERE u.status = 1
AND o.create_time >= '2024-01-01';
EXPLAIN分析
+----+-------------+-------+------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+---------+-----------------------------+
| 1 | SIMPLE | u | ALL | NULL | NULL | NULL | NULL | 1000000 | Using where |
| 1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 5000000 | Using where; Using join... |
| 1 | SIMPLE | od | ALL | NULL | NULL | NULL | NULL | 10000000| Using where; Using join... |
| 1 | SIMPLE | p | ALL | NULL | NULL | NULL | NULL | 100000 | Using where; Using join... |
+----+-------------+-------+------+---------------+------+---------+------+---------+-----------------------------+
问题:
- 4个表都是全表扫描
- 所有JOIN列都没有索引
优化方案
-- 1. 添加索引
CREATE INDEX idx_status ON users(status);
CREATE INDEX idx_user_create ON orders(user_id, create_time);
CREATE INDEX idx_order_id ON order_details(order_id);
CREATE INDEX idx_product_id ON order_details(product_id);-- 2. 优化SQL(先过滤再JOIN)
SELECT u.name,o.order_no,o.amount,p.product_name,od.quantity
FROM (SELECT id, name FROM users WHERE status = 1
) u
INNER JOIN (SELECT id, user_id, order_no, amount FROM orders WHERE create_time >= '2024-01-01'
) o ON u.id = o.user_id
INNER JOIN order_details od ON o.id = od.order_id
INNER JOIN products p ON od.product_id = p.id;
优化后EXPLAIN:
+----+-------------+-------+--------+------------------+------------------+---------+-----------+-------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+------------------+------------------+---------+-----------+-------+-------------+
| 1 | SIMPLE | u | ref | idx_status | idx_status | 1 | const | 10000 | Using where |
| 1 | SIMPLE | o | ref | idx_user_create | idx_user_create | 4 | u.id | 5 | Using where |
| 1 | SIMPLE | od | ref | idx_order_id | idx_order_id | 4 | o.id | 3 | NULL |
| 1 | SIMPLE | p | eq_ref | PRIMARY | PRIMARY | 4 | od.product_id| 1 | NULL |
+----+-------------+-------+--------+------------------+------------------+---------+-----------+-------+-------------+
优化效果:
- ⏱️ 执行时间:从12秒降到0.05秒(提升240倍)
七、监控与预防
7.1 慢查询监控
1. 数据库层监控
-- 定期检查慢查询
SELECT DIGEST_TEXT AS query,COUNT_STAR AS exec_count,AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,MAX_TIMER_WAIT / 1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000000 -- 超过1秒
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 20;
2. 应用层监控
// AOP拦截慢SQL
@Aspect
@Component
public class SlowSqlAspect {@Around("execution(* com.example.mapper..*(..))")public Object around(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();try {return pjp.proceed();} finally {long duration = System.currentTimeMillis() - start;if (duration > 1000) { // 超过1秒log.warn("慢SQL告警: {}ms, 方法: {}", duration, pjp.getSignature());// 发送告警alertService.sendSlowSqlAlert(pjp.getSignature(), duration);}}}
}
3. 监控平台
- Prometheus + Grafana:可视化监控
- Zabbix:告警通知
- 阿里云RDS:自带慢查询分析
- 腾讯云CDB:SQL审计
7.2 SQL审核
上线前审核
-- 使用EXPLAIN预审SQL
EXPLAIN SELECT ...;-- 审核标准:
-- 1. type不能是ALL
-- 2. key不能是NULL
-- 3. rows不能超过10万
-- 4. Extra不能有Using temporary
SQL审核工具
- Yearning:开源SQL审核平台
- Archery:SQL上线审核系统
- Soar:SQL优化和改写工具
7.3 数据库规范
索引规范
1. 单表索引数量不超过5个
2. 联合索引字段数不超过5个
3. 索引字段总长度不超过767字节
4. 必须为JOIN字段创建索引
5. 禁止在低区分度字段建索引(如性别)
SQL规范
1. 禁止SELECT *
2. 禁止在WHERE子句使用函数
3. 禁止隐式类型转换
4. 禁止LIKE '%keyword%'
5. 禁止大批量操作(单次超过1000条分批)
6. 禁止深度分页(OFFSET超过10000)
表设计规范
1. 单表字段数不超过30个
2. 单表数据量不超过2000万
3. 必须有主键
4. 字符字段必须指定字符集
5. 禁止使用TEXT、BLOB(必要时拆分)
八、优化效果评估
8.1 性能指标
指标 | 优化前 | 优化后 | 提升 |
---|---|---|---|
响应时间 | 8秒 | 0.01秒 | 800倍 |
扫描行数 | 1000万 | 100 | 10万倍 |
CPU使用率 | 80% | 20% | 降低75% |
QPS | 10 | 1000 | 100倍 |
并发数 | 50 | 500 | 10倍 |
8.2 对比测试
-- 开启profiling
SET profiling = 1;-- 执行优化前SQL
SELECT ...; -- Query 1-- 执行优化后SQL
SELECT ...; -- Query 2-- 查看对比
SHOW PROFILES;-- 详细分析
SHOW PROFILE FOR QUERY 1;
SHOW PROFILE FOR QUERY 2;
九、常见问题FAQ
Q1:为什么添加了索引还是慢?
可能原因:
- 索引失效(函数、类型转换、LIKE ‘%xx’)
- 优化器没有选择索引(数据量太小或太大)
- 回表开销大(可以用覆盖索引)
- 锁等待
排查方法:
EXPLAIN SELECT ...;
-- 查看key是否为NULL
-- 查看type是否为ALL
Q2:数据量不大为什么还慢?
可能原因:
- 没有WHERE条件(全表扫描)
- 锁等待(长事务、表锁)
- 硬件问题(磁盘IO、CPU)
- 网络延迟
Q3:如何选择优化优先级?
优先级排序:
1. 紧急慢查询(影响线上业务)
2. 高频慢查询(执行次数多)
3. 低频慢查询(偶尔执行)
4. 报表查询(可离线处理)
Q4:索引是不是越多越好?
不是!索引的代价:
- 占用存储空间
- 降低写入性能
- 增加维护成本
建议:
- 单表索引不超过5个
- 定期删除未使用的索引
Q5:什么时候需要分表?
建议阈值:
- 单表 < 500万:正常使用
- 500万 ~ 2000万:优化索引和查询
-
2000万:考虑分表
分表策略:
- 垂直分表:按字段拆分
- 水平分表:按数据范围拆分
总结
慢查询优化核心思路
1. 发现问题:慢查询日志 + 监控
2. 分析问题:EXPLAIN + 执行计划
3. 定位原因:索引 / SQL / 数据量 / 锁
4. 制定方案:索引优化 / SQL改写 / 分表
5. 验证效果:性能对比 + 压力测试
6. 持续监控:告警 + 定期review
优化方法速查
问题类型 | 优化方法 | 优先级 |
---|---|---|
无索引 | 创建索引 | ⭐⭐⭐⭐⭐ |
索引失效 | 改写SQL | ⭐⭐⭐⭐⭐ |
全表扫描 | 添加WHERE+索引 | ⭐⭐⭐⭐⭐ |
**SELECT *** | 只查需要的列 | ⭐⭐⭐⭐ |
深度分页 | 游标分页 | ⭐⭐⭐⭐ |
大表JOIN | 先过滤再JOIN | ⭐⭐⭐⭐ |
单表过大 | 分表 | ⭐⭐⭐ |
锁等待 | 优化事务 | ⭐⭐⭐⭐ |
最佳实践
- 开发阶段:所有SQL必须EXPLAIN分析
- 测试阶段:压力测试,发现潜在问题
- 上线阶段:SQL审核,慢查询告警
- 运维阶段:定期review,持续优化