【SQL】SUM関数完全ガイド|GROUP BY・WHERE条件・SUM(CASE WHEN)・SUM OVER()・NULL対策・HAVING・JOINの重複対策まで

SQLのSUM関数は、指定した列の値を合計する集約関数です。売上合計・在庫数合計・ポイント合計など、あらゆる集計の基本となる関数ですが、NULLの扱い・JOINによる重複・ウィンドウ関数での累計など、知らないと間違いやすいポイントがあります。

本記事ではSUM関数の基本から応用まで、実務で必要な知識を体系的に解説します。

この記事で分かること

  • SUM の基本構文と動作
  • GROUP BY と組み合わせたグループ別合計
  • WHERE 句で条件を付けた合計
  • SUM(DISTINCT) で重複を排除した合計
  • SUM(CASE WHEN …) で条件別に分けて集計する方法
  • SUM OVER() ウィンドウ関数で累計・パーティション別合計
  • NULL が混ざったときの挙動と対策
  • HAVING で合計値による絞り込み
  • JOIN と SUM を組み合わせるときの二重カウント対策
スポンサーリンク

SUM の基本構文

全体合計を求める
-- sales テーブルの amount 列をすべて合計
SELECT SUM(amount) AS total_amount
FROM sales;
-- 結果: 1,250,000(全売上の合計)

-- WHERE で絞り込んでから合計
SELECT SUM(amount) AS electronics_total
FROM sales
WHERE category = '家電';
-- 結果: 450,000(家電カテゴリだけの合計)
複数列を同時に合計
-- 複数の数値列をそれぞれ合計
SELECT
    SUM(quantity)      AS total_qty,
    SUM(unit_price)    AS total_price,
    SUM(discount)      AS total_discount,
    SUM(quantity * unit_price) AS total_revenue  -- 計算式も合計できる
FROM order_items;

GROUP BY と組み合わせる

GROUP BY を使うと、グループごとの合計を求めることができます。

グループ別合計
-- カテゴリ別の売上合計
SELECT
    category,
    SUM(amount) AS total_amount,
    COUNT(*)    AS order_count
FROM sales
GROUP BY category
ORDER BY total_amount DESC;
-- 結果:
-- 家電    | 450,000 | 120
-- 食品    | 380,000 | 250
-- 衣料品  | 220,000 | 95
複数列でグループ化
-- 年月×カテゴリ別の売上合計
SELECT
    DATE_FORMAT(sale_date, '%Y-%m') AS sale_month,  -- MySQL
    category,
    SUM(amount) AS total_amount
FROM sales
GROUP BY DATE_FORMAT(sale_date, '%Y-%m'), category
ORDER BY sale_month, category;
-- PostgreSQL: TO_CHAR(sale_date, 'YYYY-MM')
-- SQL Server: FORMAT(sale_date, 'yyyy-MM')

GROUP BY + SUM の詳しいパターン(ROLLUP で小計・合計を自動追加、CUBE で多次元集計など)はGROUP BY + SUM 集計ガイドを参照してください。

NULL の扱い

SUM における NULL の挙動は、初心者が最もつまずきやすいポイントです。

状況 SUM の挙動
NULL を含む列 NULLの行は無視して残りを合計する
対象行が0件 結果はNULL(0ではない)
全行が NULL 結果はNULL
NULL の動作確認
-- データ: amount = 100, 200, NULL, 300
SELECT SUM(amount) FROM sales;
-- 結果: 600(NULLは無視される。100+200+300)

-- 0件の場合
SELECT SUM(amount) FROM sales WHERE 1 = 0;
-- 結果: NULL(0ではない!)

-- NULL を 0 として扱いたい場合
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM sales WHERE 1 = 0;
-- 結果: 0
NULL を含む計算に注意
-- quantity=3, unit_price=NULL の行がある場合
SELECT SUM(quantity * unit_price) FROM order_items;
-- 3 * NULL = NULL → この行は合計に含まれない

-- NULL を 0 として計算したい場合
SELECT SUM(quantity * COALESCE(unit_price, 0)) FROM order_items;
-- 3 * 0 = 0 → この行は 0 として合計に含まれる

SUM の結果が NULL になるケース:対象行が0件のとき、SUM は NULL を返します。アプリ側でそのまま使うとNullPointerException等になるため、COALESCE(SUM(col), 0) で0に変換するのが実務の定石です。

SUM(DISTINCT) — 重複を排除して合計

SUM(DISTINCT col) は、重複する値を1回だけカウントして合計します。

