GBase 8a
其他
文章
精选

GBase 8a NULL 值参与比较、聚合和去重时的结果偏差

发表于2026-04-08 09:20:34231次浏览5个评论

GBase 8a NULL 值参与比较、聚合和去重时的结果偏差

我最近看资料和整理现场报表偏差时,越来越觉得 GBase 8a 里很多“不算错但结果就是不顺眼”的问题,根上其实在 NULL 的处理上。 尤其是做分析型 SQL 时,NULL 既不像普通值,也不是简单的空白。它参与比较、去重、聚合、条件判断时的行为,和很多人脑子里的直觉并不一样。

现场里最常见的情况是: 同一张表里有空值列,开发写 = '' 查不到,换成 is null 才有结果;count(col) 和 count(*) 差得很大;group by 后感觉分组数量不对;补数脚本把空串和 NULL 混着写,最后报表口径慢慢漂掉。

我自己理解下来,这类问题不是性能问题,也不是安装运维问题,更接近SQL 语义和数据治理边界。 真到现场时,如果不先把 NULL、空串、默认值分开看,后面很多争论其实都落不下来。

先把三个容易混掉的概念分开

我自己排查时,一般先把下面三种情况拆开:

值类型 我自己的理解 现场最容易出现的误判
NULL 未知、缺失、未赋值 当成空字符串
空串 '' 长度为 0 的字符串 当成 NULL
默认值 由业务或建表规则补上的值 当成真实业务值

这三者混在一起时,最容易出现“能查到一些,也漏掉一些”的问题。

现场里最常见的几类现象

  1. where col = '' 查不到预期记录。
  2. count(col) 明显小于 count(*),业务一开始以为丢数据。
  3. group by 后多了一个“空组”,但排查时又说不清到底是 NULL 还是空串。
  4. 左连接后某些维度字段为空,后续又直接参与聚合或筛选,结果越来越偏。
  5. 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

评论

登录后才可以发表评论
用户头像
柒柒天晴发表于 5个月前
22221
罗小胖发表于 5个月前
学习学习
nodddddd发表于 4个月前
学习了
流泪猫猫头发表于 3个月前
学习了。
GBase用户47954发表于 3个月前
感谢作者的精彩分享!