JOIN前必须去重,否则主键重复会引发笛卡尔积导致结果行数爆炸;应先定位重复ID,再用窗口函数预聚合生成唯一锚点,避免相关子查询和临时DISTINCT。

JOIN前必须去重,否则结果行数爆炸
主键重复意味着同一ID在表中出现多次,JOIN时会触发笛卡尔积式匹配。比如users表里id=100有3条记录,orders表里user_id=100有5条,直接JOIN就会产出15行——这不是“多对多”,是数据脏了。
实操建议:
- 先用
SELECT id, COUNT(*) FROM table GROUP BY id HAVING COUNT(*) > 1定位重复主键 - 根据业务决定保留策略:
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC)取最新;或MAX()/MIN()聚合关键字段 - 禁止在JOIN语句里临时加
DISTINCT糊弄——它只去结果行,不去源数据逻辑歧义
用窗口函数预聚合比子查询更可控
有人习惯写(SELECT MAX(amount) FROM orders o2 WHERE o2.user_id = u.id)作为字段嵌套,但这种相关子查询在大数据量下极慢,且无法处理多字段聚合(比如既要最新订单时间,又要对应金额)。
实操建议:
- 用
ROW_NUMBER()或RANK()在JOIN前生成唯一锚点:WITH clean_orders AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) SELECT u.name, o.amount, o.created_at FROM users u JOIN clean_orders o ON u.id = o.user_id AND o.rn = 1 -
RANK()适合允许并列(如多个同一天的最新订单),ROW_NUMBER()强制唯一编号 - 注意
PARTITION BY字段必须和JOIN条件一致,否则预聚合失效
LEFT JOIN后COUNT(*)为0却返回NULL?检查ON条件是否漏掉IS NULL逻辑
当用LEFT JOIN关联一个已去重的聚合表,发现本该有0条记录的用户,COUNT(*)结果却是NULL,不是0——这是因为聚合发生在JOIN之后,而空匹配行没有记录可COUNT。
实操建议:
- 把聚合移到子查询或CTE里,确保COUNT作用于实际存在的行:
SELECT u.id, COALESCE(o.order_cnt, 0) AS order_cnt FROM users u LEFT JOIN ( SELECT user_id, COUNT(*) AS order_cnt FROM orders GROUP BY user_id ) o ON u.id = o.user_id - 别依赖
COALESCE(COUNT(*), 0)——COUNT本身不会返回NULL,但整行为空时,字段就是NULL - 如果必须在主查询里聚合,用
SUM(CASE WHEN o.id IS NOT NULL THEN 1 ELSE 0 END)替代COUNT(*)
UPDATE/DELETE前务必验证JOIN的唯一性约束
用JOIN做批量更新或删除时,重复主键会导致意料外的多行命中。例如UPDATE users SET status = 'archived' FROM orders WHERE users.id = orders.user_id AND orders.amount > 1000——若orders里同一个user_id有多笔大额订单,用户会被反复更新(虽结果一样,但日志、触发器、性能全乱)。
实操建议:
- 执行前加
EXPLAIN看执行计划,确认orders侧是否走到了唯一索引扫描 - 用
EXISTS替代JOIN做条件判断更安全:UPDATE users SET status = 'archived' WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id AND orders.amount > 1000) - 生产环境跑UPDATE前,先用
SELECT COUNT(*)统计预期影响行数,再核对SELECT DISTINCT user_id看去重后数量是否合理
数据清洗不是“加个DISTINCT就完事”的环节,重复主键暴露的是上游写入逻辑缺陷。真正难的不是语法,是判断哪条重复记录代表真实业务状态——时间戳、版本号、操作人、状态机流转顺序,这些上下文信息必须参与清洗决策。










