如何为PHP项目配置慢查询日志?

wen PHP项目 2

如何为PHP项目配置慢查询日志:从基础到高级的完整指南

目录导读

  1. 为什么需要慢查询日志?
  2. PHP项目慢查询的常见场景
  3. 配置MySQL慢查询日志(核心步骤)
  4. 在PHP项目层面捕获慢查询
  5. 使用框架特性启用慢查询日志(Laravel/Symfony/ThinkPHP)
  6. 日志分析与优化实战
  7. 常见问题与解决方案(QA)
  8. 总结与最佳实践

为什么需要慢查询日志?

慢查询日志是数据库性能优化的第一道防线,对于PHP项目(尤其是高并发、大数据量的应用),数据库查询往往是性能瓶颈的根源,据不完全统计,超过70%的PHP应用性能问题与数据库相关,慢查询日志能帮你:

如何为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 BYUsing 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项目慢查询日志并不是一次性的工作,而应贯穿于整个开发与运维周期,以下是经过验证的最佳实践总结:

  1. 分层监控:同时启用MySQL原生日志和PHP应用层日志,互为补充,MySQL日志暴露硬件级性能指标,应用层日志便于关联业务上下文。

  2. 阈值动态调整:建议初始设置为long_query_time=1,随着项目稳定再逐步降低到5甚至3秒,高并发场景下,0.5秒的查询足以阻塞大量请求。

  3. 日志自动分析:使用pt-query-digest或elk(Elasticsearch + Logstash + Kibana)建立自动化分析流水线,邮件或钉钉告警。

  4. 开发阶段就集成:在本地开发环境也开启慢查询日志(阈值设300ms),将性能问题消灭在代码合并前。

  5. 关联应用上下文:应用层日志中记录$_SERVER['REQUEST_URI']、用户ID等信息,方便快速定位是哪个页面触发了慢查询。

最后记住:慢查询日志是诊断工具,而非解决方案,日志只是告诉你“哪里慢”,真正的优化需要结合EXPLAIN分析执行计划、优化索引、重构查询逻辑甚至引入读写分离架构,将慢查询日志作为持续性能监控的起点,才能让你的PHP项目在流量冲击下依然保持稳定响应。

抱歉,评论功能暂时关闭!