找回密码
立即注册
搜索
发回帖 发新帖

5051

积分

0

好友

645

主题
发表于 半小时前 | 查看: 8| 回复: 0

写在开头

项目里的表用逻辑删除:删一条不是真删,而是把 delete_flag 置为 1。LEFT JOIN 也是平时天天写的语句。这两件事我都没当回事。

结果这个月,LEFT JOIN 连着让我栽了两个跟头:一次分页查询该出的数据没出来,total 还跟着变小;另一次自连接统计,数据反而多了,有的用户被算了两遍。

两次表现相反,根子是同一件事:右表的过滤条件该放哪,两次都没写对。

一、第一次,条件写在 WHERE 里,左表的数据跟着没了

第一次是个很普通的分页查询。左表是主表,右表是它的明细,用 LEFT JOIN。业务上要求主表记录必须全部显示,哪怕这一季度一条明细都没有,也得出现在列表里。明细要按年份、季度筛选,只看 2026 年第一季度的。

这个筛选条件我当时顺手写在了 WHERE 后面。

SELECT p.id, p.name, c.amount
FROM t_parent p
LEFT JOIN t_child c 
ON p.id = c.parent_id
WHERE p.delete_flag = 0
AND c.year = 2026
AND c.quarter = 1

跑出来以后,列表少了几行,total 也跟着小了。条件筛的是右表,右表少了行我认,可左表的行凭什么也跟着没了?问题就出在“顺手”这两个字上。

二、第二次,右表出了问题

第二次是自连接,一张表连它自己。左表取今年的数据,右表取去年的数据,按 user_id 对起来,看哪条记录是今年新增的。

SELECT l.id, l.user_id
FROM t l
LEFT JOIN t r 
ON l.user_id = r.user_id 
AND r.year = 2025
WHERE l.year = 2026
AND l.delete_flag = 0

写的时候只记得滤掉左表删掉的行,右表那个 delete_flag 没出现。表里以前删过数据,留着不少 delete_flag = 1 的行。这些行在右表上也能匹配。统计出来,有的用户被算了两遍。

三、两次是同一个原因

两次的根子在这里:ON 和 WHERE 管的不是一回事。

ON 决定两张表怎么配对,WHERE 决定配对完之后哪些行不要。

放进 LEFT JOIN 里,这个区别会被放大。LEFT JOIN 的承诺是左表每一行都留下,这个承诺是 ON 在守的,WHERE 不认。执行时大致是这么走的:先拿左表每一行去右表找能配上的;配对时,右表先按 ON 后面的条件筛一遍,配上就填值,配不上就整行填成 NULL;最后一步,拿这个结果去做 WHERE 过滤。

关键就在最后一步。配不上的行,右表字段全是 NULL,拿 NULL 去跟任何值比都是假,于是被整行清掉。 LEFT JOIN 就这样退化成了 INNER JOIN。不报错,不警告,只是行数悄悄变少。

三条主表记录、一张子表,跑了一遍对照。两张表长这样:

主表记录 明细(都是 2026 年的)
张三(没删) 第一季度 100,第二季度 200
李四(没删) 只有第二季度 300,第一季度一条明细都没有
王五(已经删了) 第一季度 400

查的是 2026 年第一季度,要求主表全部显示。三种写法分别返回什么,看下面:

条件写在哪 查出来的结果
右表的 year、quarter 写 ON 张三 100 · 李四(明细列是空的)
右表的 year、quarter 写 WHERE 张三 100 —— 李四整个没了
左表条件也挪进 ON 张三 100 · 李四(空)· 王五(空)

第一行是对的,第二行是当时写的,第三行是后来试的:把左表的条件也塞进 ON,结果连删掉的主表都拦不住。所以,左表的条件该写 WHERE。

第二次那个重复,机制是同一个。左表一行,右表能配上好几行,结果就展开成好几行。去年的记录留了不止一条,右表又不过滤 delete_flag,它俩全配上,这个用户在统计里就出现了两遍。

这组也跑了对照。表里今年有三条没删的记录 101、102、103,其中 103 是今年才冒出来的,去年没有它。去年的记录里,102 留了两条,一条没删、一条删过。

右表怎么处理 查出来的记录 判为今年新增的
右表条件写 ON(正解) 101 · 102 · 103,各一条 103
右表条件写 WHERE 只剩 101 · 102 一条都没有
不去管它(当时的写法) 101 · 102 · 102 · 103 103

中间那一行要特别注意。发现右表没限制,第一反应多半是把它加进 WHERE 里。加进去,查询就变成内连接,右表配不上的行全被清掉。可是今年新增的那批用户,在右表上本来就配不到。条件往 WHERE 一写,要找的东西自己就被筛掉了,查出来的新增是空的,而且它还是不报错。

所以正确的写法是分开的:左表的条件写 WHERE,右表的条件写 ON。

你想干什么 条件写在哪
过滤右表,同时保住左表全部行 ON
过滤左表自己 WHERE
只想要配得上的行 直接写 INNER JOIN,别用 LEFT JOIN 加 WHERE 假装
想要配不上的行 条件写 ON,再在 WHERE 里加右表主键 IS NULL

四、MyBatis-Plus 在这里帮不了你

看到这儿有人会想,我用的是 MyBatis-Plus,逻辑删除不是有 @TableLogic 吗?加个注解,框架不就帮你把条件加上了。

搭了个最小工程实测:Spring Boot 2.7.18 加 MyBatis-Plus 3.5.3.2,配 H2 内存库。BaseMapper 自带的方法,比如 userMapper.selectList(null),MP 实际执行的 SQL 是这样:

SELECT id,name,delete_flag 
FROM t_user 
WHERE delete_flag=0

那个 WHERE delete_flag=0 是它自己拼上去的,一个字不用写。

换成手写 XML 的 join,它就一个字都不加了。原样执行,返回 5 行,里面既有已经删掉的用户,也有已经删掉的订单,不报错,也不提醒。同一个 mapper 接口里,两种行为都在:selectList 帮你过滤好了,手写的那句全靠自己。而手写 join 恰好是 LEFT JOIN 最容易写错的地方,这两件事凑在一块了。

写在结尾

两次踩坑,最后收在三句话上。

一,左连接里,左表的条件写 WHERE,右表的条件写 ON。

二,拿不准的时候问自己一句:右表没配上的那行,我想不想让它从结果里消失?不想,就写 ON。

三,框架帮你加的条件,只加在它自己生成的 SQL 上。手写的那句,得自己负责。




上一篇:写作Agent优化:Tessl四种文件搬进WorkBuddy,三层能用钩子失灵
下一篇:FastThreadLocal 为什么比 ThreadLocal 快?Netty 源码深度剖析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-18 08:13 , Processed in 0.067132 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表