結論から言うと、カテゴリ別の合計は「何を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, category | SELECTにも両方の列を書く |
| 確定済みだけ集計 | WHERE status = 'confirmed' | WHEREは集計前の明細を除外する |
| 合計10万円以上だけ表示 | HAVING SUM(amount) >= 100000 | HAVINGは集計後のグループを除外する |
| データなしを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_id | category | amount | status |
|---|---|---|---|
| 1 | A | 100 | confirmed |
| 2 | B | 200 | confirmed |
| 3 | A | 150 | confirmed |
| 4 | C | 250 | cancelled |
| 5 | B | 300 | confirmed |
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結果行が何を表すかを書き出します。
- 明細だけを先に確認する。
SELECT sale_id, category, amount FROM sales WHERE status = 'confirmed';を実行し、除外条件が正しいか見ます。 - GROUP BYとSUMを追加する。SELECTの非集計列とGROUP BYの式を一致させます。
- COUNTを併記する。カテゴリ別の件数合計が、WHERE適用後の全明細件数と一致するか確認します。
- 総計を照合する。
SELECT SUM(amount) FROM sales WHERE status = 'confirmed';と、カテゴリ別合計の総和が一致するか確認します。 - 必要な順序を明示する。カテゴリ順なら
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と検証します。
本番での確認と戻し方
- 最初はSELECTだけを実行し、テーブルを変更しません。
- 少量の既知データで手計算し、カテゴリ別件数、カテゴリ別合計、全体合計を照合します。
- 旧集計と新集計を同じ期間・同じ条件で並べ、差分の明細IDを特定します。
- 集計をINSERTやUPDATEへ使う場合はバックアップを確認し、トランザクション内で対象件数を先に表示します。
- 想定外の件数、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から外すか、意味のある集計関数を指定してください。
公式資料
- Microsoft Learn: SUM (Transact-SQL)
- Microsoft Learn: SELECT – GROUP BY clause (Transact-SQL)
- Microsoft Learn: SELECT – HAVING clause (Transact-SQL)
- PostgreSQL Documentation: Aggregate Functions
- PostgreSQL Documentation: Table Expressions
- MySQL Reference Manual: MySQL Handling of GROUP BY
まとめ
SUMとGROUP BYの要点は、集計粒度を先に決めることです。対象明細をWHEREで絞り、GROUP BYで結果1行の単位を作り、SUMで数値を集計し、HAVINGで集計後の条件を適用します。ORDER BYなしの順序、NULLと0件、SELECT列、JOINの行増幅、戻り値型まで確認し、件数と総計を必ず照合してください。読み取り専用の検算から始めれば、実務の集計を安全に段階的に仕上げられます。

コメント