LEFT JOIN后WHERE误写右表字段会导致缺失数据被过滤,正确做法是用WHERE t2.id IS NULL或把右表条件移至ON子句;多层JOIN需分层理解IS NULL语义;JOIN前须检查并处理重复主键;剔除黑名单记录宜用多个LEFT JOIN+IS NULL而非OR条件。
想找出左表有、右表无匹配的记录,却一条都查不出来?大概率是 WHERE 条件里写了右表的非 NULL 判断,比如 WHERE t2.status = 'active'。SQL 执行顺序是先 JOIN 再 WHERE,右表为 NULL 的行一进 WHERE 就被过滤掉了。
真正要找“缺失”,必须用 WHERE t2.id IS NULL,且不能混任何其他右表字段条件。如果确实需要筛右表状态,得把条件挪到 ON 子句里:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'。
SELECT * 看原始 JOIN 结果,确认 t2.xxx 列是否真为 NULL,再加 WHERE
IS NULL 语义不同:第一层 IS NULL 是“用户根本没订单”,第二层 IS NULL 是“有订单但没明细”NOT EXISTS 替代: SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
users 表里 id = 100 有 3 条,orders 表里 user_id = 100 有 5 条,直接 JOIN 就产出 15 行——这不是业务关系,是数据脏了。
必须在 JOIN 前定位并处理重复。先跑 SELECT id, COUNT(*) FROM users GROUP BY id HAVING COUNT(*) > 1,再按业务决定保留策略。
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC) AS rn 生成唯一锚点,JOIN 时加 AND rn = 1
DISTINCT 糊弄:它只去结果行,不去源数据歧义PARTITION BY 字段必须和后续 JOIN 条件一致,否则预聚合失效清洗目标是“剔除所有命中黑名单用户或高风险设备的订单”,用 NOT IN 或 NOT EXISTS 都不如 LEFT JOIN ... WHERE b.user_id IS NULL 直观可控,尤其当黑名单表含 NULL 或空值时。
但要注意 ON 中写 OR(如 ON o.user_id = b.user_id OR o.device_fingerprint = b.device_fingerprint)会让索引基本失效。更稳的做法是拆成两个 LEFT JOIN:
SELECT o.* FROM orders o LEFT JOIN blacklist_rules b1 ON o.user_id = b1.user_id LEFT JOIN blacklist_rules b2 ON o.device_fingerprint = b2.device_fingerprint WHERE b1.user_id IS NULL AND b2.device_fingerprint IS NULL;
blacklist_rules 很小(比如 user_id 或全 NULL 行,否则 IS NULL 判断会误判ON 里用函数,比如 ON UPPER(u.email) = r.email_pattern,索引直接作废左表 100 行,右表 50 行,INNER JOIN 出来只有 30 行?不是 SQL 漏了,而是关联字段有 NULL:只要 ON t1.id = t2.ref_id 中任一为 NULL,整行就因三值逻辑返回 UNKNOWN 被丢弃。
这等价于隐式加了 WHERE t1.id IS NOT NULL AND t2.ref_id IS NOT NULL。如果你业务上允许 ref_id 为空,又想保留左表记录,就得换 LEFT JOIN,再手动 WHERE t2.ref_id IS NOT NULL 过滤。
EXPLAIN 看执行计划,若 rows 异常低或出现 NULL 关联提示,优先查字段 NULL 值分布ON 里塞模糊条件(如 t2.name LIKE '%abc%'),既慢又难优化实际清洗中,最易被忽略的是:NULL 值在不同 JOIN 类型下的行为差异,以及它如何悄无声息地改变结果集基数。一次没注意 IS NULL 的位置,或漏查了判重字段的 NULL 分布,就可能让清洗结果偏离预期,且难以回溯。