用 count() 和 group by 快速定位脏数据:select foreign_key, count() from child_table group by foreign_key having count(*) > 1;注意 null 值影响,需单独处理。

JOIN 后结果行数暴增,怎么快速定位是哪条脏数据导致的?
先别急着改 SQL,用 COUNT(*) 和 GROUP BY 锁定问题源头。一对一关联表里出现一对多,本质是「主表某 ID 在从表中重复出现了」。最直接的办法是查从表里哪些 foreign_key 值重复:
SELECT foreign_key, COUNT(*) FROM child_table GROUP BY foreign_key HAVING COUNT(*) > 1;
如果返回结果非空,就找到了“假一对一”的真实来源。注意:别漏掉 NULL 值——有些 ORM 或导入脚本会把缺失关联写成 NULL,而 JOIN 会自动过滤掉这些行,反而掩盖问题。
LEFT JOIN 时发现主表记录消失,是不是 ON 条件写错了?
不是 ON 写错,而是你用了 INNER JOIN 却误以为它能保主表。真正保主表的是 LEFT JOIN;但即使用了 LEFT JOIN,如果从表有重复键,照样会膨胀行数。关键点在于:「保不保主表」和「会不会膨胀」是两个独立问题。
-
INNER JOIN:丢主表无匹配项,且从表重复 → 行数 = 主表 × 每个匹配的从表行数 -
LEFT JOIN:保主表所有行,但重复仍会膨胀 → 行数 ≥ 主表行数 - 想严格一对一输出?得在
LEFT JOIN后加ROW_NUMBER()去重,或提前用子查询筛出“首个”从表记录
用 ROW_NUMBER() 限制每主键只取一条从表记录,要注意什么?
这是最常用也最易踩坑的解法。核心是给从表每组 foreign_key 排序后取 rn = 1:
SELECT a.*, b.*
FROM main_table a
LEFT JOIN (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY foreign_key ORDER BY updated_at DESC) AS rn
FROM child_table
) b ON a.id = b.foreign_key AND b.rn = 1;
注意三点:
- 排序字段必须明确——别用
ORDER BY id就完事,要结合业务含义(比如用updated_at取最新,或created_at取最早) -
PARTITION BY必须用外键字段,不是主键字段 - 如果从表有
NULL外键,PARTITION BY会把它们全归到同一组,导致意外聚合,建议先WHERE foreign_key IS NOT NULL
ALTER TABLE 加 UNIQUE 约束能根治吗?
能,但得先清理存量脏数据,否则 ALTER TABLE ... ADD CONSTRAINT 会直接报错:ERROR: could not create unique index。操作顺序不能乱:
- 先跑一遍前面的
GROUP BY查询,人工或脚本处理重复行(保留一个,删其余) - 再确认
foreign_key字段没有NULL(或允许NULL但加UNIQUE NULLS NOT DISTINCT,注意 PostgreSQL 支持,MySQL 不支持) - 最后加约束:
ALTER TABLE child_table ADD CONSTRAINT uk_fk UNIQUE (foreign_key);
加完约束,后续 INSERT/UPDATE 就会强制拦截重复,但历史数据的语义歧义已经存在——比如两条“最新”记录时间戳相同,数据库不会帮你判断哪条更可信。











