2026年1月25日 Tech

2-2 集計とグループ化 - データを分析する

集計とグループ化 - データを分析する

集計関数とGROUP BYを使えば、データの「全体像」が見えてくる。件数、合計、平均、最大、最小。これらを自在に取り出す方法を学ぶ。


なぜこれを学ぶのか

前回(2-1)でSELECT文の条件指定を学んだ。「30歳以上のユーザー」「営業部の社員」など、条件に合うデータを取り出せるようになった。

しかし、実務では「データそのもの」だけでなく「データの傾向」を知りたいことが多い。

  • 「ユーザーは全部で何人いるのか」
  • 「社員の平均年齢は何歳か」
  • 「部署ごとの人数を知りたい」
  • 「最も売れた商品は何か」

こうした「集計」はレポート作成、ダッシュボード、経営判断に欠かせない。SQLの集計関数とGROUP BYを使えば、データベースが自動で計算してくれる。

flowchart LR A["生データ"] --> B["集計関数"] B --> C["件数・合計・平均"] A --> D["GROUP BY"] D --> E["グループ別の集計"]

使用するテーブル

この記事では、前回と同じusersテーブルを使う。DB Fiddle で「PostgreSQL 15」を選択し、以下のSQLを「Schema SQL」に貼り付けてほしい。

-- usersテーブルを作成
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL,
    age INTEGER NOT NULL,
    department VARCHAR(50),
    salary INTEGER NOT NULL,
    created_at DATE NOT NULL
);

-- サンプルデータを投入
INSERT INTO users (name, email, age, department, salary, created_at) VALUES
    ('田中太郎', 'tanaka@example.com', 28, '営業部', 350000, '2023-04-01'),
    ('山田花子', 'yamada@example.com', 35, '開発部', 450000, '2022-08-15'),
    ('佐藤一郎', 'sato@example.com', 42, '開発部', 550000, '2021-01-10'),
    ('鈴木次郎', 'suzuki@example.com', 25, '営業部', 300000, '2024-01-05'),
    ('高橋美咲', 'takahashi@example.com', 31, '人事部', 400000, '2023-06-20'),
    ('田村健太', 'tamura@example.com', 29, '開発部', 420000, '2023-09-01'),
    ('中村由美', 'nakamura@example.com', 38, NULL, 380000, '2020-03-15'),
    ('小林誠', 'kobayashi@example.com', 45, '営業部', 480000, '2019-11-01');

作成されるテーブル:

id name email age department salary created_at
1 田中太郎 tanaka@example.com 28 営業部 350000 2023-04-01
2 山田花子 yamada@example.com 35 開発部 450000 2022-08-15
3 佐藤一郎 sato@example.com 42 開発部 550000 2021-01-10
4 鈴木次郎 suzuki@example.com 25 営業部 300000 2024-01-05
5 高橋美咲 takahashi@example.com 31 人事部 400000 2023-06-20
6 田村健太 tamura@example.com 29 開発部 420000 2023-09-01
7 中村由美 nakamura@example.com 38 NULL 380000 2020-03-15
8 小林誠 kobayashi@example.com 45 営業部 480000 2019-11-01

今回はsalary(給与)列を追加した。集計関数の説明に使う。


集計関数の基本

集計関数は、複数の行から1つの値を計算する関数である。「全員の年齢を足す」「平均を出す」といった計算を、SQLが自動で行ってくれる。

flowchart TB subgraph 入力["複数の行"] A["28歳"] B["35歳"] C["42歳"] end subgraph 集計["集計関数"] F["AVG()"] end subgraph 出力["1つの結果"] R["35"] end 入力 --> 集計 集計 --> 出力

主要な集計関数

関数 意味
COUNT() 件数を数える 何件あるか
SUM() 合計を計算 売上の合計
AVG() 平均を計算 平均年齢
MAX() 最大値を取得 最高売上
MIN() 最小値を取得 最安値

これらを1つずつ見ていこう。


COUNT:件数を数える

COUNTは、行の件数を数える関数である。「何件あるか」を知りたいときに使う。

COUNT(*):全件数を数える

SELECT COUNT(*) FROM users;

結果:

count
8

usersテーブルには8件のデータがある。*は「すべての行」を意味する。

COUNT(列名):NULLを除いて数える

SELECT COUNT(department) FROM users;

結果:

count
7

COUNT(department)はNULLの行を除外して数える。中村由美さん(department=NULL)がカウントされないため、7件になる。