SUM(DISTINCT) の動作
-- データ: amount = 100, 200, 100, 300
SELECT SUM(amount)          AS total;       -- 700(全て合計)
SELECT SUM(DISTINCT amount) AS unique_total; -- 600(100+200+300)

-- 実務では JOINで行が膨らんだとき に使うことが多い
SELECT
    c.customer_id,
    SUM(DISTINCT o.amount) AS unique_order_total
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY c.customer_id;

SUM(DISTINCT) の落とし穴:たまたま同じ金額の異なる注文が存在する場合、片方が除外されます。JOINによる行の膨張が原因なら、サブクエリで先に集計してからJOINする方が安全です。

JOINの膨張はサブクエリで解決(推奨)
-- 先に注文テーブルで集計してからJOIN
SELECT
    c.customer_id,
    c.name,
    o_sum.total_amount
FROM customers c
JOIN (
    SELECT customer_id, SUM(amount) AS total_amount
    FROM orders
    GROUP BY customer_id
) o_sum ON c.customer_id = o_sum.customer_id;

SUM(CASE WHEN …) — 条件別の合計

CASE WHEN を SUM の中に書くことで、条件ごとに分けた合計を1つのクエリで同時に求められます。いわゆる「ピボット集計」です。

条件別合計(ピボット集計)
-- カテゴリ別の売上を横並びで表示
SELECT
    DATE_FORMAT(sale_date, '%Y-%m') AS sale_month,
    SUM(CASE WHEN category = '家電'   THEN amount ELSE 0 END) AS electronics,
    SUM(CASE WHEN category = '食品'   THEN amount ELSE 0 END) AS food,
    SUM(CASE WHEN category = '衣料品' THEN amount ELSE 0 END) AS clothing,
    SUM(amount) AS total
FROM sales
GROUP BY DATE_FORMAT(sale_date, '%Y-%m')
ORDER BY sale_month;
-- 結果:
-- 2024-01 | 150,000 | 120,000 | 80,000 | 350,000
-- 2024-02 | 180,000 | 130,000 | 70,000 | 380,000
条件別カウント(○件/×件の集計)
-- 注文ステータスごとの金額合計
SELECT
    customer_id,
    SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) AS completed_total,
    SUM(CASE WHEN status = 'cancelled' THEN amount ELSE 0 END) AS cancelled_total,
    SUM(CASE WHEN status = 'pending'   THEN amount ELSE 0 END) AS pending_total
FROM orders
GROUP BY customer_id;

SUM(CASE WHEN …) のより詳しいパターンはCASE WHEN完全ガイドを参照してください。

SUM OVER() — ウィンドウ関数で累計・小計

SUM をウィンドウ関数として使うと、GROUP BY せずに各行に合計値を付与できます。累計(ランニングトータル)やパーティション別合計が簡潔に書けます。

全体合計を各行に付与
-- 各行に全体合計と構成比を表示
SELECT
    category,
    amount,
    SUM(amount) OVER ()                      AS grand_total,
    ROUND(amount * 100.0 / SUM(amount) OVER (), 1) AS pct
FROM sales;
-- 結果:
-- 家電 | 15,000 | 1,250,000 | 1.2%
-- 食品 |  8,000 | 1,250,000 | 0.6%
パーティション別合計
-- カテゴリ別の合計を各行に付与(GROUP BY不要)
SELECT
    category,
    product_name,
    amount,
    SUM(amount) OVER (PARTITION BY category) AS category_total
FROM sales;
-- 結果:
-- 家電 | テレビ    | 50,000 | 450,000
-- 家電 | エアコン  | 80,000 | 450,000
-- 食品 | 米        |  3,000 | 380,000
累計(ランニングトータル)
-- 日付順に売上の累計を計算
SELECT
    sale_date,
    amount,
    SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM sales
ORDER BY sale_date;
-- 結果:
-- 2024-01-01 | 10,000 |  10,000
-- 2024-01-02 | 15,000 |  25,000
-- 2024-01-03 |  8,000 |  33,000

-- カテゴリ別の累計
SELECT
    category,
    sale_date,
    amount,
    SUM(amount) OVER (
        PARTITION BY category
        ORDER BY sale_date
    ) AS category_running_total
FROM sales;
移動合計(直近N件の合計)
-- 直近3件の売上合計(移動合計)
SELECT
    sale_date,
    amount,
    SUM(amount) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_sum_3
FROM sales;

HAVING — 合計値で絞り込む

