DevToolBox

JOINで行が増える(重複する)原因と対処法

最終更新日: 2026-07-22公開日: 2026-07-22執筆: DevToolBox編集部

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. 原因: 1対多のJOINは行を増やす / Why rows multiply

注文テーブル orders の1行に対して、注文明細テーブル order_items に3行がある例です。 注文1001は1件ですが、商品が3つあるため、JOIN結果は明細ごとの3行になります。

orders.idcustomerorder_idproductprice
1001佐藤10011200
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 | 佐藤 | ノート |  300

SQL 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_events3行 × 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

関連ガイド / Related guides