Beyond a plain where('column', 'value'), Eloquent's query builder has a specific method for most common filtering patterns — reaching for the dedicated method is both more readable and, in a couple of cases below, avoids a genuine correctness bug that the naive approach falls into.
1. whereIn and whereNotIn
Product::whereIn('category_id', [1, 4, 7])->get();
Product::whereNotIn('status', ['archived', 'deleted'])->get();
Equivalent to chaining several orWhere('category_id', $id) calls, but far more concise for a list of values, and it maps to a single SQL IN (...) clause rather than a chain of ORs.
2. whereNull and whereNotNull
User::whereNull('email_verified_at')->get(); // never verified
User::whereNotNull('deleted_at')->withTrashed()->get(); // soft-deleted
A plain where('column', null) doesn't work reliably for this — SQL's NULL comparison semantics mean = NULL isn't equivalent to IS NULL. whereNull() generates the correct SQL for a null check specifically.
3. Multiple where() conditions — implicit AND
Order::where('status', 'completed')
->where('total', '>', 100)
->get();
Chained where() calls combine with AND by default — this is the most common pattern and needs no special syntax.
4. orWhere — and the grouping trap
// BUG: this doesn't do what it looks like it does
Order::where('status', 'completed')
->orWhere('status', 'processing')
->where('total', '>', 100)
->get();
This reads as "completed or processing, both over $100" — but SQL's operator precedence means it actually evaluates as status = 'completed' OR (status = 'processing' AND total > 100), which returns every completed order regardless of total. This is a genuine, easy-to-miss bug, not just a style issue.
// Correct: group the OR conditions explicitly
Order::where(function ($query) {
$query->where('status', 'completed')
->orWhere('status', 'processing');
})
->where('total', '>', 100)
->get();
Passing a closure to where() wraps its contents in parentheses in the generated SQL — this is the correct way to group a set of OR conditions so they're evaluated together, before being combined with an outer AND.
5. whereBetween and whereNotBetween
Order::whereBetween('total', [50, 200])->get();
Order::whereBetween('created_at', [$startDate, $endDate])->get();
6. whereColumn — comparing two columns on the same row
Order::whereColumn('shipped_at', '>', 'promised_at')->get(); // shipped late
whereColumn() is specifically for comparing two columns against each other, rather than a column against a fixed value — a plain where('shipped_at', '>', 'promised_at') would incorrectly treat 'promised_at' as a literal string value to compare against, not as a reference to another column.
7. Combining several of these together
Order::whereIn('status', ['completed', 'processing'])
->whereNotNull('shipped_at')
->whereBetween('created_at', [now()->subMonth(), now()])
->get();
All of these compose naturally in one chain — each adds its own clause to the same underlying query, combined with AND unless explicitly grouped otherwise as shown above.