flowchart LR subgraph COUNT["COUNT の違い"] A["COUNT(*)"] --> B["8件(NULLも含む)"] C["COUNT(department)"] --> D["7件(NULLを除外)"] end

COUNT(DISTINCT 列名):重複を除いて数える

SELECT COUNT(DISTINCT department) FROM users;

結果:

count
3

部署の種類は「営業部」「開発部」「人事部」の3つ。NULLは含まれない。

WHERE句との組み合わせ

-- 30歳以上のユーザーは何人?
SELECT COUNT(*) FROM users WHERE age >= 30;

結果:

count
5

条件に合う行だけをカウントできる。


SUM:合計を計算する

SUMは、数値列の合計を計算する関数である。

基本的な使い方

SELECT SUM(salary) FROM users;

結果:

sum
3330000

全社員の給与合計は333万円。

WHERE句との組み合わせ

-- 開発部の給与合計
SELECT SUM(salary) FROM users WHERE department = '開発部';

結果:

sum
1420000

開発部3人の給与合計は142万円(45万 + 55万 + 42万)。


AVG:平均を計算する

AVGは、数値列の平均を計算する関数である。

基本的な使い方

SELECT AVG(age) FROM users;

結果:

avg
34.125

全社員の平均年齢は約34.1歳。

小数点以下を丸める

AVGの結果は小数点以下が長くなることがある。ROUND関数で丸められる。

SELECT ROUND(AVG(age), 1) FROM users;

結果:

round
34.1

ROUND(値, 桁数)で指定した桁数に丸める。

AVGとNULL

AVGはNULLの行を除外して計算する。

-- 仮に salary に NULL がある場合、その行は平均計算から除外される
SELECT AVG(salary) FROM users;

5人中2人がNULLの場合、3人の平均が計算される。「0」として扱われるわけではない点に注意。


MAX / MIN:最大値・最小値を取得

MAXは最大値、MINは最小値を取得する関数である。

最大値

SELECT MAX(salary) FROM users;

結果:

max
550000

最高給与は55万円(佐藤一郎さん)。

最小値

SELECT MIN(salary) FROM users;

結果:

min
300000

最低給与は30万円(鈴木次郎さん)。

日付にも使える

-- 最も古い入社日
SELECT MIN(created_at) FROM users;

-- 最も新しい入社日
SELECT MAX(created_at) FROM users;

結果:

min
2019-11-01
max
2024-01-05

文字列の場合はアルファベット順(辞書順)で比較される。

複数の集計関数を同時に使う

SELECT
    COUNT(*) AS 人数,
    SUM(salary) AS 給与合計,
    AVG(salary) AS 平均給与,
    MAX(salary) AS 最高給与,
    MIN(salary) AS 最低給与
FROM users;

結果:

人数 給与合計 平均給与 最高給与 最低給与
8 3330000 416250 550000 300000

1つのクエリで複数の集計を取得できる。ASで列に別名を付けると結果が見やすくなる。


DISTINCT:重複を排除する

DISTINCTは、結果から重複を排除するキーワードである。集計関数ではないが、データの種類を調べるときによく使う。

基本的な使い方

SELECT DISTINCT department FROM users;

結果:

department
営業部
開発部
人事部
NULL

重複する部署名が1つにまとめられる。NULLも1つの値として扱われる。

DISTINCTなしの場合

SELECT department FROM users;

結果:

department
営業部
開発部
開発部
営業部
人事部
開発部
NULL
営業部

DISTINCTがないと、行数分の結果が返る。

flowchart LR subgraph DISTINCT["DISTINCTの効果"] A["営業,開発,開発,営業,人事,開発,NULL,営業"] --> B["DISTINCT"] B --> C["営業,開発,人事,NULL"] end

複数列のDISTINCT

SELECT DISTINCT department, age FROM users;

「部署と年齢」の組み合わせで重複を排除する。


GROUP BY:グループごとに集計する

GROUP BYは、データをグループ分けして、グループごとに集計する機能である。「部署ごとの人数」「年齢ごとの売上」といった分析に使う。

基本構文

SELECT 列名, 集計関数(列名) FROM テーブル名 GROUP BY 列名;

部署ごとの人数を数える

SELECT department, COUNT(*) FROM users GROUP BY department;

結果:

department count
営業部 3
開発部 3
人事部 1
NULL 1

営業部3人、開発部3人、人事部1人、未所属1人。

