Laravel whereInの使い方完全ガイド|配列・サブクエリ・whereNotInとの使い分けと大量データ対策

基本文法・構文ガイド

LaravelのwhereInメソッドは、SQLのIN句を直感的なメソッドチェーンで記述できるクエリビルダ・Eloquentの基本機能です。複数のIDやステータスに一致するレコードを一括取得できるため、検索フォームの絞り込みやAPI開発で頻繁に利用されます。

しかし、「空配列を渡したときの挙動」「OR条件と組み合わせたときの優先順位」「大量データを渡した際のパフォーマンス低下やプレースホルダ上限」「渡した配列の順番でソートしたい場合」など、実務でつまずきやすいポイントが数多く存在します。

本記事では、Laravel 11・12対応の環境をもとに、whereInの基本構文からwhereNotInとの使い分け、サブクエリを使ったDB完結の絞り込み、大量配列に最適なwhereIntegerInRaw、実践的な落とし穴と対策まで、実際に動作検証したコード例付きで徹底解説します。

📌 この記事でわかること

  • whereIn / whereNotIn / orWhereIn の基本構文と発行されるSQL
  • 配列を展開せずDB内で完結させるサブクエリの書き方とパフォーマンス差
  • whereIntegerInRaw による大量整数配列の高速化とメモリ削減
  • 指定した配列の順番通りにレコードを取得・並び替える方法(FIELD関数)
  • 空配列時の挙動(WHERE 0 = 1)やNULLトラップなどの落とし穴と対策

1. whereInの基本構文とSQL変換

whereInメソッドは、指定したカラムの値が配列に含まれるいずれかの値と一致するレコードを抽出します。クエリビルダ(DB::table())およびEloquentモデルのどちらでも全く同じ構文で使用できます。

Query Builderでの基本例

use Illuminate\Support\Facades\DB;

// status_id が 1, 2, 3 のいずれかに一致するユーザーを取得
$users = DB::table('users')
    ->whereIn('status_id', [1, 2, 3])
    ->get();

toRawSql() または toSql() で生成されるSQLを確認すると、PDOのプレースホルダを用いた安全な IN 句に変換されます。

select * from `users` where `status_id` in (1, 2, 3)

Eloquentモデルでの基本例

Eloquentモデルでも同様に呼び出せます。モデルインスタンスのコレクションが返却されます。

use App\Models\User;

// 特定のロールを持つアクティブなユーザーを絞り込む
$roles = ['admin', 'manager', 'editor'];

$users = User::where('is_active', true)
    ->whereIn('role', $roles)
    ->get();

生成されるSQL:

select * from `users` where `is_active` = 1 and `role` in ('admin', 'manager', 'editor')

他の where 句とチェーンすると、デフォルトで AND 条件として結合されます。

2. whereNotIn / orWhereIn との使い分け

Laravelには whereIn のほかにも、否定条件やOR条件を扱うための関連メソッドが用意されています。

メソッド 役割 生成されるSQLの例
whereIn('col', $arr) 配列内のいずれかに一致(AND結合) AND `col` IN (1, 2)
whereNotIn('col', $arr) 配列内のいずれにも一致しない(AND結合) AND `col` NOT IN (1, 2)
orWhereIn('col', $arr) 配列内のいずれかに一致(OR結合) OR `col` IN (1, 2)
orWhereNotIn('col', $arr) 配列内のいずれにも一致しない(OR結合) OR `col` NOT IN (1, 2)
whereIntegerInRaw('col', $arr) 大量整数配列向けのバインドなし高速IN句 AND `col` IN (1, 2, 3...)

whereNotInで除外条件を指定する

指定した配列のいずれにも該当しないレコードを取得したい場合は、whereNotIn を使用します。

// 停止中・退会済みのステータスを除外して取得
$activeUsers = User::whereNotIn('status', ['suspended', 'deleted'])->get();

生成されるSQL:

select * from `users` where `status` not in ('suspended', 'deleted')

※単一値の除外(whereNot)やNULL値に関する注意点など、除外クエリの詳しい書き方はLaravel whereNot/whereNotInの使い方と違い|除外条件の書き方と実践例で詳しく解説しています。

3. サブクエリを使ったwhereIn(DB完結の最適化)

「別テーブルの検索結果に含まれるIDに一致するレコードを取得したい」場合、一度PHP側で pluck('id') して配列化してから whereIn に渡す書き方を見かけますが、これはメモリ効率とクエリ回数の観点から非推奨です。

whereIn の第2引数にはクロージャ(サブクエリ)またはクエリビルダインスタンスを直接渡すことができます。

クロージャを使ったサブクエリ

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

// 今月注文を行ったユーザーのみを抽出する
$users = User::whereIn('id', function ($query) {
    $query->select('user_id')
          ->from('orders')
          ->where('created_at', '>=', now()->startOfMonth());
})->get();

生成されるSQL:

select * from `users` where `id` in (
    select `user_id` from `orders` where `created_at` >= '2026-08-01 00:00:00'
)

