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

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

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

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

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

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

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

sql 复制代码
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",实际是三笔完全独立的订单,字符串层面一个字符都不一样。

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

sql 复制代码
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 不是。

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

sql 复制代码
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 加子查询,写法上没什么特别的:

sql 复制代码
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 过滤掉,没问题:

sql 复制代码
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,尤其是压根不知道订单表里还有这种缺客户号的记录。去掉这个过滤:

sql 复制代码
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 就没这问题:

sql 复制代码
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,我打算多问自己一句:这批数据要是不那么干净,这条语句还会按我想的那样跑吗?想不明白就拿几行边界数据实际跑一遍,比等上线以后出问题省心多了。

相关推荐
ZKNOW甄知科技1 小时前
燕千云深度集成飞书:以AI之力,开启无感IT运维体验
大数据·运维·网络·数据库·人工智能·低代码·集成学习
TDengine (老段)1 小时前
TDengine 免费版说明
java·大数据·数据库·物联网·时序数据库·tdengine
汉知宝科技1 小时前
历史案件数据迁移:知识产权管理系统落地的第一道门槛
大数据·数据库
这就是佬们吗2 小时前
Python入门⑤-异常处理、文件操作与实战项目
开发语言·数据库·python·算法·pycharm
吴声子夜歌2 小时前
MongoDB 4.x——SpringBoot框架整合
数据库·spring boot·mongodb
Database_Cool_2 小时前
阿里云 Tair(企业级内存数据库)vs 腾讯云 Redis 云缓存深度对比:性能/数据结构/成本全维度 Benchmark
数据库·阿里云·缓存
流星白龙2 小时前
【Redis】1.Redis特性
数据库·redis·缓存
奶糖 肥晨2 小时前
DBeaver 安装与 MySQL 连接配置教程
数据库·mysql
woshihuanglaoshi3 小时前
数据迁移与版本管理 - Flutter在鸿蒙平台实现数据库升级策略
数据库·学习·flutter·华为·harmonyos·鸿蒙·鸿蒙系统