订单查询丢数据,查了半天原来是 WHERE 惹的祸

前两天帮着看一个从 MySQL 迁过来的老需求,业务原话是这么说的:"把所有订单都列出来,如果这笔订单有审核中的售后申请,把售后原因也带出来"。这种需求太常见了,我第一反应就是 LEFT JOIN------左表订单表保底,右表售后表能匹配上就带出来,匹配不上就是 NULL,教科书式写法。结果这条 SQL 跑出来的结果,让我在屏幕前愣了好几秒。

这篇就把这次踩坑的过程整个记下来:怎么发现的、怎么排查的、最后怎么定位到根子上、KingbaseES(KES)这边要怎么改。环境是 KES V009R001C010,连接用普通业务账号 app_user,不是管理员:

css 复制代码
ksql -h 127.0.0.1 -p 54321 -U app_user -d app_db

场景还原:订单表 + 售后表

先把场景搭起来。两张表,订单表 t_pit_order,售后服务单表 t_pit_service,一笔订单可能有 0 条、1 条甚至多条售后记录:

sql 复制代码
create table app_schema.t_pit_order (
  order_id integer primary key,
  order_no varchar(40) not null unique,
  user_id integer not null,
  order_status varchar(20) not null,
  amount numeric(10,2) not null,
  created_at timestamp not null
);
​
create table app_schema.t_pit_service (
  service_id integer primary key,
  order_no varchar(40) not null,
  service_status varchar(20) not null,
  reason varchar(100),
  created_at timestamp not null
);

插入 5 笔订单,全部是已支付状态:

sql 复制代码
insert into app_schema.t_pit_order(order_id, order_no, user_id, order_status, amount, created_at)
values
  (1, 'PIT20260701001', 1001, 'paid', 199.00, '2026-06-28 09:00:00'),
  (2, 'PIT20260701002', 1002, 'paid',  88.00, '2026-06-28 10:00:00'),
  (3, 'PIT20260701003', 1003, 'paid', 256.00, '2026-06-28 11:00:00'),
  (4, 'PIT20260701004', 1004, 'paid', 320.00, '2026-06-28 12:00:00'),
  (5, 'PIT20260701005', 1005, 'paid', 150.00, '2026-06-28 13:00:00');

售后记录只给两笔订单配了:订单 1 是"审核中",订单 3 是"已完成"。订单 2、4、5 压根没提交过售后:

sql 复制代码
insert into app_schema.t_pit_service(service_id, order_no, service_status, reason, created_at)
values
  (1, 'PIT20260701001', '审核中', '尺码不合适', '2026-06-29 09:00:00'),
  (2, 'PIT20260701003', '已完成', '质量问题', '2026-06-29 10:00:00');

这个数据设计不是随便凑的,特意留了三种情况:有售后且是审核中的(订单1)、有售后但不是审核中的(订单3)、压根没售后的(订单2、4、5)。等下就知道为什么这三种情况都得覆盖到。

第一版 SQL:看起来完全没毛病

按业务需求写的第一版,就是标准 LEFT JOIN 加个过滤条件:

ini 复制代码
select
  o.order_no,
  o.order_status,
  o.amount,
  s.service_status,
  s.reason
from app_schema.t_pit_order o
left join app_schema.t_pit_service s
  on s.order_no = o.order_no
where s.service_status = '审核中';

我当时的预期是:5 笔订单一条不少,只有订单 1 的 service_statusreason 两列有值,其余 4 行应该是 NULL。左连接嘛,左表保底,这是常识。

实际跑出来是这样:

只有 1 行。 订单 2、3、4、5 全部消失了,包括压根没申请过售后的订单 2、4、5,也包括有售后记录但状态是"已完成"的订单 3。说白了,这条 SQL 表现得完全像一个 INNER JOINLEFT 这个关键字像是白写了。

这时候脑子里冒出来的第一个念头是"是不是数据插错了",第二个念头是"是不是 KES 这边 LEFT JOIN 有兼容性问题"。事后证明这两个念头都是错的,但排查的时候没少走弯路,就顺着这个思路记录下来。

先排除数据问题:主表到底是不是 5 条

最笨也最靠谱的办法,先确认主表本身没问题:

