Laravel Eloquent Advanced Queries
Laravel Eloquent Advanced Queries
While basic Eloquent queries are powerful, Laravel provides advanced query capabilities for complex data retrieval scenarios. This lesson explores sophisticated querying techniques that will help you build efficient database operations.
Query Builder Advanced Techniques
Beyond basic where clauses, Laravel's query builder offers advanced methods for complex conditions:
// Where with subqueries
$users = User::where('votes', '>', function ($query) {
$query->selectRaw('avg(votes)')
->from('users')
->where('status', 'active');
})->get();
// whereColumn - comparing two columns
$products = Product::whereColumn('sale_price', '<', 'regular_price')->get();
// whereIn with subquery
$activeUsers = User::whereIn('id', function ($query) {
$query->select('user_id')
->from('orders')
->where('created_at', '>=', now()->subMonth());
})->get();
// whereBetween with dates
$recentOrders = Order::whereBetween('created_at', [
now()->subDays(7),
now()
])->get();
// whereDate, whereMonth, whereDay, whereYear
$todayOrders = Order::whereDate('created_at', today())->get();
$decemberSales = Sale::whereMonth('created_at', 12)->get();
Raw Expressions and SQL Injection Safety
Sometimes you need raw SQL for complex operations. Laravel provides safe methods to include raw expressions:
// selectRaw for calculated columns
$users = User::selectRaw('name, email, age * 12 as months_lived')
->get();
// whereRaw with bindings (prevents SQL injection)
$orders = Order::whereRaw('price > IF(status = "premium", ?, ?)', [100, 50])
->get();
// orderByRaw for custom sorting
$products = Product::orderByRaw('FIELD(category, "featured", "new", "sale")')
->get();
// havingRaw for aggregate conditions
$results = DB::table('orders')
->select('customer_id', DB::raw('SUM(price) as total'))
->groupBy('customer_id')
->havingRaw('SUM(price) > ?', [1000])
->get();
// DB::raw for complex expressions
$users = User::select(DB::raw('COUNT(*) as user_count, status'))
->where('status', '<>', 'deleted')
->groupBy('status')
->get();
Chunking Large Result Sets
When dealing with thousands of records, loading them all into memory can cause issues. Chunking processes results in smaller batches:
// Chunk - processes in batches
User::where('active', true)->chunk(200, function ($users) {
foreach ($users as $user) {
// Process each user
$user->sendMonthlyReport();
}
});
// chunkById - more efficient for large datasets (uses ID ordering)
Order::where('status', 'pending')
->chunkById(100, function ($orders) {
foreach ($orders as $order) {
$order->process();
}
}, $column = 'id');
// Cursor - lazy loads one at a time (memory efficient)
foreach (User::where('votes', '>', 100)->cursor() as $user) {
// Only one user loaded in memory at a time
$user->calculateStatistics();
}
// lazy - similar to cursor but with better control
User::where('active', true)->lazy()->each(function ($user) {
// Process user
$user->updateMetrics();
});
// lazyById - even more efficient for very large datasets
User::where('subscribed', true)
->lazyById(500)
->each(function ($user) {
$user->sendNewsletter();
});
chunk() for batch processing, cursor() or lazy() when processing one at a time, and chunkById() or lazyById() for the best performance on very large tables.
Aggregate Functions and Advanced Grouping
Laravel provides methods for statistical calculations and complex grouping:
// Basic aggregates
$totalOrders = Order::count();
$averagePrice = Order::avg('price');
$maxPrice = Order::max('price');
$minPrice = Order::min('price');
$totalRevenue = Order::sum('price');
// Grouping with aggregates
$salesByCategory = Product::select('category', DB::raw('COUNT(*) as count'))
->groupBy('category')
->get();
// Having clauses with aggregates
$activeCustomers = Order::select('customer_id', DB::raw('COUNT(*) as order_count'))
->groupBy('customer_id')
->having('order_count', '>', 5)
->get();
// Multiple groupBy columns
$monthlySales = Order::select(
DB::raw('YEAR(created_at) as year'),
DB::raw('MONTH(created_at) as month'),
DB::raw('SUM(price) as revenue')
)
->groupBy('year', 'month')
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->get();
// Conditional aggregates
$stats = Order::selectRaw('
COUNT(*) as total_orders,
SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = "pending" THEN 1 ELSE 0 END) as pending,
AVG(price) as average_price
')->first();
Conditional Clauses and Dynamic Queries
Build queries dynamically based on conditions without messy if/else statements:
// when() - adds clause only if condition is true
$sortBy = request('sort_by');
$products = Product::when($sortBy, function ($query, $sortBy) {
return $query->orderBy($sortBy);
})->get();
// when() with default callback
$role = request('role');
$users = User::when($role, function ($query, $role) {
// Applied when role exists
return $query->where('role', $role);
}, function ($query) {
// Applied when role doesn't exist (default)
return $query->where('role', 'user');
})->get();
// unless() - opposite of when()
$showInactive = request('show_inactive');
$users = User::unless($showInactive, function ($query) {
// Only applied when showInactive is false
return $query->where('active', true);
})->get();
// Practical search example
$search = request('search');
$category = request('category');
$minPrice = request('min_price');
$maxPrice = request('max_price');
$products = Product::query()
->when($search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%");
})
->when($category, function ($query, $category) {
$query->where('category_id', $category);
})
->when($minPrice, function ($query, $minPrice) {
$query->where('price', '>=', $minPrice);
})
->when($maxPrice, function ($query, $maxPrice) {
$query->where('price', '<=', $maxPrice);
})
->get();
when() and unless() keeps your query building code clean and readable, avoiding deeply nested if statements.
Subqueries in Select Statements
Include calculated data from related tables efficiently using subqueries:
// Add subquery results as columns
$users = User::select(['name', 'email'])
->selectSub(function ($query) {
$query->from('orders')
->whereColumn('user_id', 'users.id')
->selectRaw('COUNT(*)');
}, 'orders_count')
->selectSub(function ($query) {
$query->from('orders')
->whereColumn('user_id', 'users.id')
->selectRaw('SUM(price)');
}, 'total_spent')
->get();
// Using addSelect for additional subqueries
$products = Product::select('id', 'name', 'price')
->addSelect([
'reviews_avg' => Review::selectRaw('AVG(rating)')
->whereColumn('product_id', 'products.id'),
'reviews_count' => Review::selectRaw('COUNT(*)')
->whereColumn('product_id', 'products.id')
])
->get();
// Subquery ordering
$users = User::orderBy(
Order::selectRaw('COUNT(*)')->whereColumn('user_id', 'users.id'),
'desc'
)->get();
Summary
Advanced Eloquent queries give you the power to handle complex data retrieval efficiently:
- Subqueries: Enable complex filtering and calculated columns
- Raw expressions: Provide SQL flexibility with security through bindings
- Chunking: Processes large datasets without memory issues
- Conditional clauses: Build dynamic queries cleanly with when() and unless()
- Aggregates: Calculate statistics directly in the database
In the next lesson, we'll explore API Resources and how to transform Eloquent models for API responses.