← 返回题目列表

SQL JOIN 为什么会产生重复行?如何定位和处理?

高频 中等 第 17 / 28 题 更新于 2026/07/29
SQLJOIN重复行数据建模

简化版

JOIN 产生重复行通常不是数据库重复了,而是连接关系本身是一对多或多对多;要先确认连接键唯一性和业务粒度,再通过补充条件、聚合、窗口函数或 EXISTS 处理。

详细版

连接会输出所有匹配组合。左表 1 行匹配右表 3 行,结果就是 3 行;左表 2 行各匹配右表 3 行,可能得到 6 行。

常见原因包括:连接条件不完整、右表连接键不唯一、维表有多版本记录、事实表本来就是明细粒度、多对多关系缺少聚合。定位时先查连接键重复度,例如 GROUP BY key HAVING COUNT(*) > 1,再判断需求到底是要明细、汇总,还是只做存在性过滤。

处理方式不能只靠 DISTINCT。如果需求是有无关系,用 EXISTS;如果要每组取一条,用窗口函数;如果要统计,用 GROUP BY;如果是连接条件漏字段,补齐条件才是根治。

完整版教学

一、JOIN 的结果是匹配组合

JOIN 不是“把右表字段贴到左表上一次”,而是生成满足条件的所有组合。

users
id | name
1  | 张三

orders
id  | user_id
101 | 1
102 | 1
103 | 1

执行:

SELECT u.id, u.name, o.id AS order_id
FROM users u
JOIN orders o ON o.user_id = u.id;

结果会有 3 行,因为张三匹配了 3 个订单。

记忆钩子:JOIN 的行数看“匹配对数”,不是看左表行数。

二、先判断业务粒度

重复行是否是问题,取决于你期望的结果粒度。如果要订单明细,一位用户出现多次是正常的;如果要用户列表,这就是重复。

需求期望粒度张三 3 个订单时
用户订单明细订单粒度张三出现 3 行正常
有订单的用户用户粒度张三应出现 1 行
用户订单数用户粒度张三 1 行,数量为 3

很多线上 SQL 问题不是语法错,而是“查询输出粒度”和“业务想看的粒度”没有对齐。

三、连接条件不完整会制造重复

有些重复来自连接条件漏字段。比如订单明细按 (order_id, item_id) 唯一,但你只按 order_id 连接价格表。

SELECT *
FROM order_items oi
JOIN item_prices p
  ON p.order_id = oi.order_id;

如果一个订单有 3 个商品,价格表也有 3 行,只按订单连接会产生 9 个组合。

正确做法是补齐连接键:

SELECT *
FROM order_items oi
JOIN item_prices p
  ON p.order_id = oi.order_id
 AND p.item_id = oi.item_id;

定位时要问:真实唯一键是什么?连接条件是否覆盖了唯一键?

四、右表连接键不唯一也很常见

维表并不一定只有一条当前记录。比如员工部门历史表可能保留多次变更:

employee_dept
emp_id | dept_id | valid_from
1      | 10      | 2025-01-01
1      | 20      | 2026-01-01

如果只按 emp_id 连接,就会把员工复制成多行。

可以先用 SQL 检查:

SELECT emp_id, COUNT(*) AS cnt
FROM employee_dept
GROUP BY emp_id
HAVING COUNT(*) > 1;

如果业务只要当前部门,就应该补充时间条件或先取最新记录,而不是直接 DISTINCT

五、处理方式要匹配需求

不同需求对应不同解法:

-- 只判断是否存在
SELECT u.*
FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- 统计数量
SELECT u.id, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;

-- 每个用户取最新订单
SELECT *
FROM (
  SELECT o.*,
         ROW_NUMBER() OVER (
           PARTITION BY user_id
           ORDER BY created_at DESC, id DESC
         ) AS rn
  FROM orders o
) t
WHERE rn = 1;

DISTINCT 可以作为结果去重工具,但它不能解释为什么重复,也可能掩盖数据建模或连接条件问题。

六、排查 JOIN 重复的步骤

可以按固定路径排查:

1. 明确期望粒度:用户、订单、商品、日志?
2. 查左表主键是否唯一。
3. 查右表连接键是否唯一。
4. 检查 ON 条件是否漏字段。
5. 判断是否需要聚合、窗口函数或 EXISTS。

常用诊断 SQL:

SELECT join_key, COUNT(*) AS cnt
FROM right_table
GROUP BY join_key
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 20;

如果最高 cnt=50,左表某一行连接后最多就可能被放大 50 倍。这个数字能帮助你快速判断重复来自哪里。

七、常见误区与追问

  • 误区:JOIN 后重复一定是数据库 bug。 JOIN 输出匹配组合,一对多导致多行是正常语义。
  • 误区:加 DISTINCT 就解决了问题。 它可能掩盖连接条件不完整或业务粒度错误。
  • 误区:只看左表主键唯一就够了。 右表连接键不唯一同样会放大结果。
  • 误区:LEFT JOIN 不会产生重复。 左连接也会为每个右表匹配行输出一行。
  • 追问:如何查右表是否一对多? 对连接键 GROUP BY,用 HAVING COUNT(*) > 1 找重复键。
  • 追问:只要判断有无关联怎么写?EXISTSNOT EXISTS,避免明细连接导致重复。

八、加强记忆

JOIN 重复题的核心不是“怎么去重”,而是“为什么被放大”。先定业务粒度,再看连接键唯一性;需要有无关系用 EXISTS,需要数量用聚合,需要每组一条用窗口函数,连接条件漏字段就补条件。