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)で関数の使い方が異なるため、
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →