Laravel 无内置慢查询日志,需通过 MySQL 的 slow_query_log(SET GLOBAL slow_query_log = ON; long_query_time = 0.5)或 DB::listen()、Telescope 配置 'slow' => 50 毫秒来捕获;N+1 表现为毫秒级密集相似单条查询,修复后应验证总查询数下降及出现 JOIN 或 IN 批量查询。
怎么打开 Laravel 的慢查询日志功能
laravel 本身不内置“慢查询日志表”,得靠数据库层或手动埋点。mysql 的 slow_query_log 是最直接的入口,但默认关闭,且需要权限配置。
开启前先确认你有 SUPER 权限(SHOW VARIABLES LIKE 'slow_query_log';),然后执行:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 0.5;
注意:long_query_time 单位是秒,设为 0.5 比设 1 更容易捕获 N+1 引发的累积延迟;但别设 0,否则日志爆炸。
- 日志路径由
slow_query_log_file决定,常见是/var/lib/mysql/xxx-slow.log,需确保 MySQL 进程有写权限 - Laravel 的
DB::listen()可以临时记录查询耗时,但它只在当前请求生命周期有效,无法替代持久化日志 - 如果用的是 RDS 或云数据库,可能要进控制台开启慢日志,且下载方式不同(比如阿里云叫“SQL 审计日志”)
从慢日志里识别 N+1 查询的典型模式
N+1 本身不会单独出现在慢日志里,但会表现为一批高度相似、仅 WHERE 条件 ID 不同的 SELECT 语句,在几秒内密集出现。
例如日志里连续看到:
# Time: 2024-06-12T09:23:41.123456Z # User@Host: app[app] @ localhost [] Id: 12 # Query_time: 0.042187 Lock_time: 0.000023 Rows_sent: 1 Rows_examined: 1 SELECT * FROM `posts` WHERE `id` = 123; <h1>Time: 2024-06-12T09:23:41.124567Z</h1><h1>User@Host: app[app] @ localhost [] Id: 13</h1><h1>Query_time: 0.041902 Lock_time: 0.000021 Rows_sent: 1 Rows_examined: 1</h1><p>SELECT * FROM <code>posts</code> WHERE <code>id</code> = 124;
这种就是典型信号:同一页面加载中,对同一个表反复查单条记录。关键是看时间戳间隔是否紧凑(毫秒级)、Rows_examined 是否都很小、但总耗时加起来很高。
- 别只盯
Query_time > 1s的语句——N+1 的单次查询往往很快,真正问题是“量大+无索引+网络往返”叠加 - 配合 Laravel 日志里的
DB::enableQueryLog()快照,能定位到具体哪个foreach循环触发了这批查询 - 如果日志里出现大量
SELECT ... WHERE id IN (?,?,?),反而是用了with()预加载,大概率不是 N+1
用 Laravel Telescope 或 DB::listen() 快速验证怀疑点
Telescope 是目前最省事的本地排查工具,但它默认不记录查询耗时阈值,得手动配。
在 config/telescope.php 里改这一项:
'watchers' => [
Watchers\QueryWatcher::class => [
'enabled' => true,
'slow' => 50, // 单位毫秒,设低一点才能抓到 N+1 的单次查询
],
],然后访问可疑页面,进 /telescope 看 Queries 标签页,按耗时排序,找重复结构的 SELECT。
-
DB::listen()更轻量,适合临时加在某个 Controller 方法开头: DB::listen(fn ($event) => logger('SQL:', ['sql' => $event->sql, 'time' => $event->time]));- 注意:监听器会显著拖慢响应,上线环境禁用;且它不记录绑定参数,看到的
?需要结合上下文猜 - 如果用 Horizon 或队列任务,Telescope 默认不记录,得显式调用
Telescope::startRecording()
修复后怎么确认 N+1 真没了
别只看页面快了,得验证 SQL 数量是否下降、是否真的用了 JOIN 或 IN 批量查。
最稳的方式是复现操作,再翻一次 Telescope 或慢日志,对比两个关键指标:
- 同一请求的总查询数:从 103 条降到 5 条,基本过关
- 是否有
SELECT ... FROM users LEFT JOIN posts ON ...或SELECT ... WHERE id IN (1,2,3,...)出现 - 检查 Eloquent 关系是否用了
with('relation'),而不是在循环里写$user->posts - 留意隐式 N+1:比如
$users->load('posts')放错位置,或在 API 资源类(Resource)里访问未预加载的关系
有个容易被忽略的点:软删除模型 + withTrashed() 会影响预加载行为,有时导致预加载失效,又掉回 N+1。查 toSql() 输出最准。


















