Advanced Subqueries and Joins for Laravel Performance
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.
The Performance Cost of Application-Layer Logic
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.
Understanding Nested Subqueries
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.
Real-World Optimization with joinSub
Let’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.
Comparison: Eloquent vs. JoinSub
| 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 |
Hands-on Exercise
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.
- Identify the aggregate calculation (e.g.,
SUM,COUNT,AVG). - Write the subquery in a separate variable.
- Use
->joinSub($subQuery, 'alias', ...)to attach it to your main query. - Verify the performance improvement by checking the query log via
DB::enableQueryLog().
Common Pitfalls
- Alias Collision: Always provide a descriptive alias for your
joinSub. If your application grows, you might end up joining multiple subqueries, and ambiguous column names will cause SQL errors. - Missing Indexes: Even with a perfect subquery, if the join keys (e.g.,
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. - Over-complication: Don't use subqueries for logic that can be handled by standard Eloquent relationships or simple
JOINstatements. If awithCountorwithSumsuffices, 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.
Work with me

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.