更新: 2026年03月 · 9 分で読める · 4,636 文字
SQL GROUP BYで集計データを効率的に取得する実践的な方法
GROUP BYは複数のレコードを指定した列でグループ化し、集計関数を使って要約データを取得するSQL機能です。この記事では、売上集計やユーザー分析など実務で頻繁に使う場面を想定し、基本から応用パターンまですぐに使えるテクニックを解説します。
GROUP BYの基本構文と仕組み
GROUP BYは、SELECT句で指定した列の値が同じレコードを1つのグループにまとめます。集計関数(COUNT、SUM、AVG、MAX、MINなど)と組み合わせることで、グループごとの統計値を得られます。
-- 基本的なGROUP BYの構文
SELECT
列1,
集計関数(列2) AS 集計結果名
FROM テーブル名
GROUP BY 列1;
重要なポイントは、SELECT句に含める列は「GROUP BYで指定した列」か「集計関数で計算した列」のみ、という制約です。この制約を理解することが、GROUP BY習得の第一歩になります。
実務で使う5つの集計パターン
1. 販売データを商品ごとに集計
商品カテゴリ別に売上合計と販売件数を取得する典型的な例です。
-- 商品カテゴリ別の売上集計
SELECT
category AS カテゴリ,
COUNT(*) AS 販売件数,
SUM(price) AS 売上合計,
AVG(price) AS 平均単価
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY category
ORDER BY 売上合計 DESC;
このクエリは、2025年1月以降の注文を対象に、カテゴリごとの統計情報を売上合計の降順で表示します。ORDER BYを使うことで、売上上位のカテゴリから順に確認できます。
2. 複数列でのグループ化
年月別かつ営業担当者別に集計する場合、GROUP BYに複数の列を指定します。
-- 年月と営業担当者で二段階グループ化
SELECT
DATE_TRUNC('month', order_date) AS 月,
sales_person AS 担当者,
COUNT(*) AS 受注件数,
SUM(amount) AS 売上
FROM orders
GROUP BY DATE_TRUNC('month', order_date), sales_person
ORDER BY 月 DESC, 売上 DESC;
複数列でグループ化すると、各営業担当者の月別パフォーマンスを一度に把握できます。DATE_TRUNCは日付を月単位に丸める関数で、PostgreSQLで標準的です(MySQLではYEAR()やMONTH()を組み合わせて使用)。
3. HAVING句で集計結果をフィルタリング
グループ化後に、集計結果に対して条件を付ける場合はHAVING句を使います。WHERE句ではできない「集計後の絞り込み」が可能です。
-- 売上1000万円以上のカテゴリのみ抽出
SELECT
category AS カテゴリ,
SUM(price) AS 売上合計,
COUNT(*) AS 件数
FROM orders
WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY category
HAVING SUM(price) >= 10000000
ORDER BY 売上合計 DESC;
WHERE句はグループ化の前に実行され、HAVING句はグループ化の後に実行されます。この順序の違いを理解することが、効率的なクエリ作成につながります。
4. 複数の集計関数を組み合わせる
1つのクエリで異なる集計結果を同時に取得できます。
-- ユーザー別の購買分析
SELECT
user_id,
COUNT(*) AS 購入回数,
SUM(amount) AS 総購入額,
AVG(amount) AS 平均購入額,
MAX(amount) AS 最大購入額,
MIN(amount) AS 最小購入額
FROM transactions
GROUP BY user_id
HAVING COUNT(*) >= 5
ORDER BY 総購入額 DESC;
HAVING COUNT(*) >= 5で、5回以上購入したユーザーのみ抽出しています。VIP顧客の特徴分析に有効なパターンです。
5. NULL値を含む場合の処理
GROUP BYではNULL値も1つのグループとして扱われます。必要に応じてNULL値を除外または別処理します。
-- NULL値を除外してグループ化
SELECT
department,
COUNT(*) AS 従業員数
FROM employees
WHERE department IS NOT NULL
GROUP BY department;
-- 別パターン: NULL値を「未分類」に置換
SELECT
COALESCE(department, '未分類') AS 部門,
COUNT(*) AS 従業員数
FROM employees
GROUP BY COALESCE(department, '未分類');
COALESCEを使うと、NULL値を指定した値に置換してからグループ化します。「未分類」として集計に含めたい場合に便利です。
よくあるハマりポイントと解決策
エラー1: 「GROUP BYに指定していない列をSELECTしている」
以下のクエリはエラーになります。
-- ❌ エラーが発生
SELECT
category,
product_name, -- ← GROUP BYに含まれていない
SUM(price)
FROM orders
GROUP BY category;
理由は、1つのcategoryに複数のproduct_nameが存在する可能性があり、どのproduct_nameを表示すべきか曖昧だからです。解決策は、GROUP BYに列を追加するか、集計関数を使う方法です。
-- ✅ 修正1: GROUP BYに列を追加
SELECT
category,
product_name,
SUM(price)
FROM orders
GROUP BY category, product_name;
-- ✅ 修正2: 集計関数を使う
SELECT
category,
MAX(product_name) AS 代表商品名,
SUM(price)
FROM orders
GROUP BY category;
エラー2: 「WHERE句とHAVING句の使い分けができていない」
パフォーマンスを考慮すると、WHERE句で先に絞り込む方が効率的です。
-- ❌ 非効率: 全レコードをグループ化してからフィルタ
SELECT category, SUM(price)
FROM orders
GROUP BY category
HAVING order_date >= '2025-01-01';
-- ✅ 効率的: 先にWHEREで絞り込む
SELECT category, SUM(price)
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY category;
エラー3: 「GROUP BYに関数を含めるとリスト」
日付を「月単位」で集計する場合、DATE_TRUNCをSELECTとGROUP BYの両方に含める必要があります。
-- ❌ エラー: DATE_TRUNCの結果が異なる可能性
SELECT
MONTH(order_date) AS 月,
SUM(amount)
FROM orders
GROUP BY MONTH(order_date);
-- ✅ 同じ関数を使用
SELECT
MONTH(order_date) AS 月,
SUM(amount)
FROM orders
GROUP BY MONTH(order_date);
GROUP BYを使うべき場面と使うべきでない場面
使うべき場面
- 売上分析や予実管理など、統計値が必要な場合
- ユーザー単位やカテゴリ単位での分類集計
- 重複排除が必要な場合(DISTINCT相当の機能)
- 月別・年別といった期間別の集計
使うべきでない場面
- 全レコードの詳細データが必要な場合(単にWHEREで絞り込む)
- テーブルジョイン後に詳細データを保持したい場合(JOINの記述を工夫)
- 単なる重複排除ならDISTINCTの方がシンプル
パフォーマンス最適化のコツ
大規模なテーブルでGROUP BYを実行する場合、以下の対策が有効です。
-- インデックスをグループ化する列に作成
CREATE INDEX idx_category ON orders(category);
-- WHERE句で事前に対象データを絞り込む
SELECT
category,
SUM(price)
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)
AND status = 'completed'
GROUP BY category;
GROUP BYに指定する列にインデックスを張ることで、クエリ実行時間を大幅に短縮できます。特に数百万件以上のレコードがある場合は必須の最適化手段です。
主要なSQLダイアレクト別の注意点
PostgreSQL: DATE_TRUNC、STRING_AGGなど豊富な関数が利用可能です。
-- PostgreSQL: 複数の値を1行に集約
SELECT
category,
STRING_AGG(product_name, ', ') AS 商品一覧,
COUNT(*)
FROM orders
GROUP BY category;
MySQL: GROUP_CONCATで複数値を連結できます。
-- MySQL: 複数の値を1行に集約
SELECT
category,
GROUP_CONCAT(product_name SEPARATOR ', ') AS 商品一覧,
COUNT(*)
FROM orders
GROUP BY category;
SQLServer: DATENAME関数で日付操作が得意です。
-- SQLServer: 年月でグループ化
SELECT
YEAR(order_date) AS 年,
MONTH(order_date) AS 月,
SUM(amount)
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date);
よくある質問
DISTINCT は重複行を排除するだけで、集計はできません。GROUP BYは重複排除に加えて集計関数を使えます。「商品カテゴリの種類を知りたい」ならDISTINCT、「カテゴリ別の売上を知りたい」ならGROUP BYを使い分けてください。
CREATE TABLE AS SELECTやINSERT INTO SELECT構文を使用します。
UNION ALLで複数のGROUP BYクエリを連結できます。ただし、WITH(CTE)を使う方がより読みやすいです。
まとめ
- GROUP BYの基本構文は「SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY」の順序で記述
- SELECT句に含められるのは「GROUP BYで指定した列」か「集計関数の結果」のみ
- WHERE句でグループ化前に絞り込み、HAVING句でグループ化後に絞り込みを行う
- 複数列でのグループ化やNULL値処理は実務で頻繁に使うパターン
- パフォーマンス向上には、GROUP BY対象列へのインデックス作成が効果的
- SQLダイアレクト(PostgreSQL、MySQL、SQLServer)で関数の使い方が異なるため、
おすすめデータベースリソース
- PostgreSQL Documentation Comprehensive PostgreSQL manual with SQL reference.
- Redis Documentation Redis command reference and best practices.
- Use The Index, Luke Classic guide to SQL index design. Free to read online.