前两天一个做运营的朋友来找我,说他们后台的人数统计“越看越不对劲”:明明注册用户有 1 万多人,活动报名报表里却只显示 3000 多人,她第一反应是数据丢了,第二反应是系统有 bug,第三反应是准备拉技术开会,结果技术看了一眼 SQL,说:“没丢,你这里用了 inner join,没报名的人当然不会出现。”
她愣了半天,说:“可我想看的是所有人啊。”
你看,很多问题不是出在数据上,而是出在我们看数据的方式上,今天就想聊聊这个特别容易被忽略、但在业务里特别重要的东西:leftouterjoin,也就是左外连接,说得直白一点,它有点像那种“不管你来不来,我都给你留个位置”的算法,尤其在真实业务里,这种兜底思维太重要了。
先用人话解释一下,假设我们有两张表:左边是用户表,右边是订单表,用 inner join 的时候,系统只保留“两边都能对上”的记录,也就是下过单的用户,没下过单的用户,直接从结果里消失,可如果你用 leftouterjoin,左表里的用户会全部保留,右表有订单就接上,没有订单就显示空值,它表达的是一种业务态度:左边这些人,我一个都不想漏。
这事儿听着技术,其实生活里到处都是。
我上个月参加同学聚会,组织者在群里统计“谁带家属”,如果用 inner join 的思路,最后名单上只会出现“回复了并且带家属”的人;单身同学、没回复的同学、临时说不带的同学,全都不见了,可组织者真正需要的是:先把所有同学都列出来,再看每个人对应的家属情况,没填的就空着,这个思路就是 leftouterjoin,左边是“所有同学”,右边是“家属登记表”,左连接一上,谁没填、谁填了、谁带了几个人,一清二楚。
再举个更接地气的例子,我们小区前阵子搞旧物置换活动,物业想统计每个楼栋的报名人数,楼栋表是左表,报名表是右表,如果只用 inner join,结果会特别“好看”:凡是出现在报表里的楼栋,都有人报名,但问题也来了,那些没人报名的楼栋压根不会出现,物业以为“已经覆盖了所有楼栋”,实际上只是看到了“有报名的楼栋”,后来改成 leftouterjoin,所有楼栋都保留,没人报名的显示 0 或空值,大家才发现 3 号楼和 7 号楼一个人都没报,不是数据错了,是查询方式把沉默的楼栋给“过滤”掉了。
我自己也踩过类似的坑,早些年做内容后台,想统计“每个作者最近有没有更新”,我一开始写了个普通连接,把作者表和文章表接起来,然后按作者分组,结果报表里只有“发过文章的作者”,那些刚注册还没发文的作者全没影了,领导问:“新来的十几个人呢?”我还嘴硬说“他们没内容所以没数据”,领导回了一句特别经典的话:“没有内容,也是一种数据。”
这句话我记到现在,leftouterjoin 的价值,恰恰在于它承认“没有”也是一种信息,用户没下单,说明可能需要激活;学生没交作业,说明需要提醒;小区没人报名,说明宣传没到位;作者没发文,说明 onboarding 可能有问题,你如果只盯着交集,就很容易把最需要被看见的那部分人漏掉。
左连接也不是随便一写就万事大吉,我自己总结几个特别容易翻车的点。
第一,过滤条件放 where 还是 on,差别很大,这个坑我见过太多次,比如你想保留所有用户,同时只看“已支付订单”,条件如果写错位置,leftouterjoin 很可能被你自己“降级”成 inner join,因为你在 where 里一过滤右表字段,空值行就被筛掉了,左表那些没订单的人又消失了,正确做法通常是把右表的限制条件放在 on 里,把左表的限制条件放在 where 里,别小看这个位置,SQL 里条件放错地方,业务含义就变了。
第二,统计数量时别只会 count(*),左连接之后,右表没匹配上的行会是空值,你想统计每个用户的订单数,应该 count 右表的主键或订单号,而不是 count 所有行,否则没订单的用户也会被算成 1,这个细节特别像生活里的“把空位置也算一个人”:合影时给没来的人留了椅子,不代表他真的到了。
第三,注意重复行,左连接如果右表是一对多,左表的人会被复制成多行,比如一个用户有三个订单,左连接后这个用户就出现三次,你要是直接数用户数,就会重复计算,所以该 distinct 的时候别手软,该分组聚合的时候别偷懒。
第四,性能要有意识,leftouterjoin 不是洪水猛兽,但左表太大、连接字段没索引、条件写得含糊,一样会慢,业务上能先过滤左表就过滤,别一上来全表连全表,技术工具再好,也架不住闭着眼睛用。
我个人有一个比较鲜明的观点:在业务报表和运营分析里,leftouterjoin 应该成为默认思考方式之一,不是说 inner join 不好,inner join 很适合找“共同发生”的关系,比如已下单用户和商品、已签到人员和活动,但只要你关心的是“全集里每个人/每个对象的状态”,左连接就更接近真实世界,因为真实世界不是只有发生关系的那部分,还有大量沉默的、没发生的、空着的那部分,那部分往往才是问题所在。
做自媒体也一样,你不能只看评论区热闹的人,还得看那些看完不说话的;不能只统计转发的人,还得看打开又划走的人;不能只分析爆款,还得看那些没起来的选题为什么没起来,交集很热闹,但全集更诚实。
leftouterjoin 对我来说不只是一个 SQL 关键字,它更像一种做事习惯:先把左边的主体都保留下来,再去匹配右边的信息;允许空值存在,也允许“没有发生”被看见,技术是冷的,但业务判断是热的,别让一个连接方式,悄悄把你最该关注的人筛没了。
下次再做报表,先问自己一句:我要看的,是“有结果的人”,还是“所有人各自的结果”?如果是后者,leftouterjoin 大概率该上场了。
