在 Laravel 中排查 N+1 问题并消除无效查询
性能优化工作通常是枯燥的。
它并不总是关于那种将延迟降低 80% 的英雄事迹。大多数时候,它关乎清理。你会发现一些你甚至不知道正在运行的查询,然后将其删除。
如果你的 Laravel 应用正在增长,你很可能正面临以下三个问题。
- 隐藏的 N+1 查询
看不见就无法修复。在本地环境中使用像 beyondcode/laravel-query-detector 这样的工具。它会在你使用“循环+延迟加载”模式时提醒你。
不要将其运行在生产环境中,因为它会增加额外开销。仅在应用处于 local 或 testing 模式时才注册它。
- 预加载(Eager Loading)造成的浪费
人们通常通过添加 with() 来修复 N+1 问题。但这会产生一个新问题:你可能会预加载一个视图不再使用的关联关系。
如果重新设计移除了表中的某个列,你必须移除控制器中相应的 with() 调用。你正在为丢弃的数据支付代价。
将每一次预加载视为一种“声明”。如果你不使用这些数据,就撤销该声明。
- 重复计算相同的数据
某些值在整个请求过程中保持不变。如果你多次计算它们,就会浪费 CPU。使用记忆化(memoization)来解决这个问题。
示例:
protected ?string $defaultConnection = null;
public function getDefaultConnectionName(): string
{
return $this->defaultConnection ??= $this->resolveDefaultConnection();
}
??= 运算符会计算一次该值,并在请求的剩余部分中重复使用它。
- 仪表盘查询陷阱
仪表盘通常会为每一个卡片运行一个独立的 count() 查询。如果你有六个卡片,就会运行六次查询。
改为执行以下两件事:
- 聚合你的计数。通过单个查询获取一张表的所有汇总数据。
- 使用短时间的缓存。仪表盘统计数据不需要精确到秒。将它们缓存五分钟。
一个访客承担查询成本,其他所有人都能获得缓存结果。
最终建议:防止回归
如果开发人员随后重新添加了错误的代码,清理工作就会失败。使用 Pest 来断言你的查询数量。为页面可以运行的查询数量设置一个上限。如果开发人员添加了 N+1,测试将会失败。
优化在很大程度上在于移除。移除未使用的加载。移除重复计算的值。移除不必要的查询。
