如何为PHP项目配置慢查询日志:从基础到高级的完整指南
目录导读
- 为什么需要慢查询日志?
- PHP项目慢查询的常见场景
- 配置MySQL慢查询日志(核心步骤)
- 在PHP项目层面捕获慢查询
- 使用框架特性启用慢查询日志(Laravel/Symfony/ThinkPHP)
- 日志分析与优化实战
- 常见问题与解决方案(QA)
- 总结与最佳实践
为什么需要慢查询日志?
慢查询日志是数据库性能优化的第一道防线,对于PHP项目(尤其是高并发、大数据量的应用),数据库查询往往是性能瓶颈的根源,据不完全统计,超过70%的PHP应用性能问题与数据库相关,慢查询日志能帮你:

- 精准定位:找出执行时间超过阈值的SQL语句
- 量化性能:记录每次查询的耗时、锁等待时间、扫描行数
- 避免资源耗尽:防止慢查询拖垮数据库连接池
- 支持监控系统:与Prometheus、Grafana等工具集成
问答环节
Q: 我的PHP项目只有几千用户,也需要配置慢查询日志吗?
A: 是的,即便是小项目,一条未优化的慢查询可能在流量高峰期导致数据库响应延迟,进而引发PHP-FPM进程堆积,最终让整站变慢,建议从项目初期就开启,作为“性能保险”。
PHP项目慢查询的常见场景
在配置日志前,先了解PHP项目中哪些操作容易产生慢查询:
- 嵌套循环中的SQL查询:例如在foreach中执行数据库查询(N+1问题)
- 未命中索引的JOIN操作:关联表时未建立合适的联合索引
- 大表全表扫描:如
SELECT * FROM logs WHERE status = 1,且status未加索引 - 不合理的ORDER BY/GROUP BY:对大量数据排序且未使用索引
- ORM懒加载导致的多次查询:例如Laravel Eloquent未使用
with()预加载
配置MySQL慢查询日志(核心步骤)
这是最基础也是最有效的方法,MySQL提供了原生慢查询日志机制,配置方式如下:
1 动态启用(无需重启MySQL)
-- 开启慢查询日志 SET GLOBAL slow_query_log = ON; -- 设置慢查询阈值(单位:秒) SET GLOBAL long_query_time = 1; -- 建议设置为0.5-2秒 -- 指定日志文件路径 SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log'; -- 记录未使用索引的查询(推荐开启) SET GLOBAL log_queries_not_using_indexes = ON;
2 永久配置(修改my.cnf)
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow-query.log long_query_time = 1 log_queries_not_using_indexes = 1 min_examined_row_limit = 1000 -- 只记录扫描行数>1000的查询
3 查看日志内容示例
# Time: 2025-03-15T10:23:45.123456Z
# User@Host: phpuser[phpuser] @ [192.168.1.100]
# Query_time: 2.345678 Lock_time: 0.001234 Rows_sent: 50 Rows_examined: 50000
SET timestamp=1742034225;
SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC LIMIT 20;
关键字段解析:
Query_time:实际查询耗时(秒),核心关注点Rows_examined:扫描的行数(决定索引效率)Rows_sent:返回的行数(两者差距大表明索引失效)
在PHP项目层面捕获慢查询
如果你无法直接操作MySQL配置(如共享主机环境),或者希望更细粒度地控制,可以借助PHP本身的机制。
1 使用PDO或MySQLi的自定义记录
<?php
class SlowQueryLogger
{
private $threshold = 1.0; // 1秒
public function execute($sql, $params = [])
{
$start = microtime(true);
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$duration = microtime(true) - $start;
if ($duration > $this->threshold) {
$this->logSlowQuery($sql, $duration, $params);
}
return $stmt;
}
private function logSlowQuery($sql, $duration, $params)
{
$log = sprintf(
"[%s] Slow Query: %.4f sec | SQL: %s | Params: %s\n",
date('Y-m-d H:i:s'),
$duration,
$sql,
json_encode($params)
);
file_put_contents('/path/to/slow-queries.log', $log, FILE_APPEND);
}
}
2 结合数据库抽象层(如Doctrine DBAL)
use Doctrine\DBAL\Logging\DebugStack;
$logger = new DebugStack();
$connection->getConfiguration()->setSQLLogger($logger);
// 执行查询后
foreach ($logger->queries as $query) {
if ($query['executionMS'] > 1.0) {
// 记录到日志
}
}
使用框架特性启用慢查询日志
1 Laravel(推荐方式)
在config/database.php中配置:
'mysql' => [
'driver' => 'mysql',
'read' => [
'host' => env('DB_HOST', '127.0.0.1'),
],
// ... 其他配置
'options' => [
PDO::ATTR_EMULATE_PREPARES => true,
],
// 增加慢查询日志配置(需要Laravel 8+)
'slowlog' => [
'enabled' => env('DB_SLOWLOG_ENABLED', false),
'threshold' => env('DB_SLOWLOG_THRESHOLD', 1000), // 毫秒
'log_file' => storage_path('logs/slow-queries.log'),
],
];
然后在AppServiceProvider中注册监听:
\DB::listen(function ($query) {
if ($query->time > 1000) { // 超过1秒
\Log::channel('slow')->warning('Slow Query', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time,
]);
}
});
2 Symfony(使用Doctrine)
在config/packages/doctrine.yaml中:
doctrine:
dbal:
logging: true
profiling_collect_backtrace: true
# 自定义慢查询监听服务
创建自定义监听器:
class SlowQueryListener
{
public function onQueryExecuted(QueryExecuted $event)
{
if ($event->time > 1000) {
// 记录慢查询
}
}
}
3 ThinkPHP 6.0+
在config/log.php中增加自定义通道:
'channels' => [
'slow_query' => [
'type' => 'file',
'path' => app()->getRootPath() . 'runtime/slow_query/',
'level' => 'debug',
],
],
在中间件中监听查询事件:
\think\facade\Db::listen(function ($event) {
if ($event['time'] > 1000) {
\think\facade\Log::channel('slow_query')->record([
'sql' => $event['sql'],
'time' => $event['time'],
]);
}
});
日志分析与优化实战
配置好日志后,如何从大量日志中找出关键问题?推荐以下流程:
1 使用mysqldumpslow工具(MySQL自带)
# 按平均查询时间排序,显示前10条 mysqldumpslow -t 10 /var/log/mysql/mysql-slow.log # 按查询次数排序,显示前5条(频繁出现的慢查询) mysqldumpslow -s c -t 5 /var/log/mysql/mysql-slow.log
2 使用pt-query-digest(Percona Toolkit)
# 安装 wget https://www.percona.com/downloads/percona-toolkit/LATEST/percona-toolkit-3.5.5.tar.gz # 分析 pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txt
3 常见优化策略(基于日志分析)
| 日志特征 | 可能原因 | 优化方案 |
|---|---|---|
Rows_examined远大于Rows_sent |
索引未命中 | 添加覆盖索引或复合索引 |
Lock_time很高(>1秒) |
死锁或行锁竞争 | 检查事务隔离级别,缩短事务跨度 |
出现大量ORDER BY且Using filesort |
排序字段无索引 | 添加(status, created_at)复合索引 |
GROUP BY后的Temporary table |
临时表空间不足 | 调整tmp_table_size或优化查询逻辑 |
常见问题与解决方案(QA)
Q1: 开启慢查询日志后,数据库性能反而下降了?
A: 日志写入本身会占用I/O,解决方案:
- 将日志写入独立的磁盘分区(如SSD)
- 设置
log_output = TABLE,将日志写入slow_log表(仅MySQL 5.6+) - 只在分析期开启,平时关闭(使用
SET GLOBAL slow_query_log = OFF)
Q2: 为什么我设置了long_query_time=1,但日志里有很多小于1秒的查询?
A: 检查是否开启了log_queries_not_using_indexes = ON,这个设置会额外记录所有未使用索引的查询,无论其耗时长短。
Q3: 我的PHP项目使用了Redis缓存,还需要配置慢查询日志吗?
A: 需要,缓存只能减少高频查询的数据库负载,但不能替代慢查询优化,未命中缓存的查询如果慢,依然会影响用户体验。
Q4: 如何在Docker容器中配置慢查询日志?
A: 将MySQL配置文件挂载到容器中:
# docker-compose.yml
mysql:
image: mysql:8.0
volumes:
- ./my.cnf:/etc/mysql/conf.d/slow.cnf
- ./logs:/var/log/mysql
environment:
- MYSQL_ROOT_PASSWORD=root
Q5: 慢查询日志文件越来越大,如何管理?
A: 使用logrotate自动轮转:
/var/log/mysql/mysql-slow.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 640 mysql mysql
postrotate
/etc/init.d/mysql reload > /dev/null
endscript
}
总结与最佳实践
配置PHP项目慢查询日志并不是一次性的工作,而应贯穿于整个开发与运维周期,以下是经过验证的最佳实践总结:
-
分层监控:同时启用MySQL原生日志和PHP应用层日志,互为补充,MySQL日志暴露硬件级性能指标,应用层日志便于关联业务上下文。
-
阈值动态调整:建议初始设置为
long_query_time=1,随着项目稳定再逐步降低到5甚至3秒,高并发场景下,0.5秒的查询足以阻塞大量请求。 -
日志自动分析:使用
pt-query-digest或elk(Elasticsearch + Logstash + Kibana)建立自动化分析流水线,邮件或钉钉告警。 -
开发阶段就集成:在本地开发环境也开启慢查询日志(阈值设300ms),将性能问题消灭在代码合并前。
-
关联应用上下文:应用层日志中记录
$_SERVER['REQUEST_URI']、用户ID等信息,方便快速定位是哪个页面触发了慢查询。
最后记住:慢查询日志是诊断工具,而非解决方案,日志只是告诉你“哪里慢”,真正的优化需要结合EXPLAIN分析执行计划、优化索引、重构查询逻辑甚至引入读写分离架构,将慢查询日志作为持续性能监控的起点,才能让你的PHP项目在流量冲击下依然保持稳定响应。