csharp 复制代码
select count(*) as total_order from app_schema.t_pit_order;
csharp 复制代码
select o.order_no, o.order_status
from app_schema.t_pit_order o
left join app_schema.t_pit_service s
  on s.order_no = o.order_no;

count(*) 确认是 5 条,去掉 WHERE 之后重新跑一遍连接,5 笔订单一条不少地全出来了。这一步很关键,它证明了两件事:数据本身没问题,ON 里的连接条件也没写错。问题百分之百出在加了 WHERE s.service_status = '审核中' 之后。

到这一步,"KES 的 LEFT JOIN 有 bug"这个猜测基本可以排除了------如果真是语法层面的问题,去掉 WHERE 也应该有问题,但它没有。真正诡异的地方在于:只有加上那一句过滤,行为才会变。

定位:执行计划里根本没有 LEFT JOIN

翻了下之前存的一些技术资料才想起来,这种"加了 WHERE 之后 LEFT JOIN 变内连接"的现象有个名字,叫"外连接消除"(Outer Join Elimination)。先不管名字,直接上 EXPLAIN ANALYZE 看看执行计划里到底发生了什么:

ini 复制代码
explain analyze
select
  o.order_no,
  o.order_status,
  o.amount,
  s.service_status,
  s.reason
from app_schema.t_pit_order o
left join app_schema.t_pit_service s
  on s.order_no = o.order_no
where s.service_status = '审核中';

看执行计划的第一行:Hash Join (cost=1.04..2.12 rows=1 width=49)。注意,这里写的是 Hash Join,不是 Hash Left Join。KES 的执行计划里,如果一个连接真的按外连接执行,通常会明确标出 Left 这个字样;这里没有,说明这条 SQL 提交上去之后,优化器压根就没按外连接去跑,它直接把整个查询当成了一个普通的内连接来处理。

再往下看细节:t_pit_service 这张表被 Seq Scan 扫描,带了个 Filter: ((service_status)::text = '审核中'::text),后面还跟着一行 Rows Removed by Filter: 1------这张表总共 2 行数据,过滤之后只剩 1 行(订单1那条"审核中"的记录,订单3的"已完成"被过滤掉了)。然后这 1 行拿去跟订单表做 Hash Join,订单表那边是全量 5 行参与哈希探测,最终匹配上的只有 1 行。

这就是问题的真相:优化器把 WHERE s.service_status = '审核中' 这个条件,理解成了"只有 t_pit_service 里状态是审核中的行才有资格参与连接",那些原本因为左连接才应该保留下来的、t_pit_service 一侧是 NULL 的行(订单2、4、5 没有售后记录,订单3 的售后记录被过滤掉),全都在这一步被连带着筛没了。原因很简单:NULL 和 '审核中' 做等值比较,结果既不是真也不是假,是"未知",在 WHERE 里"未知"等于被扔掉。既然这些左连接产生的 NULL 行反正都要被 WHERE 干掉,那优化器算了一笔账:外连接加这个过滤,跟直接内连接加这个过滤,结果集是完全一样的,内连接开销更小,于是它就悄悄把外连接换成了内连接去跑。这个换算过程不会报错、不会有任何提示,SQL 文本里写的还是 LEFT JOIN,只是最终执行的已经不是那么回事了。

这套换算规则背后其实有一套明确的判断标准,比如什么样的条件一定会触发这种改写、什么样的条件不会,细节挺多,这里不整个铺开讲,先把这次踩坑的现象和定位方法记牢------遇到"LEFT JOIN 结果比预期少",第一反应就是去看执行计划里连接节点到底是不是真的按外连接在跑。

修复:把过滤条件挪到 ON 里

原因搞清楚了,修复思路也就明确了:真正要保留左连接语义,过滤右表的条件不能写在 WHERE,得写进 ON:

ini 复制代码
select
  o.order_no,
  o.order_status,
  o.amount,
  s.service_status,
  s.reason
from app_schema.t_pit_order o
left join app_schema.t_pit_service s
  on s.order_no = o.order_no
  and s.service_status = '审核中';

