Laravel – Solving N+1 & Query Optimization

2 min read

N+1 Query là gì và vì sao nguy hiểm?

N+1 xảy ra khi:

  • Bạn chạy 1 query chính
  • Sau đó chạy thêm N query phụ cho từng record

Ví dụ:

$users = User::all();foreach ($users as $user) {
    echo $user->posts->count();
}

Nếu có 100 user:

  • 1 query lấy users
  • 100 query lấy posts

Tổng: 101 query.

Trong production, đây là sát thủ performance.

Tài liệu chính thức:
https://laravel.com/docs/eloquent-relationships


1️⃣ Cách phát hiện N+1

Trong môi trường dev bạn có thể dùng:

  • Laravel Debugbar
  • Laravel Telescope
  • DB::listen()

Ví dụ log query:

DB::listen(function ($query) {
    logger($query->sql);
});

Nếu thấy query lặp lại nhiều lần → có thể bạn đang gặp N+1.


2️⃣ Giải pháp: Eager Loading

Thay vì lazy loading:

$users = User::all();

Dùng eager loading:

$users = User::with('posts')->get();

Giờ Laravel sẽ:

  • Query users
  • Query posts bằng WHERE IN

Chỉ còn 2 query thay vì 101.


3️⃣ Eager Loading Có Điều Kiện

Bạn có thể thêm điều kiện:

$users = User::with(['posts' => function ($query) {
    $query->where('published', true);
}])->get();

Giúp giảm data không cần thiết.


4️⃣ Chỉ Select Cột Cần Thiết

Đừng bao giờ load tất cả cột nếu không cần.

Sai:

User::with('posts')->get();

Tốt hơn:

User::select('id', 'name')
    ->with(['posts:id,user_id,title'])
    ->get();

Giảm memory usage và tăng tốc đáng kể.


5️⃣ withCount() Thay Vì Load Toàn Bộ Relationship

Nếu bạn chỉ cần số lượng:

Sai:

$user->posts->count();

Đúng:

User::withCount('posts')->get();

Laravel sẽ generate:

SELECT users.*, 
(SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) AS posts_count
FROM users;

Nhanh hơn rất nhiều.


6️⃣ Chunking Khi Làm Việc Với Dữ Liệu Lớn

Nếu bạn xử lý hàng chục nghìn record:

Sai:

User::all();

Đúng:

User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // xử lý
    }
});

Giúp:

  • Tránh tràn memory
  • Giữ performance ổn định

7️⃣ Index Database – Yếu Tố Bắt Buộc

Eloquent không tự tạo index cho bạn.

Trong migration:

$table->index('user_id');

Không có index:

  • Query WHERE chậm
  • JOIN chậm
  • Full table scan

Đọc thêm về indexing:
https://use-the-index-luke.com/


8️⃣ Phân tích Query Với EXPLAIN

Nếu query chậm, hãy chạy:

EXPLAIN SELECT ...

Bạn cần quan tâm:

  • type
  • key
  • rows

Nếu thấy:

type = ALL
→ đang full table scan.


9️⃣ Khi nào nên dùng Query Builder hoặc Raw SQL?

Eloquent tiện lợi, nhưng không phải lúc nào cũng tối ưu.

Nên dùng raw query khi:

  • Report phức tạp
  • Join nhiều bảng
  • Aggregate lớn

Ví dụ:

DB::select("
    SELECT users.id, COUNT(posts.id) as total
    FROM users
    JOIN posts ON posts.user_id = users.id
    GROUP BY users.id
");

Performance cao hơn so với chain nhiều relationship.


Kết luận

Để tối ưu Eloquent trong production:

  • Luôn kiểm tra N+1
  • Dùng eager loading
  • Select đúng cột cần thiết
  • Dùng withCount khi chỉ cần số lượng
  • Thêm index database
  • Phân tích query với EXPLAIN

Eloquent rất mạnh, nhưng nếu dùng sai cách, nó sẽ làm app của bạn chậm một cách âm thầm.

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *