集計と GROUP BY
COUNT / SUM / AVG / MIN / MAX、GROUP BY、HAVING、FILTER、ROLLUP / CUBE / GROUPING SETS まで。
主要な集計関数
| 関数 | 意味 |
|---|---|
| COUNT(*) | 全行数(NULL 含む) |
| COUNT(col) | NULL 以外の行数 |
| COUNT(DISTINCT col) | ユニーク値の数 |
| SUM(col) | 合計 |
| AVG(col) | 平均 |
| MIN(col) / MAX(col) | 最小・最大 |
| STRING_AGG(col, ',') | 文字列連結(Postgres) |
| ARRAY_AGG(col) | 配列化 |
| JSON_AGG(col) | JSON 配列 |
| BOOL_AND / BOOL_OR | 真偽集計 |
| STDDEV / VARIANCE | 標準偏差・分散 |
| PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY col) | 中央値 |
基本(全体集計)
SELECT
count(*) AS total,
count(DISTINCT user_id) AS unique_users,
sum(views) AS total_views,
avg(views) AS avg_views,
max(views) AS max_views
FROM posts;
GROUP BY
集計を「グループごと」に分割する:
SELECT
user_id,
count(*) AS post_count,
avg(views) AS avg_views
FROM posts
GROUP BY user_id;
複数列で GROUP
SELECT
status, date_trunc('month', created_at) AS month,
count(*) AS posts
FROM posts
GROUP BY status, month
ORDER BY month, status;
GROUP BY と SELECT のルール
- SELECT 句に出せるのは:
- GROUP BY に含まれる列
- 集計関数の結果
- 定数
- それ以外はエラーになる(SQL 標準)
- MySQL は緩いが、推奨されない(任意の値が返る)
HAVING(集計後の WHERE)
WHERE は集計前、HAVING は集計後の絞り込み:
-- 投稿が 3 件以上のユーザ
SELECT user_id, count(*) AS post_count
FROM posts
GROUP BY user_id
HAVING count(*) >= 3;
WHERE と HAVING の使い分け
- 個別の行を絞る → WHERE
- 集計値で絞る → HAVING
- WHERE で絞った後に集計するのが速い(HAVING は集計後)
FILTER(条件付き集計)
Postgres / SQL 標準。1 クエリで複数の条件付きカウントが書ける:
SELECT
count(*) AS total,
count(*) FILTER (WHERE status = 'published') AS published,
count(*) FILTER (WHERE status = 'draft') AS draft,
sum(views) FILTER (WHERE status = 'published') AS published_views
FROM posts;
CASE で代用も可だが FILTER の方が読みやすい:
-- CASE 版(互換性高い)
SELECT
count(*) AS total,
count(CASE WHEN status = 'published' THEN 1 END) AS published
FROM posts;
JOIN + GROUP BY の典型
SELECT
u.id, u.email,
count(p.id) AS posts,
count(c.id) AS comments,
max(p.created_at) AS last_post
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
LEFT JOIN comments c ON c.user_id = u.id
GROUP BY u.id;
注意: 1 ユーザに対し posts × comments の組み合わせが出るので件数がズレる場合がある(直積問題)。 サブクエリ / CTE で個別集計するのが安全。
正しい書き方(個別集計)
SELECT
u.id, u.email,
(SELECT count(*) FROM posts WHERE user_id = u.id) AS posts,
(SELECT count(*) FROM comments WHERE user_id = u.id) AS comments
FROM users u;
累計 / 累積
集計のみだと「合計」しか取れない。累積を出すなら Window 関数(→ Window 関数)。
典型パターン集
1. 月別売上
SELECT
date_trunc('month', created_at) AS month,
sum(amount) AS revenue,
count(*) AS orders
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY 1
ORDER BY 1;
2. 日別 DAU
SELECT
date_trunc('day', logged_at) AS day,
count(DISTINCT user_id) AS dau
FROM access_logs
WHERE logged_at >= NOW() - INTERVAL '30 days'
GROUP BY 1
ORDER BY 1;
3. 上位 N(カテゴリ別)
-- 各カテゴリの売上トップ 1 → ROW_NUMBER() の方が綺麗
-- 単純な「カテゴリ別 売上」なら:
SELECT category, sum(amount) AS sales
FROM orders
GROUP BY category
ORDER BY sales DESC
LIMIT 10;
4. ヒストグラム
SELECT
width_bucket(views, 0, 1000, 10) AS bucket,
count(*) AS posts,
min(views), max(views)
FROM posts
GROUP BY 1
ORDER BY 1;
5. 文字列集約
-- 各ユーザのタグを CSV に
SELECT user_id, string_agg(tag, ', ' ORDER BY tag) AS tags
FROM user_tags
GROUP BY user_id;
6. 配列 / JSON 集約
SELECT user_id, array_agg(tag ORDER BY tag) AS tags FROM user_tags GROUP BY user_id;
SELECT
p.id,
json_agg(json_build_object('id', c.id, 'body', c.body) ORDER BY c.created_at) AS comments
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id;
ROLLUP / CUBE / GROUPING SETS
小計 + 総合計を一度に出すための拡張:
ROLLUP
SELECT
category, status, sum(amount)
FROM orders
GROUP BY ROLLUP (category, status);
-- (category, status) + (category) + () の集計
CUBE
SELECT category, status, sum(amount)
FROM orders
GROUP BY CUBE (category, status);
-- 全組み合わせ + 各単独 + 全体
GROUPING SETS
SELECT category, status, sum(amount)
FROM orders
GROUP BY GROUPING SETS (
(category, status),
(category),
()
);
GROUPING() 関数
SELECT
CASE GROUPING(category) WHEN 1 THEN '合計' ELSE category END AS cat,
sum(amount)
FROM orders
GROUP BY ROLLUP (category);
DISTINCT vs GROUP BY
- 同じ結果になることが多い(プランナが最適化)
- 集計関数を使うならGROUP BY
- 列の重複だけ排除なら DISTINCT
NULL の扱い
- 集計関数は NULL を無視(COUNT(*) を除く)
- GROUP BY では NULL は1 つのグループに集約
- 合計が NULL になることは0 行のみ
パフォーマンス
- GROUP BY 列にインデックス → ソート不要で速い
- Hash Aggregate と Sort Aggregate のプラン差を EXPLAIN で確認
- 巨大テーブルはマテビューに集計を保存
- カウンタキャッシュ列で事前計算
失敗パターン
| 症状 | 対処 |
|---|---|
| SELECT 列がエラー | GROUP BY に追加 or 集計関数で包む |
| JOIN で件数が膨らむ | サブクエリ別集計に分解 |
| HAVING で絞れない | 集計関数の結果を使う |
| 巨大テーブルで重い | WHERE で先に絞る、マテビュー化 |
| NULL が混じる | COALESCE / FILTER で対処 |
頻出パターン
「date_trunc + GROUP BY 1 + ORDER BY 1」は時系列集計の決まり文句。 覚えておけば大半のレポートが書ける。