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 |
-- データ: 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
-- 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回だけカウントして合計します。
-- データ: 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
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;
-- 直近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 は集約前のフィルタ)。
-- 売上合計が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 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 の丸め誤差を避ける

