JOINで行が増える(重複する)原因と対処法
SQLでJOINした途端にレコードが増える場合、原因の多くは結合先が1対多であることです。 見た目は「sql join 重複」でも誤った複製ではなく、一致した明細をすべて返した正しい結果です。 行数を保ちたい目的と、明細を取得したい目的を分けて対処します。
SQL JOIN duplicate rows usually come from a one-to-many relationship: one parent row is repeated once for every matching child row. This guide shows how to detect and fix that multiplication safely.
TL;DR
- 1対多のJOINでは行が増える: 親1行に子3行が一致すれば結果は3行
- 集計が目的なら子テーブルを先にGROUP BYし、1行にしてからJOINする
- 明細が必要なら重複ではなく仕様。DISTINCTで潰さずアプリ側で親子にまとめる
1. 原因: 1対多のJOINは行を増やす / Why rows multiply
注文テーブル orders の1行に対して、注文明細テーブル order_items に3行がある例です。 注文1001は1件ですが、商品が3つあるため、JOIN結果は明細ごとの3行になります。
| orders.id | customer | order_id | product | price |
|---|---|---|---|---|
| 1001 | 佐藤 | 1001 | 本 | 1200 |
| 1001 | 佐藤 | 1001 | ペン | 200 |
| 1001 | 佐藤 | 1001 | ノート | 300 |
SELECT o.id, o.customer, i.product, i.price
FROM orders o
INNER JOIN order_items i ON i.order_id = o.id;
-- 1001 | 佐藤 | 本 | 1200
-- 1001 | 佐藤 | ペン | 200
-- 1001 | 佐藤 | ノート | 300SQL INNER JOIN duplicate rowsに見えても、親1行が「多」側の一致数だけ展開された結果です。 LEFT JOINでも同様です。「sql join 重複 カラム」と見えるのは親カラムが各明細で繰り返されるためです。
2. 複数の1対多JOINは掛け算になる / Multiple one-to-many joins
1対多のJOINをさらに1対多とJOINすると、行数は掛け算になります。注文1001に明細が3行、 配送履歴が2行あり、両方を注文IDだけで結ぶと3×2=6行です。
| JOIN | 一致数 | 結果 |
|---|---|---|
| orders → order_items | 明細3行 | 3行 |
| さらに shipment_events | 3行 × 2行 | 6行 |
集計値まで水増しされる場合は、各「多」側を必要な粒度まで集約してから結合します。
3. 検出方法: JOIN前後のCOUNTを比較 / Detect the multiplication
「sql join 行が増える」「sql join レコードが増える」原因は、JOIN前後の COUNT(*) で切り分けます。
-- JOIN前
SELECT COUNT(*) FROM orders;
-- JOIN後
SELECT COUNT(*)
FROM orders o
JOIN order_items i ON i.order_id = o.id;
-- 親ごとの一致数
SELECT o.id, COUNT(*) AS joined_rows
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.id
HAVING COUNT(*) > 1;JOIN後だけ増えて親IDごとの件数が2以上なら1対多が原因です。JOINを1つずつ追加して測ると原因の表も特定できます。
4. 対処法1: 先に集約してからJOIN / Aggregate before joining
合計金額や明細数が目的なら、サブクエリやCTEで注文IDごとに1行へ集約してからJOINします。
-- サブクエリで先に集約
SELECT o.id, o.customer, i.item_count, i.total_amount
FROM orders o
LEFT JOIN (
SELECT order_id, COUNT(*) AS item_count, SUM(price) AS total_amount
FROM order_items
GROUP BY order_id
) i ON i.order_id = o.id;
-- CTEでも同じ
WITH item_totals AS (
SELECT order_id, COUNT(*) AS item_count, SUM(price) AS total_amount
FROM order_items
GROUP BY order_id
)
SELECT o.id, o.customer, t.item_count, t.total_amount
FROM orders o
LEFT JOIN item_totals t ON t.order_id = o.id;集約後は1注文につき最大1行なので、JOINしても注文の行数を増やしません。
5. 対処法2: 明細が必要なら1対多として扱う / Keep real detail rows
商品名や数量など明細行そのものが必要なら、増えた行は重複ではなく仕様です。1回のクエリで親と子をまとめて取得し、 アプリケーション側で order_id ごとにグルーピングして、1つの注文に明細配列を持たせます。
{
id: 1001,
customer: "佐藤",
items: [
{ product: "本", price: 1200 },
{ product: "ペン", price: 200 },
{ product: "ノート", price: 300 }
]
}6. 対処法3: DISTINCTによる重複削除の注意 / DISTINCT is not a universal fix
「sql join 重複削除」で使われるSELECT DISTINCTは、選択した全カラムが同じ行をまとめます。 明細IDを選ばず、同じ商品名と価格の別明細が2件あれば、本来2件なのに1行へ潰れる可能性があります。
-- 危険: 本来別の明細でも選択値が同じなら1行になる
SELECT DISTINCT o.id, i.product, i.price
FROM orders o
JOIN order_items i ON i.order_id = o.id;
-- 件数が目的なら粒度を明示して集約
SELECT order_id, COUNT(*) AS item_count
FROM order_items
GROUP BY order_id;DISTINCTは万能の対処法ではありません。結合キー、データの粒度、求める結果を見直してください。 JOIN句が長いときはSQL整形ツールで構造を確認できます。
7. English summary
SQL JOIN duplicate rows are usually the correct result of a one-to-many relationship. One order with three items produces three rows, and multiple one-to-many joins multiply their matches. Compare COUNT(*) before and after each join. For one-row-per-order reports, aggregate each child table with GROUP BY in a subquery or CTE before joining. If details are required, group them under their parent in the application. DISTINCT can collapse genuinely different detail records and is not a blanket fix.
関連ツール / Related tools
- SQL Formatter - 複数JOINや集約サブクエリを読みやすく整形