本篇介紹 Laravel 中最常見的效能問題 N+1 Query,如何偵測它的存在,以及各種解決方式。
什麼是 N+1 問題?
N+1 是 ORM 使用者最容易踩到的效能陷阱。以查詢文章與作者為例:
// 看起來只有一行,但實際上觸發了 N+1 筆 SQL $posts = Post::all(); foreach ($posts as $post) { echo $post->user->name; // 每次都對 users 表發出一次查詢 }
當 posts 表有 100 筆資料,以上程式碼會產生:
- 1 筆:
SELECT * FROM posts - 100 筆:
SELECT * FROM users WHERE id = ?(每筆 post 各查一次)
合計 101 筆查詢,資料量越大問題越嚴重。
如何偵測 N+1
方法 1:DB::listen() 印出所有查詢
// AppServiceProvider::boot() 中加入 DB::listen(function ($query) { logger()->info($query->sql, $query->bindings); }); // 或直接印出 DB::enableQueryLog(); $posts = Post::all(); foreach ($posts as $post) { echo $post->user->name; } dd(DB::getQueryLog()); // 查看所有執行過的 SQL
方法 2:Model::preventLazyLoading()(Laravel 8.43+)
在開發環境直接讓 Lazy Loading 拋出例外,強制你一定要處理 N+1:
// AppServiceProvider::boot() use Illuminate\Database\Eloquent\Model; public function boot(): void { Model::preventLazyLoading(! app()->isProduction()); } // 觸發 Lazy Loading 時會直接拋出: // Illuminate\Database\LazyLoadingViolationException
方法 3:安裝 Laravel Debugbar
composer require barryvdh/laravel-debugbar --dev
安裝後在開發環境頁面底部會顯示 Queries 分頁,一眼看出重複的 SQL 查詢。
解決方式
基本:with() Eager Loading
// N+1(101 筆 SQL) $posts = Post::all(); // Eager Loading(2 筆 SQL) $posts = Post::with('user')->get(); // 巢狀關聯一起載入 $posts = Post::with('user', 'user.profile', 'comments')->get();
withCount():計算關聯數量
// N+1(每篇文章各查一次留言數) $posts = Post::all(); foreach ($posts as $post) { echo $post->comments->count(); } // withCount(只多 1 筆 SQL) $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count; }
條件式 Eager Loading
// 只載入已審核通過的留言
$posts = Post::with([
'comments' => fn ($query) => $query->where('status', 'approved'),
])->get();
load():已查出資料後補充載入
// 已取得 $posts,需要在某個條件下才載入關聯
$posts = Post::all();
if ($needComments) {
$posts->load('comments');
}
大量資料:chunk() 搭配 Eager Loading
// 一次載入 100 筆並處理,避免記憶體爆炸
Post::with('user')->chunk(100, function ($posts) {
foreach ($posts as $post) {
echo $post->user->name;
}
});
查詢次數對比
| 方式 | 100 筆 Posts 的查詢次數 |
|---|---|
| Lazy Loading(N+1) | 101 次 |
with('user') |
2 次 |
with('user', 'comments') |
3 次 |
withCount('comments') |
1 次(子查詢合併) |
小結
N+1 是效能問題的高頻原因,但也是最容易解決的問題。開發階段建議開啟 preventLazyLoading(),讓框架幫你強制把 N+1 抓出來;生產環境搭配 Laravel Telescope 持續監控查詢數量,養成使用 with() 的習慣是最根本的解法。
參考文獻:
https://laravel.com/docs/11.x/eloquent-relationships#eager-loading