Laravel nâng cao

Tối ưu Eloquent Query cho hệ thống lớn: Guide thực chiến

Hướng dẫn chi tiết cách tối ưu Eloquent queries trong Laravel - Giải quyết N+1, chunking, indexing và các kỹ thuật cho production system.

newspaper

BlogDev Team

14 tháng 12, 2024
Tối ưu Eloquent Query cho hệ thống lớn: Guide thực chiến
Featured Image

Giới thiệu

Eloquent là ORM tuyệt vời cho development, nhưng có thể là performance killer trong production nếu không cẩn thận. Bài viết này chia sẻ các kỹ thuật tối ưu cho hệ thống hàng triệu records.

Vấn đề phổ biến: N+1 Query

Phát hiện N+1

// ❌ N+1 Problem
$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name; // Query thêm CHO MỖI post
}
// Result: 1 + N queries (N = số posts)

Giải pháp: Eager Loading

// ✅ Fixed với with()
$posts = Post::with('author')->get();

foreach ($posts as $post) {
    echo $post->author->name; // Không query thêm
}
// Result: 2 queries (posts + authors)

Nested Eager Loading

// Load multiple levels
$posts = Post::with([
    'author',
    'comments.user',
    'tags',
])->get();

// Constrained eager loading
$posts = Post::with(['comments' => function ($query) {
    $query->where('approved', true)
          ->orderBy('created_at', 'desc')
          ->limit(5);
}])->get();

Prevent N+1 trong Development

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(!app()->isProduction());
}

Sẽ throw exception nếu có lazy loading trong dev environment.

Chunking cho Large Datasets

Vấn đề: Memory Exhaustion

// ❌ Load tất cả vào memory
$users = User::all(); // 1 triệu users = crash

foreach ($users as $user) {
    // Process
}

Giải pháp: chunk()

// ✅ Process từng batch
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process user
    }
});

chunkById() cho Update Operations

// ✅ An toàn khi update/delete trong loop
User::where('active', false)
    ->chunkById(1000, function ($users) {
        foreach ($users as $user) {
            $user->delete();
        }
    });

Lazy Collections

// ✅ Memory efficient iterator
User::lazy()->each(function ($user) {
    // Chỉ load 1 record tại một thời điểm
});

// Với chunk size
User::lazy(1000)->each(function ($user) {
    // Process
});

Select Only What You Need

Vấn đề: SELECT *

// ❌ Load tất cả columns
$users = User::all();

// ✅ Chỉ load columns cần thiết
$users = User::select(['id', 'name', 'email'])->get();

Với Relationships

// ✅ Select specific columns trong eager loading
$posts = Post::with(['author:id,name,avatar'])->get();

// ✅ Combine với main select
$posts = Post::select(['id', 'title', 'author_id'])
    ->with(['author:id,name'])
    ->get();

Database Indexing

Kiểm tra Query Execution

// Xem query plan
$query = User::where('email', 'test@example.com');
DB::enableQueryLog();
$query->get();
dd(DB::getQueryLog());

// Hoặc explain()
User::where('email', 'test@example.com')->explain();

Tạo Indexes đúng cách

// Migration
Schema::table('users', function (Blueprint $table) {
    // Single column index
    $table->index('email');
    
    // Composite index - thứ tự quan trọng!
    $table->index(['status', 'created_at']);
    
    // Unique index
    $table->unique('username');
});

Index Strategy

// Composite index: order matters!
// Index on ['status', 'created_at'] works for:
User::where('status', 'active')->get(); // ✅
User::where('status', 'active')->where('created_at', '>', now()->subDays(7))->get(); // ✅

// But NOT for:
User::where('created_at', '>', now()->subDays(7))->get(); // ❌ Won't use index

Query Caching

Basic Caching

// Cache query results
$products = Cache::remember('active_products', 3600, function () {
    return Product::where('is_active', true)
        ->with('category')
        ->get();
});

Cache Tags

// Cache với tags để invalidate dễ dàng
$product = Cache::tags(['products', 'product_' . $id])
    ->remember('product_' . $id, 3600, function () use ($id) {
        return Product::with(['category', 'reviews'])->find($id);
    });

// Invalidate khi update
Cache::tags(['products'])->flush();

Query Cache Package

composer require genealabs/laravel-model-caching
// Model sử dụng caching tự động
use GeneaLabs\LaravelModelCaching\Traits\Cachable;

