一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

订单号查出了三笔:我以为是数据脏了:其实是自己写错了

时间:2026-07-24 09:28:49 编辑:袖梨 来源:一聚教程网

订单号查出了三笔,我以为是数据脏了,其实是自己写错了

前几天翻一个批量对账脚本,里面有一条查订单的 SQL,条件是 where order_no = 123,没加引号。当时扫了一眼没在意,order_no 看着就是个数字,谁没事会给数字加引号。直到后来跑起来发现同一个订单号查出了三条记录,才回头去看这条 SQL 到底出了什么问题。

顺手把这个坑和另一个坑放一起记一下,都是那种语法完全没错、跑起来不报错、结果却是错的类型。环境是 KES V009R001C010

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

订单号不加引号,多查出两条不相干的数据

先照着原来那张表复现一下。订单号存成 varchar,这很常见,因为它可能带前导零、可能带字母前缀,不是纯数字:

 复制代码create table app_schema.t_trap_order (
  id integer primary key,
  order_no varchar(20) not null,
  amount numeric(10,2) not null
);insert into app_schema.t_trap_order(id, order_no, amount) values
  (1, '00123', 100.00),
  (2, '123',   200.00),
  (3, '0123',  300.00),
  (4, '9999',  400.00);create index idx_trap_order_no on app_schema.t_trap_order(order_no);

001231230123 这三个订单号是我故意凑的,看着都像"123",实际是三笔完全独立的订单,字符串层面一个字符都不一样。

带引号查一次,先看正常情况:

 复制代码explain analyze
select * from app_schema.t_trap_order where order_no = '123';

Filter: ((order_no)::text = '123'::text),过滤掉 3 行,剩 1 行,就是 id=2。执行计划是 Seq Scan,不是走索引——这张表就 4 行数据,优化器觉得直接扫一遍比查索引还省事,跟这个坑本身没关系,先别搞混了。这一步没什么意外,字符串比字符串,123 就是 12300123 不是。

去掉引号,就是我最开始踩的那条:

 复制代码explain analyze
select * from app_schema.t_trap_order where order_no = 123;

能跑,不报错,但 Filter 变成了 ((order_no)::integer = 123)。KES 把 order_no 这一列转成整数再去跟 123 比,'00123' 转成整数是 123,'123' 是 123,'0123' 还是 123——仨订单号转完全撞一块了。执行计划里写着 rows=3,跟我实际看到的结果对上了:这条 SQL 把三笔订单一起捞出来了。

这种问题比直接报错麻烦。报错至少会被日志和监控揪出来,"多查出两笔看着有点关系的订单"反而没什么动静,尤其是嵌在批量脚本里,多出来的数据可能就跟着一起处理掉了,等发现的时候已经过了好几个环节。

这次实验表小,两条查询都是全表扫描,没法从索引角度对比。但有一点值得记一下:转换函数一旦包住了索引列本身(这里是 (order_no)::integer),普通 B-tree 索引就用不上了,表一大,这种写法不只是数据查错,索引也会跟着报废,变成一次完整的全表扫描。

改起来不难,字段是什么类型就按什么类型写,order_no 是字符串就老老实实加引号。真要按数值比较也不是不行,前提是清楚这么写就意味着 '00123''123' 要被当成一回事,这是业务决定,不该是手滑漏了个引号带出来的副作用。

查"没下过单的客户",NOT IN 给我整了个空集

另一个坑是查"没有下过订单的客户",用 NOT IN 加子查询,写法上没什么特别的:

 复制代码create table app_schema.t_trap_customer (
  cust_id integer primary key,
  cust_name varchar(50) not null
);create table app_schema.t_trap_order_ref (
  order_id integer primary key,
  cust_id integer,
  order_no varchar(20) not null
);insert into app_schema.t_trap_customer(cust_id, cust_name) values
  (1, 'customer-a'),
  (2, 'customer-b'),
  (3, 'customer-c');insert into app_schema.t_trap_order_ref(order_id, cust_id, order_no) values
  (1, 1, 'ORD-001'),
  (2, null, 'ORD-002-异常订单,客户号缺失');

第二条订单记录的 cust_id 是 NULL,模拟的是那种数据导入漏映射、或者匿名下单没关联上客户号的脏数据,实际项目里这种记录不算罕见。customer-b、customer-c 都没下过单,按理说应该被查出来。

子查询里先把 NULL 过滤掉,没问题:

 复制代码select cust_id, cust_name
from app_schema.t_trap_customer
where cust_id not in (
  select cust_id from app_schema.t_trap_order_ref where cust_id is not null
);

customer-b、customer-c,两行,对的。

但没人会没事往子查询里加个 is not null,尤其是压根不知道订单表里还有这种缺客户号的记录。去掉这个过滤:

 复制代码select cust_id, cust_name
from app_schema.t_trap_customer
where cust_id not in (
  select cust_id from app_schema.t_trap_order_ref
);

0 行。 customer-b、customer-c 全部没了,跟那条异常订单八竿子打不着,也一起被牵连了。

这个我第一次看到是懵的:子查询里就多了一条不相干的记录,怎么就把两个正经客户也带没了。刨根问底是 NOT IN 展开之后的样子——cust_id not in (1, null) 相当于 cust_id <> 1 and cust_id <> null<> null 这个比较的结果不是真也不是假,是"不知道"。and 链条里只要有一项"不知道",又没有别的项判定为假,整个表达式就是"不知道",WHERE 对"不知道"和"假"一视同仁,全部扔掉。所以子查询里只要混进一个 NULL,NOT IN 对外层每一行的判断都跟着遭殃,不是漏判几行,是全部判定失败。

换成 NOT EXISTS 就没这问题:

 复制代码select c.cust_id, c.cust_name
from app_schema.t_trap_customer c
where not exists (
  select 1 from app_schema.t_trap_order_ref o
  where o.cust_id = c.cust_id
);

customer-b、customer-c 又回来了,那条异常订单还在表里,不影响结果。NOT EXISTS 只关心"有没有匹配的行",一条 cust_id 是 NULL 的记录,天然跟谁都匹配不上,也就没法拖累别的判断。我以前一直以为"排除类查询用 NOT EXISTS 而不是 NOT IN"是个写法偏好,这回算是搞明白了,这不是偏好,是子查询里但凡可能出现 NULL,NOT IN 就真的不安全。

写完想到的

这两个坑隔得挺远,一个跟类型有关,一个跟 NULL 的三值逻辑有关,但我踩坑的过程挺像的:都是在干净的测试数据上跑得好好的,换了一批带点"瑕疵"的真实数据——带前导零的订单号、混进一条脏记录的子查询——就翻车了。以后写完 SQL,我打算多问自己一句:这批数据要是不那么干净,这条语句还会按我想的那样跑吗?想不明白就拿几行边界数据实际跑一遍,比等上线以后出问题省心多了。

热门栏目