本文目录导读:

针对PHP项目的慢查询SQL定期分析与优化,可以按以下步骤建立一套完整的监控-分析-优化-复检闭环流程:
基础监控:开启慢查询日志
这是最根本的数据来源,需要在MySQL中开启慢查询日志。
-- 查看当前状态 SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time'; -- 开启慢查询(生产环境建议动态开启,不要重启) SET GLOBAL slow_query_log = ON; SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log'; SET GLOBAL long_query_time = 1; -- 超过1秒的记录,单位秒 SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未用索引的查询
注意事项:
long_query_time设为1秒或5秒,不要设为0(全量记录会打爆磁盘)。- 生产环境建议使用 PT-Query-Digest 或 mysqlsla 等工具进行日志聚合,避免手动阅读大量日志。
定期收集与聚合(每周/每日)
推荐工具:pt-query-digest(Percona Toolkit)
# 分析昨天的慢查询日志 pt-query-digest /var/log/mysql/mysql-slow.log > /tmp/slow_report_$(date +%Y%m%d).txt # 或直接分析最近1小时的日志 pt-query-digest --since=1h /var/log/mysql/mysql-slow.log
报告会提供:
- 总执行时间、总次数、平均时间
- 最慢的N条SQL(按总时间倒序)
- 每一条SQL的执行频率、响应时间分布、锁时间、返回行数
人工分析(关键步骤)
拿到报告后,重点看 Rank 1-10 的SQL,从以下5个维度分析:
索引问题(占80%以上慢查询)
- 全表扫描:
EXPLAIN看type = ALL - 没用到索引:
possible_keys不为NULL但key为NULL - 索引选择性差:
rows扫描行数很大,但Extra显示Using index condition可能是索引不够精准
分析命令:
EXPLAIN SELECT * FROM orders WHERE status = 1 AND create_time > '2024-01-01';
数据量问题
- 单表数据量 > 500万行且全表扫描
- 分页查询:
LIMIT 100000,20这种深度分页
锁与死锁
EXPLAIN的Extra出现Using temporary; Using filesort表示需要优化排序- 观察慢查询日志中的
Lock_time是否过高(排除锁等待)
SQL写法问题
- 隐式类型转换:
WHERE phone = 123456(phone是varchar) - 函数导致索引失效:
WHERE DATE(create_time) = '2024-01-01'应改为WHERE create_time >= ... AND create_time < ... - **SELECT ***:经常只需要3个字段却全字段查询
业务逻辑问题
- N+1查询:循环中查数据库,常见于ORM(如Laravel/ThinkPHP的延迟加载)
- 非必要的大查询:后台报表需要跑5分钟,是否可以改成异步或缓存?
优化方案落地
针对分析结果,按优先级实施:
| 问题类型 | 解决方案 |
|---|---|
| 缺少索引 | 添加复合索引,注意字段顺序(等值条件放前面,范围条件放后面) |
| 索引失效 | 改写SQL(避免函数、隐式转换、LIKE左模糊等) |
| 深度分页 | 使用游标分页(WHERE id > last_id LIMIT 20)或覆盖索引 |
| 数据量过大 | 分库分表(按时间/用户ID拆分)、归档历史数据 |
| 锁竞争 | 缩小事务范围、调整隔离级别、使用乐观锁 |
| 查询频率高 | 加Redis缓存(缓存热点数据,注意缓存穿透/雪崩) |
具体例子:
-- 原慢SQL(status、create_time各自独立索引) SELECT * FROM orders WHERE status = 1 AND create_time > '2024-06-01' ORDER BY id DESC LIMIT 100; -- 优化后:添加复合索引 (status, create_time, id),且去掉不必要的SELECT * SELECT id, order_no, amount FROM orders WHERE status = 1 AND create_time > '2024-06-01' ORDER BY id DESC LIMIT 100;
自动化定期执行
方案1:Crontab + Shell脚本(每周一凌晨执行)
# /usr/local/bin/slow_query_analyze.sh #!/bin/bash LOGFILE="/var/log/mysql/mysql-slow.log" REPORT_DIR="/data/slow_report" DATE=$(date +%Y%m%d) # 1. 生成报告 pt-query-digest $LOGFILE > $REPORT_DIR/report_$DATE.txt # 2. 每日清理旧日志(重要!防止日志无限增长) mv $LOGFILE $LOGFILE.$DATE mysqladmin flush-logs # 3. 保留最近30天报告 find $REPORT_DIR -name "*.txt" -mtime +30 -delete
# crontab -e 0 3 * * 1 /bin/bash /usr/local/bin/slow_query_analyze.sh
方案2:接入APM工具(更适合大团队)
- 阿里云RDS慢查询分析(自带)
- Datadog / NewRelic:自动抓取慢查询,甚至给出优化建议
- Yearning / Archery:开源的SQL审核平台,可对接慢查询日志并生成工单
优化效果验证(闭环)
每次优化后必须验证:
- 再次执行
EXPLAIN确认type从ALL变成ref或range,rows大幅下降 - 上线后观察该SQL的平均响应时间(建议用SkyWalking或Xhprof)
- 下一周的慢查询报告中,该SQL是否跌出Rank前10
- 压力测试:用JMeter/Siege模拟高并发,观察QPS是否提升
补充:PHP层面的常见坑
-
ORM框架不当使用
- Laravel:
Model::all()->chunk()→ 应改为chunkById()或原生SQL - ThinkPHP:
db('user')->where(...)->select()在数据量 > 5000条时务必加limit
- Laravel:
-
长连接/连接池:PHP-FPM短连接天生慢,建议使用 Swoole 或连接池(如
Hyperf) -
索引未生效:PHP代码中拼SQL时,参数类型要与数据库字段类型一致(
$userId是int,SQL中不要加引号) -
未关闭查询:特别是
PDOStatement::closeCursor()未调用,可能导致锁长时间未释放
建设制度化流程
| 频率 | 动作 |
|---|---|
| 每日 | 查看慢查询日志中是否有新增的极端慢SQL(>10秒) |
| 每周 | pt-query-digest 生成周报,开发组Review Top10 |
| 每次上线 | 对新增SQL进行 EXPLAIN 强制审核 |
| 每月 | 清理历史慢查询日志,复盘未优化的瓶颈 |
这套体系不需要昂贵的软件,用好MySQL自带工具 + PT工具 + 定期人工审核,就能覆盖90%的慢查询问题,如果团队人力充足,建议引入SQL审核平台(如Yearning)实现流程自动化。