在排查接口慢查询、定位 N+1 问题或做数据库审计时,能够拿到 Laravel 实际发送给数据库的 SQL 语句非常关键。Laravel 的查询构建器和 Eloquent 最终都会经由 Illuminate\Database\Connection 执行 SQL,并在执行完成时派发 QueryExecuted 事件。开发者可以在 AppServiceProvider 中通过 DB::listen 注册一个全局监听器,捕获每一条 SQL 的模板、绑定参数和执行时间。这种机制既适合本地调试,也可以在经过裁剪后用于生产环境的慢查询监控。

使用 DB::listen 注册全局 SQL 监听器
Laravel 的 DB 门面提供了 listen 方法,接收一个闭包,闭包参数是 Illuminate\Database\Events\QueryExecuted 实例。该实例包含 sql、bindings、time 和 connectionName 属性,分别表示预编译 SQL 模板、绑定值数组、执行耗时毫秒数以及当前连接名称。通常会把监听逻辑放在 AppServiceProvider 的 boot 方法中,因为服务提供者启动时数据库组件已经可用,而且能覆盖应用生命周期内的所有数据库操作。
下面这段代码在应用启动时注册全局监听器,并把每一条 SQL 的原始模板与绑定参数写入 Laravel 日志。注意 bindings 数组里的值可能包含敏感字段,比如用户手机号、密码哈希或 token,开发环境可以接受,但生产环境需要做脱敏处理,后面会单独说明。
<?php
namespace App\Providers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Events\QueryExecuted;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
DB::listen(function (QueryExecuted $query) {
Log::debug('SQL 执行记录', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time . 'ms',
'connection' => $query->connectionName,
]);
});
}
}
注册完成后,每次执行查询都会触发闭包。比如调用 User::where('id', 1)->first() 时,日志中会出现 select * from users where id = ?,绑定参数显示为 [1]。这个日志粒度已经能帮助开发者确认 ORM 是否生成了预期的查询结构,也便于发现循环中反复查询导致的性能问题。
替换问号占位符生成可执行 SQL
DB::listen 拿到的 sql 属性是带问号占位符的预处理语句,不能直接复制到数据库客户端执行。为了调试方便,可以写一个辅助函数,按顺序把 bindings 中的值替换到问号位置。替换时要注意类型:整型、浮点型直接转字符串,字符串需要用单引号包裹并处理转义,null 要转换成 NULL,避免拼出来的 SQL 语法错误。
下面的 formatSql 函数实现了基础的绑定参数替换逻辑。它只用于日志输出,不建议把返回值直接交给数据库执行,因为手动拼接 SQL 存在注入风险,而且 Laravel 已经通过 PDO 预处理机制保证了安全。调试场景中复制到客户端执行是可以的,但不要在生产代码中依赖拼接结果作为查询入口。
function formatSql(string $sql, array $bindings): string
{
foreach ($bindings as $binding) {
if ($binding === null) {
$value = 'NULL';
} elseif (is_int($binding) || is_float($binding)) {
$value = (string) $binding;
} else {
$value = "'" . addslashes((string) $binding) . "'";
}
$sql = preg_replace('/\?/', $value, $sql, 1);
}
return $sql;
}
DB::listen(function ($query) {
$fullSql = formatSql($query->sql, $query->bindings);
Log::debug('完整 SQL:' . $fullSql);
});
这个函数每次只替换一个问号,避免把同一个绑定值错误地填入所有占位符。对于日期对象或二进制数据,addslashes 处理不一定完善,可以根据业务需要扩展 Carbon 实例转换、JSON 字符串保留等逻辑。真正需要可读性好、可执行的 SQL 时,建议优先使用开发调试工具,因为它们已经处理了大量边界情况。
使用查询日志与开发期调试工具
除了全局 DB::listen,Laravel 还提供 DB::enableQueryLog 和 DB::getQueryLog 方法。它们适合在某个局部代码块中临时开启查询记录,执行完目标逻辑后立即取出日志并关闭,避免整站长期记录带来的内存占用。与 DB::listen 不同,查询日志返回的也是包含 query、bindings、time 的数组,但不会实时触发回调,需要手动取出。
DB::enableQueryLog();
$users = User::where('status', 1)->orderBy('created_at', 'desc')->take(10)->get();
$queries = DB::getQueryLog();
foreach ($queries as $query) {
echo formatSql($query['query'], $query['bindings']) . PHP_EOL;
}
DB::disableQueryLog();
在本地开发阶段,更推荐使用 Laravel Debugbar 或 Laravel Telescope。Debugbar 会在页面底部展示当前请求执行的所有 SQL、耗时、调用来源,并自动把问号替换为实际参数,省去手动拼 SQL 的步骤。Telescope 则把 SQL 记录进独立的调试面板,支持按请求、按慢查询浏览,还可以查看某条 SQL 对应的调用栈。对团队协作排查问题非常有帮助,但要注意这些工具通常会记录绑定参数,不应默认部署到生产环境。
如果只想在命令行或测试环境中快速查看 SQL,也可以直接监听到 php://stdout 或者使用 dump 输出。关键在于根据场景选择监听方式:需要实时处理时用 DB::listen,需要局部快照时用 enableQueryLog,需要可视化和历史追溯时用 Debugbar、Telescope。
生产环境的慢查询监控与脱敏
把 DB::listen 直接搬到生产环境会带来两个问题:一是每条 SQL 都写日志会造成大量磁盘 IO 和存储成本;二是绑定参数可能包含手机号、身份证、密码哈希等敏感数据,明文写入日志不符合安全规范。因此生产监控应当设定筛选条件,比如只记录执行耗时超过 200 毫秒的 SQL,或者只记录写操作,并同步对绑定参数做脱敏。
下面的监听器只针对慢查询输出告警日志,并且用 maskBindings 函数把非数字类型的绑定值统一替换为星号,避免敏感信息进入日志。实际项目中还可以把告警发送到监控平台,或者配合 Laravel 的日志通道单独写入 slow_sql.log,方便后续分析。
function maskBindings(array $bindings): array
{
return array_map(function ($value) {
if (is_int($value) || is_float($value) || $value === null) {
return $value;
}
return '***';
}, $bindings);
}
DB::listen(function ($query) {
if ($query->time > 200) {
Log::warning('慢查询告警', [
'sql' => $query->sql,
'bindings' => maskBindings($query->bindings),
'time' => $query->time . 'ms',
'connection' => $query->connectionName,
]);
}
});
阈值需要根据业务接口的响应时间目标来设定,例如 200 毫秒适合大多数 Web 请求,异步任务或报表服务可以放宽到 1000 毫秒。超过阈值的 SQL 日志应保留足够上下文,包括链接、绑定的脱敏值、耗时和发生时间,便于后续定位是缺少索引、数据量增长还是 ORM 查询结构不合理。即使只记录慢查询,也建议配置日志轮转和保留周期,防止长期运行后磁盘被占满。
综合来看,Laravel 获取执行 SQL 的关键手段是 DB::listen 和 QueryExecuted 事件,它们提供了原始模板、绑定参数和执行时间。开发环境可以追求完整和可读性,生产环境则要聚焦慢查询、做好脱敏和成本控制。把这两套策略结合起来,就能在不影响线上稳定性的前提下,持续观察应用的真实数据库行为。