In relational databases you often have to find parent records that don't have any related children records. For example, you may want to find all inactive users who have not made an order, or articles that were published without tags, or empty categories to delete them from navigation menu.
The simplest way to do this is to use whereDoesntHave() or write a LEFT JOIN query with IS NULL condition for a foreign key. But Eloquent provides a cleaner solution for such cases and named it doesntHave().
Table of Contents
The Traditional Approaches
When searching for records which does not have a specific relationship, developers mostly write queries like these:
// Approach 1:
$users = User::whereDoesntHave('orders')->get();
// Approach 2:
$users = User::has('orders', '<', 1)->get();
// Approach 3:
$users = User::leftJoin('orders', 'users.id', '=', 'orders.user_id')
->whereNull('orders.id')
->select('users.*')
->get();
Although all these above approaches will work, but these are little complex and might make the query more cluttered.
The Clean Solution: doesntHave()
Laravel's doesntHave() method is the direct alternative to has(). It queries the model based on the complete absence of a relationship:
use App\Models\User;
// Retrieve all users who have zero orders
$usersWithoutOrders = User::doesntHave('orders')->get();
Eloquent converts this into a faster NOT EXISTS subquery under the hood:
SELECT * FROM `users`
WHERE NOT EXISTS (
SELECT 1 FROM `orders` WHERE `users`.`id` = `orders`.`user_id`
);
Querying Nested Relationships
doesntHave() also supports multi-level relationships. For example, to find all authors who have written posts, but none of those posts have received any comments:
use App\Models\Author;
// Authors whose posts have received zero comments
$authors = Author::doesntHave('posts.comments')->get();
You can also chain multiple calls together:
// Users who have no orders AND have no support tickets
$idleUsers = User::doesntHave('orders')
->doesntHave('tickets')
->get();
doesntHave() vs whereDoesntHave()
Understanding the difference between these two methods keeps your query code readable and intentional:
| Method | Accepts Constraints? | Primary Use Case |
|---|---|---|
doesntHave('orders') |
❌ No (relation name only) | Checking for pure absence of any related record. |
whereDoesntHave('orders', fn...) |
✅ Yes (closure required) | Checking absence of records matching specific criteria (e.g. no paid orders). |
If you need conditions on the related table (e.g., users who have no orders where status = 'completed'), use whereDoesntHave(). If you are strictly checking for the existence of any related records at all, doesntHave() is the simpler option.
Summary
Whenever you need to filter out the ones without children, don't do manual joins or use some unconstrained whereDoesntHave() calls, but utilize doesntHave('relation') to generate a clean, self-documenting query that under the hood performs a NOT EXISTS subquery.
Thank you for reading this article 😊
For any query, do not hesitate to comment 💬