Introduction
Fast Eloquent starts with knowing which patterns hit the database efficiently and which don't. Eager loading, column selection, chunking, and indexing are the four levers that separate fast apps from slow ones.
Key Concepts
- N+1 prevention: Eager loading via
with()or enablingpreventLazyLoading()in dev. - Column selection:
select('id', 'name')instead of*to reduce memory and network transfer. - Chunk / lazy / cursor: Three strategies for processing large datasets with progressively smaller memory footprints.
withCount/ aggregate methods: Database-level counts instead of loading and counting in PHP.- Indexed columns: Columns with database indexes support fast
WHERE,ORDER BY, andJOINoperations.
Real World Context
The difference between a Laravel app that ships at 100ms and one that ships at 2s is usually eager loading and memory-efficient iteration. Learning these patterns early pays dividends on every feature you ship.
Deep Dive
Optimizing database queries is crucial for application performance. Learn techniques to write efficient queries and avoid common pitfalls.
Preventing N+1 Queries
The most common performance problem:
php// ❌ N+1 Problem: 101 queries for 100 posts $posts = Post::all(); foreach ($posts as $post) { echo $post->user->name; // Query for each post! } // ✅ Eager loading: 2 queries total $posts = Post::with('user')->get(); foreach ($posts as $post) { echo $post->user->name; // No additional query }
Enable Strict Mode in Development
php// AppServiceProvider public function boot(): void { Model::preventLazyLoading(! app()->isProduction()); }
Now lazy loading throws an exception during development!
Select Only What You Need
php// ❌ Selects all columns $users = User::all(); // ✅ Select only needed columns $users = User::select('id', 'name', 'email')->get(); // ✅ When eager loading, specify columns (include foreign keys!) $posts = Post::with('user:id,name')->get(); // ❌ Pluck still fetches all columns first $names = User::all()->pluck('name'); // ✅ Use pluck directly on query $names = User::pluck('name');
Use Chunking for Large Datasets
php// ❌ Loads all records into memory $users = User::all(); foreach ($users as $user) { // Process } // ✅ Process in chunks User::chunk(100, function ($users) { foreach ($users as $user) { // Process } }); // ✅ Even better: lazy loading foreach (User::lazy() as $user) { // Process one at a time } // ✅ Best for memory: cursor foreach (User::cursor() as $user) { // Minimal memory usage }
When to Use Each
| Method | Memory | Use Case |
|---|---|---|
get() | High | Small datasets |
chunk() | Medium | Updates with save() |
lazy() | Low | Read-only operations |
cursor() | Lowest | Very large datasets |
Optimize Counts
php// ❌ Loads all models just to count $count = Post::all()->count(); // ✅ Count at database level $count = Post::count(); // ❌ Loads all comments to count $posts = Post::all(); foreach ($posts as $post) { echo $post->comments->count(); } // ✅ Use withCount $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count; // Already loaded! }
Conditional Eager Loading
php// Load based on conditions $posts = Post::when($includeAuthor, function ($query) { $query->with('user'); })->get(); // Load after the fact if needed $posts = Post::all(); if ($showComments) { $posts->load('comments'); // Single query for all } // Don't re-load if already loaded $posts->loadMissing('tags');
Database Indexing
php// In migration - add indexes for frequently queried columns Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained(); $table->string('status'); $table->timestamp('published_at')->nullable(); $table->timestamps(); // Composite index for common query $table->index(['status', 'published_at']); $table->index(['user_id', 'status']); });
Queries that benefit:
php// Uses the index Post::where('status', 'published') ->where('published_at', '<=', now()) ->get();
Optimize Existence Checks
php// ❌ Loads the whole model if (User::find($id)) { // ... } // ✅ Just check existence if (User::where('id', $id)->exists()) { // ... } // ❌ Loads all related models if ($user->posts->count() > 0) { // ... } // ✅ Check at database level if ($user->posts()->exists()) { // ... }
Use Query Caching
phpuse Illuminate\Support\Facades\Cache; // Cache expensive queries $posts = Cache::remember('popular_posts', 3600, function () { return Post::withCount('comments') ->orderBy('comments_count', 'desc') ->take(10) ->get(); }); // Invalidate when data changes // In PostObserver: public function saved(Post $post): void { Cache::forget('popular_posts'); }
Raw Queries for Complex Operations
php// Sometimes raw SQL is more efficient $results = DB::select(' SELECT users.*, COUNT(posts.id) as posts_count FROM users LEFT JOIN posts ON users.id = posts.user_id WHERE users.active = 1 GROUP BY users.id HAVING posts_count > 10 ORDER BY posts_count DESC LIMIT 10 '); // Or use query builder raw expressions $users = User::select('users.*') ->selectRaw('COUNT(posts.id) as posts_count') ->leftJoin('posts', 'users.id', '=', 'posts.user_id') ->where('users.active', true) ->groupBy('users.id') ->havingRaw('COUNT(posts.id) > 10') ->orderByDesc('posts_count') ->limit(10) ->get();
Monitor Your Queries
php// Log all queries in development DB::listen(function ($query) { Log::debug('Query', [ 'sql' => $query->sql, 'bindings' => $query->bindings, 'time' => $query->time, ]); }); // Or use Laravel Debugbar / Telescope
Common Pitfalls
select *when you need three columns — Wastes memory and bandwidth.chunk()on unordered queries — Can produce duplicates or skip rows; always order by primary key.- Unnecessary eager loading — Loading relationships you never read makes queries heavier, not faster.
Best Practices
- Select only needed columns — Start narrow, expand if necessary.
- Chunk with ordered queries —
Post::orderBy('id')->chunk(100, fn ($posts) => ...). - Profile with Debugbar or Telescope — Don't guess at hotspots; measure them.
Summary
- Eager load to prevent N+1; enable
preventLazyLoadingin development. - Select specific columns instead of
*. - Chunk / lazy / cursor for memory-efficient iteration over large datasets.
withCountand aggregate methods keep counts and sums at the database level.- Profile queries with Debugbar or Telescope; don't guess at performance.