flowchart TB subgraph 全データ["usersテーブル(8人)"] A["田中,鈴木,小林(営業部)"] B["山田,佐藤,田村(開発部)"] C["高橋(人事部)"] D["中村(NULL)"] end subgraph GROUP["GROUP BY department"] G1["営業部: 3人"] G2["開発部: 3人"] G3["人事部: 1人"] G4["NULL: 1人"] end A --> G1 B --> G2 C --> G3 D --> G4

部署ごとの平均給与

SELECT department, AVG(salary) AS 平均給与 FROM users GROUP BY department;

結果:

department 平均給与
営業部 376666.67
開発部 473333.33
人事部 400000
NULL 380000

開発部の平均給与が最も高い。

複数の集計を同時に

SELECT
    department,
    COUNT(*) AS 人数,
    SUM(salary) AS 給与合計,
    ROUND(AVG(salary)) AS 平均給与,
    MAX(salary) AS 最高給与,
    MIN(salary) AS 最低給与
FROM users
GROUP BY department;

結果:

department 人数 給与合計 平均給与 最高給与 最低給与
営業部 3 1130000 376667 480000 300000
開発部 3 1420000 473333 550000 420000
人事部 1 400000 400000 400000 400000
NULL 1 380000 380000 380000 380000

1つのクエリで部署ごとの詳細な分析ができる。

GROUP BYの重要なルール

SELECT句に書ける列は、GROUP BY句に含まれている列か、集計関数だけである。

-- エラー: name は GROUP BY に含まれていない
SELECT department, name, COUNT(*) FROM users GROUP BY department;

なぜエラーになるか?営業部には3人いるが、nameには「田中太郎」「鈴木次郎」「小林誠」のどれを表示すればよいか決められないため。

-- 正しい: GROUP BY に含まれる列と集計関数だけ
SELECT department, COUNT(*) FROM users GROUP BY department;

複数列でグループ化

-- 部署と入社年ごとの人数
SELECT
    department,
    EXTRACT(YEAR FROM created_at) AS 入社年,
    COUNT(*) AS 人数
FROM users
GROUP BY department, EXTRACT(YEAR FROM created_at)
ORDER BY department, 入社年;

EXTRACT(YEAR FROM 日付)は日付から年を取り出す関数。


HAVING:グループを絞り込む

HAVINGは、GROUP BYで作ったグループに対して条件を指定する句である。「2人以上の部署だけ」といった絞り込みができる。

WHEREとHAVINGの違い

対象 タイミング
WHERE 個々の行 グループ化の前
HAVING グループ グループ化の後
flowchart LR A["全データ"] --> B["WHERE(行を絞る)"] B --> C["GROUP BY(グループ化)"] C --> D["HAVING(グループを絞る)"] D --> E["結果"]

基本的な使い方

-- 2人以上いる部署だけ表示
SELECT department, COUNT(*) AS 人数
FROM users
GROUP BY department
HAVING COUNT(*) >= 2;

結果:

department 人数
営業部 3
開発部 3

人事部(1人)とNULL(1人)は除外される。

HAVINGで集計関数を使う

-- 平均給与が40万円以上の部署
SELECT department, AVG(salary) AS 平均給与
FROM users
GROUP BY department
HAVING AVG(salary) >= 400000;

結果:

department 平均給与
開発部 473333.33
人事部 400000

WHEREとHAVINGの併用

-- 30歳以上の社員だけで集計し、2人以上いる部署を表示
SELECT department, COUNT(*) AS 人数
FROM users
WHERE age >= 30
GROUP BY department
HAVING COUNT(*) >= 2;

結果:

department 人数
開発部 2

処理の流れ:

  1. WHERE: 30歳以上の社員に絞る(5人)
  2. GROUP BY: 部署ごとにグループ化
  3. HAVING: 2人以上のグループだけ残す
flowchart LR A["8人"] -->|"WHERE age >= 30"| B["5人"] B -->|"GROUP BY"| C["開発2,営業1,人事1,NULL1"] C -->|"HAVING >= 2"| D["開発2人"]

よくある間違い:HAVINGでWHEREを使う

-- 誤り: 集計結果の条件にWHEREは使えない
SELECT department, COUNT(*) FROM users GROUP BY department WHERE COUNT(*) >= 2;

-- 正しい: 集計結果の条件にはHAVINGを使う
SELECT department, COUNT(*) FROM users GROUP BY department HAVING COUNT(*) >= 2;

WHEREはグループ化の前、HAVINGはグループ化の後に適用される。


ORDER BYとの組み合わせ

集計結果を並べ替えるには、ORDER BYを使う。

