SQLで重複データを確認する方法|GROUP BY・COUNTで原因を特定

SQLの重複を見つける/GROUP BY・COUNTで原因を特定 実務で使うデータ・AIスキル
SQLの重複を見つける/GROUP BY・COUNTで原因を特定

SQLを実行したら、想定より行数が多い。売上を合計すると元データより大きい。このとき、いきなりDISTINCTを追加しても、何が重複していたかは分かりません。

まず決めるのは、「本来、何を1行として数えたいか」です。注文単位なら注文ID、注文明細単位なら注文IDと明細番号の組み合わせで、次のSQLを実行します。

SELECT order_id, line_no, COUNT(*) AS row_count
FROM order_lines
GROUP BY order_id, line_no
HAVING COUNT(*) > 1;

結果が返れば、そのキーに複数行あります。ただし、複数行あることと、削除すべき誤ったデータであることは同じではありません。

この記事でわかること

  • GROUP BY・COUNT・HAVINGで重複キーを見つける方法
  • 重複したキーの明細を確認し、原因を絞る手順
  • 元データの重複と、JOIN後に増えた行の見分け方

結論:重複は「期待する1行の単位」で確認する

例えば、次の2行があるとします。

order_idline_no商品
1021商品A
1022商品B

order_idだけで見ると「102が2行ある」ため重複に見えます。しかし、1行=注文明細の表なら、これは自然な状態です。

一方、同じorder_idとline_noの組み合わせが2行あるなら、明細番号が注文内で一意になる設計では調査が必要です。

確認したい単位GROUP BYに指定する列分かること
注文order_id1注文に何行あるか
注文明細order_id, line_no同じ明細キーが複数行あるか
顧客と月customer_id, target_month同じ顧客・月の組み合わせが複数行あるか

GROUP BYは同じキーの行をグループにまとめ、COUNT(*)は各グループの行数を数えます。HAVING COUNT(*) > 1は、集計後に2行以上のグループだけを残します。

実行例:重複キーを見つける

以下の例はPostgreSQL向けです。サンプルの一時テーブルを作るため、ほかのDB製品では一時テーブルの作成構文を調整してください。

CREATE TEMP TABLE order_lines (
  order_id integer,
  line_no integer,
  amount integer
);

INSERT INTO order_lines (order_id, line_no, amount)
VALUES
  (101, 1, 100),
  (101, 1, 100),
  (102, 1, 200),
  (102, 2,  50),
  (103, 1,  80);

入力データ

order_idline_noamount
1011100
1011100
1021200
102250
103180

order_idだけを確認すると、101と102がどちらも2行です。

SELECT order_id, COUNT(*) AS row_count
FROM order_lines
GROUP BY order_id
HAVING COUNT(*) > 1
ORDER BY order_id;

実行結果

order_idrow_count
1012
1022

これだけでは、注文101の2行と注文102の2行を区別できません。そこで、注文明細の単位で確認します。

SELECT order_id, line_no, COUNT(*) AS row_count
FROM order_lines
GROUP BY order_id, line_no
HAVING COUNT(*) > 1
ORDER BY order_id, line_no;

実行結果

order_idline_norow_count
10112

注文102は明細番号1と2が各1行なので、結果に出ません。注文101の明細番号1は2行あるため、調査対象になります。

この段階で分かったのは「同じ明細キーが2行ある」ことです。再取り込みによる二重登録なのか、キーの定義が不足しているのかは、元データや業務上のルールで確認します。

確認手順:どの段階で行が増えたか調べる

実務では、次の順番で調べると、原因の範囲を狭められます。

順番確認すること使う方法
1想定する1行の単位を決める注文ID、明細番号などを確認
2元データの行数・合計を記録するCOUNT(*)、SUM(amount)
3期待するキーで複数行の組を探すGROUP BY、HAVING COUNT(*) > 1
4該当キーの全行を見るWHEREでキーを指定
5JOINを使う場合は結合後にも同じ確認をするJOIN前後のキー別行数を比較
6原因に応じて修正し、件数と合計を再確認する元データ・集計結果を検算

まず、元データの基準値を記録します。

SELECT
  COUNT(*) AS rows_total,
  SUM(amount) AS amount_total
FROM order_lines;

このサンプルでは、5行・合計530です。

続いて、重複したキーの明細を見ます。

SELECT order_id, line_no, amount
FROM order_lines
WHERE order_id = 101
  AND line_no = 1
ORDER BY order_id, line_no;

同じ内容の2行が返ります。ただし、実際の調査では、取り込み日時・更新日時・データの出所を示す列なども確認してください。表示される3列が同じでも、業務上は異なる記録かもしれません。

JOINしてから増えた場合は、結合前後を分けて確認する

元の表ではキーが一意なのに、JOIN後の結果で同じキーが増える場合もあります。この場合は、元データの重複と、結合で生じた複数行を分けて調べます。

