MySQL GROUP BY と HAVING
GROUP BY 句は、指定したカラム内で同じ値を持つ行(レコード)を一つにまとめ、結果セットを集計行(サマリー行)として再構成します。SUM() や COUNT() といった集約関数(Aggregate Functions)はデータセット全体に対して計算を行いますが、GROUP BY を併用することで、データの特定のサブセットに対して正確に計算を適用するという粒度の高い制御が可能になります。
1. GROUP BY によるデータのグループ化
GROUP BY を使用すると、MySQLはグループ化対象のカラムで同じ値を持つすべての行を収集し、それらを単一のエンティティとして処理します。
例えば、orders(注文)テーブルを持つECサイトのデータベースを考えてみましょう。顧客ごとの総売上を計算したい場合、全体の総計ではなく、各顧客ごとの合計値が必要になります。
SELECT
customer_id,
SUM(order_amount) AS total_spent
FROM orders
GROUP BY customer_id;この実行プロセスにおいて、MySQLは orders テーブルをスキャンし、一意の customer_id をそれぞれ識別します。そして、各IDに対応する仮想的な「バケット(bucket)」を作成し、そのバケット内の order_amount に対して SUM() 関数を適用します。もし GROUP BY 句を省略した場合、サーバーの設定(ONLY_FULL_GROUP_BY SQLモードなど)によっては、MySQLはエラーを返すか、テーブル全体を代表する単一の行を返します。
2. HAVING を使用した集計結果のフィルタリング
WHERE 句は、行がグループ化される前に個々のレコードをフィルタリングします。集計が行われた「後」の結果に対してフィルタリングを行いたい場合(例えば、総支出が 1,000 ドルを超える顧客のみを抽出したい場合)は、HAVING 句を使用しなければなりません。
SELECT
customer_id,
SUM(order_amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING total_spent > 1000;集約されたデータに対して WHERE を使用してフィルタリングしようとするのは、初心者が陥りやすい典型的なミスです。データベースエンジンはグループ化のロジックを開始する前に WHERE を実行するため、その段階では「総支出」という概念自体が存在しません。HAVING は、最終的な結果セットに対する「ゲートキーパー(門番)」として機能します。
3. 実行順序と論理的な処理フロー
これらの句を使用する際、クエリのライフサイクルを理解することは極めて重要です。データベースは特定の順序で各句を処理します。まずソースデータを特定し、生のレコードをフィルタリングし、それらをグループ化して計算を実行し、最後に集計後の出力をフィルタリングします。
- FROM/JOIN: データソースの特定
- WHERE: 生のレコード(行)のフィルタリング
- GROUP BY: 行を「バケット」に整理
- 集約関数:
SUM,COUNT,AVGなどの計算を実行 - HAVING: グループ化された後のグループ(集計結果)をフィルタリング
- SELECT: 最終的に確定したカラムの出力
- ORDER BY: 結果セットのソート
4. 複数カラムを組み合わせた GROUP BY の活用
複数のカラムを指定してグループ化を行うことで、階層的なサマリーを作成できます。例えば、カテゴリ(category)と年(year)ごとの総売上を確認したい場合、GROUP BY 句にこれら両方のカラムを指定します。
SELECT
category,
YEAR(order_date) AS order_year,
COUNT(order_id) AS total_orders
FROM orders
GROUP BY category, YEAR(order_date)
ORDER BY order_year DESC, total_orders DESC;複数カラムでグループ化する場合、MySQLはこれらのカラム値のすべての「一意な組み合わせ」に対して一つの行を生成します。
注意点: SELECT リストに含まれるカラムのうち、集約関数の一部ではないものは、必ず GROUP BY 句にも含まれている必要があります。そうでない場合、データベースはそのカラムに具体的にどの値を表示すべきか判断できず、不整合の原因となります。