-- 人数が多い順に表示
SELECT department, COUNT(*) AS 人数
FROM users
GROUP BY department
ORDER BY 人数 DESC;

結果:

department 人数
営業部 3
開発部 3
人事部 1
NULL 1

SQLの実行順序

SQLの句には処理される順序がある。記述順序とは異なる点に注意。

実行順 役割
1 FROM テーブルを指定
2 WHERE 行を絞り込む
3 GROUP BY グループ化
4 HAVING グループを絞り込む
5 SELECT 列を選択、集計
6 ORDER BY 並べ替え
7 LIMIT 件数制限
flowchart TB A["FROM(テーブル指定)"] --> B["WHERE(行を絞る)"] B --> C["GROUP BY(グループ化)"] C --> D["HAVING(グループを絞る)"] D --> E["SELECT(列を選択)"] E --> F["ORDER BY(並べ替え)"] F --> G["LIMIT(件数制限)"]

集計のパフォーマンス

集計処理は、データ量が増えると重くなりやすい。大量データを扱う際のポイントを押さえておこう。

インデックスの活用

WHERE句で絞り込む列にインデックスがあると、集計対象のデータを効率的に取得できる。

-- departmentにインデックスがあると速くなる
SELECT department, COUNT(*) FROM users WHERE department = '開発部' GROUP BY department;

ただし、GROUP BY自体にはインデックスが直接効かないケースも多い。

COUNT(*)とCOUNT(1)

COUNT(*)COUNT(1)は同じ結果を返し、パフォーマンスもほぼ同じ。どちらを使っても問題ない。

-- どちらも同じ
SELECT COUNT(*) FROM users;
SELECT COUNT(1) FROM users;

大量データでの注意点

データ量 COUNT(*) GROUP BY + 複数集計
1万件 一瞬 一瞬
100万件 数百ミリ秒 数秒
1億件 数秒 数十秒〜数分

大量データで集計が遅い場合の対策:

  1. WHERE句で対象を絞る: 全件ではなく必要な範囲だけ集計
  2. インデックスを活用: 絞り込み条件にインデックスを作成
  3. マテリアライズドビュー: 集計結果を事前計算して保存
  4. サマリーテーブル: 日次・月次の集計を別テーブルに保存

これらの詳細はPart 4「パフォーマンス編」で扱う。


実践例:売上分析レポート

ここまでの知識を組み合わせた、実践的なクエリを見てみよう。

部署別の人員・給与サマリー

SELECT
    COALESCE(department, '未所属') AS 部署,
    COUNT(*) AS 人数,
    SUM(salary) AS 給与合計,
    ROUND(AVG(salary)) AS 平均給与,
    MAX(salary) AS 最高給与,
    MIN(salary) AS 最低給与,
    MAX(salary) - MIN(salary) AS 給与幅
FROM users
GROUP BY department
HAVING COUNT(*) >= 1
ORDER BY 平均給与 DESC;

結果:

部署 人数 給与合計 平均給与 最高給与 最低給与 給与幅
開発部 3 1420000 473333 550000 420000 130000
人事部 1 400000 400000 400000 400000 0
未所属 1 380000 380000 380000 380000 0
営業部 3 1130000 376667 480000 300000 180000

このクエリのポイント:

  • COALESCE(department, '未所属'): NULLを「未所属」に置き換え
  • 複数の集計関数を同時に使用
  • MAX(salary) - MIN(salary): 給与幅を計算
  • ORDER BY 平均給与 DESC: 平均給与の高い順に並べ替え

まとめ

  • COUNT(*)で全件数、COUNT(列名)でNULL以外をカウント
  • SUMは合計、AVGは平均、MAXは最大、MINは最小
  • DISTINCTで重複を排除
  • GROUP BYでグループごとに集計
  • HAVINGでグループを条件で絞り込む
  • WHEREは行、HAVINGはグループに対する条件
  • SQLの実行順序: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

用語

用語 説明
集計関数 複数の行から1つの値を計算する関数。COUNT, SUM, AVG, MAX, MIN など
COUNT 件数を数える集計関数
SUM 合計を計算する集計関数
AVG 平均を計算する集計関数
MAX 最大値を取得する集計関数
MIN 最小値を取得する集計関数
DISTINCT 重複を排除するキーワード
GROUP BY データをグループ分けして集計する句
HAVING GROUP BYで作ったグループに条件を指定する句
ROUND 数値を指定桁数で丸める関数
COALESCE NULLを別の値に置き換える関数
EXTRACT 日付から年・月・日などを取り出す関数
目次