Optimize your Laravel application's database performance with this comprehensive guide covering query debugging, caching strategies, performance monitoring, and optimization techniques.
Table of Contents
- Database Query Debugging
- Query Optimization Techniques
- Caching Strategies
- Performance Monitoring
- N+1 Query Prevention
- Database Configuration
- Best Practices
Database Query Debugging
Laravel Debug Bar Integration
The most comprehensive way to monitor database queries during development:
composer require --dev barryvdh/laravel-debugbar
The debug bar provides:
- Query count and execution time
- Duplicate query detection
- N+1 query identification
- Memory usage tracking
Custom Query Logging
For production debugging or CLI scripts, implement custom query logging:
// In AppServiceProvider boot() method
DB::listen(function ($query) {
Log::info('Database Query', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time . 'ms'
]);
});
Separate Query Log File
Create a dedicated query log for easier analysis:
DB::listen(function($query) {
File::append(
storage_path('/logs/query.log'),
sprintf(
'[%s] %s [%s] %sms%s%s',
date('Y-m-d H:i:s'),
$query->sql,
implode(', ', $query->bindings),
$query->time,
PHP_EOL,
PHP_EOL
)
);
});
Conditional Query Logging
Enable query logging only in specific environments:
if (app()->environment(['local', 'staging'])) {
DB::listen(function ($query) {
if ($query->time > 100) { // Only log slow queries
Log::warning('Slow Query Detected', [
'sql' => $query->sql,
'time' => $query->time . 'ms',
'bindings' => $query->bindings
]);
}
});
}
Query Optimization Techniques
Eager Loading
Prevent N+1 queries with proper eager loading:
// Bad: N+1 query problem
$users = User::all();
foreach ($users as $user) {
echo $user->profile->name; // Executes N queries
}
// Good: Eager loading
$users = User::with('profile')->get();
foreach ($users as $user) {
echo $user->profile->name; // Single query
}
Lazy Eager Loading
Load relationships after the initial query:
$users = User::all();
// Lazy eager load when needed
$users->load('posts', 'comments');
Limiting Eager Loaded Relationships (Laravel 11+)
$posts = Post::with([
'comments' => fn ($query) => $query->limit(5)
])->get();
Query Constraints
Add constraints to eager loaded relationships:
$users = User::with([
'posts' => function ($query) {
$query->where('published', true)
->orderBy('created_at', 'desc');
}
])->get();
Database Indexes
Ensure proper indexing for frequently queried columns:
// Migration
Schema::table('posts', function (Blueprint $table) {
$table->index('user_id');
$table->index(['status', 'published_at']);
$table->index('slug');
});
Caching Strategies
Query Result Caching
Cache expensive query results:
$expensiveData = Cache::remember('expensive_query', 3600, function () {
return DB::table('analytics')
->join('users', 'users.id', '=', 'analytics.user_id')
->where('created_at', '>=', now()->subDays(30))
->get();
});
Model Caching
Implement automatic model caching:
class User extends Model
{
protected static function booted()
{
static::retrieved(function ($user) {
Cache::put("user.{$user->id}", $user, 3600);
});
static::saved(function ($user) {
Cache::forget("user.{$user->id}");
});
static::deleted(function ($user) {
Cache::forget("user.{$user->id}");
});
}
public static function findCached($id)
{
return Cache::remember("user.{$id}", 3600, function () use ($id) {
return static::find($id);
});
}
}
Cache Tags for Grouped Invalidation
Use cache tags to invalidate related cache entries:
// Store with tags
Cache::tags(['users', 'posts'])->put('user.posts.1', $posts, 3600);
// Invalidate all user-related cache
Cache::tags(['users'])->flush();
Authenticated User Caching
Cache the authenticated user to reduce database calls:
// Custom User Provider
class CacheUserProvider extends EloquentUserProvider
{
public function retrieveById($identifier)
{
return Cache::remember("auth.user.{$identifier}", 3600, function () use ($identifier) {
return parent::retrieveById($identifier);
});
}
}
// Register in AuthServiceProvider
Auth::provider('cache-user', function ($app, array $config) {
return new CacheUserProvider($app['hash'], $config['model']);
});
// Update config/auth.php
'providers' => [
'users' => [
'driver' => 'cache-user',
'model' => App\Models\User::class,
],
],
Performance Monitoring
Database Query Analysis
Monitor query performance in production:
DB::listen(function ($query) {
if ($query->time > 1000) { // Queries over 1 second
Log::critical('Very Slow Query', [
'sql' => $query->sql,
'time' => $query->time,
'bindings' => $query->bindings,
'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
]);
}
});
Memory Usage Tracking
Monitor memory consumption:
class DatabasePerformanceMiddleware
{
public function handle($request, Closure $next)
{
$startTime = microtime(true);
$startMemory = memory_get_usage();
$queryCount = 0;
DB::listen(function () use (&$queryCount) {
$queryCount++;
});
$response = $next($request);
$endTime = microtime(true);
$endMemory = memory_get_usage();
Log::info('Request Performance', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'execution_time' => ($endTime - $startTime) * 1000 . 'ms',
'memory_used' => ($endMemory - $startMemory) / 1024 / 1024 . 'MB',
'query_count' => $queryCount
]);
return $response;
}
}
N+1 Query Prevention
Detecting N+1 Queries
Use Laravel's built-in detection:
// In AppServiceProvider boot() method
Model::preventLazyLoading(! app()->isProduction());
Custom N+1 Detection
class N1QueryDetector
{
protected static $queryCount = 0;
protected static $models = [];
public static function start()
{
static::$queryCount = 0;
static::$models = [];
DB::listen(function ($query) {
static::$queryCount++;
if (static::$queryCount > 10) {
Log::warning('Potential N+1 Query Detected', [
'query_count' => static::$queryCount,
'sql' => $query->sql
]);
}
});
}
}
Eager Loading Solutions
// Load multiple relationships
$posts = Post::with(['user', 'comments', 'tags'])->get();
// Nested relationships
$posts = Post::with(['comments.user', 'user.profile'])->get();
// Conditional eager loading
$posts = Post::with([
'comments' => function ($query) {
$query->where('approved', true);
}
])->get();
Database Configuration
Read/Write Connections
Configure separate read/write database connections:
// config/database.php
'mysql' => [
'read' => [
'host' => [
'192.168.1.1',
'192.168.1.2',
],
],
'write' => [
'host' => [
'196.168.1.3',
],
],
'sticky' => true, // Enables sticky reads
'driver' => 'mysql',
'database' => 'database',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
],
Connection Pooling
Optimize connection usage:
// config/database.php
'mysql' => [
'options' => [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
Best Practices
1. Use Database Transactions
Wrap related operations in transactions:
DB::transaction(function () {
$user = User::create($userData);
$profile = Profile::create($profileData + ['user_id' => $user->id]);
$user->assignRole('customer');
});
2. Implement Query Scopes
Create reusable query logic:
class Post extends Model
{
public function scopePublished($query)
{
return $query->where('published', true);
}
public function scopeRecent($query, $days = 7)
{
return $query->where('created_at', '>=', now()->subDays($days));
}
}
// Usage
$posts = Post::published()->recent(14)->get();
3. Use Chunking for Large Datasets
Process large datasets efficiently:
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// Process user
}
});
// Or with lazy collections
User::lazy()->each(function ($user) {
// Process user one by one
});
4. Optimize Migrations
Create efficient database migrations:
Schema::table('posts', function (Blueprint $table) {
$table->index(['user_id', 'published_at']);
$table->index('slug');
// Add foreign key constraints
$table->foreign('user_id')->references('id')->on('users');
});
5. Monitor Query Performance
Set up alerts for slow queries:
DB::listen(function ($query) {
if ($query->time > config('database.slow_query_threshold', 1000)) {
// Send alert or log
Mail::to('[email protected]')->send(new SlowQueryAlert($query));
}
});
Frequently Asked Questions
How do I identify slow queries in production?
Use query logging with time thresholds and monitoring tools like Laravel Telescope or custom logging middleware.
What's the difference between lazy and eager loading?
Eager loading loads relationships upfront (preventing N+1), while lazy loading loads relationships when accessed (potential N+1 problem).
How do I cache complex queries?
Use Cache::remember()
with appropriate cache keys and tags. Consider using Redis for complex data structures.
When should I use database transactions?
Use transactions when multiple related database operations must succeed or fail together (data consistency).
Related Laravel Performance Techniques
Combine these database optimizations with other Laravel performance strategies:
- Prunable Eloquent models for database cleanup
- Virtual database columns for computed fields
- Laravel optimize command for application caching
- MySQL query optimization for advanced database techniques
Conclusion
Database performance is crucial for scalable Laravel applications. By implementing proper query logging, caching strategies, and monitoring techniques, you can ensure your application remains fast and responsive as it grows.
Start with query debugging to identify bottlenecks, implement caching for expensive operations, and use proper eager loading to prevent N+1 queries. Regular monitoring and optimization will keep your database performance optimal.
Remember: measure first, optimize second. Always profile your application to identify real bottlenecks before implementing optimizations.