groupBy — キーやコールバックで要素をグループ化する

未分類
  • カテゴリ: collection
  • 掲載バージョン: Laravel 12・PHP 8.4
  • 名前空間 / FQCN: Illuminate\Support\Collection::groupBy
  • 関連: countBy / mapToGroups / partition / keyBy / sortBy
  • 変更履歴: なし

LaravelのgroupByは2種類ある

Laravelで「groupBy」と検索すると、2つの異なるgroupByがヒットします。混同しやすいため、最初に整理しておきます。

Collection groupBy()Query Builder / Eloquent groupBy()
処理場所PHP(メモリ上)MySQL / PostgreSQL 等のDB
対象取得済みのコレクションDBテーブルの行
実行タイミングget() 等で取得したSQLを生成するとき
戻り値Collection(キー => Collectionクエリ結果(レコード集合)
集計count() / sum() を後からPHPで実行COUNT() / SUM() をDBが実行
大量データ全件をメモリに保持するため注意DB側で集計するため効率的
典型的な使い方$users->groupBy('role')DB::table('orders')->groupBy('status')->get()

このページでは Collection の groupBy()(PHPのメモリ上で動作)を解説します。SQL の GROUP BY を使いたい場合は Query Builder の groupBy() を参照してください。

要点(TL;DR)

  • 要素を同じキー(または計算結果)ごとにネストしたコレクションへまとめる
  • 使い方:$users->groupBy('team_id')
  • 罠:
    • 既定でキーは再採番$preserveKeys=false
    • 文字列キーは dot記法user.team.id)で辿れ、見つからないと null グループに入る
    • 多段グループは ['year', 'status']順序がそのままネスト順になる

概要

groupBy は配列/オブジェクトの配列を、指定キー・クロージャ・複数条件で階層的に分類します。Eloquentの取得結果を「月×状態」「部署×職種」などでまとめ、後段で集計・整形に使うのが実務パターンです。クエリビルダの GROUP BY とは別物で、取得後のメモリ内処理です。

構文 / シグネチャ

public function groupBy(string|callable|array $groupBy, bool $preserveKeys = false): Illuminate\Support\Collection
  • 引数(表)
引数必須既定値説明
$groupBy`stringcallablearray`
$preserveKeysboolfalse各グループ内で元のキーを保持するか(false だと0始まりに再採番)
  • 戻り値Collection(キー => Collection)。配列指定時は多重のネストコレクション
  • 例外/副作用
    • コールバック内での例外はそのまま伝播
    • 文字列キーで未定義を辿ると null キーにグループ化
    • LazyCollection でも使用可だが全件を実体化するためメモリ使用量に注意

使用例

最小例

<?php

use Illuminate\Support\Collection;

$items = collect([
    ['type' => 'fruit', 'name' => 'apple'],
    ['type' => 'fruit', 'name' => 'banana'],
    ['type' => 'vegetable', 'name' => 'carrot'],
]);

$grouped = $items->groupBy('type'); // dot記法も可: 'user.team.id'

print_r($grouped->toArray());
// [
//   'fruit' => [
//     ['type' => 'fruit', 'name' => 'apple'],
//     ['type' => 'fruit', 'name' => 'banana'],
//   ],
//   'vegetable' => [
//     ['type' => 'vegetable', 'name' => 'carrot'],
//   ],
// ]

実務例:月×ステータスで売上集計

<?php

use Illuminate\Support\Collection;
use Carbon\Carbon;
use App\Models\Order;

// 例: 今月から過去6ヶ月の注文を取得
$orders = Order::query()
    ->where('created_at', '>=', now()->subMonths(6)->startOfMonth())
    ->get(['id', 'status', 'amount', 'created_at']);

$grouped = $orders->groupBy([
    fn ($o) => $o->created_at->format('Y-m'), // 月
    'status',                                 // ステータス
]);

// 各月×ステータスの件数と売上合計を算出
$summary = $grouped->map(fn ($byMonth) => $byMonth->map(function ($ordersByStatus) {
    return [
        'count' => $ordersByStatus->count(),
        'total_amount' => $ordersByStatus->sum('amount'),
    ];
}));

// 例: 2025-09 の paid 合計
$paidSep = data_get($summary, '2025-09.paid.total_amount', 0);

キー保持(preserveKeys)

$grouped = $items->groupBy('type', preserveKeys: true);
// 各グループ内で、元配列のキーを保持

map() / pluck() / count() との組み合わせ

<?php

use Illuminate\Support\Collection;

$orders = collect([
    ['status' => 'paid',    'amount' => 1000],
    ['status' => 'pending', 'amount' => 500],
    ['status' => 'paid',    'amount' => 2000],
    ['status' => 'failed',  'amount' => 300],
    ['status' => 'pending', 'amount' => 700],
]);

$grouped = $orders->groupBy('status');

// count(): ステータスごとの件数
$counts = $grouped->map(fn ($g) => $g->count());
// ['paid' => 2, 'pending' => 2, 'failed' => 1]

// sum(): ステータスごとの合計金額
$totals = $grouped->map(fn ($g) => $g->sum('amount'));
// ['paid' => 3000, 'pending' => 1200, 'failed' => 300]

// pluck(): ステータスごとにamountの配列だけ抜き出す
$amounts = $grouped->map(fn ($g) => $g->pluck('amount'));
// ['paid' => [1000, 2000], 'pending' => [500, 700], 'failed' => [300]]

