DB::listen() 是唯一可靠的全局SQL监听方式,应注册在AppServiceProvider::boot()中,队列和命令场景需单独注册,参数还原需安全拼接,日志须按耗时/错误/频率分级过滤。

DB::listen() 是唯一靠谱的全局监听方式,DB::enableQueryLog() 只适合单次请求内手动调试,生产环境别碰它——开就卡,查还常为空。
DB::listen() 必须在哪儿注册才不漏查
注册太晚会漏掉服务提供者初始化阶段的查询(比如某些包在 boot() 里就查配置表),注册太早又可能因连接未就绪而失效。
- 最稳妥的位置是
AppServiceProvider::boot()—— 够早、够稳、大多数项目都适用 - 如果用了 Horizon 或自定义队列 worker,必须在
QueueServiceProvider::boot()或 worker 启动脚本里再调一次DB::listen(),队列进程不共享 Web 请求的监听器 - Tinker 或 Artisan 命令中想监听?得自己敲一遍
DB::listen(),它不会继承任何配置 - 重复注册会导致同一条 SQL 被记录多次,检查
config/app.php和所有服务提供者里有没有重复绑定
怎么拿到带参数的真实 SQL,而不是一堆 ?
$query->sql 是带 ? 占位符的模板,$query->bindings 是参数数组,两者不拼起来等于白看。别用 vsprintf() 或 str_replace() —— 遇到 null、false、含单引号的字符串或时间对象会崩。
- 字符串值要加单引号,并转义内部单引号:
''.str_replace("'", "''", $binding).'' -
bool转成1或0,null直接写NULL - 数字、浮点数直接拼入,不加引号
- Carbon 实例通常已转为字符串,按字符串处理即可
- 大文本、JSON、BLOB 类型建议截断(如
mb_strimwidth($value, 0, 200)),否则日志文件会膨胀失控
为什么 DB::enableQueryLog() 总返回空数组
它不是“开启日志开关”,而是给当前数据库连接实例挂一个内存里的查询列表。问题出在三处:
- 必须在查询发生前调用,比如控制器方法开头;
AppServiceProvider::boot()里调用完全无效——那时还没进请求生命周期,连接对象早被丢弃了 - 只记录当前连接(如
mysql),用了读写分离时,DB::connection('read')的查询不会出现在DB::getQueryLog()里 - 队列任务、Artisan 命令、Tinker 会话各自有独立连接,
getQueryLog()拿不到其他上下文的记录 - 性能敏感:每次查询都要追加到内存数组,高并发下可能 OOM,仅限单次请求调试
生产环境怎么避免日志爆炸
$query->time 单位是毫秒,这是分级过滤的关键依据。全量记录很快会让磁盘撑爆,必须加条件。
- 慢查询:只记
$query->time > 500的语句,附上当前路由、用户 ID(如有) - 错误查询:监听
Illuminate\Database\Events\QueryExecuted本身不区分成败,需结合异常捕获或自定义包装 - 高频查询:对相同
$query->sql做简单哈希去重,或统计单位时间调用次数 - 别用
file_put_contents()直写文件——高并发下有锁竞争风险,改用Log::channel('slow_sql')->debug()并配独立日志驱动


