class Product extends Model
{
    use Cachable;
}

// Queries tự động được cache
Product::where('is_active', true)->get(); // Cached

Raw Queries khi cần thiết

Khi nên dùng Raw Queries

// Complex aggregations
$stats = DB::select("
    SELECT 
        DATE(created_at) as date,
        COUNT(*) as orders,
        SUM(total) as revenue,
        AVG(total) as avg_order
    FROM orders
    WHERE created_at >= ?
    GROUP BY DATE(created_at)
    ORDER BY date DESC
", [now()->subDays(30)]);

Raw trong Eloquent

// selectRaw()
$products = Product::selectRaw('category_id, COUNT(*) as count, AVG(price) as avg_price')
    ->groupBy('category_id')
    ->get();

// whereRaw()
$users = User::whereRaw('YEAR(created_at) = ?', [2024])->get();

// orderByRaw()
$products = Product::orderByRaw('FIELD(status, "active", "pending", "inactive")')->get();

Batch Operations

Insert Many

// ❌ Slow: Individual inserts
foreach ($data as $item) {
    Product::create($item);
}

// ✅ Fast: Batch insert
Product::insert($data); // Không trigger events

// ✅ Với timestamps
$data = array_map(function ($item) {
    $item['created_at'] = now();
    $item['updated_at'] = now();
    return $item;
}, $data);

Product::insert($data);

Upsert (Insert or Update)

// Insert hoặc update nếu tồn tại
Product::upsert(
    $products, // Data array
    ['sku'],   // Unique keys để check
    ['name', 'price', 'stock'] // Columns to update if exists
);

Batch Update

// Update nhiều records với điều kiện
Product::where('category_id', 5)
    ->update(['is_featured' => true]);

// Update với expression
Product::where('stock', '<', 10)
    ->increment('stock', 100);

Query Optimization Tools

Laravel Debugbar

composer require barryvdh/laravel-debugbar --dev

Shows:

  • Số queries
  • Query time
  • Duplicate queries
  • Memory usage

Laravel Telescope

composer require laravel/telescope --dev

Provides:

  • Query monitoring
  • Slow query detection
  • Request profiling

N+1 Query Detector

composer require beyondcode/laravel-query-detector --dev

Alerts khi có N+1 queries.

Real-world Example: Dashboard Stats

Before Optimization

// ❌ Multiple queries, N+1
public function getDashboardStats()
{
    $orders = Order::all();
    
    $totalRevenue = 0;
    $productsSold = 0;
    
    foreach ($orders as $order) {
        $totalRevenue += $order->total;
        foreach ($order->items as $item) { // N+1!
            $productsSold += $item->quantity;
        }
    }
    
    return [
        'orders' => $orders->count(),
        'revenue' => $totalRevenue,
        'products_sold' => $productsSold,
    ];
}
// Result: 1 + N queries, high memory

After Optimization

// ✅ Single aggregated query
public function getDashboardStats()
{
    $stats = Order::selectRaw('
        COUNT(*) as order_count,
        SUM(total) as total_revenue
    ')->first();
    
    $productsSold = OrderItem::sum('quantity');
    
    return [
        'orders' => $stats->order_count,
        'revenue' => $stats->total_revenue,
        'products_sold' => $productsSold,
    ];
}
// Result: 2 queries, minimal memory

Checklist Tối ưu

Development Phase

  • Enable preventLazyLoading()
  • Install Debugbar
  • Review N+1 queries

Before Production

  • Add proper indexes
  • Use eager loading
  • Implement caching
  • Review slow queries

Monitoring

  • Setup Telescope
  • Monitor query counts
  • Alert slow queries

Kết luận

Eloquent performance không phải magic - cần understanding và discipline:

  1. Luôn dùng eager loading cho relationships
  2. Select only needed columns
  3. Index columns used in WHERE, ORDER BY
  4. Cache expensive queries
  5. Use chunking cho large datasets
  6. Monitor queries trong production

Rule of thumb: 1 page request nên có < 20 queries. Nếu nhiều hơn, có gì đó cần review.

history_edu Góc học tập & giải trí

Thử Thách Kiến Thức Lịch Sử?

Khám phá hàng trăm câu hỏi trắc nghiệm lịch sử thú vị tại HistoQuiz. Vừa học vừa chơi, nâng cao kiến thức ngay hôm nay!

Chơi Ngay arrow_forward