4. 慢查询优化实战:慢查询日志配置,pt-query-digest工具使用,常见慢SQL场景优化
说到慢查询优化,这可能是咱们DBA日常工作中最常打交道的事情了。我个人的习惯是,接手一个新系统,第一件事就是打开慢查询日志看看。说白了,慢查询就是数据库里的「交通拥堵点」,不解决它,系统迟早要出问题。
4.1 慢查询日志配置
MySQL的慢查询日志,就像飞机的黑匣子。它记录着所有执行时间超过阈值的SQL。嗯,这里要注意,默认情况下这个功能是关闭的。
注意:生产环境开启慢查询日志,一定要控制好阈值和文件大小。我曾经见过有人把阈值设成0,结果一天下来日志文件把磁盘撑爆了。
先看看当前配置:
-- 查看慢查询相关参数
SHOW VARIABLES LIKE '%slow_query%';
SHOW VARIABLES LIKE '%long_query_time%';
SHOW VARIABLES LIKE '%log_queries_not_using_indexes%';
我个人建议的配置方案是这样的:
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
-- 设置慢查询阈值(单位:秒),我一般设成1秒
SET GLOBAL long_query_time = 1;
-- 指定日志文件路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
-- 记录没有使用索引的查询
SET GLOBAL log_queries_not_using_indexes = ON;
小技巧:线上环境建议用1秒作为阈值。如果系统压力不大,可以设成0.5秒。但别设太短,否则日志量会很大。
如果要永久生效,记得写到my.cnf配置文件里:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
4.2 pt-query-digest工具使用
光有日志还不够,你得会分析。pt-query-digest是Percona Toolkit里的明星工具。说实话,我用了这么多年,还没找到比它更好用的慢查询分析工具。
安装很简单:
# 下载安装
wget https://www.percona.com/downloads/percona-toolkit/LATEST/
tar -xzf percona-toolkit-*.tar.gz
cd percona-toolkit-*
perl Makefile.PL
make && make install
基本用法:
# 分析慢查询日志
pt-query-digest /var/log/mysql/mysql-slow.log
# 分析最近1小时的慢查询
pt-query-digest --since=1h /var/log/mysql/mysql-slow.log
# 输出到文件
pt-query-digest /var/log/mysql/mysql-slow.log > slow_analysis.txt
输出结果怎么看?我一般重点关注这几个指标:
| 指标 | 说明 | 重点关注 |
|---|---|---|
| Query_time | 查询总耗时 | 平均值和最大值 |
| Lock_time | 锁等待时间 | 占比超过10%要警惕 |
| Rows_sent | 返回行数 | 跟Rows_examined对比 |
| Rows_examined | 扫描行数 | 越大说明索引效率越低 |
实战经验:我曾经遇到一个案例,有个查询Rows_examined是100万,但Rows_sent只有10条。这就是典型的索引没用好,扫描了大量不必要的数据。
4.3 常见慢SQL场景优化
好了,工具会用了,咱们来看看实际工作中最常见的几种慢SQL场景。说白了,大部分慢查询就那几类问题。
4.3.1 索引失效
这是最常见的问题。你明明建了索引,但MySQL就是不用。为什么会这样?
-- 场景1:对索引列使用函数
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
-- 优化:使用范围查询
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
-- 场景2:隐式类型转换
SELECT * FROM users WHERE phone = 13800138000; -- phone是varchar类型
-- 优化:保持类型一致
SELECT * FROM users WHERE phone = '13800138000';
-- 场景3:LIKE以通配符开头
SELECT * FROM products WHERE name LIKE '%手机%';
-- 优化:考虑全文索引
SELECT * FROM products WHERE MATCH(name) AGAINST('手机');
4.3.2 大表全表扫描
我记得有个项目,订单表有5000万数据。每次查历史订单都慢得要命。后来怎么解决的?
-- 问题SQL
SELECT * FROM orders WHERE status = 1 AND create_time > '2023-01-01';
-- 优化方案1:复合索引
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);
-- 优化方案2:分页查询(避免大偏移量)
-- 坏的写法
SELECT * FROM orders LIMIT 1000000, 20;
-- 好的写法
SELECT * FROM orders WHERE id > 1000000 LIMIT 20;
避坑指南:我曾经犯过一个错误,给大表加了太多索引。结果写入性能下降了30%。记住,索引不是越多越好,关键是要精准。
4.3.3 JOIN查询优化
多表关联查询,是慢查询的重灾区。我见过最夸张的一个SQL,关联了8张表,跑了30秒。
-- 问题场景:大表驱动小表
-- 坏的写法(用小表驱动大表)
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.status = 1;
-- 优化:确保被驱动表有索引
ALTER TABLE orders ADD INDEX idx_user_id (user_id);
-- 更好的写法:先过滤再关联
SELECT * FROM (
SELECT * FROM users WHERE status = 1
) u
JOIN orders o ON o.user_id = u.id;
4.3.4 ORDER BY和GROUP BY优化
排序和分组操作,如果处理不好,很容易产生临时表和文件排序。嗯,这里要特别注意。
-- 问题:没有利用索引排序
EXPLAIN SELECT * FROM products
WHERE category_id = 10
ORDER BY price DESC;
-- 优化:建立覆盖索引
ALTER TABLE products ADD INDEX idx_category_price (category_id, price DESC);
-- GROUP BY优化
-- 坏的写法
SELECT user_id, COUNT(*) FROM orders
GROUP BY user_id
HAVING COUNT(*) > 10;
-- 优化:先过滤再分组
SELECT user_id, cnt FROM (
SELECT user_id, COUNT(*) as cnt
FROM orders
GROUP BY user_id
) t WHERE cnt > 10;
核心原则:慢查询优化的本质,就是减少MySQL扫描的数据量。你想想看,如果每次查询只扫描100条数据,怎么可能慢呢?
4.3.5 子查询优化
MySQL对子查询的优化一直是个短板。我建议能用JOIN就用JOIN,别用子查询。
-- 坏的写法:子查询
SELECT * FROM products
WHERE id IN (
SELECT product_id FROM order_items
WHERE order_id IN (1,2,3)
);
-- 优化:改为JOIN
SELECT DISTINCT p.*
FROM products p
JOIN order_items oi ON p.id = oi.product_id
WHERE oi.order_id IN (1,2,3);
好了,慢查询优化这块就讲这么多。记住一个原则:先分析,后优化,再验证。别一上来就改SQL,先用pt-query-digest看看问题到底出在哪。我这些年踩过的坑,十有八九都是因为没做好分析就动手。