SQLでGROUP BYしたら、件数が元データと合わない。売上の合計が大きすぎる。月別にまとめたはずなのに、同じ月が何行も出る。
このとき、最初に確認したいのはGROUP BYに渡す前のデータです。GROUP BYは入力された行を指定した単位にまとめます。JOINやWHEREによって入力行が想定と違えば、集計結果も変わります。
この記事でわかること
- 件数・合計が合わない5つの原因と見分け方
GROUP BYの前後を比較する確認手順- 原因に応じたSQLの直し方
結論:集計前の行数と、集計したい単位を先に確認する
例えば「顧客ごとの注文数」が欲しいとき、注文データが1行=注文ならCOUNT(*)で数えられます。一方、データが1行=注文明細なら、COUNT(*)は注文数ではなく明細行数です。
まずはクエリのGROUP BYより前にあるFROM、JOIN、WHEREを残し、次の3点を調べます。
- 集計に入る行は何行か。
- 何を1件として数えたいか。
- その単位を表すIDは、集計前に何行あるか。
元データにある行が集計前に消えているのか、JOINで増えているのか、行は正しくても数え方が違うのかを分けると、修正箇所が見えてきます。
まずここから確認:数字がずれた段階を探す
次の順番で、元のSQLを少しずつ確認してください。
| 順番 | 確認するもの | 分かること |
|---|---|---|
| 1 | 元テーブルの行数・合計 | 比較の基準 |
| 2 | JOIN後、WHERE前の行数・合計 | 結合で行が増減したか |
| 3 | WHERE後、GROUP BY前の行数・合計 | 条件で行が消えたか |
| 4 | グループごとのCOUNT(*)・COUNT(列)・SUM(列) | 数え方や合計対象が違うか |
| 5 | HAVINGを付けた後のグループ数 | 集計結果の一部を除外していないか |
例えば、元の集計が次のSQLなら、
SELECT region, COUNT(*) AS rows_count, SUM(amount) AS total_amount
FROM orders
WHERE order_date >= DATE '2026-09-01'
GROUP BY region;
まずGROUP BYを外し、集計に入る行全体を確認します。
SELECT COUNT(*) AS input_rows, SUM(amount) AS input_amount
FROM orders
WHERE order_date >= DATE '2026-09-01';
続いて、問題が出る地域の明細を直接見ます。
SELECT order_id, region, order_date, amount
FROM orders
WHERE order_date >= DATE '2026-09-01'
AND region = '東日本'
ORDER BY order_id;
この時点の明細が想定と違えば、集計関数を変更する前に、参照元・JOIN・WHEREを調べます。
原因1:GROUP BYの単位が欲しい数字と違う
問題と原因
「月別の件数」を出したいのに、同じ月が複数行になる。または、注文数のつもりで注文明細数を数えている。
GROUP BYに指定した列の組み合わせごとに1行が作られます。GROUP BY 月, 地域なら月・地域ごと、GROUP BY 注文IDなら注文ごとです。
確認方法
元データの1行が注文なのか、注文明細なのかを確認します。そのうえで、現在のGROUP BYに入っている列をすべて見ます。集計結果が細かく分かれすぎているなら、意図しない列が入っていないか調べてください。
解決方法
欲しい結果の単位に合わせて列を指定します。例えば地域別売上なら、基本形は次のとおりです。
SELECT region, SUM(amount) AS total_amount
FROM order_lines
GROUP BY region;
ただし、order_linesに同じ注文の複数明細がある場合、COUNT(*)を加えても得られるのは明細行数です。注文数が必要なら、注文IDをどう数えるか別に決めます。
GROUP BYの基本構文から確認したい場合は、サイト内のSQL入門記事も参照できます。
原因2:COUNT(*)・COUNT(列)・COUNT(DISTINCT列)を混同している
問題と原因
グループごとのCOUNTが、手元で数えた行数より少ない。または、注文数が多すぎる。
3つの数え方は異なります。
| 書き方 | 数えるもの |
|---|---|
COUNT(*) | 入力された行 |
COUNT(order_id) | order_idがNULLでない行 |
COUNT(DISTINCT order_id) | NULLを除いた異なる注文ID |
実行例
次の例はPostgreSQLで実行可能です。
WITH sample (region, order_id, amount) AS (
VALUES
('東日本', 101, 100),
('東日本', 101, 50),
('東日本', 102, 200),
('東日本', NULL, 30)
)
SELECT
region,
COUNT(*) AS rows_count,
COUNT(order_id) AS non_null_ids,
COUNT(DISTINCT order_id) AS distinct_orders,
SUM(amount) AS total_amount
FROM sample
GROUP BY region;
実行結果
| region | rows_count | non_null_ids | distinct_orders | total_amount |
|---|---|---|---|---|
| 東日本 | 4 | 3 | 2 | 380 |
101は2明細あるため、COUNT(*)では2行として数えます。NULLの注文IDを持つ行も、COUNT(*)と売上合計には含まれます。したがって、COUNT(DISTINCT order_id) = 2だから売上380も「2注文分」と断定することはできません。注文IDが欠けた30の扱いを確認する必要があります。
解決方法
「データ行数」「IDが入った行数」「異なる注文ID数」のどれが必要かを定義し、それに合う関数を使います。COUNT(DISTINCT ...)で期待値に合っても、同じIDが複数行ある理由は確認してください。
どのキーが複数行あるか調べるSQLは、次の記事で手順を示しています。
原因3:JOIN後に行が増えてから集計している
問題と原因
JOINを追加した後からCOUNT(*)もSUM(amount)も大きくなった。
例えば注文テーブルの1行に、対応履歴テーブルの2行が一致すると、結合結果には注文の金額が2行現れます。その後にGROUP BYして注文金額を足せば、同じ金額を2回加算します。
確認方法
JOINの前後で、元テーブルのIDごとの行数を比較します。
SELECT
o.order_id,
COUNT(*) AS rows_after_join
FROM orders AS o
LEFT JOIN order_events AS e
ON o.order_id = e.order_id
GROUP BY o.order_id
HAVING COUNT(*) > 1;
このSQLで行が返ったら、その注文IDについて、元のorders側とorder_events側をそれぞれ確認します。複数の対応履歴が正しいこともあるため、行数が増えたというだけで履歴を削除しないでください。
解決方法
注文ごとの対応件数が欲しいなら、対応履歴を注文単位に集計してから注文表へ結合します。
SELECT
o.order_id,
o.amount,
e.event_count
FROM orders AS o
LEFT JOIN (
SELECT order_id, COUNT(*) AS event_count
FROM order_events
GROUP BY order_id
) AS e
ON o.order_id = e.order_id;
この例はordersが1行=1注文であることを前提にしています。結合キーの重複や、1対多・多対多の見分け方は既存記事で詳しく解説しています。
原因4:WHEREとHAVINGで異なる行・グループを除外している
問題と原因
HAVINGを足したら結果の地域が消えた。あるいは、WHEREの条件を変えたら合計額が想定以上に変わった。
WHEREは集計に入れる行を選び、HAVINGは集計後に残すグループを選びます。両者を入れ替えると、同じ条件を書いたつもりでも結果の意味が変わります。
確認方法
まずHAVINGを一時的に外し、消えたグループが戻るか確認します。次に、WHEREを適用した直後の明細を見て、合計に含めたい行が残っているか調べます。
解決方法
例えば「完了した注文だけを対象に、売上合計が一定以上の地域を表示する」なら、対象行を選ぶ条件をWHERE、地域の合計を判定する条件をHAVINGへ書きます。
SELECT region, COUNT(*) AS rows_count, SUM(amount) AS total_amount
FROM orders
WHERE status = '完了'
GROUP BY region
HAVING SUM(amount) >= 1000;
この結果のrows_countは完了した注文の行数です。また、売上合計が1000未満の地域は、元データに存在しても結果から消えます。
原因5:日付条件・NULLの扱いが比較元と違う
問題と原因
月別集計だけ合わない。売上のあるはずの月が結果に出ない。比較元の集計と数件だけずれる。
日付範囲、どの日付列を使うか、時刻を含む列をどう絞るかで、GROUP BYに入る行が変わります。また、SUM(amount)はNULLの金額を合計に加えません。
確認方法
比較元と次の項目をそろえます。
- 注文日・計上日など、どの日付列を使うか
- 集計期間の開始・終了
- 時刻を含む列か
amountにNULLがあるか- 返品やキャンセルを含めるか
特にタイムスタンプ列では、<= '2026-09-30'のような条件が9月30日の時刻付きデータまで含むとは限りません。
解決方法
PostgreSQLで、9月1日から9月30日までを含むよう時刻付き列を絞るなら、次のように翌月1日を含めない条件にできます。
SELECT
COUNT(*) AS rows_count,
COUNT(amount) AS rows_with_amount,
SUM(amount) AS total_amount
FROM orders
WHERE order_timestamp >= TIMESTAMP '2026-09-01 00:00:00'
AND order_timestamp < TIMESTAMP '2026-10-01 00:00:00';
rows_countとrows_with_amountが違えば、金額がNULLの行があります。その行を売上にどう扱うか、業務上の定義を確認してください。日時型やタイムゾーンの扱いはDB製品と列の型によって異なるため、実際の環境でも境界日の行を確認します。
それでも合わないとき:差が出る最小単位まで分解する
全体の合計が違う場合は地域別、違う地域が見つかったら日別、さらに注文ID別へと分けます。「東日本の9月12日の注文101」のように、具体的な数行まで絞ることが目標です。
SELECT
region,
order_id,
COUNT(*) AS rows_count,
COUNT(amount) AS rows_with_amount,
SUM(amount) AS total_amount
FROM orders
WHERE order_timestamp >= TIMESTAMP '2026-09-01 00:00:00'
AND order_timestamp < TIMESTAMP '2026-10-01 00:00:00'
GROUP BY region, order_id
ORDER BY region, order_id;
対象IDが存在しないなら、参照元、JOIN、WHEREを確認します。IDはあるが複数行なら、元データとJOIN先の粒度を確認します。行数が合っていて金額だけ違うなら、金額列、NULL、返品・キャンセルなどの定義を確認します。
現場では、次の2点に気を付けてください。
DISTINCTで行数を合わせて調査を終えない。 選択列が変わると結果も変わり、二重計上の原因が残る場合があります。- 総額だけで検算を終えない。 増えた注文と消えた注文があれば、総額が偶然一致しても地域別・日別の内訳は違います。
数字の定義や元データの状態も含めた確認観点は、次の記事でも整理しています。
まとめ
GROUP BYの件数・合計が合わない場合は、次の5つを確認します。
GROUP BYの単位は、欲しい数字の単位と合っているか。COUNT(*)、COUNT(列)、COUNT(DISTINCT 列)を使い分けているか。- JOINで集計前の行が増えていないか。
WHEREとHAVINGで意図しない行・グループを除外していないか。- 日付の範囲と
NULLの扱いが比較元と一致しているか。
最短の調べ方は、集計前の行を確認し、差が出るIDまで絞ることです。原因を特定してからSQLを修正し、全体と内訳の両方を再計算しましょう。
次に読む記事
同じIDが何行あるか調べる方法はこちらです。
JOINを追加してから件数・合計が増えた場合はこちらです。
GROUP BYや集計関数の基本を復習したい場合はこちらです。
-160x90.png)




