db::listen() 是慢查询监控的起点,因其在查询执行后立即触发回调,轻量可控,适合实时判断和采样,但需加耗时阈值过滤、避免i/o、合理使用闭包变量,并区分环境启用。

为什么 think\facade\Db::listen() 是慢查询监控的起点
ThinkPHP 自带的 SQL 监听机制不是装饰品,而是最轻量、最可控的切入点。它不依赖日志轮转或外部中间件,直接在查询执行后触发回调,适合做实时判断和采样。
常见错误是把它当成日志开关——只写 Db::listen(function(){}) 却不加条件过滤,结果线上全量记录,磁盘爆满、性能反降。
- 只对耗时超过阈值的查询做处理:
if ($time > 500)(单位毫秒,按业务调低) - 避免在监听回调里做 I/O 操作(如写文件、发 HTTP 请求),改用队列或内存暂存
- 注意闭包内无法直接访问
$this,需显式 use 变量,比如use ($app) - 开发环境可开启全量监听,生产环境必须加条件,否则
Db::listen()本身会成为性能瓶颈
如何让慢 SQL 自动落库而不是堆在日志里
把慢查询写进数据库表,才能做聚合分析、看趋势、设告警。但 ThinkPHP 默认不提供现成模型,得自己建表 + 封装写入逻辑。
典型坑是字段设计不合理:比如用 TEXT 存 sql 却没索引,查某条慢 SQL 时全表扫描;或者用 DATETIME 存时间却忽略时区,导致凌晨的慢查显示成前一天。
- 推荐表结构至少含:
id、sql(MEDIUMTEXT)、time(DECIMAL(10,3))、run_time(DATETIME(3),用date('Y-m-d H:i:s.u')截取前23位)、trace(TEXT,简化后的调用栈) - 写入用
Db::table('slow_sql_log')->insertGetId(...),别用模型 save(),避免触发额外钩子拖慢监听流程 - 不要每条都 insert,可先用
array_push()缓存,每 5 条或 1 秒 flush 一次(配合register_shutdown_function补漏)
think\db\Connection::getRealSql() 为什么比 $query->getRawSql() 更可靠
你在监听回调里拿到的 $sql 参数,常是带问号占位符的预处理模板,比如 SELECT * FROM user WHERE id = ?。真要分析慢因,得看到实际执行的值。
很多人直接拼接参数,结果遇上字符串里的单引号、JSON 字段、二进制内容就崩了。这不是 bug,是手动拼接本就不该干这事。
-
getRealSql()是连接实例方法,需从监听回调的$connection参数获取,再传入$sql和$bind:$realSql = $connection->getRealSql($sql, $bind) -
$query->getRawSql()在部分版本里返回空,尤其用了with()或子查询时,稳定性差 - 注意
getRealSql()对大字段(如longtext)可能截断,若需完整值,应单独记录$bind数组并脱敏后存params字段
监控数据多了之后,怎么避免 slow_sql_log 表变“死表”
跑一周后发现查一条慢查要 8 秒?不是 SQL 写得差,是表没维护。没索引、没分区、没归档,监控系统自己先拖垮数据库。
最常被忽略的是时间字段没索引。你查“今天慢查”,SQL 是 WHERE run_time >= '2024-06-01',没索引就是全表扫。
- 必建联合索引:
INDEX idx_time_sql (run_time, time),覆盖最常用的时间范围 + 耗时排序场景 - 每月自动归档:用
Db::execute("RENAME TABLE slow_sql_log TO slow_sql_log_202405")配合定时任务,别用 DELETE,锁表太久 - 如果用 MySQL 8.0+,可考虑按
run_time做 RANGE 分区,但上线前务必压测,分区管理不当反而更慢
真正难的不是记下哪条 SQL 慢,而是让这些记录一直能查得动、看得清。表结构定型、索引跟上、归档机制跑起来,这三步漏一步,监控就从工具变成累赘。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











