inner join本意就是丢数据,只保留两表匹配行,数据减少属正常;left join后数据变少,多因where误写右表条件,应将右表筛选移至on子句。

先确认是不是INNER JOIN本意就是丢数据
数据变少,第一反应不该是“出错了”,而是问:你写的到底是哪种JOIN?INNER JOIN语义就是只保留左右表都能匹配的行——它不承诺保留主表全部记录,丢数据是正常行为。比如查“有订单的客户”,INNER JOIN返回1200行,而客户总表有5000行,这不异常,只是业务口径本来就不含无订单客户。
容易踩的坑:
- 误把业务主语当成了左表:想查“所有客户”,却写了
FROM customers INNER JOIN orders,结果漏掉4880人 - 没意识到
INNER JOIN对NULL天然免疫:orders.customer_id为NULL的订单,哪怕客户表里真有对应记录,也永远无法匹配成功
LEFT JOIN后数据变少,90%是WHERE里写了右表字段
这是线上最隐蔽、复现率最高的问题。LEFT JOIN本该保住左表所有行,但只要WHERE里出现右表字段的非空条件(比如t2.status = 'paid'),整行就会被过滤——因为没匹配上的右表字段全是NULL,NULL = 'paid'判定为FALSE,直接踢掉。
正确做法只有1个:把右表筛选条件从WHERE挪进ON子句。
错误写法:LEFT JOIN orders t2 ON t1.id = t2.user_id WHERE t2.status = 'paid'
正确写法:LEFT JOIN orders t2 ON t1.id = t2.user_id AND t2.status = 'paid'
验证技巧:执行时加SELECT t1.id, t2.id, t2.status,看t2.id是否批量为NULL;如果是,再检查WHERE里有没有碰t2.开头的字段。
连接字段本身就有NULL或非法值
INNER JOIN和LEFT JOIN都会跳过任一连接字段为NULL的行。如果orders.customer_id里存了'0'、'-1'、'unknown'或空字符串,而customers.id是严格数字主键,那这些订单就永远匹配不上。
排查步骤:
- 查左表连接字段空值率:
SELECT COUNT(*) FILTER (WHERE customer_id IS NULL OR customer_id = '' OR customer_id IN ('0', '-1')) FROM orders - 查右表是否有对应值:
SELECT id FROM customers WHERE id IN (0, -1)(注意类型,若customers.id是BIGINT,而orders.customer_id是VARCHAR,隐式转换可能失败) - 用
TRIM()和LENGTH()检查不可见字符:SELECT customer_id, LENGTH(customer_id), ASCII(SUBSTR(customer_id, 1, 1)) FROM orders WHERE customer_id ~ '^[[:space:]]'
多表JOIN时,前层NULL污染后层连接
连续LEFT JOIN时,第二层依赖第一层右表字段(比如t2.product_id),而t2.product_id本身可能是NULL,导致ON t2.product_id = t3.id永远不成立,t3字段全为NULL——这不是BUG,是逻辑必然。
解决思路不是硬调,而是重定向连接源:
- 避免链式依赖:
LEFT JOIN products t3 ON t1.preferred_product_id = t3.id,直接用左表字段关联 - 必要时用
COALESCE(t2.product_id, t1.fallback_product_id)兜底 - 分步验证:用CTE先查
t1 LEFT JOIN t2结果,确认t2.product_id是否大量为NULL,再决定是否需要调整关联路径
真正麻烦的不是某一行没出来,而是你默认它“应该出来”,却没验证连接字段在真实数据里的分布和质量。上线前跑一遍COUNT(*)对比主表,比什么都管用。











