[SQL实战] JOIN 后数据变多了,不一定是数据库错了,先查关联键
做 SQL 查询时很多人遇到过一个很典型的问题明明只是 JOIN 一张表结果数据行数突然变多了。第一反应往往是怀疑数据库是不是算错了或者 JOIN 语法是不是写错了。但在大多数情况下数据库没有错真正的问题通常出在关联键。JOIN 后数据变多本质上是连接关系发生了放大。你以为是一对一数据库实际按一对多、多对多在匹配。一、先别看 SELECT先看主表行数排查 JOIN 问题时第一步不是改 SQL而是确认主表原始行数。SELECTCOUNT(*)ASmain_countFROMorders;假设主表 orders 有 10000 行。然后你 JOIN 客户表SELECTCOUNT(*)ASjoined_countFROMorders oLEFTJOINcustomers cONo.customer_idc.customer_id;如果 joined_count 变成 12000就说明 JOIN 发生了行数放大。这一步很重要。因为只有先知道连接前后行数变化才能判断问题发生在 JOIN而不是后面的 GROUP BY、WHERE 或其他计算。二、最常见原因右表关联键不唯一很多人默认维表里的 key 是唯一的但实际并不一定。比如 customers 表里 customer_id 本来应该唯一但因为历史导入、数据合并或多系统同步可能出现重复。先查右表 key 是否唯一SELECTcustomer_id,COUNT(*)AScntFROMcustomersGROUPBYcustomer_idHAVINGCOUNT(*)1;如果这条 SQL 有结果就说明一个 customer_id 在右表里对应多行。这时orders 里一行订单就可能匹配到 customers 里的多行客户记录JOIN 后行数自然会变多。三、一对多不是错误但要确认是不是你想要的结果有些 JOIN 本来就是一对多。比如订单表 JOIN 订单明细表orders一张订单一行order_items一张订单多行商品SELECT*FROMorders oJOINorder_items iONo.order_idi.order_id;这时候行数变多是正常的因为你已经从“订单粒度”变成了“商品明细粒度”。问题不在于行数变多而在于你有没有意识到统计口径已经变了。如果你后面继续统计订单金额就可能重复计算。四、先定清统计粒度再写 JOINJOIN 前必须问一个问题最终结果应该是一行代表什么一行代表一个订单一行代表一个客户一行代表一个商品明细一行代表一个客户在一个月的汇总如果这个问题没定清JOIN 后行数变多时就很难判断是正常展开还是错误放大。比如你想得到“订单级别”的结果就不应该直接把订单明细表全量 JOIN 上来后再随便 SUM。更清楚的方式是先把明细表聚合到订单粒度。WITHitem_summaryAS(SELECTorder_id,SUM(quantity)AStotal_quantity,SUM(amount)ASitem_amountFROMorder_itemsGROUPBYorder_id)SELECTo.order_id,o.customer_id,o.order_amount,s.total_quantity,s.item_amountFROMorders oLEFTJOINitem_summary sONo.order_ids.order_id;这样 JOIN 的右表在 order_id 上已经唯一行数更容易控制。五、条件放错位置也会让结果看起来异常LEFT JOIN 时条件放在 ON 里还是 WHERE 里结果可能不同。比如SELECT*FROMorders oLEFTJOINcustomers cONo.customer_idc.customer_idWHEREc.statusactive;这会把没有匹配到 active 客户的订单过滤掉结果不再是完整主表。如果你的目标是保留所有订单只是只匹配 active 客户条件应该更可能写在 ON 里SELECT*FROMorders oLEFTJOINcustomers cONo.customer_idc.customer_idANDc.statusactive;这类问题不一定会让行数变多但会让你误判 JOIN 的结果所以也应该放进排查清单。六、用 COUNT 对比每一步排查 JOIN 时不建议一上来就看完整结果。更有效的是做行数对比。SELECTCOUNT(*)FROMorders;SELECTCOUNT(*)FROMorders oLEFTJOINcustomers cONo.customer_idc.customer_id;SELECTCOUNT(DISTINCTo.order_id)FROMorders oLEFTJOINcustomers cONo.customer_idc.customer_id;这三个数字能回答三个问题主表原来有多少行JOIN 后结果有多少行主表主键是否仍然保持原来的去重数量如果第二个数字变大但第三个数字没变说明主表被右表重复匹配放大了。七、查出被放大的主键可以进一步找出哪些主表记录被重复匹配。SELECTo.order_id,COUNT(*)ASmatched_rowsFROMorders oLEFTJOINcustomers cONo.customer_idc.customer_idGROUPBYo.order_idHAVINGCOUNT(*)1ORDERBYmatched_rowsDESC;这条 SQL 很适合定位问题样本。拿到具体 order_id 后再反查它匹配到的右表记录SELECT*FROMcustomersWHEREcustomer_id某个 customer_id;这样就能看到到底是右表重复、状态多版本还是同一个 key 下本来就有多条业务记录。八、维表有多版本时要先选有效版本很多维表不是简单的一行一个 key而是带版本或生效时间。比如客户表可能有customer_idcustomer_namestatuseffective_dateupdated_at同一个 customer_id 有多行记录这未必是脏数据。它可能表示历史版本。这时不能直接 JOIN而应该先选出当前有效版本。WITHlatest_customerAS(SELECT*FROM(SELECTc.*,ROW_NUMBER()OVER(PARTITIONBYcustomer_idORDERBYupdated_atDESC)ASrnFROMcustomers c)tWHERErn1)SELECT*FROMorders oLEFTJOINlatest_customer cONo.customer_idc.customer_id;这一步把右表先压到每个 customer_id 一行再 JOIN 主表。九、不要用 DISTINCT 掩盖问题很多人看到 JOIN 后行数变多会直接加 DISTINCT。SELECTDISTINCT...这有时能让结果看起来恢复正常但它不一定解决了问题。它只是把重复结果压掉了。如果重复来自右表多版本、关联键不唯一、统计粒度错误DISTINCT 可能会掩盖真正原因甚至把本来应该保留的明细也去掉。更好的顺序是先确认主表粒度再检查右表 key 唯一性再判断一对多是否符合业务再决定是否预聚合或选版本最后才考虑是否需要去重十、一套可复用的 JOIN 排查顺序遇到 JOIN 后数据变多可以按这个顺序查第一步查主表原始行数。第二步查 JOIN 后行数。第三步查主表主键 DISTINCT 数量是否变化。第四步查右表关联键是否重复。第五步查业务关系到底是一对一、一对多还是多对多。第六步查条件应该放在 ON 还是 WHERE。第七步必要时先聚合右表或选最新版本。第八步再决定是否去重。这个顺序能避免把问题归因到“数据库不对”或“SQL 语法不对”。大多数 JOIN 行数异常最后都会回到一个核心问题连接前没有先确认关联键和统计粒度。十一、最终要记住的不是语法而是口径JOIN 是 SQL 里最常用的操作之一但它不是简单地“把两张表拼起来”。每一次 JOIN 都是在改变数据关系。如果关联键不唯一行数会放大。如果一对多被当成一对一金额会重复。如果统计粒度没定清结果会看起来对、实际错。所以JOIN 后数据变多时不要第一时间怀疑数据库也不要第一时间加 DISTINCT。先查关联键再查粒度再查右表是否需要预处理。这样排查路径会清楚很多。