Explore all courses in this category.
Explore all courses in this category.
Nguyễn Đình Cường
0 comment
09 Nov, 2026
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.
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.
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.
Đừ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.
$post->comments->count() trong vòng lặp, hãy dùng withCount('comments') rồi đọc comments_count.with(['comments' => fn ($q) => $q->where('approved', true)]) để chỉ lấy bình luận đã duyệt.$posts->load('author') hoặc loadMissing() để tránh nạp trùng.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.
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: 2Hã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.
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.
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.
Nguyễn Đình Cường
0 comment