青の統計学-DS Playground-

1対多と重複: JOIN後に行数が増える理由

Stage 3 — 第4章 | データ分析基礎カリキュラム 推定学習時間:35〜45分 | 難易度:★★★☆☆


この章で学ぶこと

JOINを使い始めると、「JOINしただけなのに行数が増えた」という場面に出会います。 これはSQLのミスとは限りません。 テーブル同士の関係が 1対多 になっていると、JOIN後に行が増えるのは自然なことです。

この章では、JOIN後の重複や行数増加をどう理解するかを学びます。 分析で合計や件数を出すときに、ここを理解していないと結果が大きくずれることがあります。

この章を終えると、こんなことができるようになります:

  • 1対多の関係を説明できる
  • JOIN後に行数が増える理由を理解できる
  • 注文単位と明細単位の違いを見分けられる
  • 集計前にデータの粒度を確認できる

1. 1対多とは何か

1対多とは、片方のテーブルの1行に対して、もう片方のテーブルの複数行が対応する関係です。

ECデータでは、よく次のような関係があります。

関係 説明
顧客と注文 1人の顧客が複数回注文することがある
注文と注文明細 1つの注文に複数の商品が含まれることがある
商品と注文明細 1つの商品が複数の注文で購入されることがある

たとえば、1人の顧客が3回注文していれば、customers の1行に対して orders の3行が対応します。 この状態でJOINすると、顧客情報は3行に繰り返されます。


2. 顧客と注文をJOINすると行数が増える

次のSQLは、顧客と注文をつなぎます。

SELECT
            c.customer_id,
            c.customer_name,
            o.order_id,
            o.order_date,
            o.total_amount
          FROM customers AS c
          INNER JOIN orders AS o
            ON c.customer_id = o.customer_id;
          

もしC001の顧客が3件注文していれば、結果にはC001が3行出ます。 これは重複しているように見えますが、注文単位で見ると正しい結果です。

customer_id customer_name order_id total_amount
C001 Aさん O001 5200
C001 Aさん O003 3400
C001 Aさん O008 7600

この結果の1行は「顧客1人」ではなく「注文1件」です。 JOIN後の結果を読むときは、1行が何を表しているかを必ず確認しましょう。


3. 注文と注文明細をJOINするとさらに増える

注文テーブル orders は、1行が注文1件です。 一方、order_items は、1行が注文に含まれる商品1行です。

SELECT
            o.order_id,
            o.order_date,
            oi.product_id,
            oi.quantity,
            oi.unit_price
          FROM orders AS o
          INNER JOIN order_items AS oi
            ON o.order_id = oi.order_id;
          

1つの注文に3商品が含まれていれば、その注文はJOIN後に3行になります。

この結果の1行は「注文1件」ではなく「注文明細1行」です。 注文金額を扱うときは、この違いが重要です。


4. 合計金額の二重計上に注意する

次のSQLは、JOIN後に orders.total_amount を合計しています。

SELECT
            SUM(o.total_amount) AS total_sales
          FROM orders AS o
          INNER JOIN order_items AS oi
            ON o.order_id = oi.order_id;
          

このSQLは危険です。 1つの注文に複数の商品明細がある場合、同じ total_amount が明細の数だけ繰り返されるため、売上が大きく見えてしまいます。

注文金額を合計したいなら、注文テーブルだけで集計するほうが自然です。

SELECT
            SUM(total_amount) AS total_sales
          FROM orders
          WHERE status = 'completed';
          

一方で、商品別の売上を見たいなら、明細単位で計算します。

SELECT
            oi.product_id,
            SUM(oi.quantity * oi.unit_price) AS item_sales
          FROM order_items AS oi
          GROUP BY oi.product_id;
          

どのテーブルのどの粒度で集計するかを意識しましょう。


5. JOIN前に粒度を確認する

JOINを書く前に、「結果の1行を何にしたいか」を決めます。 ここが曖昧なままJOINすると、集計結果がずれやすくなります。

