© 2026 Laravel

Eloquent Performance Deep Dive – N+1, Memory, Query Optimization

9 phút đọc 34 lượt xem
#laravel #eloquent #performance #optimization #database

Trong production, 80% vấn đề performance đến từ:

  • Query không tối ưu
  • Lạm dụng Eloquent
  • Không hiểu cách ORM hoạt động

Và đa số dev chỉ biết mỗi:

Fix N+1 bằng with()

Nhưng thực tế phức tạp hơn rất nhiều.

#1. Problem (Thực tế production)

Giả sử bạn có:

  • 10,000 users
  • Mỗi user có 20 posts

Code:

$users = User::all();

foreach ($users as $user) {
    foreach ($user->posts as $post) {
        echo $post->title;
    }
}

Nghe có vẻ bình thường, nhưng hệ thống bắt đầu:

  • Chậm dần
  • CPU tăng
  • DB quá tải

#2. Naive Solution (Dev thường làm)

“Dùng with() là xong”

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

Đúng, nhưng chưa đủ.

#3. Vấn đề thực sự (Root Cause)

#N+1 Query là gì?

Flow:

1 query lấy users
+ N query lấy posts

Nếu N = 10,000 → 10,001 queries

#Nhưng vấn đề chưa dừng ở đó

Ngay cả khi dùng eager loading:

Bạn vẫn có thể gặp:

  • Memory overflow
  • Query quá nặng
  • Response chậm

#4. Giải pháp đúng (Deep Dive)

#4.1 Eager Loading – nhưng đúng cách

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

Laravel sẽ:

  • Query users
  • Query posts bằng WHERE IN

#Problem: Over-fetching

Bạn load:

  • 10,000 users
  • 200,000 posts

Memory explode 💥

#4.2 Select Field (cực quan trọng)

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

Giảm:

  • Memory
  • Network

#4.3 Chunk – xử lý batch

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

Ưu điểm:

  • Giảm memory

Nhược điểm:

  • Không dùng được cho pagination logic phức tạp

#4.4 Cursor – streaming data

foreach (User::cursor() as $user) {
    // xử lý từng record
}

Ưu điểm:

  • Memory cực thấp

Nhược điểm:

  • Chậm hơn chunk
  • Không eager loading tốt

#4.5 Lazy Eager Loading

$users = User::all();
$users->load('posts');

Khi nào dùng?

  • Khi bạn conditionally cần relation

#5. Trade-off (rất quan trọng)

Technique Ưu điểm Nhược điểm
eager loading ít query tốn memory
chunk tiết kiệm RAM phức tạp logic
cursor RAM thấp nhất chậm

Không có giải pháp “best”, chỉ có phù hợp.

#6. Khi nào Eloquent trở thành bottleneck?

#Case 1: Dataset lớn

  • 1M records

Eloquent không phù hợp

#Case 2: Query phức tạp

DB::select(...);

Raw query nhanh hơn

#Case 3: Bulk insert/update

DB::table('users')->insert([...]);

Rule:

Eloquent = convenience Query Builder / Raw = performance

#7. Failure Case (thực tế rất hay gặp)

#Case 1: Eager loading everything

User::with(['posts', 'comments', 'likes'])->get();

API chết

#Case 2: Loop query

foreach ($users as $user) {
    Post::where('user_id', $user->id)->get();
}

Classic N+1

#Case 3: Load toàn bộ data

User::all();

Không bao giờ làm trong production nếu data lớn

#8. Tips & Tricks (thực chiến)

#1. Luôn dùng select

User::select('id')->get();

#2. Limit data

User::limit(100)->get();

#3. Dùng index DB

Không phải Laravel nhưng cực quan trọng

#4. Debug query

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

#5. Dùng pagination

User::paginate(20);

#9. Mindset

Junior:

Fix N+1 là xong

Senior:

Phải hiểu trade-off giữa query, memory và latency

#10. Interview Questions

1. N+1 query là gì?

Là việc query lặp lại nhiều lần gây performance issue

2. Eager loading có luôn tốt không?

Không, có thể gây tốn memory

3. Chunk vs Cursor khác nhau như thế nào?

Chunk xử lý theo batch, Cursor xử lý từng record

4. Khi nào nên dùng raw query?

Khi cần performance cao hoặc query phức tạp

5. Làm sao debug performance query?

Dùng log, debugbar, explain

#11. So sánh Eloquent vs Query Builder vs Raw SQL (Benchmark Style)

Trong production, câu hỏi quan trọng không phải là:

