A common pattern when working with Laravel is to query some record existence in the DB. It can be something as simple as checking if an email is already taken or a little more complex like ensuring the user doesn't have any unpaid invoices or that the coupon is still valid.
The most common pattern I see people use (including myself) is Model::where(...)->count() > 0. While this works, its not the best approach, since you are forcing your database engine to do more work than needed. Instead, Eloquent provides a neat and clean helper for such cases called exists().
Table of Contents
The Flawed Approach: count() > 0
Here is the syntax developers commonly write for checking whether the record exist or not:
// forces full table/index scan
if (Order::where('user_id', $user->id)->where('status', 'unpaid')->count() > 0) {
// User has unpaid orders
}
This generates the following SQL:
SELECT COUNT(*) FROM `orders` WHERE `user_id` = 12 AND `status` = 'unpaid';
When MySQL or PostgreSQL processes COUNT(), they cannot stop upon finding the first record matching the WHERE clause. They have to traverse through all the matching rows to find the exact aggregated sum. If the customer has 5000 unpaid records, the DB will look through all 5000 rows so that PHP could simply throw the number away and check > 0.
The Optimized Way: exists()
The exists() method terminates early and returns a clean boolean directly to PHP:
// Clean and fast: stops at the very first match
if (Order::where('user_id', $user->id)->where('status', 'unpaid')->exists()) {
// User has unpaid orders
}
You can also call exists() directly on relationship query builders without loading child models into memory:
if ($user->orders()->where('status', 'unpaid')->exists()) {
// Relationship check without hydrating models
}
Checking the Inverse: doesntExist()
Instead of prefixing ! to negate the condition, Laravel provides an expressive companion method called doesntExist() opposite to exitsts():
// Check if an email is not taken yet
if (User::where('email', $incomingEmail)->doesntExist()) {
// Proceed with registration
}
SQL Execution Under the Hood
When you call exists(), Laravel translates the query into an optimized SQL wrapper:
SELECT EXISTS(
SELECT 1 FROM `orders` WHERE `user_id` = 12 AND `status` = 'unpaid'
) AS `exists`;
The database engine halts its index traversal the millisecond it finds a single matching row, instantly returning 1 (true) or 0 (false).
| Method | Database Behavior | Return Type | Performance |
|---|---|---|---|
count() > 0 |
Scans and tallies every matching index row. | int evaluated in PHP |
Slow on medium-to-large datasets. |
exists() |
Short-circuits at the first found record. | bool directly from SQL |
Optimal (O(1) lookups on indexed fields). |
first() != null |
Fetches all model columns and hydrates a PHP model. | Model|null |
Wasteful memory allocation for a boolean check. |
Summary
When all you need is a true/false response, don't do count() > 0 or first(). Use exists() and doesntExist() so your database can short-circuit on the first record found!
Thank you for reading this article 😊
For any query, do not hesitate to comment 💬