这次 5 行全部老老实实地回来了。只有订单 1 的 service_status/reason 有值,订单 2、3、4、5 全是空。这里特别要盯一眼订单 3------它明明有一条售后记录("已完成"),但因为不满足 and s.service_status = '审核中' 这个条件,它的售后字段依然是空。这一点很能说明问题:这不是运气好凑对了行数,是逻辑真的对了。ON 决定的是"怎么连",先按"审核中"这个条件筛一遍右表,筛完了再拿去跟左表做外连接;WHERE 决定的是"连完了再筛掉什么",加在 WHERE 里就等于是连接做完之后才动手,为时已晚,NULL 行早被判了死刑。

我自己的理解是这么记的:只要是左连接,右表相关的过滤条件,除非目的就是"顺便把左表也筛掉一批",不然一律先考虑放 ON 里,别图省事直接甩进 WHERE。

顺手纠正一个容易搞混的需求:查"没有审核中售后"的订单

排查这条 SQL 的时候,同事顺嘴问了一句:"那如果我就是想查哪些订单没有审核中的售后,是不是直接把等号换成 != 就行?"------这也是个常见的误区,赶紧一起验证了一下:

csharp 复制代码
select o.order_no, o.order_status
from app_schema.t_pit_order o
left join app_schema.t_pit_service s
  on s.order_no = o.order_no
  and s.service_status = '审核中'
where s.service_id is null;

结果返回订单 2、3、4、5,四条。注意订单 3 也在里面------它虽然有售后记录,但状态是"已完成",在这次以"审核中"为条件的关联里,等于没匹配上,所以 service_id 也是 NULL,也算进了"没有审核中售后"这一批。这条 SQL 的关键在于 IS NULL 判断的是右表整体有没有匹配上,而不是某个值等不等于什么,用它才能真正抓住"外连接产生的空行"。如果真按同事说的把等号换成不等号写进 WHERE,会重新掉进最开始那个坑里,这里就不重复验证了,逻辑和前面一样。

这次踩坑的收获

整理成几条能直接抄进 Code Review 清单的东西:

  1. 看到 LEFT/RIGHT JOIN 后面又跟着 WHERE 右表字段 = xxx,先停一下。这种写法十有八九是把过滤条件放错了地方,先问自己一句:这个条件是要"筛掉左表一部分数据",还是单纯想看右表某个值,前者才适合放 WHERE。
  2. 右表的过滤条件,只要目的是保留外连接语义,一律下推到 ON。这条不是"看情况",是默认动作。
  3. 怀疑外连接有问题,直接跑 EXPLAIN ANALYZE,别猜 。看执行计划里连接节点是不是明确带着 Left 字样,不带的话十有八九是被优化器改写了。
  4. 查"缺失"用 IS NULL,别用不等号 。不等号一样会被 WHERE 的 NULL 语义坑一遍,IS NULL 才是唯一可靠的判断"没匹配上"的方法。
  5. 这个问题从 MySQL/PostgreSQL 迁移过来的人尤其容易中招,因为写法上跟以前一模一样,SQL 一个字没改,行为却因为优化器的这个动作变了,代码审查根本看不出来,只能靠跑一遍执行计划确认。

这条坑最恶心的地方就在这儿:语法完全合法,SQL 正常执行,不报任何错误,只是安安静静地把你以为该在的数据丢掉了。真要命的问题从来不是报错的那种,是这种"看起来什么都没发生"的。

相关推荐
叫我Paul就好1 小时前
自从用上 Agents View 之后, 我的心力交瘁感消失了
后端
SamDeepThinking1 小时前
组合还是继承?我在项目里主要看这两个判断
java·后端·面试
niuju1 小时前
一种基于 Agent 的需求规范化闭环实践:让合规与规范内建于流程
后端
团团崽_七分甜1 小时前
四大数据库底层原理详解
后端
ATA88881 小时前
数据库管理工具的数据安全机制对比:权限管控与审计
数据库·人工智能·数据分析·数据库管理
glartchen1 小时前
使用 Pixi 配置 Whisper 环境,实现采用本地大模型读取视频字幕
后端
站大爷IP1 小时前
明明传了参数,进程池却说没找到?记一次变量丢失的排查实录
后端
麦聪聊数据2 小时前
业务自助取数(下):安全可控前提下,实现取数效率百倍提升
数据库·sql
2501_915716722 小时前
Repository-学习指南_小白版
数据库·人工智能