Back to Blog
Lesson 8 of the Advanced Laravel: Architecture, Scaling & Performance course
LaravelJune 27, 20264 min read

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.

LaravelSQLDatabasePerformanceEloquentArchitecturephpbackend

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.

PHP
use 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.

PHP
public 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

MethodApproachPerformanceMemory Usage
Collection/MapPull all records into PHPPoorHigh
Correlated SubqueryQuery inside select()ModerateLow
joinSubTemp table joinExcellentLow

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.

  1. Identify the aggregate calculation (e.g., SUM, COUNT, AVG).
  2. Write the subquery in a separate variable.
  3. Use ->joinSub($subQuery, 'alias', ...) to attach it to your main query.
  4. 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 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.

Similar Posts