GROUP BY の結果を SUM の値で絞り込むには HAVING を使います(WHERE は集約前のフィルタ)。

HAVING で合計値による絞り込み
-- 売上合計が100,000以上のカテゴリだけ抽出
SELECT
    category,
    SUM(amount) AS total_amount
FROM sales
GROUP BY category
HAVING SUM(amount) >= 100000
ORDER BY total_amount DESC;

-- WHERE と HAVING の併用
SELECT
    category,
    SUM(amount) AS total_amount
FROM sales
WHERE sale_date >= '2024-01-01'    -- 集約前: 行単位のフィルタ
GROUP BY category
HAVING SUM(amount) >= 100000       -- 集約後: グループ単位のフィルタ
ORDER BY total_amount DESC;

WHERE vs HAVING:WHERE は集約前の行フィルタ、HAVING は集約後のグループフィルタです。WHERE SUM(amount) > 100000 はエラーになります。

サブクエリ内の SUM

SUM の結果をサブクエリとして他の条件や値に使うパターンです。

全体合計との比較
-- 平均を超える売上のカテゴリを抽出
SELECT category, SUM(amount) AS total
FROM sales
GROUP BY category
HAVING SUM(amount) > (
    SELECT AVG(category_total) FROM (
        SELECT SUM(amount) AS category_total
        FROM sales
        GROUP BY category
    ) sub
);
サブクエリで合計値を UPDATE に使う
-- 顧客の累計購入額を更新
UPDATE customers c
SET total_spent = (
    SELECT COALESCE(SUM(o.amount), 0)
    FROM orders o
    WHERE o.customer_id = c.customer_id
);

データ型とオーバーフロー

SUM の結果は元の列の型によって異なります。大量データの合計ではオーバーフローに注意が必要です。

元の型 SUM の結果型 備考
INT BIGINT(MySQL/PG/SS) 自動で大きい型に昇格するが上限あり
DECIMAL(10,2) DECIMAL(精度が拡張される) 精度は保持される
FLOAT / DOUBLE FLOAT / DOUBLE 浮動小数点の誤差が累積する可能性あり
オーバーフローと対策
-- INT列の合計がBIGINTの上限を超えるケース(極端に大量のデータ)
-- → CAST で事前に型を拡張する
SELECT SUM(CAST(amount AS BIGINT)) FROM huge_table;

-- 金額はDECIMAL型で保持するのが安全
-- FLOAT/DOUBLE は誤差が出る
SELECT SUM(price) FROM products;  -- price が FLOAT だと 999.999...98 のような誤差が出得る

金額の合計にはDECIMAL型を使う:FLOAT/DOUBLE型の列でSUMすると浮動小数点の丸め誤差が累積します。請求書や会計処理など正確性が求められる場面では、列をDECIMAL(NUMERIC)型で定義してください。

集約関数の比較

関数 動作 NULLの扱い
SUM(col) 合計 NULLを無視
AVG(col) 平均 NULLの行を分母からも除外
COUNT(col) 件数 NULLの行をカウントしない
COUNT(*) 全件数 NULLも含めてカウント
MAX(col) / MIN(col) 最大値 / 最小値 NULLを無視

各集約関数の詳細はCOUNT関数完全ガイドを参照してください。

まとめ

目的 書き方
全体合計 SELECT SUM(col) FROM table
グループ別合計 SUM(col) … GROUP BY key
条件付き合計 SUM(col) … WHERE 条件
重複排除して合計 SUM(DISTINCT col)
条件別の横並び集計 SUM(CASE WHEN … THEN col ELSE 0 END)
累計・小計 SUM(col) OVER (ORDER BY …)
パーティション別合計 SUM(col) OVER (PARTITION BY key)
合計値で絞り込み HAVING SUM(col) >= N
NULL を 0 に変換 COALESCE(SUM(col), 0)
  • SUM は NULL を無視する。ただし対象行が0件なら結果は NULL → COALESCE(SUM(col), 0) で安全に
  • GROUP BY と組み合わせてグループ別集計 — ROLLUP/CUBE を使った小計・合計はGROUP BY + SUM 集計ガイドを参照
  • SUM(CASE WHEN …) で条件ごとの横並び集計ができる
  • SUM OVER() で累計・パーティション別合計・移動合計を GROUP BY なしで実現
  • JOIN と SUM を組み合わせるときは二重カウントに注意 — サブクエリで先に集計する
  • 金額は DECIMAL 型を使い、FLOAT の丸め誤差を避ける