クエリビルダインスタンスを直接渡す

以下のようにEloquentのクエリビルダをそのまま渡すことも可能です。

$recentOrderUserIds = Order::where('created_at', '>=', now()->startOfMonth())
    ->select('user_id');

$users = User::whereIn('id', $recentOrderUserIds)->get();

💡 PHP配列渡し vs サブクエリ渡しの違い:
pluck('user_id')->toArray() を使うと、一度大量のID一覧がPHPのメモリ上にロードされ、さらに長大なSQL文字列としてデータベースへ再送信されます。サブクエリを渡せば、DBサーバー内だけで完結するため、ネットワーク転送量とPHPメモリ消費を最小限に抑えられます。

4. 実務で役立つ応用テクニック

① OR条件とwhereInの組み合わせ(グループ化の必須パターン)

orWhereIn を他の where 条件と混在させるときは、SQLの演算子優先順位(ANDOR より優先される)に注意が必要です。

// ❌ NG例: 意図しない条件評価
// email未認証でもstatusがactiveならヒットしてしまう
User::whereNotNull('email_verified_at')
    ->whereIn('role', ['admin', 'manager'])
    ->orWhereIn('status', ['active'])
    ->get();
// 発行SQL: WHERE email_verified_at IS NOT NULL AND role IN (...) OR status IN (...)

// ⭕ OK例: クロージャで囲んでOR条件をグループ化(丸括弧で囲む)
User::whereNotNull('email_verified_at')
    ->where(function ($query) {
        $query->whereIn('role', ['admin', 'manager'])
              ->orWhereIn('status', ['active']);
    })
    ->get();
// 発行SQL: WHERE email_verified_at IS NOT NULL AND (role IN (...) OR status IN (...))

② 多対多リレーションでのwhereIn(whereHasとの連携)

「特定のタグID群のいずれかを持つ記事一覧」など、多対多(BelongsToMany)リレーションを介した絞り込みには whereHas のクロージャ内で whereIn を利用します。

use App\Models\Post;

$tagIds = [3, 7, 12];

$posts = Post::whereHas('tags', function ($query) use ($tagIds) {
    $query->whereIn('tags.id', $tagIds);
})->get();

生成されるSQL:

