join本身不清洗数据,它只是暴露脏数据的放大镜;真正清洗靠的是在join前后加约束、转换和条件控制——尤其要防null、类型隐式转换、重复主键这三类静默错误。

直接说结论:JOIN本身不清洗数据,它只是暴露脏数据的放大镜;真正清洗靠的是在JOIN前后加约束、转换和条件控制——尤其要防NULL、类型隐式转换、重复主键这三类静默错误。
LEFT JOIN + IS NULL 是找缺失数据的唯一可靠写法
想查“哪些订单没关联到用户”,却一条结果都没有?大概率你写了 WHERE u.name = 'active' 或 WHERE u.id IS NOT NULL。SQL 先 JOIN 再 WHERE,右表为 NULL 的行一进 WHERE 就被踢掉。
- 正确姿势是:
LEFT JOIN users u ON o.user_id = u.id WHERE u.id IS NULL——只保留左表有、右表无匹配的行 - 如果还要筛用户状态,必须挪到
ON里:LEFT JOIN users u ON o.user_id = u.id AND u.status = 'active' - 多层 LEFT JOIN 时,
u.id IS NULL和o.order_id IS NULL语义完全不同,别混用;不确定时优先用NOT EXISTS
JOIN 前必须处理 NULL、空字符串和类型不一致
旧系统字段常含 NULL、''、'N/A',直接用于 ON 条件会导致匹配失败或意外笛卡尔积。更危险的是类型隐式转换——比如 VARCHAR 主键连 BIGINT,MySQL 可能转成 DOUBLE,'111111111111111111' 和 111111111111111110 被判相等。
- 统一空值语义:
COALESCE(old.ref_id, '')或NULLIF(old.ref_id, 'N/A'),但注意加函数会让索引失效 - 强制类型对齐:
ON CAST(old.ref_id AS CHAR) = CAST(new.user_id AS CHAR),比old.ref_id = new.user_id安全得多 - 先跑
SELECT COUNT(*), COUNT(DISTINCT ref_id) FROM old_table对比,差值大说明有脏值或重复
用 INNER JOIN 驱动规则表执行清洗逻辑
把清洗规则(如黑名单、状态映射、金额阈值)抽成独立表,比硬写 WHERE IN 或嵌套 EXISTS 更易维护、可版本化。但规则表若含重复 rule_id 或 NULL 字段,JOIN 后会行数爆炸。
- 规则表必须有唯一约束,关联字段建复合索引,顺序按
ON中等值字段优先 - 避免在
ON里写函数:ON UPPER(o.email) = r.pattern会让索引失效;改用预处理列或生成表达式索引 - 高优先级规则需生效优先,就让规则表当驱动表:
INNER JOIN clean_rules r ON ... GROUP BY o.id, r.priority ORDER BY r.priority LIMIT 1,再用ROW_NUMBER()取 top 1
DELETE JOIN 删重前务必验证判重逻辑和保留策略
删重复不是为了“看起来干净”,而是确保业务含义正确。按 email 删重,但 email 允许 NULL?那所有 NULL 会被归为一组,删完只剩一条——可能把有效用户也干掉了。
- 先确认判重维度:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1 - 明确保留哪条:
ROW_NUMBER() OVER (PARTITION BY email ORDER BY updated_at DESC) AS rn,再删rn > 1 - MySQL 5.7 不支持直接
DELETE FROM t1 JOIN t2,得绕成派生表;MySQL 8.0+ 可用DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id
最易被忽略的点:JOIN 不报错,但结果每天差几条——比如类型隐式转换导致某天少匹配 3 个订单,直到对账才发现。每次写 JOIN,第一件事不是写 SELECT,而是看 EXPLAIN 和 COUNT(*) 对比,确认连接结果集大小是否合理。










