做了几年PostgreSQL相关的数据治理工作,我一直觉得JSONB是个让人又爱又恨的类型。爱它是因为灵活,想存什么存什么;恨它是因为一旦字段多了、数据杂了,想搞清楚"这张表里哪些字段是真正被填了的、哪些字段全是空的"就成了个大麻烦。最近正好有项目需要把订单表里JSONB扩展字段做一轮数据质量巡检,我就写了个通用的非空字段统计函数,在这里把完整的踩坑思路和代码实现分享一下。
这篇文章适合正在用PostgreSQL、有JSONB字段、并且需要做数据质量分析或者字段治理的开发者。无论你是刚接触JSONB不久,还是已经写过不少相关查询,这篇内容都能提供一些可以立刻上手的方案,也会讲清楚那些文档里不会明说的判定细节。
1. 为什么需要给JSONB做"非空字段统计"
1.1 来自真实数据巡检的需求
先说具体的业务背景。我们有一张用户行为订单表,核心业务字段是固定的那十几列,但业务方为了支持快速迭代,把大量扩展属性都塞进了一个叫 ext_info 的JSONB字段里。里面存过用户的设备信息、渠道来源、偏好标签、活动参与记录,甚至还有前端埋点带上来的一些临时参数。三个月跑下来,这个JSONB里到底出现过多少种key、每个key的填充率是多少,根本没人能说清楚。
这时候就有人提出了需求:"帮我统计一下,JSONB字段里哪些字段是空的,哪些字段有值,最好能一次性统计出所有字段的填充率。"听起来很简单,但实际操作起来,比想象中麻烦得多。原因在于JSONB不像普通表字段,它没有一个固定的schema,你想知道它有哪些字段,必须先扫描一遍数据才能拿到key列表,然后再逐个统计非空情况。两个步骤都得做全表遍历。
1.2 字段治理和结构变更前的摸底
除了日常巡检,这类统计在表结构变更评估时也特别有用。比如业务方说"我想把 ext_info 里的 user_level 字段提升成独立的表字段",那你在做变更之前就必须知道这个字段的填充率到底是多少。如果填充率不到30%,那提升成独立字段后,大量行的该列都是NULL,不仅浪费存储空间,还容易让下游误以为数据缺失是bug。
另一个常见场景是文档型数据迁移。当你需要把PostgreSQL里的JSONB数据同步到其他存储系统,或者反过来把别的系统的JSON数据导入PostgreSQL时,"哪些字段是可靠的、哪些字段基本是空的"这个信息,直接决定了迁移方案的取舍。有的字段可能只出现在历史数据里,新数据根本不写了,这些情况都能通过非空统计暴露出来。
1.3 这种"空"远比想象中复杂
真正动手之后你会发现,"非空"这两个字在JSONB上的定义比在普通字段上复杂得多。一个JSONB字段,某个key可能:
- 完全不存在(键缺失)
- 存在,但值是JSON格式的
null - 存在,值是空字符串
"" - 存在,值是空对象
{}或者空数组[] - 存在,是有实际意义的值
这些情况在业务上的"空"含义完全不一样,但在SQL层面,它们的表现各有不同。普通字段你只需要判断 IS NULL 和 IS NOT NULL,JSONB却得借助函数仔细区分。这一点我会在后面专门展开讲,因为这里混淆了,统计结果就全错了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 统计前必须搞懂的三个JSONB判定边界
2.1 SQL NULL、键缺失、JSON null 三者截然不同
新手最容易犯的错误,就是把JSONB取出来的值和普通字段一样用 IS NULL 来判断。直接说结论:在PostgreSQL里,data -> 'some_key' 取出来的结果,分三种情况:
| 情况 | 表达式结果 | IS NULL 判断 |
|---|---|---|
| 键不存在 | SQL NULL | 为真 |
| 键存在,值为JSON null | jsonb的 'null' 值 |
为假 |
| 键存在,值正常 | jsonb值 | 为假 |
也就是说,如果一个key的值是JSON null,你用 col -> 'key' IS NULL 来判断,会得到"非空"的错误结论。反过来,如果你用 col ? 'key' 来判断键是否存在,它只会判断键存不存在,不会告诉你值是null还是有实际内容。
我在项目里就吃过这个亏。第一次写统计SQL时,用的是 WHERE (ext_info -> 'user_level') IS NOT NULL 来统计非空,结果把一堆JSON null的数据全算进去了,填充率虚高了好几个百分点。说白了,JSON null在JSONB里是一个实实在在的值,它不等于SQL NULL,也不等于键不存在。
2.2 用 jsonb_typeof 把JSON null揪出来
要准确区分"键存在但值是JSON null"这种情况,唯一的可靠手段是 jsonb_typeof() 函数。这个函数返回一个文本值,表示参数的类型:
sql复制SELECT jsonb_typeof('{"a": 1}'::jsonb -> 'a'); -- number
SELECT jsonb_typeof('{"a": null}'::jsonb -> 'a'); -- null
SELECT jsonb_typeof('{"a": {}}'::jsonb -> 'a'); -- object
SELECT jsonb_typeof('{"a": []}'::jsonb -> 'a'); -- array
SELECT jsonb_typeof('{"b": 1}'::jsonb -> 'a'); -- NULL(因为键缺失返回SQL NULL)
注意最后一行:如果键缺失,jsonb_typeof() 输入的是SQL NULL,返回值也是SQL NULL。所以判断"非空值"的条件,应该是:
sql复制jsonb_typeof(col -> 'key') IS NOT
