SQL JOIN 为什么会产生重复行?如何定位和处理?
简化版
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找重复键。 - 追问:只要判断有无关联怎么写? 用
EXISTS或NOT EXISTS,避免明细连接导致重复。
八、加强记忆
JOIN 重复题的核心不是“怎么去重”,而是“为什么被放大”。先定业务粒度,再看连接键唯一性;需要有无关系用 EXISTS,需要数量用聚合,需要每组一条用窗口函数,连接条件漏字段就补条件。