select * from `posts` where exists (
    select * from `tags`
    inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id`
    where `posts`.`id` = `post_tag`.`post_id`
      and `tags`.`id` in (3, 7, 12)
)

※リレーション絞り込みの基礎についてはLaravelのwhereHasメソッドを使った効率的なクエリ構築ガイドをご覧ください。

③ 指定した配列の順番通りにレコードを取得する(FIELD関数)

SQLの IN 句は順序を保証しないため、[5, 2, 8] という配列を渡しても 1, 2, 3... の昇順で返ってくるのが一般的です。渡した配列の並び順通りに取得したい場合は、orderByRawFIELD()(MySQL/MariaDB)を組み合わせます。

$ids = [5, 2, 8, 1];

$users = User::whereIn('id', $ids)
    ->orderByRaw('FIELD(id, ' . implode(',', array_map('intval', $ids)) . ')')
    ->get();

※PostgreSQLの場合は array_position 関数(例: orderByRaw('array_position(ARRAY[' . implode(',', $ids) . '], id)'))を使用します。

④ CollectionのwhereInとの使い分け

Laravelには、データベースに対してクエリを発行する「クエリビルダ版 whereIn」と、すでにメモリ上にあるコレクションをフィルタリングする「Collection版 whereIn」の2種類があります。

// ❌ NG: DBから全件取得した後にメモリ上でwhereIn(データ量が多いとメモリ枯渇・激遅)
$users = User::all()->whereIn('status', ['active', 'pending']);

// ⭕ OK: SQLのWHERE句でDB側に絞り込ませる(必要なレコードだけ取得)
$users = User::whereIn('status', ['active', 'pending'])->get();

コレクション操作としての whereIn の詳細は、リファレンス記事whereIn — 指定キーの値が配列に含まれる要素だけを抽出およびLaravelコレクションのwhereNotInの使い方を参照してください。

5. パフォーマンスの注意点と大量データ対策

whereIntegerInRawで大量の数値ID検索を高速化

数千〜数万件のID配列を whereIn に渡すと、Laravelは配列の要素数分だけPDOのプレースホルダ(?)を生成し、バインド処理を行います。これにより、PHP側のメモリ消費とバインド処理のオーバーヘッドが肥大化します。

対象カラムが整数の場合は、Laravel公式が提供している whereIntegerInRaw(または whereIntegerNotInRaw)を使用するのがベストプラクティスです。

$largeIds = [1001, 1002, 1003, /* ... 数千件 ... */];

// PDOバインディングをバイパスし、整数にキャストしてSQL文字列に直接埋め込むため高速・省メモリ
$products = Product::whereIntegerInRaw('id', $largeIds)->get();

⚠️ 安全性について:
whereIntegerInRaw は内部で各要素を整数型((int))にバリデーション・キャストしてからSQLを生成するため、生の生クエリ(whereRaw)と異なりSQLインジェクションの危険性はありません。

MySQLプレースホルダ上限(65,535個)とarray_chunk分割

MySQLのPrepared Statementには、1クエリあたり最大65,535個のプレースホルダ制限があります。文字列を含む巨大な配列を扱う場合は、array_chunk で小分けにして処理します。

$hugeCodeList = ['CODE_A001', 'CODE_B002', /* ... 10万件 ... */];

$results = collect();

foreach (array_chunk($hugeCodeList, 1000) as $chunk) {
    $items = Product::whereIn('code', $chunk)->get();
    $results = $results->merge($items);
}

ループ内でwhereInを呼ぶ疑似N+1問題の解消

ループの中で都度 whereIn を実行すると、ループ回数分のクエリが発行されて深刻なパフォーマンス劣化を引き起こします。

// ❌ NG: ループ内でwhereInを発行(N+1問題)
foreach ($orders as $order) {
    $items = OrderItem::whereIn('order_id', [$order->id])->get();
}

// ⭕ OK: ループ外で1回のwhereInで全件取得し、groupByでグルーピング
$orderIds = $orders->pluck('id');
$itemsGrouped = OrderItem::whereIn('order_id', $orderIds)->get()->groupBy('order_id');

// または Eloquent の Eager Loading を使う(推奨)
$orders = Order::with('items')->get();

※Eager LoadingについてはLaravel with — Eloquentのリレーションをまとめて取得するメソッドで詳しく解説しています。

6. よくある落とし穴と注意点

① 空配列を渡したときの挙動(where 0 = 1)

動的に生成した配列が空([])のまま whereIn に渡されると、Laravelは構文エラーを防ぐために WHERE 0 = 1(常に偽)というSQLを生成します。

$ids = []; // 空配列

$users = User::whereIn('id', $ids)->get();
// 生成SQL: select * from `users` where 0 = 1

この挙動自体は安全ですが、「他の条件(例: where('status', 'active'))があっても全体が0件になる」ため、不要なDB問い合わせを避けるなら事前に空チェックを行って即時リターンするのが理想的です。

if (empty($ids)) {
    return collect(); // DBクエリを実行せずに空コレクションを返却
}

② whereNotIn に NULL が含まれる場合の「NULLトラップ」

SQLの仕様上、NOT IN のリスト内に NULL が1つでも含まれると、クエリ全体の結果が常に 0 件(空)になります(value != NULL の評価が UNKNOWN になるため)。

// サブクエリや配列に NULL が混ざる可能性がある場合
$excludedUserIds = [1, 2, null];

// ❌ 期待通りに除外されず、1件も取得できなくなる
$users = User::whereNotIn('id', $excludedUserIds)->get();

// ⭕ array_filter で null を取り除いてから渡す
$cleanIds = array_filter($excludedUserIds, fn($id) => !is_null($id));
$users = User::whereNotIn('id', $cleanIds)->get();

7. よくある質問(FAQ)

Q. whereInにCollectionインスタンスをそのまま渡せますか?

はい、渡せます。Laravelの whereIn は配列だけでなく Illuminate\Support\CollectionArrayable を実装したオブジェクトをそのまま受け取ることができます。->toArray() を明示的に呼ぶ必要はありません。

$categoryIds = Category::where('is_visible', true)->pluck('id'); // Collection
$products = Product::whereIn('category_id', $categoryIds)->get(); // 動作可能

Q. whereIn と複数の orWhere はどちらが高速ですか?

一般的に whereIn の方が高速です。orWhere を連続して連結するとSQL文が長くなりオプティマイザの解析コストが増加しますが、whereIn は1つの IN 句としてインデックス検索(Index Range Scan)に最適化されやすくなります。候補値が2個以上の場合は whereIn を使用してください。

Q. 大文字・小文字を区別してwhereInで検索できますか?

MySQLのデフォルト照合順序(utf8mb4_unicode_ci 等の _ci)は大文字小文字を区別しません。厳密に区別したい場合は whereIn(DB::raw('BINARY column_name'), $values) のように BINARY 演算子を指定します。

8. まとめ

Laravelの whereIn メソッドは、複数条件によるデータ抽出を簡潔かつ安全に実現するための強力なツールです。

  • 基本構文:配列を渡すだけで安全な WHERE IN (?, ?) が構築される
  • サブクエリ活用:PHP側で配列化せずクロージャを渡すことでDB内完結&メモリ削減
  • 大量整数データwhereIntegerInRaw を活用してバインド処理を省略し劇的に高速化
  • OR条件併用:クロージャでグループ化(丸括弧)して優先順位バグを防ぐ
  • 空配列・NULL対策:空配列時の 0 = 1whereNotIn のNULL混入に配慮する

適切な書き方とパフォーマンスへの配慮を身につけて、効率的でメンテナンス性の高いLaravelアプリケーションを構築しましょう。

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする

コメント