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行を作るために必要なテーブルはどれか
- JOINすると行数は増えるか
- 集計で元の粒度に戻す必要があるか
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行が何を表すべきか考えてください。
- 顧客ごとの購入回数を見たい。
- 商品カテゴリごとの売上を見たい。
- 注文ごとの商品数を見たい。
- 顧客ごとの直近注文日を見たい。
それぞれについて、JOIN後に GROUP BY が必要かどうかも考えてみましょう。
まとめ
| 観点 | 確認すること |
|---|---|
| 粒度 | JOIN後の1行が何を表すか |
| 1対多 | 片方の1行にもう片方の複数行が対応するか |
| 行数 | JOIN前後で行数がどう変わるか |
| 集計 | 合計や件数が二重計上になっていないか |
この章のキーメッセージ: JOIN後に行数が増えるのは、1対多の関係では自然です。重複を消す前に、結果の1行が顧客・注文・明細のどの単位なのかを確認しましょう。
この章の確認
- 顧客と注文の関係が1対多になる理由を説明してください。
ordersとorder_itemsをJOINすると、注文IDが複数行に出ることがあるのはなぜですか?- JOIN後に
orders.total_amountを合計すると、二重計上が起きることがある理由を説明してください。 - JOIN結果に同じIDが複数回出たとき、
DISTINCTの前に何を確認すべきですか?