Dùng cái nào đúng?

Mà là:

Dùng cái nào phù hợp với workload?

#Benchmark Scenario

Giả sử:

  • 100,000 users
  • Query: lấy danh sách user + count posts

#1. Eloquent

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

Ưu điểm:

  • Code sạch
  • Readable
  • Maintain tốt

Nhược điểm:

  • Overhead ORM
  • Hydration object tốn CPU + RAM

#2. Query Builder

$users = DB::table('users')
    ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
    ->select('users.id', 'users.name', DB::raw('COUNT(posts.id) as post_count'))
    ->groupBy('users.id')
    ->get();

Ưu điểm:

  • Nhanh hơn Eloquent
  • Ít overhead hơn

Nhược điểm:

  • Code dài hơn
  • Ít abstraction

#3. Raw SQL

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

Ưu điểm:

  • Nhanh nhất
  • Full control

Nhược điểm:

  • Khó maintain
  • Dễ lỗi

#Kết quả (ước lượng thực tế)

Method Time Memory Maintain
Eloquent 120ms High ⭐⭐⭐⭐⭐
Query Builder 80ms Medium ⭐⭐⭐⭐
Raw SQL 60ms Low ⭐⭐

Insight:

  • Eloquent = DX tốt
  • Query Builder = balance
  • Raw SQL = performance tối đa

#Rule thực chiến

  • CRUD bình thường → Eloquent
  • Query phức tạp → Query Builder
  • Critical path → Raw SQL

#12. EXPLAIN Query Analysis

Nếu bạn không đọc được EXPLAIN → bạn không tối ưu DB được

#Ví dụ query

EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

#Output (đơn giản hóa)

type key rows Extra
ALL NULL 100k Using where

Ý nghĩa:

  • type = ALL → full table scan
  • key = NULL → không dùng index

#Sau khi thêm index

CREATE INDEX idx_users_email ON users(email);

#EXPLAIN lại

type key rows Extra
ref idx_users_email 1 NULL

Improvement:

  • rows: 100k → 1
  • tốc độ tăng cực mạnh

#Các field quan trọng

  • type: ALL → index → ref → const (càng tốt)
  • rows: càng nhỏ càng tốt
  • key: index đang dùng

#Red flags

  • type = ALL
  • rows rất lớn
  • Using filesort
  • Using temporary

#13. Case Study

Problem

API:

GET /api/users

Code:

$users = User::with(['posts', 'comments'])->get();

Symptoms

  • Response: 3.2s
  • RAM: 512MB
  • CPU: cao

Root Cause

  • Load quá nhiều relation
  • Không select field
  • Không pagination

Fix

$users = User::select('id', 'name')
    ->with(['posts:id,user_id,title'])
    ->paginate(20);

#Kết quả

  • Response: 3.2s → 300ms (~10x)
  • RAM giảm mạnh

#Bài học

  • Không bao giờ load toàn bộ data
  • Luôn giới hạn field
  • Pagination là bắt buộc

#14. Composite Index (Cực hay bị sai)

#Vấn đề

Rất nhiều dev tạo index kiểu:

CREATE INDEX idx_users_name_email ON users(name, email);

Nhưng query lại là:

SELECT * FROM users WHERE email = 'a@example.com';

Index KHÔNG được dùng

#Quy tắc vàng (Left-most prefix rule)

Composite index chỉ hoạt động nếu query bắt đầu từ cột bên trái

Index:

(name, email)

#Dùng được

WHERE name = 'A'
WHERE name = 'A' AND email = 'a@example.com'

#Không dùng được

WHERE email = 'a@example.com'

#Sai lầm phổ biến

  • Đặt thứ tự cột sai
  • Index nhưng không match query

#Cách thiết kế đúng

Dựa trên query thực tế, không phải intuition

CREATE INDEX idx_users_email_name ON users(email, name);

#Insight senior

  • Column có selectivity cao → đặt trước
  • Query filter chính → đặt trước

#15. Covering Index (rất mạnh)

#Khái niệm

Query có thể trả kết quả chỉ từ index mà không cần đọc table

#Ví dụ

CREATE INDEX idx_users_email_name ON users(email, name);

Query:

SELECT email, name FROM users WHERE email = 'a@example.com';

DB chỉ đọc index → cực nhanh

#Khi nào xảy ra?

  • SELECT chỉ chứa các cột nằm trong index

#Sai lầm

SELECT * FROM users WHERE email = 'a@example.com';

Không dùng covering index

