Eloquent queries: null checks, exists, count and columns

The Eloquent ORM shipped with Laravel gives you an easy way to talk to your database. It simplifies every CRUD operation (create, read, update and delete) and every other query you need. In this tutorial you will learn how to check whether a given column is NULL, whether a record exists, how to count rows and how to select only the columns you need.

Eloquent queries: null checks, exists, count and columns
Quick answer

To test for a null value, use whereNull() and whereNotNull(). To find out whether a row exists, use exists() rather than count() > 0: the database returns a boolean instead of counting. On a collection that is already loaded, test isEmpty() and never if ($collection), which is always true.

Four questions come round again and again as soon as you write Eloquent queries: is this column null, does this record exist, how many are there, and how do you fetch only the columns you need. This guide covers all four, with the SQL actually emitted and the pitfalls that cost you one query too many.

This article is part of the web development path. Every example was run on Laravel 13.30.1 with PHP 8.4.25, against a tasks table holding the columns id, title, assignee (nullable), active, priority and the timestamp columns, whose absence causes an error on the very first save, and whose conversion into Carbon objects is automatic.

Testing whether a column is null: whereNull and whereNotNull

In SQL you write IS NULL and IS NOT NULL, because = NULL never returns true. Eloquent exposes both conditions as methods:

php
// the tasks with no assignee
Task::whereNull('assignee')->get();

// the tasks that have one
Task::whereNotNull('assignee')->get();

The SQL produced is exactly what you would expect:

sql
select * from "tasks" where "assignee" is not null

The same thing works on the query builder, without going through a model:

php
use Illuminate\Support\Facades\DB;

DB::table('tasks')->whereNull('assignee')->get();

You can pass an array to test several columns at once, the condition then applying to all of them:

php
Task::whereNull(['assignee', 'closed_at'])->get();
A myth worth correcting

You often read that where('assignee', null) does not work. That has been false for a long time: Laravel spots the null value and generates is null. Checked on Laravel 13, Task::where('assignee', null) produces select * from "tasks" where "assignee" is null and returns the same rows as whereNull(). Prefer whereNull() all the same: the intent is explicit, and it does not rely on that normalisation.

The variants follow the usual builder logic, with or and with negation:

php
Task::whereNull('assignee')->orWhereNotNull('closed_at')->get();

Testing whether a record exists

To find out whether a row matches, do not go and fetch it. exists() asks the database for a boolean:

php
if (Task::where('title', 'Publier')->exists()) {
    // at least one row matches
}

if (Task::where('title', 'Publier')->doesntExist()) {
    // no rows at all
}

The difference with count() shows up in the query log. Here is what the three forms actually send to the database:

sql
-- exists()
select exists(select * from "tasks" where "active" = ?) as "exists"

-- count() > 0
select count(*) as "aggregate" from "tasks" where "active" = ?

-- get()->isNotEmpty()
select * from "tasks" where "active" = ?

All three answer the question, but the third one fetches every row and hydrates it into an object, only to throw the contents away straight afterwards. On a large table, that is the difference between a query of constant cost and one that grows with the data.

Creating only if the record does not exist

The “check, then insert” pair has a name: firstOrCreate(). The first array does the searching, the second supplies the values used only when a record is created.

php
$post = Post::firstOrCreate(
    ['slug' => $slug],                                  // search criteria
    ['title' => $title, 'body' => $body],               // used only when creating
);

If the row already exists, the second array is ignored: the record comes back as it is, with no write. Two variants round out the family:

php
// same logic, but without saving to the database
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists;   // false until you call save()

// finds and updates, or creates
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);
This is not atomic

firstOrCreate() runs a SELECT and then an INSERT. Two concurrent requests can clear the SELECT at the same time and attempt two inserts. Put a unique constraint on the column in the database, through a migration: it is that constraint, not the PHP code, that guarantees uniqueness.

Counting, and spotting an empty collection

Two different count() methods need telling apart. On the query builder it is a COUNT(*) run by the database. On a collection that is already loaded, it is a plain count in memory.

php
// COUNT(*) in the database, no rows fetched
Task::where('active', true)->count();      // 3

// the rows are loaded, then counted in PHP
$tasks = Task::where('active', true)->get();
$tasks->count();                           // 3

If you need the rows anyway, the second form is the right one: do not fire off a counting query on top. If all you want is the number, take the first.

To test whether a collection is empty, two explicit methods:

php
$tasks = Task::where('title', 'Inexistant')->get();

$tasks->isEmpty();      // true
$tasks->isNotEmpty();   // false
An empty collection is not falsy

if ($tasks) is always true, even when the collection holds nothing: it is an object. The test has to go through isEmpty(), isNotEmpty() or blank(). This is the most common mistake in this section.

Note in passing that isEmpty() takes no argument. You often come across $posts->isEmpty($posts), which works by accident because the argument is ignored.

In a Blade view, @forelse handles the empty case without an explicit condition:

resources/views/tasks/index.blade.php
@forelse ($tasks as $task)
    <li>{{ $task->title }}</li>
@empty
    <li>Aucune tâche pour le moment.</li>
@endforelse

Selecting only the columns you need

By default, Eloquent does a select *. On a wide table, or when you only display two fields, that is transfer and hydration for nothing. Three forms let you restrict the columns:

php
// 1. select() on the builder
Task::select('title', 'priority')->where('active', true)->get();

// 2. as an argument to get() or first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);

// 3. adding to an existing selection
Task::select('id')->addSelect('title')->first();

When you only want one value, two methods spare you a full model:

php
// a single scalar value
Task::where('assignee', 'Damien')->value('title');   // 'Écrire le guide'

// a list of values
Task::where('active', true)->pluck('title');         // ['Écrire le guide', 'Relire']

// a list keyed by another column
Task::pluck('title', 'id');                          // [1 => 'Écrire le guide', …]

value() is a clear improvement on the first(['title'])->title written out of habit: it returns the string directly, and null when no row matches, where the other form errors on a null object.

A column left out of the select is absent, not null

After a first(['title']), the priority attribute does not exist on the model. Reading it returns null with no error, which hides the problem until it reaches the page. Remember to include the primary key in your selection too: without id, relationships and save() can no longer find the row.

Common errors

if ($collection) is always true An empty collection is still an object, so it evaluates to true. Test isEmpty(), isNotEmpty() or blank().
firstOrCreate() is not atomic A SELECT and then an INSERT: two concurrent requests can insert twice. Add a unique constraint in the database.
A column left out of the select is absent, not null After first(['title']), reading priority returns null with no error. Always include the primary key, or relationships and save() stop working.
get()->count() fetches everything If all you need is the number, call count() on the builder: it is a COUNT(*) that fetches no rows at all.
isEmpty() takes no argument $posts->isEmpty($posts) shows up often and works by accident, since the argument is ignored.

EloquentLaravelPHPSQL

Damien Flandrin Web developer since 2010, creator of Gekkode and Email Impact. Every article is tested on a real project before publication. Contact
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.