Model::whereIn

Constrains a query to rows whose column value appears in a given array or subquery.

Signature
public function whereIn(string|\Illuminate\Contracts\Database\Query\Expression $column, mixed $values, string $boolean = 'and', bool $not = false): $this

Parameters

ParameterTypeDescription
$columnstring|\Illuminate\Contracts\Database\Query\ExpressionThe column whose value must appear in the set.
$valuesmixedA flat array, an Arrayable such as a Collection, or a query builder instance used as a subquery.
$booleanoptionalstring= 'and'How this clause joins the previous one — 'and' or 'or'.
$notoptionalbool= falseWhen true, compiles to NOT IN. This is how whereNotIn() is implemented.

Return value

$thisThe same builder instance, so calls can be chained.

Errors

Throws InvalidArgumentException with the message "Nested arrays may not be passed to whereIn method" when the values array contains arrays.

whereIn() verifies that a column's value is contained within the given set. The set can be a plain array, any Arrayable (an Eloquent Collection works directly), or a query builder that becomes a correlated-free subquery.

The subquery form

Passing a builder as the second argument compiles the inner query into the IN (...) parentheses rather than executing it first. That keeps the whole thing in one round trip and avoids pulling a potentially huge id list into PHP just to send it back out. Comment::whereIn('user_id', User::select('id')->where('is_active', 1)) becomes where user_id in (select id from users where is_active = 1).

What trips people up

An empty array is legal and matches nothing — the grammar emits 0 = 1. That is technically correct but it silently produces an empty result when the array was empty by accident, so guard it if an empty set should mean "no filter".

Nested arrays throw. If you pass $users->pluck('id') from a nested structure, or an array of rows rather than an array of scalars, you get an InvalidArgumentException rather than a query.

Each value becomes a bound parameter, so very large arrays can hit driver placeholder limits and consume real memory. For big lists of integers, whereIntegerInRaw() is the documented escape hatch.

Example

Example · php
<?php

use App\Models\User;
use App\Models\Comment;

// A flat array of values.
$users = User::whereIn('id', [1, 2, 3])->get();
// select * from users where id in (?, ?, ?)

echo $users->count(), "\n";

// The inverse.
$others = User::whereNotIn('id', [1, 2, 3])->get();

// A builder as the second argument becomes a subquery.
$activeUsers = User::select('id')->where('is_active', 1);

$comments = Comment::whereIn('user_id', $activeUsers)->get();
// select * from comments where user_id in (
//     select id from users where is_active = 1
// )

echo $comments->count(), "\n";

When to use it

  • Load a batch of models for a set of ids collected from a queue payload.
  • Restrict a listing to the statuses a role is permitted to see.
  • Filter child records to the parents returned by an earlier subquery without a join.

Frequently asked questions

What happens when I pass an empty array to whereIn()?

The clause compiles to 0 = 1, so the query matches no rows and returns an empty collection without any error. This is logically correct — nothing is a member of the empty set — but it means an accidentally empty filter list produces a blank page rather than an unfiltered one. Guard with when() or an empty() check if a missing selection should mean "apply no filter".

Can I pass a subquery to whereIn() instead of an array?

Yes — pass a query builder or an Eloquent builder as the second argument and Laravel compiles it into the IN parentheses, producing where column in (select ...). This runs as a single statement, so the candidate ids never travel through PHP. Use select() on the inner query to return exactly one column, otherwise the database rejects the subquery.

Why do I get "Nested arrays may not be passed to whereIn method"?

The values array contained an array rather than only scalars, which whereIn() rejects because each element must become a single bound parameter. It usually comes from passing rows instead of ids — use pluck('id') or modelKeys() to flatten a collection of models, and Arr::flatten() if a map or groupBy left you with nested structure.

More examples

Empty arrays and the guard you need

The first count is zero and no error is raised — this is the behaviour that produces "my page went blank" reports. The second and third counts cover the whole table because the clause is skipped when the list is empty.

Example · php
<?php

use App\Models\Product;

$ids = [];   // e.g. a filter the user left blank

// Compiles to: select * from products where 0 = 1
echo Product::whereIn('id', $ids)->count(), "\n";   // 0

// If "no selection" should mean "no filter", say so explicitly.
$query = Product::query();

if (! empty($ids)) {
    $query->whereIn('id', $ids);
}

echo $query->count(), "\n";   // every product

// when() expresses the same guard fluently.
echo Product::when($ids, fn ($q, $ids) => $q->whereIn('id', $ids))
    ->count(), "\n";

Collections, models, and nested arrays

Both working forms produce the same count: modelKeys() and pluck('id') each yield scalars, and the Collection is converted via toArray() internally. The commented line is the InvalidArgumentException you hit when a map or groupBy leaves you with an array of arrays.

Example · php
<?php

use App\Models\Order;
use App\Models\User;

$vips = User::where('tier', 'vip')->get();

// A Collection is Arrayable - pluck the scalar keys first.
$orders = Order::whereIn('user_id', $vips->modelKeys())->get();
echo $orders->count(), "\n";

// pluck() returns a Collection, which whereIn accepts directly.
$orders = Order::whereIn('user_id', $vips->pluck('id'))->get();
echo $orders->count(), "\n";

// This throws: Nested arrays may not be passed to whereIn method.
// Order::whereIn('user_id', [[1, 2], [3, 4]])->get();

Subquery versus two round trips

The first approach binds one placeholder per customer, which is where drivers start rejecting queries and memory climbs. The subquery form keeps the id set inside the database entirely, and whereIntegerInRaw inlines validated integers when you genuinely must send the list from PHP.

Example · php
<?php

use App\Models\Order;
use App\Models\Customer;

// Two queries, and the whole id list travels through PHP.
$ids = Customer::where('country', 'GB')->pluck('id');
$orders = Order::whereIn('customer_id', $ids)->get();

echo $ids->count(), " ids bound\n";

// One query - the database never hands the ids to PHP.
$orders = Order::whereIn(
    'customer_id',
    Customer::select('id')->where('country', 'GB')
)->get();

echo $orders->count(), "\n";

// Large integer lists: cheaper bindings, no placeholders.
$orders = Order::whereIntegerInRaw('customer_id', $ids->all())->get();

Discussion

  • Be the first to comment on this lesson.