LaravelでのUnionクエリの実装方法と活用例:複数クエリ結果の統合分析

基本文法・構文ガイド

LaravelでUNIONするには、クエリビルダの union() メソッドを使います。最初のクエリを変数に保存し、->union($secondQuery) で別のクエリを結合するだけです。重複行を含めたい場合は unionAll() を使います。

Laravelを利用していると、データベースクエリをより簡単かつ効率的に扱うことができます。特に複雑なクエリに対処する際、Eloquent ORMやクエリビルダが役立ちます。その中で、Unionクエリは非常に強力なツールです。この記事では、LaravelでのUnionクエリの実装方法と活用例について詳しく解説します。

Unionクエリとは

Unionクエリは、複数のSELECT文の結果を一つにまとめるSQLクエリです。この特長により、同じデータセットに対して異なる視点や条件で取得されたデータを統合し、より包括的な分析が可能となります。

union() と unionAll() の違い

LaravelのUnionクエリには union()unionAll() の2種類があります。違いは重複行の扱いです。

メソッド 重複行 対応するSQL 使いどころ
union() 除去する UNION 重複を排除してユニークな結果が欲しいとき
unionAll() 保持する UNION ALL 全件数が必要なとき・重複行も集計対象のとき

一般的に UNION ALL は重複チェックが不要な分だけ高速です。重複が発生しないことが保証されている場合は unionAll() を選ぶとパフォーマンスが向上します。

2つのクエリを結合する最小例

最もシンプルな実装は次のとおりです。

$first = DB::table('users')->select('name', 'email');
$second = DB::table('admins')->select('name', 'email');

$results = $first->union($second)->get();

union() はチェーン前のクエリ($first)に対して呼び出し、引数に結合したい2つ目のクエリ($second)を渡します。結果は1つのコレクションとして返ります。

カラム数と型を合わせる必要がある

SQLの仕様上、UNIONするクエリはすべてカラム数・順序・データ型が一致している必要があります。一致していない場合はデータベースエラーが発生します。

// NG: カラム数が異なる
$first  = DB::table('users')->select('name', 'email');
$second = DB::table('admins')->select('name'); // カラムが1つ少ない

// OK: カラム数を揃える
$first  = DB::table('users')->select('name', 'email');
$second = DB::table('admins')->select('name', DB::raw("'' as email"));

型の差異はデータベースが暗黙変換することが多いですが、明示的に型を揃えることで予期しない挙動を防げます。

LaravelでのUnionクエリの基本実装

Laravelでは、クエリビルダを使用して簡単にUnionクエリを実装することができます。以下は基本的な使用例です。

$firstQuery = DB::table('users')
                ->select('name', 'email');

$secondQuery = DB::table('customers')
                 ->select('name', 'email')
                 ->where('active', 1);

$unionQuery = $firstQuery->union($secondQuery)->get();

この例では、usersテーブルとcustomersテーブルの名前とメールアドレスを統合しています。unionメソッドを使用することにより、異なるテーブルのデータを一つの結果セットとして取得できます。

活用例1: モデル間の比較

Unionクエリを使用すると、異なるモデル間で共通のフィールドを比較するのが容易になります。例えば、同じプラットフォーム上でのアクティブなユーザーと顧客のリストを取得し、統合したデータセットを分析することができます。

$activeUsers = DB::table('users')
                 ->select('id', 'name', 'type')
                 ->where('status', 'active');

$activeCustomers = DB::table('customers')
                      ->select('id', 'name', 'type')
                      ->where('status', 'active');

$combinedActive = $activeUsers->union($activeCustomers)->get();

このクエリは、アクティブなユーザーと顧客を統合し、typeフィールドを保持することで、それぞれがどのテーブル出身であるかを示します。

活用例2: orderBy() を組み合わせる際の注意点

Unionクエリに orderBy() を追加することは可能ですが、重要な注意点があります。

$sortedResults = $activeUsers
                  ->union($activeCustomers)
                  ->orderBy('name')
                  ->get();

union() の後に orderBy() をチェーンすると、生成されるSQLでは各サブクエリにORDER BYが付くのではなく、UNION全体を囲むSELECTにORDER BYが付きます。意図した並び順になっているか toSql() で確認することを推奨します。

また、UNION内の各サブクエリに orderBy() を付けても、多くのデータベースでは無視されます。ソートはUNION後にまとめて行うのが正しいパターンです。

// UNION後にまとめてソートする(推奨)
$results = $activeUsers
            ->union($activeCustomers)
            ->orderByDesc('created_at')
            ->get();

活用例3: Devへの進化的レポート作成

複数のデータセットを統合して、発展的なレポートを作成することも可能です。例えば、異なる部門の売上データを一つのレポートにまとめ、部署ごとのパフォーマンスを分析する場合などです。

$marketingSales = DB::table('sales')
                    ->select('department', 'total_sales')
                    ->where('department', 'marketing');

