Lỗi N+1 query trong Laravel Eloquent và cách khắc phục

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Trang danh sách chạy mượt trên máy bạn nhưng lên production thì chậm như rùa? Rất có thể bạn đang dính lỗi N+1 query, một cái bẫy kinh điển khi dùng ORM. Bài này giải thích nó là gì và cách xử lý trong Laravel Eloquent.

N+1 query là gì?

Giả sử bạn lấy 50 bài viết rồi in tên tác giả của từng bài. Eloquent mặc định dùng lazy loading: mỗi lần bạn truy cập quan hệ $post->author lần đầu, nó sẽ chạy thêm một câu SQL. Kết quả là 1 câu lấy danh sách bài viết cộng thêm 50 câu lấy tác giả, tổng cộng 51 câu thay vì 2 câu.

$posts = Post::all();foreach ($posts as $post) {    echo $post->author->name; // mỗi vòng lặp chạy thêm 1 query}

Với 50 bản ghi bạn khó nhận ra, nhưng với vài nghìn bản ghi và nhiều người dùng cùng lúc, cơ sở dữ liệu sẽ bị quá tải bởi hàng loạt truy vấn nhỏ lặp đi lặp lại. Mỗi truy vấn đều tốn một vòng đi về qua mạng, nên tổng thời gian tăng rất nhanh.

Cách sửa bằng eager loading

Giải pháp là nạp trước quan hệ bằng with(). Laravel sẽ chạy đúng hai câu: một cho bài viết, một cho tất cả tác giả liên quan bằng mệnh đề WHERE id IN (...).

$posts = Post::with('author')->get();foreach ($posts as $post) {    echo $post->author->name; // không phát sinh query mới}

Bạn có thể nạp nhiều quan hệ hoặc quan hệ lồng nhau:

Post::with(['author', 'comments.user'])->get();

Nếu chỉ cần vài cột, hãy giới hạn để tiết kiệm bộ nhớ: with('author:id,name'). Lưu ý phải luôn có cột khóa (ở đây là id) để Laravel ghép dữ liệu được.

Phát hiện N+1 trước khi lên production

Đừng đợi người dùng phàn nàn. Laravel có sẵn cách chặn lazy loading trong môi trường phát triển. Thêm đoạn sau vào AppServiceProvider:

use Illuminate\Database\Eloquent\Model;public function boot(): void{    Model::preventLazyLoading(! app()->isProduction());}

Khi code lỡ lazy load một quan hệ, ứng dụng sẽ ném ngoại lệ ngay lúc bạn đang code, và bạn sửa được liền. Ngoài ra, các công cụ như Laravel Debugbar hay Telescope hiển thị số lượng query mỗi request, giúp bạn nhìn ra bất thường ngay.

Một số tình huống hay gặp

  • Đếm quan hệ: thay vì gọi $post->comments->count() trong vòng lặp, hãy dùng withCount('comments') rồi đọc comments_count.
  • Nạp có điều kiện: dùng with(['comments' => fn ($q) => $q->where('approved', true)]) để chỉ lấy bình luận đã duyệt.
  • Đã lấy model rồi mới nhớ nạp quan hệ: dùng $posts->load('author') hoặc loadMissing() để tránh nạp trùng.
  • Quan hệ trong accessor hoặc Resource: đây là nơi N+1 ẩn rất kỹ, vì bạn không thấy vòng lặp ngay trong controller.

Khi nào không nên eager loading?

Eager loading không phải lúc nào cũng tốt hơn. Nếu bạn chỉ cần quan hệ của một vài bản ghi trong số rất nhiều, nạp tất cả sẽ lãng phí bộ nhớ. Với tập dữ liệu khổng lồ, hãy kết hợp phân trang hoặc chunk(), để mỗi lần chỉ xử lý một lượng vừa phải. Hãy đo bằng số liệu thực (số query, thời gian phản hồi) thay vì đoán.

Ví dụ đo thử trước và sau

Cách tốt nhất để tin vào lý thuyết là tự đo. Bạn có thể bật log truy vấn tạm thời để đếm số câu SQL chạy trong một request:

use Illuminate\Support\Facades\DB;DB::enableQueryLog();$posts = Post::with('author')->limit(50)->get();$posts->each(fn ($p) => $p->author->name);dump(count(DB::getQueryLog())); // kỳ vọng: 2

Hãy thử bỏ with('author') đi và chạy lại, bạn sẽ thấy con số tăng lên thành 51. Việc tự tay thấy con số này thay đổi giúp bạn nhớ bài học lâu hơn bất kỳ lời giải thích nào. Đây cũng là cách rất tốt để viết bài kiểm thử tự động: một test khẳng định rằng endpoint danh sách không chạy quá một số lượng query nhất định sẽ giúp bạn phát hiện hồi quy khi ai đó vô tình thêm lazy loading về sau.

Thói quen phòng tránh trong nhóm

Khi làm việc nhóm, hãy đưa việc kiểm tra N+1 vào quy trình review code. Với mỗi endpoint trả về danh sách, người review nên hỏi: dữ liệu quan hệ nào được dùng, và đã được nạp trước chưa? Một quy ước đơn giản như luôn khai báo with() ngay tại chỗ truy vấn, thay vì dựa vào $with mặc định trong model, sẽ giúp ý định rõ ràng hơn và tránh nạp thừa ở những nơi không cần thiết.

Kết luận

N+1 là lỗi hiệu năng phổ biến nhất của người mới dùng ORM, nhưng cũng dễ trị nhất. Hãy tập thói quen: thấy vòng lặp truy cập quan hệ thì nghĩ ngay đến with(), bật preventLazyLoading khi phát triển, và thỉnh thoảng nhìn lại số query của các trang quan trọng.

Muốn học bài bản và có lộ trình rõ ràng? Bạn có thể xem các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment