SQLでSUM関数とGROUP BY句を使ってカテゴリ別の合計を計算する方法

結論から言うと、カテゴリ別の合計は「何を1グループとするか」をGROUP BYに書き、足す列をSUMへ渡します。対象となる明細行はWHERE、合計後のグループはHAVINGで絞ります。最後にORDER BYを付け、件数と総計を別クエリで照合すれば、構文が動くだけでなく数値も正しい集計にできます。

最小形はSELECT category, SUM(amount) FROM sales GROUP BY category;です。ただし実務では、NULL、データ0件、JOINによる行の増幅、未確定データの混入、金額型の桁あふれが誤集計の主因になります。本記事では読み取り専用の例から始め、安全に検算する順序まで解説します。

目次

目的別の判断表

やりたいこと使う句・式確認ポイント
カテゴリごとの合計GROUP BY categoryとSUM(amount)1行が1取引か、カテゴリの粒度が正しいか
部署とカテゴリごとの合計GROUP BY department, categorySELECTにも両方の列を書く
確定済みだけ集計WHERE status = 'confirmed'WHEREは集計前の明細を除外する
合計10万円以上だけ表示HAVING SUM(amount) >= 100000HAVINGは集計後のグループを除外する
データなしを0表示COALESCE(SUM(amount), 0)GROUP BY結果が0行なら別途マスタ起点が必要
条件別の合計を横並びSUM(CASE WHEN ... THEN amount ELSE 0 END)ELSE 0と対象条件を確認する
JOIN後に合計が増えた明細側を先に集計してからJOIN結合前後の行数とキー重複を比較する

基本例:カテゴリ別の売上合計

例として、salesテーブルに次の5行があるとします。amountは固定小数点の金額列、statusは売上の状態です。

sale_idcategoryamountstatus
1A100confirmed
2B200confirmed
3A150confirmed
4C250cancelled
5B300confirmed
SELECT
    category,
    COUNT(*) AS row_count,
    SUM(amount) AS total_amount
FROM sales
WHERE status = 'confirmed'
GROUP BY category
ORDER BY category;

結果はAが2件・250、Bが2件・500です。Cはcancelledなので、WHEREの段階で集計対象から外れます。COUNT(*)を同時に出すのは、合計だけでは気づきにくい行の重複や欠落を見つけるためです。

安全にクエリを組み立てる手順

  1. 集計粒度を日本語で決める。「確定済み売上をカテゴリごとに1行」のように、1結果行が何を表すかを書き出します。
  2. 明細だけを先に確認する。SELECT sale_id, category, amount FROM sales WHERE status = 'confirmed';を実行し、除外条件が正しいか見ます。
  3. GROUP BYとSUMを追加する。SELECTの非集計列とGROUP BYの式を一致させます。
  4. COUNTを併記する。カテゴリ別の件数合計が、WHERE適用後の全明細件数と一致するか確認します。
  5. 総計を照合する。SELECT SUM(amount) FROM sales WHERE status = 'confirmed';と、カテゴリ別合計の総和が一致するか確認します。
  6. 必要な順序を明示する。カテゴリ順ならORDER BY category、合計の大きい順ならORDER BY total_amount DESCを付けます。

WHEREとHAVINGの違い

WHEREはグループを作る前に明細行を選び、HAVINGは作った後のグループを選びます。SQL Serverの論理処理順も、おおむねFROM、WHERE、GROUP BY、HAVING、SELECT、ORDER BYです。

SELECT
    category,
    SUM(amount) AS total_amount
FROM sales
WHERE status = 'confirmed'
GROUP BY category
HAVING SUM(amount) >= 300
ORDER BY total_amount DESC;

この例では、最初に未確定行を除き、その後で合計300以上のカテゴリだけを残します。WHERE SUM(amount) ...とは書けません。逆に、単純な状態条件をHAVINGへ置くと不要な行まで集計してから捨てることになり、意図も性能も分かりにくくなります。

複数列でグループ化する

部署別かつカテゴリ別に集計するなら、両方をSELECTとGROUP BYへ書きます。GROUP BYの列を増やすほどグループは細かくなります。「カテゴリごと」のつもりで日付や担当者を追加すると、同じカテゴリが複数行へ分かれるため注意してください。

SELECT
    department,
    category,
    SUM(amount) AS total_amount
FROM sales
WHERE status = 'confirmed'
GROUP BY department, category
ORDER BY department, category;

月別集計の式はDB製品で異なります。SQL ServerのDATEFROMPARTSやYEAR、PostgreSQLのdate_trunc、MySQLのDATE_FORMATを無理に共通化せず、使用中のDBの公式文書に合わせてください。日付式をSELECTへ出す場合は、同じ式をGROUP BYへ指定するのが移植しやすい書き方です。

条件付き集計で列を横並びにする

確定・取消などの状態別合計を1行へ並べる場合は、CASEをSUMの中へ入れます。この方法なら同じテーブルを何度も読み直すクエリを避けられます。

SELECT
    category,
    SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) AS confirmed_amount,
    SUM(CASE WHEN status = 'cancelled' THEN amount ELSE 0 END) AS cancelled_amount
FROM sales
GROUP BY category
ORDER BY category;

ELSE 0を省くと、条件に合う行がないグループでCASEがすべてNULLとなり、SUMもNULLになる場合があります。0表示が要件ならELSE 0を明記します。なお、状態の種類が頻繁に増える場合は、列を固定するより「category, status」で縦に集計した方が保守しやすいこともあります。

NULLとデータ0件を正しく扱う

SUMはNULLの金額を足しません。100、NULL、200なら結果は300です。しかし「NULLを0円として扱ってよいか」と「不明な金額を集計から外すか」は業務上の意味が違います。NULLを機械的に0へ変える前に、欠損値なのか未計上なのかを確認してください。

SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM sales
WHERE status = 'confirmed';

この式は対象行が0件でも1行を返す単一集計で、NULLを0へ変えます。一方、GROUP BY categoryの入力が0件なら結果行そのものがありません。売上がないカテゴリも0で表示したい場合は、カテゴリマスタを起点にLEFT JOINします。

SELECT
    c.category,
    COALESCE(SUM(s.amount), 0) AS total_amount
FROM categories c
LEFT JOIN sales s
    ON s.category = c.category
   AND s.status = 'confirmed'
GROUP BY c.category
ORDER BY c.category;

LEFT JOINでは、右側テーブルの絞り込み条件をWHEREへ移すと、売上のないカテゴリまで消えてINNER JOINと同じ結果になり得ます。この例のように、右側の状態条件をONへ置くのがポイントです。

GROUP BYのSELECT列エラーを直す

次のクエリは、カテゴリ内に複数の商品名があり得るため不正または不定です。

-- product_nameの値を1つに決められない
SELECT category, product_name, SUM(amount)
FROM sales
GROUP BY category;

直し方は、商品名も集計軸ならGROUP BY category, product_nameへ追加する、カテゴリだけが必要ならSELECTから外す、代表値に明確な意味があるならMIN(product_name)などを使う、のいずれかです。エラーを消すだけのMAXは避け、なぜその値を1つ選べるのか説明できる形にします。

MySQLではONLY_FULL_GROUP_BYが有効な構成が既定で、GROUP BYにない非集計列がグループ列へ機能従属しなければ拒否されます。SQLモードを無効化すると、サーバーがグループ内のどの値を選ぶか不定になるため、設定変更ではなくクエリの粒度を直してください。

JOINでSUMが2倍・3倍になる原因

注文に複数の商品行があり、さらに複数の支払行を同時にJOINすると、商品行と支払行の組み合わせが掛け算になり、元の金額が繰り返されます。SUM(DISTINCT amount)で隠すのは危険です。同額の別取引まで1件として扱うため、今度は過少集計になります。

WITH item_totals AS (
    SELECT order_id, SUM(amount) AS order_amount
    FROM sales_items
    GROUP BY order_id
)
SELECT
    o.category,
    SUM(i.order_amount) AS total_amount
FROM orders o
JOIN item_totals i
    ON i.order_id = o.order_id
GROUP BY o.category
ORDER BY o.category;

明細側を注文ID単位へ先にまとめれば、注文1件につき1行で結合できます。実行前後にCOUNT(*)だけでなくCOUNT(DISTINCT order_id)も出し、想定した1対1または1対多になっているか確認します。外部結合では欠損側のNULLも検算対象です。

型・精度・並び順の注意

  • 金額型:floatやrealは近似値です。金額はDBに合うDECIMAL/NUMERICなどの固定小数点型を使います。
  • 桁あふれ:SUMの戻り値型は方言と入力型で異なります。SQL ServerではintのSUMはintなので、上限を超える可能性があればSUM(CAST(amount AS bigint))などを設計時に検討します。
  • SUM(DISTINCT):値の重複を除いて足す機能です。重複行の除去機能ではありません。
  • 並び順:GROUP BYはソートを保証しません。画面、CSV、後続処理で順序が必要ならORDER BYを付けます。
  • 性能:大量データではWHERE条件、JOINキー、GROUP BY列の索引と実行計画を確認します。ただし索引追加は書き込み性能にも影響するため、DBAと検証します。

本番での確認と戻し方

  1. 最初はSELECTだけを実行し、テーブルを変更しません。
  2. 少量の既知データで手計算し、カテゴリ別件数、カテゴリ別合計、全体合計を照合します。
  3. 旧集計と新集計を同じ期間・同じ条件で並べ、差分の明細IDを特定します。
  4. 集計をINSERTやUPDATEへ使う場合はバックアップを確認し、トランザクション内で対象件数を先に表示します。
  5. 想定外の件数、NULL、総計差があればROLLBACKし、WHEREとJOIN条件を修正します。検算できない状態でCOMMITしません。

よくある質問

GROUP BYなしでSUMだけ使えますか?

使えます。SELECT SUM(amount) FROM sales;は対象全体を1グループとして1つの合計を返します。カテゴリ別など複数行の集計が必要なときにGROUP BYを追加します。

COUNT(*)とCOUNT(amount)は同じですか?

同じではありません。COUNT(*)は行を数え、COUNT(amount)はamountがNULLでない行だけを数えます。NULL金額の存在を調べるなら両方を出し、差を確認できます。

合計の大きい順に並べるには?

SUM(amount) AS total_amountと別名を付け、ORDER BY total_amount DESCを追加します。別名をORDER BYで使う方法は主要DBで広く対応しますが、HAVINGで別名を使えるかは方言差があるため、HAVINGではSUM(amount)を明記すると移植しやすくなります。

NULLカテゴリを「未分類」と表示できますか?

できます。COALESCE(category, '未分類')を使います。DBによってはSELECTの別名をGROUP BYで使えないため、SELECTとGROUP BYの両方へ同じCOALESCE式を書くのが安全です。

GROUP BYのエラーを避けるためONLY_FULL_GROUP_BYを無効にしてよいですか?

推奨しません。非集計列の値が不定になる問題を設定で隠すためです。必要な列をGROUP BYへ追加するか、SELECTから外すか、意味のある集計関数を指定してください。

公式資料

まとめ

SUMとGROUP BYの要点は、集計粒度を先に決めることです。対象明細をWHEREで絞り、GROUP BYで結果1行の単位を作り、SUMで数値を集計し、HAVINGで集計後の条件を適用します。ORDER BYなしの順序、NULLと0件、SELECT列、JOINの行増幅、戻り値型まで確認し、件数と総計を必ず照合してください。読み取り専用の検算から始めれば、実務の集計を安全に段階的に仕上げられます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次