$developmentSales = DB::table('sales')
                       ->select('department', 'total_sales')
                       ->where('department', 'development');

$combinedSales = $marketingSales
                  ->union($developmentSales)
                  ->groupBy('department')
                  ->selectRaw('department, SUM(total_sales) as total_sales')
                  ->get();

この実装例では、マーケティングと開発の売上データを統合し、部門ごとの総売上を集計しています。

Union Allの活用

標準のUnionクエリでは重複した行が削除されますが、unionAllを使うことで、重複行を含むすべての結果を統合することもできます。

$duplicateIncludedResults = $firstQuery
                            ->unionAll($secondQuery)
                            ->get();

このように、Duplicationを必要とする場面でもunionAllを用いることで、全てのデータを俯瞰できます。

toSql() で発行されるSQLを確認する

Unionクエリが期待通りのSQLを生成しているか確認するには toSql() が便利です。

$first  = DB::table('users')->select('name', 'email');
$second = DB::table('admins')->select('name', 'email');

$sql = $first->union($second)->orderBy('name')->toSql();

// 出力例:
// (select `name`, `email` from `users`)
// union
// (select `name`, `email` from `admins`)
// order by `name` asc
echo $sql;

toSql() はクエリを実行せずにSQL文字列を返します。開発・デバッグ中に実行前のSQLを目視確認することで、予期しないクエリの発行を防げます。バインディングの値も確認したい場合は getBindings() を組み合わせてください。

$query = $first->union($second);

echo $query->toSql();
print_r($query->getBindings());

Eloquentモデルでのunion

クエリビルダだけでなく、Eloquentモデルの Builder インスタンス同士でも union() は利用できます。DB::table() の代わりにモデルの ::query() を起点にすることで、Eloquentの属性キャストやアクセサをそのまま活かせます。

$users = User::query()->select('id', 'name')->where('status', 'active');
$admins = Admin::query()->select('id', 'name')->where('status', 'active');

$results = $users->union($admins)->get();

ただし union() したEloquentクエリを get() すると返るのは通常の Collection(モデルインスタンスではなく生の属性を持つオブジェクト)になる点に注意してください。モデルのメソッドやリレーションをそのまま使いたい場合は、union() の結果からIDを取得し、改めて whereIn() でモデルを取得し直す方法が安全です。

UNIONクエリとページネーション

union() の結果に対しても paginate() は利用できますが、内部的にはUNION全体をラップしたクエリで COUNTLIMIT/OFFSET を発行するため、件数が多いテーブル同士を結合すると重くなりがちです。

$results = $activeUsers
            ->union($activeCustomers)
            ->orderByDesc('created_at')
            ->paginate(20);

ページ番号が大きくなるほどOFFSETのコストが増えるため、大量データを扱う一覧画面では cursorPaginate() の利用や、UNION対象テーブルへの絞り込み条件(where)を先に効かせて母数を減らす工夫が有効です。

パフォーマンス上の注意点

  • インデックス:UNIONする各SELECTの WHERE 句・ORDER BY 句に使うカラムには、それぞれ個別にインデックスが必要です。UNION自体はインデックスを最適化してくれません。
  • union() の重複除去コストunion() は内部的にソート+重複排除を行うため、大量行を結合する場合は unionAll() より遅くなります。重複が発生しない設計なら unionAll() を選びましょう。
  • テーブル数が増えるとN回分のスキャンが発生union() をチェーンで複数回呼ぶと、その数だけSELECTが実行されます。3つ以上のテーブルを頻繁に結合する場合は、設計段階で正規化やビューの活用も検討してください。

よくある質問(FAQ)

LaravelでUNIONするには?

クエリビルダで1つ目のクエリを作り、->union($secondQuery) で2つ目のクエリを結合し、最後に ->get() で取得します。両クエリのカラム数と順序を合わせることが必須です。

$results = DB::table('users')->select('name', 'email')
             ->union(
                 DB::table('admins')->select('name', 'email')
             )
             ->get();

unionとunionAllの違いは?

union() は結合後に重複行を除去します(SQL の UNION)。unionAll() は重複行をそのまま残します(SQL の UNION ALL)。重複がないことが分かっている場合や全件数が必要な場合は unionAll() の方が高速です。

union後にorderByできる?

できます。->union($second)->orderBy('column') のようにチェーンするだけです。ただし、orderByはUNION全体の最終結果に対してかかります。各サブクエリへのorderByは多くのDBで無視されるため、ソートはUNION後にまとめて指定してください。

関連記事

まとめ

LaravelでUnionクエリを使用することにより、様々なデータセットを効果的に統合して分析することが可能です。この機能は、特に複雑なデータ分析シナリオにおいて非常に有用です。ここで紹介した基本テクニックと活用例を活かし、プロジェクトにおけるデータ処理を効率化しましょう。Unionクエリをマスターすることで、データ分析能力がさらに向上すること間違いありません。

レン (Wren)

こんにちは。レンです。

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

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

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

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

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

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

コメント