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

张开发
2026/4/8 22:18:44 15 分钟阅读

分享文章

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。我一般先把列里的分布拆出来看selectcount(*)astotal_cnt,sum(casewhencust_levelisnullthen1else0end)asnull_cnt,sum(casewhencust_levelthen1else0end)asempty_cnt,sum(casewhencust_levelisnotnullandcust_levelthen1else0end)asvalid_cntfromdwd_customer;这一步最大的价值是把问题先坐实。很多时候大家凭印象说“都是空的”但真查下来NULL 和空串可能是两套完全不同的来源。第二步核对聚合函数是不是选对了selectcount(*)astotal_rows,count(cust_level)asnon_null_rowsfromdwd_customer;如果业务口径想算“总行数”那就不能随手写count(cust_level)。我自己更关注的是count 的目标到底是统计记录还是统计非空值。第三步检查条件判断是否把 NULL 漏掉了比如下面这种写法我现场里见过很多次select*fromdwd_customerwherecust_levelVIP;业务以为这会拿到所有“不是 VIP”的记录但实际里cust_level is null的行并不会自动进来。如果口径上要把未知值也算进去就得写得更明确select*fromdwd_customerwherecust_levelVIPorcust_levelisnull;一个更接近现场的例子我自己把一个用户标签场景做了下简化。某张客户表里channel_code来源很多既有正常值也有 NULL 和空串createtabledwd_customer(cust_idbigint,channel_codevarchar(20),city_namevarchar(50));现在业务要看各渠道用户数量原始写法可能是selectchannel_code,count(*)ascust_cntfromdwd_customergroupbychannel_code;这条 SQL 看起来没错但真正落到现场时结果里常常会出现一个“空渠道”这时大家最容易开始争这个空到底是不是一个渠道这里面是 NULL还是空串后面报表应该显示为空白、未识别还是直接过滤掉我自己更倾向于先把口径写清楚再做聚合selectcasewhenchannel_codeisnullthenNULL_VALUEwhenchannel_codethenEMPTY_STRINGelsechannel_codeendaschannel_tag,count(*)ascust_cntfromdwd_customergroupbycasewhenchannel_codeisnullthenNULL_VALUEwhenchannel_codethenEMPTY_STRINGelsechannel_codeend;这样至少能把争议从“感觉不对”变成“具体是哪类值在影响结果”。NULL 最容易影响到哪几类 SQLSQL 类型常见偏差我优先检查的点比较过滤漏掉 NULL 行is null / is not null是否明确写出聚合统计count(col)偏小是否误把非空计数当总数条件表达式case when col null无效是否用了错误比较写法分组去重NULL、空串混在一起讨论是否先标准化口径几个特别容易踩的坑坑一把 NULL 当成空串处理这在文本字段里最常见。看起来都是“空”语义上却不是一回事。坑二业务口径没说清楚技术先写了默认处理比如有的报表需要把 NULL 视为“未知”有的场景需要直接剔除。如果前面没统一后面每个人会按自己的理解写。坑三左连接后没意识到新产生了大量 NULL左连接本来就是允许右表缺失的。但很多人后面继续拿右表字段做过滤或分组结果不知不觉把口径改掉了。坑四NULL 只在查询时处理入库层一直混乱如果某类字段长期同时存在 NULL 和空串说明上游治理本身就不稳。只靠查询层修补后面还会反复出问题。我自己更倾向的处理方式先把口径写在 SQL 里不要放在脑子里selectcasewhenchannel_codeisnullthenUNKNOWNwhenchannel_codethenEMPTYelsechannel_codeendaschannel_tag,count(*)ascust_cntfromdwd_customergroupbycasewhenchannel_codeisnullthenUNKNOWNwhenchannel_codethenEMPTYelsechannel_codeend;对关键字段定期做空值分布检查selectchannel_codeascol_name,sum(casewhenchannel_codeisnullthen1else0end)asnull_cnt,sum(casewhenchannel_codethen1else0end)asempty_cntfromdwd_customer;对下游口径影响大的列尽量在明细层先标准化如果业务已经明确 NULL 要转成某个业务标签我个人更倾向于在明细层或主题层先固化不要让每个下游 SQL 各写各的。一个简单的批检查脚本示意#!/bin/bashDBHOST192.0.2.71DBPORT5258DBNAMEdw_userDBUSERqa_userLOGDIR/data/gbase/log/null_checkDAYSTR$(date%F)mkdir-p${LOGDIR}gccli-h${DBHOST}-P${DBPORT}-u${DBUSER}${DBNAME}SQL${LOGDIR}/null_check_${DAYSTR}.log21select 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

更多文章