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を含むクエリを書いたら、以下の点を確認してください。
- SELECT句の全列がGROUP BYか集計関数か:どちらでもない列が含まれていればエラーか意図しない動作の原因になります。
- NULLのグループが意図しない集計をしていないか:必要に応じてWHERE IS NOT NULLを付けます。
- ゼロ件の場合の取り扱いが決まっているか:実績がない行を結果に含めるかどうかをLEFT JOINで明示します。
- WHEREとHAVINGの使い所が正しいか:集計前の絞り込みはWHERE、集計後の絞り込みはHAVINGです。
- 期待する行数を事前に見積もっているか:GROUP BY後の件数はGROUP BYで指定した列のユニーク数と一致するはずです。
まとめ:GROUP BYはデータを「変換」する命令
GROUP BYは「データを消す」のではなく、「個別のレコードを集計された要約に変換する」命令です。
この変換の仕組みを正確に理解することで、「なぜ件数が変わったのか」「なぜこの列は書けないのか」という疑問のほとんどは自然に解消されます。集計クエリを設計する際は、まず「何を単位として束ねるか」を明確に定め、その後に「束ねた後に何を取り出すか」を決める。この順序で考えることが、GROUP BYを正確に扱うための最も確実なアプローチです。