0
user-people-family-house-home
>

【Laravel】N+1 問題:偵測與解決完整指南

本篇介紹 Laravel 中最常見的效能問題 N+1 Query,如何偵測它的存在,以及各種解決方式。什麼是 N+1 問...

Posted by Roy on 2026-02-26 18:00:12
1 目前 1 人正在閱讀
|
| Views

本篇介紹 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

留言區

請先登入才能發表留言