GBase 8a NULL 值参与比较、聚合和去重时的结果偏差
GBase 8a NULL 值参与比较、聚合和去重时的结果偏差
我最近看资料和整理现场报表偏差时,越来越觉得 GBase 8a 里很多“不算错但结果就是不顺眼”的问题,根上其实在 NULL 的处理上。 尤其是做分析型 SQL 时,NULL 既不像普通值,也不是简单的空白。它参与比较、去重、聚合、条件判断时的行为,和很多人脑子里的直觉并不一样。
现场里最常见的情况是:
同一张表里有空值列,开发写 = '' 查不到,换成 is null 才有结果;count(col) 和 count(*) 差得很大;group by 后感觉分组数量不对;补数脚本把空串和 NULL 混着写,最后报表口径慢慢漂掉。
我自己理解下来,这类问题不是性能问题,也不是安装运维问题,更接近SQL 语义和数据治理边界。 真到现场时,如果不先把 NULL、空串、默认值分开看,后面很多争论其实都落不下来。
先把三个容易混掉的概念分开
我自己排查时,一般先把下面三种情况拆开:
| 值类型 | 我自己的理解 | 现场最容易出现的误判 |
|---|---|---|
| NULL | 未知、缺失、未赋值 | 当成空字符串 |
空串 '' |
长度为 0 的字符串 | 当成 NULL |
| 默认值 | 由业务或建表规则补上的值 | 当成真实业务值 |
这三者混在一起时,最容易出现“能查到一些,也漏掉一些”的问题。
现场里最常见的几类现象
where col = ''查不到预期记录。count(col)明显小于count(*),业务一开始以为丢数据。group by后多了一个“空组”,但排查时又说不清到底是 NULL 还是空串。- 左连接后某些维度字段为空,后续又直接参与聚合或筛选,结果越来越偏。
case when col = null then ...这种写法逻辑上看着自然,结果却不对。
我最近整理下来觉得,这类故障特别容易被误判成“抽数有问题”,其实很多时候数据根本没丢,只是NULL 的语义被错误处理了。
我实际排查时一般先看哪几步
第一步:先统计 NULL、空串和有效值的分布
不要一上来就改 SQL。 我一般先把列里的分布拆出来看:
select
count(*) as total_cnt,
sum(case when cust_level is null then 1 else 0 end) as null_cnt,
sum(case when cust_level = '' then 1 else 0 end) as empty_cnt,
sum(case when cust_level is not null and cust_level <> '' then 1 else 0 end) as valid_cnt
from dwd_customer;
这一步最大的价值是把问题先坐实。 很多时候大家凭印象说“都是空的”,但真查下来,NULL 和空串可能是两套完全不同的来源。
第二步:核对聚合函数是不是选对了
select
count(*) as total_rows,
count(cust_level) as non_null_rows
from dwd_customer;
如果业务口径想算“总行数”,那就不能随手写 count(cust_level)。
我自己更关注的是:count 的目标到底是统计记录,还是统计非空值。
第三步:检查条件判断是否把 NULL 漏掉了
比如下面这种写法,我现场里见过很多次:
select *
from dwd_customer
where cust_level <> 'VIP';
业务以为这会拿到所有“不是 VIP”的记录,但实际里,cust_level is null 的行并不会自动进来。
如果口径上要把未知值也算进去,就得写得更明确:
select *
from dwd_customer
where cust_level <> 'VIP'
or cust_level is null;
一个更接近现场的例子
我自己把一个用户标签场景做了下简化。
某张客户表里,channel_code 来源很多,既有正常值,也有 NULL 和空串:
create table dwd_customer (
cust_id bigint,
channel_code varchar(20),
city_name varchar(50)
);
现在业务要看各渠道用户数量,原始写法可能是:
select
channel_code,
count(*) as cust_cnt
from dwd_customer
group by channel_code;
这条 SQL 看起来没错,但真正落到现场时,结果里常常会出现一个“空渠道”,这时大家最容易开始争: 这个空到底是不是一个渠道? 这里面是 NULL,还是空串? 后面报表应该显示为空白、未识别,还是直接过滤掉?
我自己更倾向于先把口径写清楚,再做聚合:
select
case
when channel_code is null then 'NULL_VALUE'
when channel_code = '' then 'EMPTY_STRING'
else channel_code
end as channel_tag,
count(*) as cust_cnt
from dwd_customer
group by
case
when channel_code is null then 'NULL_VALUE'
when channel_code = '' then 'EMPTY_STRING'
else channel_code
end;
这样至少能把争议从“感觉不对”变成“具体是哪类值在影响结果”。
NULL 最容易影响到哪几类 SQL
| SQL 类型 | 常见偏差 | 我优先检查的点 |
|---|---|---|
| 比较过滤 | 漏掉 NULL 行 | is null / is not null 是否明确写出 |
| 聚合统计 | count(col) 偏小 |
是否误把非空计数当总数 |
| 条件表达式 | case when col = null 无效 |
是否用了错误比较写法 |
| 分组去重 | NULL、空串混在一起讨论 | 是否先标准化口径 |
几个特别容易踩的坑
坑一:把 NULL 当成空串处理
这在文本字段里最常见。 看起来都是“空”,语义上却不是一回事。
坑二:业务口径没说清楚,技术先写了默认处理
比如有的报表需要把 NULL 视为“未知”,有的场景需要直接剔除。 如果前面没统一,后面每个人会按自己的理解写。
坑三:左连接后没意识到新产生了大量 NULL
左连接本来就是允许右表缺失的。 但很多人后面继续拿右表字段做过滤或分组,结果不知不觉把口径改掉了。
坑四:NULL 只在查询时处理,入库层一直混乱
如果某类字段长期同时存在 NULL 和空串,说明上游治理本身就不稳。 只靠查询层修补,后面还会反复出问题。
我自己更倾向的处理方式
先把口径写在 SQL 里,不要放在脑子里
select
case
when channel_code is null then 'UNKNOWN'
when channel_code = '' then 'EMPTY'
else channel_code
end as channel_tag,
count(*) as cust_cnt
from dwd_customer
group by
case
when channel_code is null then 'UNKNOWN'
when channel_code = '' then 'EMPTY'
else channel_code
end;
对关键字段定期做空值分布检查
select
'channel_code' as col_name,
sum(case when channel_code is null then 1 else 0 end) as null_cnt,
sum(case when channel_code = '' then 1 else 0 end) as empty_cnt
from dwd_customer;
对下游口径影响大的列,尽量在明细层先标准化
如果业务已经明确 NULL 要转成某个业务标签,我个人更倾向于在明细层或主题层先固化,不要让每个下游 SQL 各写各的。
一个简单的批检查脚本示意
#!/bin/bash
DBHOST=192.0.2.71
DBPORT=5258
DBNAME=dw_user
DBUSER=qa_user
LOGDIR=/data/gbase/log/null_check
DAYSTR=$(date +%F)
mkdir -p "${LOGDIR}"
gccli -h ${DBHOST} -P ${DBPORT} -u ${DBUSER} ${DBNAME} <<'SQL' >> "${LOGDIR}/null_check_${DAYSTR}.log" 2>&1
select count(*) as total_cnt from dwd_customer;
select count(channel_code) as non_null_cnt from dwd_customer;
select sum(case when channel_code is null then 1 else 0 end) as null_cnt from dwd_customer;
select sum(case when channel_code = '' then 1 else 0 end) as empty_cnt from dwd_customer;
SQL
我自己更关注的是把这类检查做成固定动作,而不是每次等到报表偏了才临时想起来查。
结尾
我最近回头看 GBase 8a 里这类问题时,一个很明显的感受是: NULL 带来的麻烦很少是“SQL 报错”,更多是“SQL 不报错,但结果跟业务理解不一致”。
真正落到现场时,先把 NULL、空串、默认值分开,再谈比较、聚合和分组,往往比直接改 SQL 更快把问题收住。
参考资料
[1] GBase 社区个人中心
https://www.gbase.cn/community/user/46723
[2] GBase 8a 社区优质文章区
https://www.gbase.cn/community/section/11
[3] GBase 8a MPP Cluster SQL 参考手册
https://www.gbase.cn/community/post/1772
[4] GBase 8a
https://www.gbase.cn/community/section/11
热门帖子
- 12025-12-01浏览数:183468
- 22023-05-09浏览数:26192
- 42023-09-25浏览数:19863
- 52020-05-11浏览数:18525