GROUP BYの正しい理解と落とし穴。集計でデータが「消える」ように見える理由

SQLのGROUP BY句は、大量のレコードを「特定の属性で束ねて集計する」ための命令です。担当者別の売上合計、月別の受注件数、商品カテゴリ別の平均単価など、あらゆる「分類別の分析」はGROUP BYによって実現されます。

しかし、GROUP BYはSQL学習者が最もつまづきやすい構文の一つでもあります。「なぜかデータ件数が減った」「集計結果がおかしい」という現象の多くは、GROUP BYの動作原理の誤解から生じています。本記事では、基本から代表的な誤りまでを体系的に整理します。

GROUP BYの基本動作:行を「束ねる」

GROUP BYの本質は、 「同じ値を持つ行を1行にまとめる」 操作です。

SELECT department, COUNT(*) AS member_count
FROM employees
GROUP BY department;

このクエリは、社員テーブルから部署ごとに行を束ね、各部署の人数を集計します。元のテーブルに100行あっても、部署が5種類であれば結果は5行になります。

これは「データが消えた」わけではなく、「束ねられた」のです。この認識の差が、GROUP BY理解の第一歩です。

よくある誤り① SELECTとGROUP BYの不整合

GROUP BYを使う際に、最も頻繁に発生するエラーが 「GROUP BY句に含まれていない列をSELECTしようとする」 ケースです。

-- エラーになる例
SELECT department, name, COUNT(*) AS cnt
FROM employees
GROUP BY department;

このクエリは多くのデータベースでエラーになります。理由は論理的です。「部署でグループ化したとき、その部署に複数人の社員がいた場合、nameは誰の名前を表示すればよいのか」が一意に決まらないからです。

GROUP BYを使うSELECT句には、以下のいずれかしか含められません。

  • GROUP BYで指定した列(グループ化の基準)
  • 集計関数(COUNT, SUM, AVG等)の結果

この制約を「SELECTできるのは、束ねた後に意味が確定するものだけ」と理解すると覚えやすいです。

よくある誤り② データが「消えた」ように見える現象

次のケースを考えます。売上テーブルから「担当者別の合計売上」を出したいとします。

-- 意図した結果にならない例
SELECT salesperson_id, SUM(amount)
FROM sales
GROUP BY salesperson_id;

これ自体は正しいSQLです。しかし、実行結果の件数が「担当者の総人数より少ない」場合、データが消えたと感じることがあります。

原因は 「売上がゼロ件の担当者は集計結果に登場しない」 ことです。GROUP BYは存在するデータを束ねるため、対象の行が1件もなければその担当者は結果に含まれません。

売上ゼロを含めて全担当者を表示したい場合は、担当者マスタを起点にした 外部結合(LEFT JOIN) が必要になります。「GROUP BYで集計できない=その期間に実績がない」という解釈に注意が必要です。

よくある誤り③ NULLのグループ化

GROUP BYでNULLを含む列を指定した場合、NULLは 「一つのグループ」として扱われます。

SELECT region, COUNT(*) AS cnt
FROM customers
GROUP BY region;

regionがNULLのレコードが複数あれば、それらはまとめて「region = NULL のグループ」として1行に集計されます。これは仕様どおりの動作ですが、「NULL件数がなぜかまとまって見える」と混乱する原因になります。NULLの行を除外したい場合は、WHERE句で WHERE region IS NOT NULL を追加します。

よくある誤り④ WHEREとHAVINGの混同

GROUP BYと組み合わせて使う絞り込み条件として、 WHERE句HAVING句 の2種類があります。

評価タイミング 対象
WHERE GROUP BY処理の 個別の行(集計前のレコード)
HAVING GROUP BY処理の 集計された結果
-- 誤り:集計関数はWHEREに書けない
SELECT department, COUNT(*) AS cnt
FROM employees
WHERE COUNT(*) >= 5      -- エラー
GROUP BY department;

-- 正しい:集計結果への条件はHAVING
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
HAVING COUNT(*) >= 5;

「5人以上いる部署のみ表示する」という条件は集計後の判定なので、HAVINGで記述します。WHEREに集計関数を書くとエラーになります。ただし、「特定の入社年の社員のみを集計対象にする」という処理はGROUP BY前の絞り込みなのでWHEREが正しい使い方です。

よくある誤り⑤ COUNT(*)とCOUNT(列名)の違い

GROUP BYと組み合わせる集計関数として最もよく使われるCOUNTには、2種類の書き方があります。

書き方 動作
COUNT(*) グループ内の全行数を数える(NULLを含む)
COUNT(列名) グループ内でその列がNULLでない行数を数える
SELECT department, COUNT(*) AS total, COUNT(project_id) AS assigned
FROM employees
GROUP BY department;

このクエリは、部署ごとに「総人数(total)」と「プロジェクトにアサインされている人数(assigned)」を同時に取得します。total - assigned がアサインなしの人数を意味します。

意図せず COUNT(*)COUNT(列名) を混同すると、集計値の意味が変わるため、どちらを使うかを明示的に意識することが重要です。

正しく使うためのチェックリスト

GROUP BYを含むクエリを書いたら、以下の点を確認してください。

  1. SELECT句の全列がGROUP BYか集計関数か:どちらでもない列が含まれていればエラーか意図しない動作の原因になります。
  2. NULLのグループが意図しない集計をしていないか:必要に応じてWHERE IS NOT NULLを付けます。
  3. ゼロ件の場合の取り扱いが決まっているか:実績がない行を結果に含めるかどうかをLEFT JOINで明示します。
  4. WHEREとHAVINGの使い所が正しいか:集計前の絞り込みはWHERE、集計後の絞り込みはHAVINGです。
  5. 期待する行数を事前に見積もっているか:GROUP BY後の件数はGROUP BYで指定した列のユニーク数と一致するはずです。

まとめ:GROUP BYはデータを「変換」する命令

GROUP BYは「データを消す」のではなく、「個別のレコードを集計された要約に変換する」命令です。

この変換の仕組みを正確に理解することで、「なぜ件数が変わったのか」「なぜこの列は書けないのか」という疑問のほとんどは自然に解消されます。集計クエリを設計する際は、まず「何を単位として束ねるか」を明確に定め、その後に「束ねた後に何を取り出すか」を決める。この順序で考えることが、GROUP BYを正確に扱うための最も確実なアプローチです。