文章目录
-
- 一、什么是慢查询
-
- [1.1 定义](#1.1 定义)
- [1.2 慢查询的影响](#1.2 慢查询的影响)
- [1.3 性能基准](#1.3 性能基准)
- 二、如何发现慢查询
-
- [2.1 方法1:慢查询日志(推荐)](#2.1 方法1:慢查询日志(推荐))
- [2.2 方法2:performance_schema(MySQL 5.6+)](#2.2 方法2:performance_schema(MySQL 5.6+))
- [2.3 方法3:sys系统库(MySQL 5.7+)](#2.3 方法3:sys系统库(MySQL 5.7+))
- [2.4 方法4:SHOW PROCESSLIST](#2.4 方法4:SHOW PROCESSLIST)
- [2.5 方法5:应用层监控](#2.5 方法5:应用层监控)
- 三、慢查询的常见原因
-
- [3.1 索引问题(80%的慢查询原因)](#3.1 索引问题(80%的慢查询原因))
-
- [1. 没有索引](#1. 没有索引)
- [2. 索引失效](#2. 索引失效)
- [3. 索引选择不当](#3. 索引选择不当)
- [3.2 SQL写法问题](#3.2 SQL写法问题)
-
- [1. SELECT *](#1. SELECT *)
- [2. 深度分页](#2. 深度分页)
- [3. 大批量操作](#3. 大批量操作)
- [3.3 数据量问题](#3.3 数据量问题)
-
- [1. 单表数据量过大](#1. 单表数据量过大)
- [2. 数据分布不均](#2. 数据分布不均)
- [3.4 锁和并发问题](#3.4 锁和并发问题)
-
- [1. 锁等待](#1. 锁等待)
- [2. 长事务](#2. 长事务)
- [3.5 JOIN问题](#3.5 JOIN问题)
-
- [1. 大表JOIN](#1. 大表JOIN)
- [2. JOIN列没有索引](#2. JOIN列没有索引)
- [3.6 服务器资源问题](#3.6 服务器资源问题)
-
- [1. CPU瓶颈](#1. CPU瓶颈)
- [2. 内存不足](#2. 内存不足)
- [3. 磁盘IO瓶颈](#3. 磁盘IO瓶颈)
- 四、系统的排查流程
-
- [4.1 五步排查法](#4.1 五步排查法)
- [4.2 详细排查步骤](#4.2 详细排查步骤)
-
- [Step 1:获取慢SQL](#Step 1:获取慢SQL)
- [Step 2:EXPLAIN分析](#Step 2:EXPLAIN分析)
- [Step 3:定位问题](#Step 3:定位问题)
- [Step 4:制定方案](#Step 4:制定方案)
- [Step 5:验证效果](#Step 5:验证效果)
- 五、优化方法详解
-
- [5.1 索引优化](#5.1 索引优化)
- [5.2 SQL改写优化](#5.2 SQL改写优化)
-
- [优化1:避免SELECT *](#优化1:避免SELECT *)
- 优化2:深度分页优化
- 优化3:IN优化
- 优化4:OR改写为UNION
- 优化5:子查询改写为JOIN
- [5.3 表结构优化](#5.3 表结构优化)
- [5.4 查询优化](#5.4 查询优化)
- [5.5 缓存优化](#5.5 缓存优化)
-
- 优化1:应用层缓存
- [优化2:查询缓存(Query Cache)](#优化2:查询缓存(Query Cache))
- [5.6 读写分离和分库分表](#5.6 读写分离和分库分表)
- 六、实战优化案例
- 七、监控与预防
- 八、优化效果评估
-
- [8.1 性能指标](#8.1 性能指标)
- [8.2 对比测试](#8.2 对比测试)
- 九、常见问题FAQ
- 总结
一、什么是慢查询
1.1 定义
慢查询(Slow Query) 是指执行时间超过指定阈值的SQL语句。
判断标准:
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:慢查询日志(推荐)
开启慢查询日志
sql
-- 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行才记录
分析慢查询日志
bash
# 使用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+)
sql
-- 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+)
sql
-- 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
sql
-- 实时查看正在执行的查询
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
// Java示例
long startTime = System.currentTimeMillis();
try {
// 执行SQL
List<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. 没有索引
sql
-- ❌ 全表扫描
SELECT * FROM users WHERE email = 'test@example.com';
-- ✅ 添加索引
CREATE INDEX idx_email ON users(email);
2. 索引失效
sql
-- ❌ 函数导致索引失效
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. 索引选择不当
sql
-- 优化器选错索引
-- 使用 FORCE INDEX 强制指定
SELECT * FROM orders FORCE INDEX(idx_create_time)
WHERE create_time > '2024-01-01';
3.2 SQL写法问题
1. SELECT *
sql
-- ❌ 查询所有列
SELECT * FROM orders WHERE user_id = 123;
-- ✅ 只查询需要的列
SELECT id, order_no, amount FROM orders WHERE user_id = 123;
2. 深度分页
sql
-- ❌ 偏移量过大
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. 大批量操作
sql
-- ❌ 一次插入10万条
INSERT INTO logs VALUES (1,...), (2,...), ... (100000,...);
-- ✅ 分批插入(每次1000条)
INSERT INTO logs VALUES (1,...), ... (1000,...);
-- 循环100次
3.3 数据量问题
1. 单表数据量过大
建议:
- 单表 < 500万行:正常使用
- 500万 ~ 2000万:需优化索引和查询
- > 2000万:考虑分表
解决方案:
sql
-- 水平分表(按时间)
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. 数据分布不均
sql
-- 查询倾斜数据
SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status;
-- 结果:
-- status=0: 1000万(95%)
-- status=1: 50万(5%)
-- 查询status=0会很慢(即使有索引)
3.4 锁和并发问题
1. 锁等待
sql
-- 查看锁等待
SELECT * FROM sys.innodb_lock_waits;
-- 查看当前锁信息
SELECT * FROM performance_schema.data_locks;
-- 查看锁等待超时时间
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 默认50秒
2. 长事务
sql
-- 查找长事务(执行超过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
sql
-- ❌ 两个大表直接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列没有索引
sql
-- ❌ 关联列无索引
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瓶颈
bash
# 查看CPU使用率
top
# 或
vmstat 1
# 查看MySQL进程CPU占用
ps aux | grep mysql
2. 内存不足
sql
-- 查看内存配置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- InnoDB缓冲池
SHOW VARIABLES LIKE 'key_buffer_size'; -- MyISAM缓冲
-- 查看缓存命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
3. 磁盘IO瓶颈
bash
# 查看磁盘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
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分析
sql
-- 执行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:验证效果
sql
-- 1. 执行优化后的SQL,记录耗时
SET profiling = 1;
SELECT ...; -- 优化后的SQL
SHOW PROFILES;
-- 2. 对比EXPLAIN结果
EXPLAIN 优化前的SQL;
EXPLAIN 优化后的SQL;
-- 3. 压力测试
-- 使用sysbench或mysqlslap测试
-- 4. 监控线上效果
-- 观察慢查询日志、QPS、响应时间
五、优化方法详解
5.1 索引优化
优化1:创建合适的索引
sql
-- 场景: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:联合索引顺序
sql
-- 高频查询
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:删除冗余索引
sql
-- 查看表的所有索引
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 *
sql
-- ❌ 查询所有列
SELECT * FROM orders WHERE user_id = 123;
-- ✅ 只查询需要的列
SELECT id, order_no, amount, status FROM orders WHERE user_id = 123;
-- 优势:
-- 1. 减少网络传输
-- 2. 可能使用覆盖索引
-- 3. 减少内存占用
优化2:深度分页优化
sql
-- ❌ 偏移量过大(扫描+跳过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优化
sql
-- ❌ 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
sql
-- ❌ 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
sql
-- ❌ 关联子查询(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:选择合适的数据类型
sql
-- ❌ 使用过大的类型
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:字段拆分
sql
-- ❌ 大字段和常用字段混在一起
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:表分区
sql
-- 按时间分区(适合历史数据查询)
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:避免全表扫描
sql
-- ❌ 没有WHERE条件
SELECT * FROM orders;
-- ✅ 添加WHERE和LIMIT
SELECT * FROM orders WHERE status = 1 LIMIT 1000;
优化2:使用LIMIT限制结果
sql
-- ❌ 返回全部结果
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:批量操作
sql
-- ❌ 逐条插入
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:应用层缓存
java
// 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)
sql
-- 注意: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:主从复制+读写分离
java
// 使用动态数据源
@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:分库分表
java
// 使用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-inline
spring.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
sql
SELECT * FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 10;
排查过程
Step 1:EXPLAIN分析
sql
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:优化方案
sql
-- 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:验证优化效果
sql
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
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:添加冗余字段
sql
-- 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:汇总表(更优)
sql
-- 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 UPDATE
order_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
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列都没有索引
优化方案
sql
-- 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. 数据库层监控
sql
-- 定期检查慢查询
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. 应用层监控
java
// 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审核
上线前审核
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 对比测试
sql
-- 开启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')
- 优化器没有选择索引(数据量太小或太大)
- 回表开销大(可以用覆盖索引)
- 锁等待
排查方法:
sql
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,持续优化