6.

Laravel Query Builder の集約関数 (count/max/avg) 完全ガイド

編集
この記事の要点
  • Laravel Query Builder の集約関数は count() / max() / min() / avg() / sum() の 5 つ。SQL の COUNT/MAX/MIN/AVG/SUM と 1:1 対応
  • 戻り値はスカラー。コレクションではないので foreach できない
  • groupBy() と組み合わせるときは selectRaw("category_id, COUNT(*) as cnt") で集約列を明示
  • Eloquent でも同じメソッド名で呼べる: Order::where(...)->count()
  • distinct()->count("user_id") で重複除外カウント。count(DISTINCT user_id) 相当
  • HAVING は having("sum_amount", ">=", 1000) で書ける

5 つの集約関数

Laravel Query Builder には SQL の標準集約関数に対応する 5 つのメソッドがあります:

メソッドSQL戻り値用途
count()COUNT(*)int行数
count('col')COUNT(col)intNULL を除いた行数
max('col')MAX(col)同じ型最大値
min('col')MIN(col)同じ型最小値
avg('col')AVG(col)float平均
sum('col')SUM(col)numeric合計

基本的な使い方

use Illuminate\Support\Facades\DB;

// 行数
$cnt   = DB::table('orders')->count();

// 条件付き
$cnt   = DB::table('orders')->where('status', 'paid')->count();

// 最大・最小・平均・合計
$max   = DB::table('orders')->max('price');
$min   = DB::table('orders')->min('price');
$avg   = DB::table('orders')->avg('price');
$sum   = DB::table('orders')->sum('price');

// 戻り値はスカラー
echo "件数: {$cnt}, 合計: {$sum}, 平均: " . number_format($avg, 2);

Eloquent でも同じメソッドが使える

use App\Models\Order;

// Eloquent でもまったく同じ
$cnt = Order::where('status', 'paid')->count();
$max = Order::max('price');

// リレーション経由
$user = User::find(1);
$total = $user->orders()->sum('amount');         // この user の注文合計
$cnt   = $user->orders()->where('status', 'paid')->count();

// withCount で N+1 を避けてカウント
$users = User::withCount(['orders', 'comments'])->get();
foreach ($users as $u) {
    echo "{$u->name}: 注文 {$u->orders_count} 件 / コメント {$u->comments_count} 件\n";
}

// withSum, withMax, withMin, withAvg もある(Laravel 8+)
$users = User::withSum('orders', 'amount')->get();
echo $users[0]->orders_sum_amount;

GROUP BY と組み合わせる

「カテゴリ毎の売上集計」のような場合は selectRaw() で集約列を明示します:

// カテゴリ別に件数・合計・平均
$rows = DB::table('orders')
    ->selectRaw('category_id, COUNT(*) as cnt, SUM(amount) as total, AVG(amount) as avg_amount')
    ->where('created_at', '>=', now()->startOfMonth())
    ->groupBy('category_id')
    ->orderByDesc('total')
    ->get();

foreach ($rows as $r) {
    echo "category {$r->category_id}: {$r->cnt} 件 / 合計 {$r->total} / 平均 {$r->avg_amount}\n";
}

HAVING — 集約結果でフィルタ

// 売上 100 万円以上のカテゴリだけ
$rows = DB::table('orders')
    ->selectRaw('category_id, SUM(amount) as total')
    ->groupBy('category_id')
    ->having('total', '>=', 1000000)
    ->orderByDesc('total')
    ->get();

// havingRaw で集約関数を直接書く
$rows = DB::table('orders')
    ->select('category_id')
    ->groupBy('category_id')
    ->havingRaw('SUM(amount) >= ?', [1000000])
    ->get();

DISTINCT COUNT

// ユニークユーザー数
$uniqueUsers = DB::table('orders')->distinct()->count('user_id');
// → SELECT COUNT(DISTINCT user_id) FROM orders

// 複数列の組み合わせでユニーク
$cnt = DB::table('logs')
    ->select('user_id', 'session_id')
    ->distinct()
    ->count();

JOIN しての集約

// ユーザー名と注文合計を一覧
$rows = DB::table('users')
    ->leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->selectRaw('users.id, users.name, COUNT(orders.id) as order_count, COALESCE(SUM(orders.amount), 0) as total_amount')
    ->groupBy('users.id', 'users.name')
    ->orderByDesc('total_amount')
    ->get();

サブクエリで集約結果を参照

// 平均より高い注文を抽出
$avgAmount = DB::table('orders')->avg('amount');

$expensive = DB::table('orders')
    ->where('amount', '>', $avgAmount)
    ->get();

// 1 クエリで書きたい場合は selectSub / whereRaw
$expensive = DB::table('orders')
    ->whereRaw('amount > (SELECT AVG(amount) FROM orders)')
    ->get();

条件付き集約(CASE WHEN)

// ステータス別の件数を 1 行で取得(ピボット)
$row = DB::table('orders')
    ->selectRaw("
        COUNT(*) as total,
        SUM(CASE WHEN status = 'paid'    THEN 1 ELSE 0 END) as paid_count,
        SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
        SUM(CASE WHEN status = 'paid'    THEN amount ELSE 0 END) as paid_amount
    ")
    ->where('created_at', '>=', now()->subDays(30))
    ->first();

echo "支払い済: {$row->paid_count} 件 / 合計 {$row->paid_amount} 円";

FAQ

Q: avg() の戻り値が文字列になる
A: DB ドライバによっては精度保持のため文字列で返します。(float) $avg でキャストするか、number_format() で表示。

Q: count() でめちゃくちゃ遅い
A: MyISAM は COUNT(*) が即時ですが、InnoDB は MVCC のためフルスキャンが発生します。インデックスをカバーするカウントにするか、概算でよければ SHOW TABLE STATUS

Q: groupBy 後に paginate したい
A: 通常の paginate() は count クエリが GROUP BY を理解できず壊れます。simplePaginate() か手動で OFFSET/LIMIT を計算してください。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. SELECT
  2. INSERT
  3. UPDATE
  4. DELETE
  5. order by句のキャスト
  6. count / max / average (集計)
  7. 配列を条件にする方法
  8. where句の入れ子(ネスト)