#Best practice

  • Tránh SELECT *
  • Design index theo query

#16. Transaction & Deadlock (Production thực tế)

#Transaction là gì?

DB::transaction(function () {
    // nhiều query
});

Đảm bảo ACID

#Deadlock là gì?

#Scenario

Transaction A:

UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;

Transaction B:

UPDATE users SET balance = balance - 50 WHERE id = 2;
UPDATE users SET balance = balance + 50 WHERE id = 1;

A lock id=1, B lock id=2 → deadlock 💀

#Cách fix

#1. Lock theo thứ tự cố định

DB::transaction(function () {
    User::whereIn('id', [1,2])->lockForUpdate()->get();
});

#2. Retry transaction

DB::transaction(function () {
    // logic
}, 5);

#3. Giữ transaction ngắn

Không call API bên trong transaction

#Insight

  • Deadlock không tránh được hoàn toàn
  • Quan trọng là detect + retry

#17. Query Plan thực chiến

#Ví dụ query phức tạp

SELECT * FROM orders
WHERE user_id = 10
AND status = 'completed'
ORDER BY created_at DESC;

#EXPLAIN (bad case)

type key rows Extra
ALL NULL 500k Using filesort

Full scan + sort → rất chậm

#Fix bằng index

CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);

#EXPLAIN (good case)

type key rows Extra
ref idx_orders_user_status_created 50 NULL

Không filesort → nhanh hơn nhiều

#Insight cực quan trọng

  • ORDER BY + WHERE → phải nằm cùng index
  • Không match → DB phải sort lại

#Kết luận

Eloquent rất mạnh nhưng:

Dùng sai → hệ thống chết

Hiểu đúng giúp bạn:

  • Tăng performance
  • Giảm chi phí
  • Scale hệ thống

Mục lục bài viết

  1. 1. Problem (Thực tế production)
  2. 2. Naive Solution (Dev thường làm)
  3. 3. Vấn đề thực sự (Root Cause)
    1. N+1 Query là gì?
    2. Nhưng vấn đề chưa dừng ở đó
  4. 4. Giải pháp đúng (Deep Dive)
    1. 4.1 Eager Loading – nhưng đúng cách
    2. Problem: Over-fetching
    3. 4.2 Select Field (cực quan trọng)
    4. 4.3 Chunk – xử lý batch
    5. 4.4 Cursor – streaming data
    6. 4.5 Lazy Eager Loading
  5. 5. Trade-off (rất quan trọng)
  6. 6. Khi nào Eloquent trở thành bottleneck?
    1. Case 1: Dataset lớn
    2. Case 2: Query phức tạp
    3. Case 3: Bulk insert/update
  7. 7. Failure Case (thực tế rất hay gặp)
    1. Case 1: Eager loading everything
    2. Case 2: Loop query
    3. Case 3: Load toàn bộ data
  8. 8. Tips & Tricks (thực chiến)
    1. 1. Luôn dùng select
    2. 2. Limit data
    3. 3. Dùng index DB
    4. 4. Debug query
    5. 5. Dùng pagination
  9. 9. Mindset
  10. 10. Interview Questions
  11. 11. So sánh Eloquent vs Query Builder vs Raw SQL (Benchmark Style)
    1. Benchmark Scenario
    2. Kết quả (ước lượng thực tế)
    3. Rule thực chiến
  12. 12. EXPLAIN Query Analysis
    1. Ví dụ query
    2. Output (đơn giản hóa)
    3. Sau khi thêm index
    4. EXPLAIN lại
    5. Các field quan trọng
    6. Red flags
  13. 13. Case Study
    1. Kết quả
    2. Bài học
  14. 14. Composite Index (Cực hay bị sai)
    1. Vấn đề
    2. Quy tắc vàng (Left-most prefix rule)
    3. Sai lầm phổ biến
    4. Cách thiết kế đúng
    5. Insight senior
  15. 15. Covering Index (rất mạnh)
    1. Khái niệm
    2. Ví dụ
    3. Khi nào xảy ra?
    4. Sai lầm
    5. Best practice
  16. 16. Transaction & Deadlock (Production thực tế)
    1. Transaction là gì?
    2. Deadlock là gì?
    3. Cách fix
    4. Insight
  17. 17. Query Plan thực chiến
    1. Ví dụ query phức tạp
    2. EXPLAIN (bad case)
    3. Fix bằng index
    4. EXPLAIN (good case)
    5. Insight cực quan trọng
  18. Kết luận

Sử dụng các mục để điều hướng nhanh