「集計したいけど、1列だけじゃ足りない…」
そんなときに活用できるのが、GROUP BY 複数列指定です。
売上データやアンケート結果のような複数の要素ごとの集計に欠かせないテクニックです。
この記事では、SQLのGROUP BY句に複数のカラムを指定する方法と、その実用的な使い方・注意点・ORDER BYとの併用例を初心者にもわかりやすく解説します。
GROUP BYとは?【基本のおさらい】
GROUP BYは、SQLで同じ値を持つ行をまとめて集計処理を行うための句です。COUNT、SUM、AVGなどの集計関数と一緒に使われます。
GROUP BYの基本的な働き
単一列でのグループ化例
-- 地域ごとの売上合計を求める
SELECT 地域, SUM(金額) AS 合計金額
FROM 売上データ
GROUP BY 地域;
結果のイメージ
地域 | 合計金額
-------|--------
関東 | 5000
関西 | 4500
九州 | 3200
なぜ複数列が必要なのか?
現実のビジネスシーンでは
- 地域だけでなく商品カテゴリ別にも知りたい
- 年度と月の両方で売上を分析したい
- 性別と年代を組み合わせたアンケート分析
- 部署と役職別の人事データ分析
単一列では限界がある場面
- 「関東の食品カテゴリの売上は?」
- 「2024年1月の商品別売上は?」
- 「男性・20代のアンケート回答傾向は?」
このような複合的な分析には、複数列でのグループ化が必要になります。
GROUP BY 複数列の基本構文
基本的な書き方
SELECT 列1, 列2, 集計関数(列3)
FROM テーブル名
GROUP BY 列1, 列2;
構文の解説
- 複数の列をカンマ(,)で区切って指定
- GROUP BYで指定した列は、SELECT句にも含める必要がある
- 集計関数(SUM、COUNT、AVG等)で数値を計算
具体例での理解
-- 地域と商品カテゴリ別の売上合計
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM 売上テーブル
GROUP BY 地域, 商品カテゴリ;
この例が意味すること
- 「関東 × 食品」の組み合わせ
- 「関東 × 衣料」の組み合わせ
- 「関西 × 食品」の組み合わせ
- …それぞれで金額を合計
実用例:売上データでのグループ化
サンプルデータの準備
売上テーブル(sales)の内容
CREATE TABLE sales (
地域 VARCHAR(10),
商品カテゴリ VARCHAR(10),
金額 INTEGER,
売上日 DATE
);
INSERT INTO sales VALUES
('関東', '食品', 1200, '2024-01-15'),
('関東', '食品', 800, '2024-01-20'),
('関西', '食品', 1500, '2024-01-18'),
('関東', '衣料', 800, '2024-01-22'),
('関西', '衣料', 1100, '2024-01-25'),
('九州', '食品', 900, '2024-01-28');
基本的なグループ化クエリ
例1:地域と商品カテゴリ別の集計
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ;
実行結果
地域 | 商品カテゴリ | 合計金額
-------|-------------|--------
関東 | 食品 | 2000
関西 | 食品 | 1500
関東 | 衣料 | 800
関西 | 衣料 | 1100
九州 | 食品 | 900
例2:複数の集計関数を使用
SELECT
地域,
商品カテゴリ,
COUNT(*) AS 取引件数,
SUM(金額) AS 合計金額,
AVG(金額) AS 平均金額
FROM sales
GROUP BY 地域, 商品カテゴリ;
実行結果
地域 | 商品カテゴリ | 取引件数 | 合計金額 | 平均金額
-----|-------------|---------|---------|--------
関東 | 食品 | 2 | 2000 | 1000
関西 | 食品 | 1 | 1500 | 1500
関東 | 衣料 | 1 | 800 | 800
関西 | 衣料 | 1 | 1100 | 1100
九州 | 食品 | 1 | 900 | 900
年月日を使った時系列分析
例3:年月別の売上分析
SELECT
YEAR(売上日) AS 年,
MONTH(売上日) AS 月,
COUNT(*) AS 取引件数,
SUM(金額) AS 月間売上
FROM sales
GROUP BY YEAR(売上日), MONTH(売上日)
ORDER BY 年, 月;
例4:曜日と時間帯別の分析
-- より詳細な時間分析(時間データがある場合)
SELECT
DAYNAME(売上日) AS 曜日,
商品カテゴリ,
COUNT(*) AS 件数,
SUM(金額) AS 売上
FROM sales
GROUP BY DAYNAME(売上日), 商品カテゴリ
ORDER BY
FIELD(曜日, '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'),
売上 DESC;
GROUP BY 複数列を使うときの注意点
SELECT句に含める列のルール
基本ルール GROUP BYで指定した列と集計関数を使った列のみが、SELECT句に含められます。
正しい例
-- ✅ 正しい:GROUP BYの列と集計関数のみ
SELECT 地域, 商品カテゴリ, SUM(金額)
FROM sales
GROUP BY 地域, 商品カテゴリ;
間違った例
-- ❌ エラー:売上日はGROUP BYに含まれていない
SELECT 地域, 商品カテゴリ, 売上日, SUM(金額)
FROM sales
GROUP BY 地域, 商品カテゴリ;
エラーの理由 売上日は地域・商品カテゴリの組み合わせごとに複数の値が存在するため、どの値を表示すべきか決められません。
GROUP BYの順序について
順序は結果に影響しない
-- この2つのクエリは論理的に同じ結果
SELECT 地域, 商品カテゴリ, SUM(金額)
FROM sales
GROUP BY 地域, 商品カテゴリ;
SELECT 地域, 商品カテゴリ, SUM(金額)
FROM sales
GROUP BY 商品カテゴリ, 地域;
ただし表示順序は変わる可能性 データベースによっては、GROUP BYの順序により結果の表示順が変わる場合があります。確実な順序が必要な場合は、ORDER BYを使用してください。
NULL値の扱い
NULL値は一つのグループとして扱われる
-- NULLを含むデータの例
INSERT INTO sales VALUES
(NULL, '食品', 500, '2024-01-30'),
(NULL, '食品', 300, '2024-01-31');
-- 結果:NULLも一つのグループになる
SELECT 地域, 商品カテゴリ, SUM(金額)
FROM sales
GROUP BY 地域, 商品カテゴリ;
実行結果
地域 | 商品カテゴリ | 合計金額
-------|-------------|--------
NULL | 食品 | 800
関東 | 食品 | 2000
関西 | 食品 | 1500
...
ORDER BYとの併用で見やすく整理
基本的な並び替え
例1:地域順、金額の降順
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
ORDER BY 地域 ASC, 合計金額 DESC;
実行結果
地域 | 商品カテゴリ | 合計金額
-------|-------------|--------
関東 | 食品 | 2000
関東 | 衣料 | 800
関西 | 食品 | 1500
関西 | 衣料 | 1100
九州 | 食品 | 900
集計結果による並び替え
例2:売上上位から表示
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
ORDER BY 合計金額 DESC;
例3:複合的な並び替え
SELECT
地域,
商品カテゴリ,
COUNT(*) AS 件数,
SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
ORDER BY
件数 DESC, -- 取引件数が多い順
合計金額 DESC; -- 同じ件数なら金額が多い順
HAVINGとの組み合わせ
GROUP BY結果の絞り込み
HAVINGの使い方 GROUP BYで集計した結果を、さらに条件で絞り込みたい場合にHAVINGを使用します。
例1:一定金額以上の組み合わせのみ表示
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
HAVING SUM(金額) >= 1000;
実行結果
地域 | 商品カテゴリ | 合計金額
-------|-------------|--------
関東 | 食品 | 2000
関西 | 食品 | 1500
関西 | 衣料 | 1100
例2:複数件の取引がある組み合わせのみ
SELECT 地域, 商品カテゴリ, COUNT(*) AS 件数, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
HAVING COUNT(*) > 1;
WHEREとHAVINGの違い
WHERE:集計前の絞り込み
-- 2024年1月15日以降のデータで集計
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
WHERE 売上日 >= '2024-01-15'
GROUP BY 地域, 商品カテゴリ;
HAVING:集計後の絞り込み
-- 集計結果が1000以上の組み合わせのみ表示
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
HAVING SUM(金額) >= 1000;
JOINとの組み合わせ活用
複数テーブルを使った集計
テーブル設計例
商品マスタ(products)
CREATE TABLE products (
商品ID INT,
商品名 VARCHAR(50),
カテゴリ VARCHAR(20),
単価 INTEGER
);
売上明細(order_details)
CREATE TABLE order_details (
注文ID INT,
商品ID INT,
数量 INTEGER,
地域 VARCHAR(10),
注文日 DATE
);
JOINを使った複合集計
SELECT
od.地域,
p.カテゴリ,
COUNT(DISTINCT od.注文ID) AS 注文件数,
SUM(od.数量 * p.単価) AS 売上金額,
SUM(od.数量) AS 販売数量
FROM order_details od
JOIN products p ON od.商品ID = p.商品ID
GROUP BY od.地域, p.カテゴリ
ORDER BY 売上金額 DESC;
より複雑な分析例
例:月別・地域別・カテゴリ別の詳細分析
SELECT
YEAR(od.注文日) AS 年,
MONTH(od.注文日) AS 月,
od.地域,
p.カテゴリ,
COUNT(DISTINCT od.注文ID) AS 注文数,
COUNT(od.商品ID) AS 商品数,
SUM(od.数量) AS 販売数量,
SUM(od.数量 * p.単価) AS 売上金額,
AVG(od.数量 * p.単価) AS 平均注文単価
FROM order_details od
JOIN products p ON od.商品ID = p.商品ID
WHERE od.注文日 >= '2024-01-01'
GROUP BY YEAR(od.注文日), MONTH(od.注文日), od.地域, p.カテゴリ
HAVING 売上金額 >= 10000
ORDER BY 年, 月, 地域, 売上金額 DESC;
パフォーマンスの考慮事項
インデックスの重要性
GROUP BYのパフォーマンス向上
-- GROUP BYで使用する列にインデックスを作成
CREATE INDEX idx_sales_region_category ON sales (地域, 商品カテゴリ);
CREATE INDEX idx_sales_date ON sales (売上日);
複合インデックスの注意点
- GROUP BYで使用する列の順序でインデックスを作成
- 最も絞り込み効果の高い列を先頭に配置
大量データでの最適化
LIMIT句の活用
-- 上位10件のみ取得(パフォーマンス向上)
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ
ORDER BY 合計金額 DESC
LIMIT 10;
サブクエリよりもJOINを選択
-- ❌ パフォーマンスが悪い例
SELECT 地域, 商品カテゴリ, SUM(金額)
FROM sales
WHERE 地域 IN (SELECT 地域 FROM regions WHERE 活性フラグ = 1)
GROUP BY 地域, 商品カテゴリ;
-- ✅ パフォーマンスが良い例
SELECT s.地域, s.商品カテゴリ, SUM(s.金額)
FROM sales s
JOIN regions r ON s.地域 = r.地域
WHERE r.活性フラグ = 1
GROUP BY s.地域, s.商品カテゴリ;
データベース別の特徴
MySQL での特徴
ONLY_FULL_GROUP_BY モード
-- MySQL 5.7以降では厳密なGROUP BYチェック
-- エラーになる例
SELECT 地域, 商品カテゴリ, 金額 -- 金額は集計されていない
FROM sales
GROUP BY 地域, 商品カテゴリ;
-- 正しい例
SELECT 地域, 商品カテゴリ, SUM(金額)
FROM sales
GROUP BY 地域, 商品カテゴリ;
PostgreSQL での特徴
より厳密な型チェック
-- 日付関数の使用例
SELECT
EXTRACT(YEAR FROM 売上日) AS 年,
EXTRACT(MONTH FROM 売上日) AS 月,
地域,
SUM(金額)
FROM sales
GROUP BY EXTRACT(YEAR FROM 売上日), EXTRACT(MONTH FROM 売上日), 地域;
SQL Server での特徴
WITH ROLLUP の使用
-- 小計と総計を含む集計
SELECT 地域, 商品カテゴリ, SUM(金額) AS 合計金額
FROM sales
GROUP BY 地域, 商品カテゴリ WITH ROLLUP;
実践的な応用例
EC サイトの売上分析
顧客属性別の購買分析
SELECT
c.年代,
c.性別,
p.カテゴリ,
COUNT(DISTINCT o.顧客ID) AS 購入者数,
COUNT(o.注文ID) AS 注文回数,
SUM(od.数量 * p.単価) AS 売上金額,
AVG(od.数量 * p.単価) AS 平均注文単価
FROM orders o
JOIN customers c ON o.顧客ID = c.顧客ID
JOIN order_details od ON o.注文ID = od.注文ID
JOIN products p ON od.商品ID = p.商品ID
WHERE o.注文日 >= '2024-01-01'
GROUP BY c.年代, c.性別, p.カテゴリ
ORDER BY 売上金額 DESC;
アクセス ログ分析
時間帯・ページ別のアクセス分析
SELECT
HOUR(アクセス時刻) AS 時間帯,
ページカテゴリ,
デバイス種別,
COUNT(*) AS アクセス数,
COUNT(DISTINCT ユーザーID) AS ユニークユーザー数,
AVG(滞在時間秒) AS 平均滞在時間
FROM access_logs
WHERE アクセス日 BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY HOUR(アクセス時刻), ページカテゴリ, デバイス種別
ORDER BY 時間帯, アクセス数 DESC;
まとめ
複数列のGROUP BYは、より細かくデータを分類して分析したいときに不可欠な手法です。
項目 | 内容 | 重要度 |
---|---|---|
基本構文 | GROUP BY 列1, 列2 のようにカンマ区切りで指定 | ★★★ |
使用目的 | 地域×カテゴリなど、複合条件での詳細な集計分析 | ★★★ |
注意点 | SELECT句に含める列と集計関数の整合性を保つ | ★★★ |
併用技法 | ORDER BY、HAVING、JOINとの組み合わせ | ★★☆ |
パフォーマンス | 適切なインデックス設計とクエリ最適化 | ★★☆ |
ポイント
- 明確な分析目的:何を知りたいかを明確にする
- 適切な列選択:意味のある組み合わせでグループ化
- 集計関数の活用:COUNT、SUM、AVG等を適切に使用
- 結果の可読性:ORDER BYで見やすく整理
- パフォーマンス考慮:インデックスとクエリ最適化
コメント