SELECT count(*) FILTER (WHERE nickname IS NULL) AS nulls,
       count(*) FILTER (WHERE nickname = '') AS empties
FROM users;

用户表 nickname 有 4.2 万行是 NULL,1.8 万行是空字符串,统计非空昵称时用 IS NOT NULL 得 12 万,业务报表却按空串过滤,两个口径差了 1.8 万。这类现象通常不是突然出现的,往前翻半天日志能看到苗头。

追下去是这么回事:@NotNull 只拦 null,空字符串能通过,前端不填时传的是空串而不是 null。不同版本客户端传法不一致,库里两种值并存。查询条件只写了一种,另一部分数据就被漏掉。

「空串与 NULL」排查到最后往往是很小的一处差异,前面那些复杂猜测可以先放一放。监控和日志的时间要对上,时区不一致会多绕很大一圈。

临时改动用注释写上日期和原因,过两周自己都记不清为什么这么写。动手前把原文件复制一份带时间戳,出问题立刻换回去,比现场分析快得多。

处理上按这个顺序:在 DTO 上用 @NotBlank 同时拦 null 和空白串;入库前统一把空串转成 null 或在列上建检查约束;写一条迁移把历史空串统一成 null;统计 SQL 改成 NULLIF 兼容两种值。

确认没问题再收工,新提交空昵称返回 400 迁移后空串行数为 0 两个统计口径一致。

作者 9823189