見たいもの 結果の1行 主に使うテーブル
注文ごとの売上 注文1件 orders
商品ごとの販売数 商品1つ order_items, products
顧客ごとの購入回数 顧客1人 customers, orders
注文明細ごとの金額 明細1行 order_items

たとえば「顧客ごとの購入回数」を見たいなら、JOIN後に顧客単位へ GROUP BY c.customer_id で戻す必要があります。 一方、「注文に含まれる商品」を見たいなら、明細単位の行数増加は自然です。

SQLを書く前に、次の順で考えると安全です。

  1. 最終結果の1行は何か
  2. その1行を作るために必要なテーブルはどれか
  3. JOINすると行数は増えるか
  4. 集計で元の粒度に戻す必要があるか

6. JOIN前後の行数を確認する

JOINの結果が想定通りか確認するには、行数を数えるのが有効です。

SELECT COUNT(*) AS customer_rows
          FROM customers;
          
SELECT COUNT(*) AS order_rows
          FROM orders;
          
SELECT COUNT(*) AS joined_rows
          FROM customers AS c
          INNER JOIN orders AS o
            ON c.customer_id = o.customer_id;
          

JOIN後の行数が増えていても、1対多の関係なら自然です。 ただし、想定以上に増えている場合は、JOIN条件が不足している可能性があります。

分析SQLを書くときは、最初に数行だけ確認し、必要なら COUNT(*) で行数も確認しましょう。


7. 重複を消す前に意味を考える

JOIN後に同じ顧客IDが何度も出ると、すぐに DISTINCT を使いたくなるかもしれません。 しかし、重複に見える行が分析上必要な行であることも多いです。

見えている状態 まず考えること
顧客IDが複数回出る その顧客が複数注文しているだけではないか
注文IDが複数回出る その注文に複数商品が含まれているだけではないか
商品IDが複数回出る 複数注文で同じ商品が買われているだけではないか

DISTINCT は結果を整える道具ですが、原因を理解せずに使うと、必要な情報まで消してしまうことがあります。


8. 良くない修正と良い修正

JOIN後に行数が増えたとき、原因を見ずに DISTINCT を足すのは危険です。

SELECT DISTINCT
            c.customer_id,
            c.customer_name,
            o.total_amount
          FROM customers AS c
          INNER JOIN orders AS o
            ON c.customer_id = o.customer_id;
          

このSQLは一見すっきりしますが、「顧客単位で見たいのか」「注文単位で見たいのか」が曖昧です。 顧客ごとの購入金額を見たいなら、顧客単位に集計します。

SELECT
            c.customer_id,
            c.customer_name,
            COUNT(o.order_id) AS order_count,
            SUM(o.total_amount) AS customer_sales
          FROM customers AS c
          INNER JOIN orders AS o
            ON c.customer_id = o.customer_id
          GROUP BY
            c.customer_id,
            c.customer_name;
          

良い修正は、行を無理に消すことではなく、目的の粒度に合わせて集計することです。


ミニ演習

次の分析では、最終結果の1行が何を表すべきか考えてください。

  1. 顧客ごとの購入回数を見たい。
  2. 商品カテゴリごとの売上を見たい。
  3. 注文ごとの商品数を見たい。
  4. 顧客ごとの直近注文日を見たい。

それぞれについて、JOIN後に GROUP BY が必要かどうかも考えてみましょう。


まとめ

観点 確認すること
粒度 JOIN後の1行が何を表すか
1対多 片方の1行にもう片方の複数行が対応するか
行数 JOIN前後で行数がどう変わるか
集計 合計や件数が二重計上になっていないか

この章のキーメッセージ: JOIN後に行数が増えるのは、1対多の関係では自然です。重複を消す前に、結果の1行が顧客・注文・明細のどの単位なのかを確認しましょう。


この章の確認

  1. 顧客と注文の関係が1対多になる理由を説明してください。
  2. ordersorder_items をJOINすると、注文IDが複数行に出ることがあるのはなぜですか?
  3. JOIN後に orders.total_amount を合計すると、二重計上が起きることがある理由を説明してください。
  4. JOIN結果に同じIDが複数回出たとき、DISTINCT の前に何を確認すべきですか?

関連演習