例えば、注文表のorder_id = 101が1行でも、結合先の対応履歴に101が2行あれば、JOIN結果では注文101が2行になります。

SELECT order_id, COUNT(*) AS row_count
FROM order_events
GROUP BY order_id
HAVING COUNT(*) > 1;

このSQLは、結合先で注文IDが複数行あるかを見るものです。ただし、対応履歴が「1行=1回の対応」なら複数行は正常です。問題は、結合後に注文の売上を対応件数分だけ合計してしまうことにあります。

JOIN後にどのキーで行が増えたかは、元のクエリを副問い合わせにして調べられます。

SELECT order_id, COUNT(*) AS row_count
FROM (
  -- ここに調査対象のSELECT文を入れる。
  -- 最後のセミコロンとORDER BYは外す。
  SELECT o.order_id
  FROM orders AS o
  LEFT JOIN order_events AS e
    ON o.order_id = e.order_id
) AS joined_rows
GROUP BY order_id
HAVING COUNT(*) > 1;

この例ではordersとorder_eventsが既に存在することを前提としています。手元のクエリでは、調査対象の結合結果とキーに置き換えてください。

JOIN条件、結合先の粒度、多対多の結合については、既存記事で詳しく解説しています。

原因別の直し方:重複を見つけた後に判断する

元データに意図しない二重登録がある

取り込み処理や元システムを確認し、どの行を正しい記録として扱うか決めます。集計用SQLで一時的に調整する場合も、残す行を決める基準が必要です。

例えば「更新日時が最も新しい行を採用する」と決めるなら、同時刻の行があった場合に備え、別の一意なIDなども含めて並び順を定義します。基準がないまま任意の1行を残さないでください。

複数行が正しいデータである

注文102の2明細や、1注文に複数の対応履歴がある場合です。行を削除せず、必要な集計単位を決め直します。

例えば売上は注文明細から合計し、対応件数は対応履歴から数えるなど、指標ごとに元となる表と単位を明確にすることが必要です。

JOINによって売上が二重計上されている

結合先から何を取得したいかによって対処が変わります。注文ごとの対応件数が欲しいなら、対応履歴を注文単位に集計してから結合します。

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;

このSQLは、ordersが1行=1注文であることを前提にしています。元の注文表自体に同じ注文IDが複数行ある場合は、そこも先に確認してください。

詰まりやすいポイント3つ

1. GROUP BYに指定するキーが粗すぎる

原因: 注文明細の表をorder_idだけで調べる。

なぜ起きるか: 1注文に複数明細がある正常なデータも「2行以上」として表示されるため。

対策: 「1行=何か」を確認し、注文明細ならorder_id, line_noのように必要な列を組み合わせる。

2. COUNT(column)で全行を数えたつもりになる

原因: NULLになり得る列をCOUNT(column)に指定する。

なぜ起きるか: COUNT(*)は入力行数を数えますが、COUNT(column)はその列がNULLでない行だけを数えるため。

対策: キーごとの行数を確認する際は、まずCOUNT(*)を使う。欠損の有無は別に確認する。

3. WHERE COUNT(*) > 1と書く

原因: 集計後の件数をWHEREで絞ろうとする。

なぜ起きるか: WHEREはグループ化より前の入力行を絞り、HAVINGはグループ化後の結果を絞るため。

対策: 重複キーを探す条件はHAVING COUNT(*) > 1に書く。

現場で避けたい2つの直し方

DISTINCTを追加して、数字が合ったところで調査を終える。 DISTINCTは選択した列の組み合わせが同じ行をまとめます。列を追加すると結果が変わることもあり、元データの二重登録やJOIN条件の問題は残ります。まず増えたキーと明細を確認してください。

合計値だけを比較して完了にする。 一部の注文で行が増え、別の注文が条件によって消えていれば、合計件数が偶然一致することがあります。修正後は総行数・金額に加え、対象IDがそろっているか、IDごとの行数が期待どおりかを確認します。

また、サンプルの注文101の2行を誤登録と確認できた場合、1行を除いた売上合計は430です。この「430」は今回のサンプルでの検算値であり、同じキーが2行あるだけで自動的に片方を除外してよい、という意味ではありません。

まとめ

SQLの結果が増えたときは、次の順で確認します。

  1. 期待する1行の単位を決める。
  2. GROUP BY・COUNT(*)・HAVINGで複数行のキーを探す。
  3. 該当キーの元の明細を見る。
  4. JOINを使うなら、結合前と結合後の両方で調べる。
  5. 重複が誤登録なのか、正しい複数明細なのか判断してから修正する。

GROUP BYは「どのキーが何行あるか」を示してくれます。その行をどう扱うべきかは、データの単位と業務上の定義で決めましょう。

次に読む記事

JOINした直後から件数が増える場合は、結合キーと表の粒度を確認してください。

SELECT・GROUP BY・JOINの基本を整理したい場合はこちらです。

集計値が合わないときの確認観点を広げたい場合はこちらです。

タイトルとURLをコピーしました