「布尔列」未必存 0/1:写 SQL 前先 GROUP BY 实测
接手一个老系统,查一张关联表时怎么也拿不到数据。Service 层的过滤条件写的是 eq("del_flag", "0"),逻辑上一点问题没有——删除标记为 0 的才算有效嘛。然而返回空集,JOIN 出来的外层查询也跟着全部失配,业务页面一片空白。
排查了半小时,才发现问题出在一个极其隐蔽的地方:这张表的 del_flag 列根本就没有值 '0'。
现象
- Service 代码
eq("del_flag", "0")运行后返回 0 行 - 同表直接
SELECT COUNT(*) FROM t有上千条记录 - 换成
WHERE del_flag IS NULL立刻返回正常数据 - 代码没有改过,某次数据迁移前还是正常的
根因
数据库里「布尔语义」的实现方式从来就不是统一的。同一个系统在不同时期建的表,可能用了五种不同的实现:
| 实现方式 | 实际取值 | 备注 |
|----------|----------|------|
| tinyint(1) | 0 / 1 | 最常见的标准写法 |
| tinyint(1) | 1 / 2 | 启用/停用语义,没有 0 |
| varchar(1) | NULL / '1' | 有效=NULL,删除='1',没有 '0' |
| varchar(1) | 'Y' / 'N' | Oracle 移植项目常见 |
| tinyint(1) | 0 / 1 / 2 | 三态,!=1 不包含 NULL |
这张表经历过迁移,del_flag 从 tinyint 0/1 改成了 varchar(1) NULL/'1' 的语义——有效行不写这个字段(NULL),删除时写入 '1'。老代码 eq("del_flag", "0") 没有跟着改,就这么静默地坏掉了。
MySQL 的弱类型转换让这个坑更深:varchar 列与数字 0 比较时,'a'、''、NULL 等各种值都可能被强制类型转换,命中你完全没预期的行。
COMMENT '0=有效 1=已删' 这类字段注释经常是过时的。数据迁移、字段类型变更之后,注释很少被同步更新。唯一可信的是表里真实存在的值。
正解
第 1 步:DESC + GROUP BY 双管齐下
在写任何过滤条件之前,先跑两条 SQL:
-- 看字段类型、是否允许 NULL、默认值
DESC your_table;
-- 看真实取值分布,不要相信注释
SELECT del_flag, COUNT(*)
FROM your_table
GROUP BY del_flag
ORDER BY 2 DESC;这两步会告诉你:
- 字段是
varchar还是tinyint(影响是否要加引号) - 到底有哪些值在跑(NULL、'0'、'1'、'Y' 还是别的)
- 新插入行的默认初始值是什么
第 2 步:用兼容写法代替假设
如果实测发现是 NULL/'1' 语义:
-- 选"有效"行
WHERE (del_flag IS NULL OR del_flag = '0')
-- 反向更稳(避免字符型强转陷阱)
WHERE del_flag IS NULL OR del_flag != '1'
-- 或 COALESCE 统一处理
WHERE COALESCE(del_flag, '0') = '0'不要写:
WHERE del_flag = 0 -- varchar 列与数字比,弱类型转换结果不可预期
WHERE del_flag = '0' -- 表里根本没有 '0',永远 0 行第 3 步:发现历史 bug 不要直接改全表数据
如果确认是历史代码写错了,优先改代码使用兼容写法,而不是 UPDATE 全表把 NULL 改成 '0'。原因:
- 可能有其他代码路径已经依赖了「NULL=有效」这个语义,改数据会把正常运行的逻辑也反过来
- 改代码可以被 Git 追踪、Code Review、回滚;改数据一旦错了代价极高
接手任何不是你建的表,写 flag 过滤前先 SELECT flag, COUNT(*) FROM t GROUP BY flag,用真实数据驱动你的 SQL,而不是用字段名猜语义。
一句话外卖
字段叫
del_flag不代表表里有0—— 先 GROUP BY 看分布,再写 WHERE。