返回博客

「布尔列」未必存 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_flagtinyint 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。