一、什么是 MySQL 慢查询?
首先要明确一个核心问题:什么样的查询算是 "慢查询"?
MySQL 官方定义为:执行时间超过long_query_time
参数设定阈值的 SQL 语句(默认值为 10 秒)。但在实际业务中,这个阈值需要根据场景调整 ------ 比如电商秒杀场景,超过 500ms 的查询就可能影响用户体验,而后台统计报表查询允许 10 秒甚至更久的执行时间。
除此之外,还有一个容易被忽略的 "隐性慢查询":虽然单条语句执行时间未超过阈值,但由于调用频率极高(如每秒执行上千次),累计耗时会严重占用数据库资源,这类查询同样需要重点优化。
二、如何开启慢查询日志?
要处理慢查询,首先得 "抓住" 它 ------ 这就需要开启 MySQL 的慢查询日志(slow query log) 。慢查询日志会记录所有执行时间超过long_query_time
的 SQL 语句,包括执行时长、执行时间、锁等待时间等关键信息。
2.1 查看当前慢查询配置
先通过 SQL 命令查看当前数据库的慢查询相关配置,确认是否已开启:
java
-- 开启慢查询日志
set global slow_query_log=ON;
-- 设置慢查询阈值为1秒(根据业务调整,建议从1秒开始,逐步缩小范围)
set global long_query_time=1;
-- 开启"未使用索引的查询"记录(帮助发现隐形问题)
set global log_queries_not_using_indexes=ON;
-- 注意:设置后需重新连接数据库才能生效
2.2 临时开启慢查询日志(重启失效)
如果只是临时排查问题,不需要重启 MySQL,可以通过set global
命令动态开启:
-- 开启慢查询日志
set global slow_query_log = ON;
-- 设置慢查询阈值为1秒(根据业务调整,建议从1秒开始,逐步缩小范围)
set global long_query_time = 1;
-- 开启"未使用索引的查询"记录(帮助发现隐性问题)
set global log_queries_not_using_indexes = ON;
-- 注意:设置后需重新连接数据库才能生效
2.3 永久开启慢查询日志(推荐)
临时配置会在 MySQL 重启后失效,生产环境建议通过修改配置文件my.cnf
(Linux)或my.ini
(Windows)永久生效:
[mysqld]
# 开启慢查询日志
slow_query_log = 1
# 慢查询日志存储路径(需确保MySQL有写入权限)
slow_query_log_file = /var/lib/mysql/mysql-slow.log
# 慢查询时间阈值(单位:秒,支持小数,如0.5表示500ms)
long_query_time = 0.5
# 记录未使用索引的查询(即使执行时间很短)
log_queries_not_using_indexes = 1
# 记录慢查询的同时,排除管理类语句(如OPTIMIZE TABLE)
log_slow_admin_statements = 0
修改后重启 MySQL 服务使配置生效:
# Linux重启命令(CentOS/RHEL)
systemctl restart mysqld
# 验证是否生效
show variables like 'slow_query_log';
三、如何分析慢查询日志?
慢查询日志开启后,会不断积累符合条件的 SQL 语句。但直接打开日志文件查看(尤其是大文件)会非常繁琐,推荐使用 MySQL 自带的工具mysqldumpslow
或第三方工具pt-query-digest
进行分析。
3.1 用 mysqldumpslow 快速分析(自带工具)
mysqldumpslow
是 MySQL 默认安装的工具,支持按 "执行次数、执行时间、锁等待时间" 等维度排序,适合快速定位 TOP N 慢查询。
常用命令示例:
# 1. 查看帮助文档(了解所有参数)
mysqldumpslow --help
# 2. 统计执行次数最多的10条慢查询
mysqldumpslow -s c -t 10 /var/lib/mysql/mysql-slow.log
# 3. 统计总执行时间最长的10条慢查询
mysqldumpslow -s t -t 10 /var/lib/mysql/mysql-slow.log
# 4. 统计锁等待时间最长的10条慢查询
mysqldumpslow -s l -t 10 /var/lib/mysql/mysql-slow.log
# 5. 过滤出包含"order by"的慢查询,并按执行时间排序
mysqldumpslow -s t -t 10 -g 'order by' /var/lib/mysql/mysql-slow.log
参数说明:
-s
:排序方式(c = 执行次数,t = 总时间,l = 锁等待时间,r = 返回行数)
-t
:显示前 N 条记录
-g
:按关键字过滤(支持正则表达式)
3.2 用 pt-query-digest 深度分析(推荐)
mysqldumpslow
功能较简单,无法分析 SQL 的执行频率、平均耗时、占比等细节。生产环境更推荐使用 Percona Toolkit 中的pt-query-digest
工具,它能生成更详细的分析报告。
3.2.1 安装 pt-query-digest(Linux)
# CentOS/RHEL系统
yum install percona-toolkit -y
# Ubuntu/Debian系统
apt-get install percona-toolkit -y
# 验证安装
pt-query-digest --version
3.2.2 生成深度分析报告
# 1. 分析慢查询日志,输出到屏幕
pt-query-digest /var/lib/mysql/mysql-slow.log
# 2. 分析慢查询日志,输出到文件(方便后续查看)
pt-query-digest /var/lib/mysql/mysql-slow.log > slow_query_analysis.txt
# 3. 分析最近1小时的慢查询(避免全量日志过大)
pt-query-digest --since=1h /var/lib/mysql/mysql-slow.log
3.2.3 报告核心信息解读
pt-query-digest
的报告分为 3 个关键部分:
总体统计:日志时间范围、总查询数、慢查询数、总执行时间等;
慢查询排名:按 "总执行时间占比" 排序,展示每条 SQL 的执行次数、平均耗时、锁等待时间等;
SQL 详情:每条慢查询的完整语句、执行计划(explain 结果)、涉及的表和索引等。
例如,报告中可能会出现这样的条目:
# Query 1: 0.00 QPS, 0.02x concurrency, ID 0xABCDEF123456 at byte 12345
# Scores: V/M = 10.0
# Time range: 2024-05-01 10:00:00 to 10:30:00
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 5 10 1 1 1 1 0 1
# Exec time 80 20 2 2 2 2 0 2
# Lock time 0 0 0 0 0 0 0 0
# Rows sent 1 10 1 1 1 1 0 1
# Rows examine 99 10000 1000 1000 1000 1000 0 1000
# Query_time distribution
# 1us
# 10us
# 100us
# 1ms
# 10ms
# 100ms
# 1s ##########
# 10s+
# String:
# Hosts 192.168.1.100 (10)
# Users app_user (10)
# Databases test_db (10)
# Tables order_info (10)
# Indexes NULL
# SQL hash 0x123456789ABCDEF
# SQL:
select * from order_info where user_id = 123 and create_time > '2024-05-01';
从上述报告可看出:
- 这条 SQL 执行了 10 次,总执行时间占比 80%(核心慢查询);
- 平均执行时间 2 秒,每次扫描 10000 行数据,但只返回 1 行(说明未使用索引,存在全表扫描);
- 涉及表
order_info
,过滤条件是user_id
和create_time
。
四、慢查询优化实战:从 SQL 到索引
分析出慢查询后,优化的核心思路是减少 "数据扫描量" 和 "执行步骤" ,常见手段包括 "优化 SQL 语句" 和 "优化索引",两者通常需要结合进行。
4.1 先看执行计划:explain 命令
优化前必须先通过explain
命令查看 SQL 的执行计划,了解 MySQL 是如何处理这条语句的(是否全表扫描、是否使用索引、连接方式等)。
例如,对上述慢查询执行explain
:
explain select * from order_info where user_id = 123 and create_time > '2024-05-01';
执行结果可能如下(关键列解读):
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | order_info | ALL | NULL | NULL | NULL | NULL | 10000 | Using where |
关键列含义:
type
:连接类型,ALL
表示全表扫描 (性能最差),理想值是range
(范围查询)或ref
(非唯一索引查找);
possible_keys
:可能使用的索引(NULL 表示无可用索引);
key
:实际使用的索引(NULL 表示未使用索引);
rows
:MySQL 预估需要扫描的行数(数值越大,性能越差);
Extra
:额外信息,Using where
表示需要在内存中过滤数据(无索引时常见)。
4.2 优化手段 1:添加合适的索引
上述案例中,SQL 的过滤条件是user_id = 常量
和create_time > 常量
,符合 "最左前缀原则 ",适合创建联合索引(user_id, create_time)
。
创建索引并验证:
# 创建联合索引
create index idx_order_user_create on order_info(user_id, create_time);
# 再次查看执行计划
explain select * from order_info where user_id = 123 and create_time > '2024-05-01';
优化后的执行计划可能如下:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | order_info | range | idx_order_user_create | idx_order_user_create | 10 | NULL | 10 | Using where |
可以看到:
type
从ALL
变为range
(范围查询,性能大幅提升);
key
显示使用了新创建的联合索引;
rows
从 10000 变为 10(扫描行数减少 99.9%)。
此时再执行这条 SQL,执行时间通常会从 2 秒降至 10ms 以内。
索引优化注意事项:
- 避免过度索引:索引会加速查询,但会减慢插入 / 更新 / 删除操作(需维护索引结构),一张表的索引建议不超过 5 个;
- 优先联合索引:当 SQL 有多个过滤条件时,联合索引比单字段索引更高效(需符合最左前缀原则);
- 避免 "选择性差" 的索引:如性别(只有男 / 女)、状态(只有 0/1)等字段,索引过滤效果差,反而会增加开销。
4.3 优化手段 2:重构 SQL 语句
有些时候,即使有索引,不合理的 SQL 写法也会导致慢查询,常见问题及优化方案如下:
问题 1:使用select *
获取所有字段
select *
会导致 MySQL 读取所有字段的数据,尤其是包含TEXT
/BLOB
等大字段时,会增加 IO 开销。
优化方案:只查询需要的字段(按需取值)
-- 优化前
select * from order_info where user_id = 123;
-- 优化后(只查询订单ID、金额、创建时间)
select order_id, amount, create_time from order_info where user_id = 123;
问题 2:使用or
连接多个条件(无索引时)
or
会导致 MySQL 放弃索引,进行全表扫描(除非所有or
条件的字段都有索引)。
优化方案 :用union all
替代or
(需确保字段有索引)。
-- 优化前(无索引时全表扫描)
select * from order_info where user_id = 123 or order_id = 456;
-- 优化后(若user_id和order_id有索引,会走索引查询)
select * from order_info where user_id = 123
union all
select * from order_info where order_id = 456;
问题 3:like
以%
开头(如%abc
)
like '%abc'
会导致索引失效,进行全表扫描;而abc%
可以正常使用索引。
优化方案:尽量避免前缀模糊查询,若业务必须,可考虑使用 Elasticsearch 等搜索引擎。
-- 优化前(索引失效)
select * from user where username like '%zhang';
-- 优化后(可使用索引)
select * from user where username like 'zhang%';
问题 4:在索引字段上做函数操作
对索引字段使用函数(如date(create_time)
)会导致索引失效,MySQL 无法直接使用索引进行过滤。
优化方案:将函数操作转移到常量端。
-- 优化前(索引失效)
select * from order_info where date(create_time) = '2024-05-01';
-- 优化后(可使用索引)
select * from order_info where create_time between '2024-05-01 00:00:00' and '2024-05-01 23:59:59';
4.4 优化手段 3:其他高级方案
如果上述优化后性能仍不满足需求,可考虑以下高级方案:
1. 分表分库
当单表数据量超过 1000 万行时,即使有索引,查询性能也会明显下降。此时可通过 "分表" 将大表拆分为小表:
水平分表:按时间(如每月一张订单表)、按用户 ID 哈希(如 user_id%10 拆为 10 张表);
垂直分表 :将大字段(如
remark
、content
)拆分到单独的表,减少主表数据量。
2. 读写分离
大部分业务场景中,"读" 操作远多于 "写" 操作(如电商商品详情页,查询量是更新量的 100 倍)。此时可通过 "读写分离" 将读请求分流到从库,主库只处理写请求:
主库:插入、更新、删除(写操作);
从库:查询(读操作);
同步方式:MySQL 主从复制(异步 / 半同步)。
3. 缓存热点数据
对于高频访问且更新不频繁的数据(如商品分类、热门商品列表),可将其缓存到 Redis、Memcached 等缓存中间件中,直接从缓存返回结果,避免访问数据库。