Master nested subqueries and `joinSub` in Laravel to optimize complex reports and push logic to the database, ensuring your SaaS platform remains performant.
Previously in this course, we explored querying with strict Eloquent to maintain data integrity and prevent common performance pitfalls. While strict loading is essential, scaling a high-traffic SaaS platform often requires moving beyond standard ORM methods. When reports require calculating aggregates across millions of rows, pulling data into PHP memory is a non-starter.
In this lesson, we’ll use SQL subqueries and the joinSub method to push complex filtering and aggregation logic directly into the database engine.
In our running project, we often need to display a dashboard showing "The most active user per subscription plan." If you fetch all users and their subscription counts into a collection to sort them in PHP, you'll likely hit memory limits or latency spikes as the user base grows.
We need the database to do the heavy lifting. By nesting a subquery, we can isolate the aggregation logic and join it against our primary tables, ensuring the database only returns the precise result set we need.
A subquery is essentially a query within a query. In Eloquent, we can define a subquery using a closure that returns a Query\Builder instance. This is particularly useful for selecting calculated values that depend on related tables.
Consider this requirement: Fetch all users along with the date of their most recent login.
PHPuse App\Models\User; use Illuminate\Support\Facades\DB; $latestLogins = DB::table('logins') ->select('user_id', DB::raw('MAX(created_at) as last_login_at')) ->groupBy('user_id'); $users = User::query() ->joinSub($latestLogins, 'latest_logins', function ($join) { $join->on('users.id', '=', 'latest_logins.user_id'); }) ->get();
By using joinSub, we treat the subquery result like a temporary table. This is significantly more efficient than running a separate query for every user or using a correlated subquery in the select statement, which can lead to slow execution plans.
joinSubLet’s advance our project. We need to generate a monthly report showing the total revenue per tenant, but only for those who have exceeded a specific transaction volume.
Instead of iterating through collections, we define a subquery that identifies the heavy-hitting tenants, then join that result set back to our Tenants table.
PHPpublic function getHighVolumeTenantReport(int $minTransactions) { #6A9955">// 1. Define the subquery for high-volume transactions $subQuery = DB::table('transactions') ->select('tenant_id', DB::raw('SUM(amount) as total_revenue')) ->where('status', 'completed') ->groupBy('tenant_id') ->havingRaw('COUNT(*) > ?', [$minTransactions]); #6A9955">// 2. Join the subquery to the main Tenants model return Tenant::query() ->joinSub($subQuery, 'tenant_revenue', function ($join) { $join->on('tenants.id', '=', 'tenant_revenue.tenant_id'); }) ->select('tenants.*', 'tenant_revenue.total_revenue') ->orderByDesc('tenant_revenue.total_revenue') ->get(); }
This approach keeps our memory footprint constant, regardless of whether we have 100 or 1,000,000 transactions, because the database filters the data before it ever reaches our application.
| Method | Approach | Performance | Memory Usage |
|---|---|---|---|
| Collection/Map | Pull all records into PHP | Poor | High |
| Correlated Subquery | Query inside select() | Moderate | Low |
| joinSub | Temp table join | Excellent | Low |
Refactor an existing report in your project that currently uses a foreach loop to calculate totals. Replace the logic by creating a dedicated method using joinSub.
SUM, COUNT, AVG).->joinSub($subQuery, 'alias', ...) to attach it to your main query.DB::enableQueryLog().joinSub. If your application grows, you might end up joining multiple subqueries, and ambiguous column names will cause SQL errors.tenant_id) aren't indexed, the database will perform a full table scan. Refer back to our Database Indexing Strategies to ensure your join columns are covered.JOIN statements. If a withCount or withSum suffices, use those; they are more readable and maintainable.By pushing logic to the database, you reduce the bridge between your app and the data, which is the single most effective way to scale a high-traffic Laravel application. We've moved beyond simple CRUD; we are now treating the database as a powerful processing engine.
Up next: We will dive into Raw Expressions for Performance, where we'll learn when and how to drop down to native SQL to squeeze out every drop of performance from our queries.
Master SQL indexing for joins by learning to analyze execution plans and build covering indexes that eliminate table scans in high-traffic Laravel applications.
Read moreMaster database indexing strategies to boost performance. Learn to analyze execution plans, implement composite indexes, and optimize complex search queries.
Advanced Subqueries and Joins
Custom Middleware Development
Database Connection Pooling
Handling Large Data Exports
Security Header Configuration
Database Sharding Concepts
Real-time Data Synchronization
Database Deadlock Prevention
Managing Third-Party API Integrations