よくある落とし穴・注意

  • キー再採番$preserveKeys=false では各グループ内が 0,1,2… に再採番。元キーが必要なら true
  • 未定義経路の集約'user.team.id' が存在しない要素は null グループに入る。?? で補正するならコールバックを使う。
  • 多段グループの順序['year','status'] の順がそのまま ネスト順。アクセスは $groups->get($year)->get($status)
  • LazyCollectionの実体化:全件を保持するため大規模データではメモリ圧迫。必要ならDB側で GROUP BY し、結果を小さくしてから groupBy する。
  • ソートは別groupBy は並び替えない。並べたい場合は sortKeys() / sortBy() を合わせて使う。
  • SQLの GROUP BY と混同する:Query Builder の ->groupBy('status') はSQLの GROUP BY を生成します。Collection の groupBy() とはまったく別の処理です。SQLの GROUP BY は集計関数(COUNT / SUM 等)と組み合わせないとエラーになりますが、Collection の groupBy() は全レコードをグループ別に保持します。大量データの集計はSQL側 (->selectRaw('status, COUNT(*) as cnt')->groupBy('status')->get()) が効率的です。
  • 戻り値のネスト構造を把握するgroupBy() の戻り値は Collection<string, Collection> です。$grouped['paid'] でアクセスした値はさらに Collection なので $grouped['paid'][0]['amount'] のように配列ライクにアクセスできます。多段グループ(['year', 'status'])の場合は $grouped->get('2025-09')->get('paid') と2段アクセスが必要です。dd($grouped->toArray()) でネスト構造を確認するとデバッグしやすいです。

代替・関連APIとの比較

  • countBy:個数だけ欲しいならこちらが軽量(値→件数)。
  • mapToGroups:コールバックで key => value のペア群を返し、1要素を複数のグループに振り分け可能。groupBy は1層ごとに1キーへ分類
  • partition:真偽で 2分割するだけなら簡潔。
  • keyBy:キーを付け替えるだけでグループ化はしない

コレクション特性(カテゴリ追記)

  • チェーン可:可
  • 破壊的/非破壊:非破壊(新しい Collection を返す)
  • キー保持:オプション($preserveKeys
  • LazyCollection:可(ただし全件実体化
  • 計算量の目安:O(n)(nは要素数)

入出力対応(ミニサンプル)

入力呼び出し出力(概念)
[['t'=>'A'],['t'=>'B'],['t'=>'A']]->groupBy('t')['A'=>[[…],[…]], 'B'=>[[…]]]
[['y'=>2025,'s'=>'paid'], …]->groupBy(['y','s'])[2025=>['paid'=>[[…]], 'fail'=>[[…]]]]

テスト例(Pest)

<?php

use Illuminate\Support\Collection;

it('groups by key and closure', function () {
    $c = collect([
        ['team' => 'A', 'score' => 10],
        ['team' => 'B', 'score' => 20],
        ['team' => 'A', 'score' => 30],
    ]);

    $byTeam = $c->groupBy('team');
    expect($byTeam->keys())->toEqual(collect(['A','B']));
    expect($byTeam['A']->sum('score'))->toBe(40);

    $byHighLow = $c->groupBy(fn($x) => $x['score'] >= 20 ? 'high' : 'low');
    expect($byHighLow['high']->count())->toBe(2);
    expect($byHighLow['low']->count())->toBe(1);
});

トラブルシュート(エラー別)

症状/エラー原因対処
Call to a member function format() on nullcreated_atnulloptional($o->created_at)?->format('Y-m') か nullを別グループに分ける
期待したグループが空dot記法のパス違い / キー名誤りdata_get($item, 'path') で確認、またはクロージャで明示
並びがバラバラgroupBy はソートしない->sortKeys()->map(fn($g)=>$g->sortBy('…')) を併用
メモリ不足大量データをすべて保持DBで GROUP BY → 少量結果に groupBy、もしくは countBy で代替

FAQ

LaravelのgroupByとは?

Laravelの groupBy() とは、コレクション内の要素を指定キーや条件でグループ分けするメソッドです。戻り値は「グループキー => Collection」の形式になり、各グループをさらに count()sum() で集計できます。例えば「ユーザーを役職別にまとめる」「注文をステータス別・月別に分類する」といった用途に使います。なお、同名のメソッドがQuery Builderにも存在しますが、それはSQL側のGROUP BYであり別物です。

CollectionとSQLのgroupByは違う?

はい、まったく異なります。SQLの GROUP BY(Query Builderの ->groupBy())はデータベース側で集計を行い、集計関数(COUNT / SUM 等)と一緒に使います。一方、Collectionの groupBy() はPHP上でコレクションを分類するだけで、すべての要素をメモリ上に保持します。大量データの集計にはSQL側のGROUP BYが適しており、取得後の二次整理にCollectionのgroupByが向いています。

複数条件でグループ化できる?

はい、配列で複数のキーを渡すことで多段グループ化が可能です。

<?php

// ['year', 'status'] の順にネストされる
$grouped = $orders->groupBy(['year', 'status']);

// アクセス例(2段 get())
$paidIn2025 = $grouped->get('2025')->get('paid');

// クロージャと文字列を混在させることも可能
$grouped2 = $orders->groupBy([
    fn ($o) => $o->created_at->format('Y'), // 年(クロージャ)
    'status',                                 // ステータス(文字列キー)
]);

配列の先頭から順に外側(第1階層)→内側(第2階層)とネストしていきます。アクセスは $grouped->get($year)->get($status) のように2段で行います。

参考リンク

レン (Wren)

こんにちは。レンです。

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

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

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

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

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

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