Reader Stacks

How to Use LEFT JOIN in Laravel Eloquent

leftJoin() syntax, why it returns null columns instead of dropping rows, and the specific case where it behaves differently from a whereHas() relationship query.

How to Use LEFT JOIN in Laravel Eloquent

A LEFT JOIN keeps every row from the left (first) table even when there's no matching row on the right — the columns from the missing side come back as null instead of the row disappearing entirely, which is exactly what makes it different from an inner join or a whereHas() relationship filter.

Basic syntax

$users = DB::table('users')
    ->leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.*', 'orders.total as order_total')
    ->get();

Users with no orders still appear in the result, with order_total as null — this is usually the entire reason to reach for a LEFT JOIN instead of a normal relationship query: you want the "zero" case represented, not filtered out.

The same thing with the Eloquent query builder

$users = User::leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.*', DB::raw('COUNT(orders.id) as order_count'))
    ->groupBy('users.id')
    ->get();

Why leftJoin() and whereHas() give different results

This is the mistake that actually costs people time: whereHas('orders') filters users down to only those with at least one order — the opposite of what a LEFT JOIN is usually used for:

// Only users who HAVE orders
User::whereHas('orders')->get();

// Every user, with order data where it exists
User::leftJoin('orders', 'users.id', '=', 'orders.user_id')->get();

// Only users with NO orders at all
User::whereDoesntHave('orders')->get();

If the actual goal is "users with no orders," whereDoesntHave() is both simpler and clearer than a LEFT JOIN plus a WHERE order_id IS NULL filter — reach for the join specifically when you need the joined columns themselves, not just a filter based on the relationship's existence.

Watch for column name collisions

If both tables have an id or created_at column, an unqualified select('*') will silently let one overwrite the other in the result. Always qualify ambiguous columns explicitly, as in the examples above (users.*, not a bare *).

Topics: Database Queries & Eloquent