GBase 8a
其他
文章
精选

GBase 8a 字符集、排序规则和字符串比较结果偏差

发表于2026-04-07 11:23:55164次浏览3个评论

GBase 8a 字符集、排序规则和字符串比较结果偏差

我最近看资料和整理现场问题时,越来越觉得 GBase 8a 里很多“查出来不对”的问题,并不是表没导对,也不是 SQL 逻辑写错了,而是字符集、排序规则、大小写处理和字符串比较语义没有统一。 真正落到现场时,这类问题经常表现得很隐蔽:同一条 SQL 在测试环境和生产环境结果不一致,join 能跑但匹配行数偏少,按业务键去重时发现还有重复,group by 看着正常但汇总结果总对不上,甚至连 where 条件里一个很普通的字符串过滤都会出现“明明有数据却查不到”的情况。

我自己理解下来,这类问题最容易被归到“数据质量不行”或者“应用写入不规范”上,但从排查顺序看,如果库里同时存在不同来源、不同编码、不同大小写习惯的数据,而对象定义和 SQL 习惯又不够统一,GBase 8a 最后暴露出来的就不只是显示乱码,更多是匹配结果偏差、聚合口径漂移、去重判断失真和下游报表不稳定

这条线和常见的慢 SQL、大表查询、分布键、导数吞吐并不是一回事。我最近整理下来觉得,它更接近 SQL 行为差异和对象治理问题:平时不一定报错,但一旦进入宽表、主题汇总、跨系统对接这些场景,影响会持续放大。

现场里常见的几个现象

我自己排查过几类比较典型的情况,表面都不像字符集和比较规则问题,但往回追时又常常能落到这里。

  1. 两张表业务主键看起来一样,join 后匹配率却明显偏低。
  2. group by user_code 后分组数量偏多,人工看又像是同一个值。
  3. where shop_name = 'Beijing_01' 在测试能查到,生产查不到。
  4. 同一个客户号既有大写版本又有小写版本,下游去重后仍然重复。
  5. 从不同系统导入的文本字段肉眼一致,但比较时就是不相等。
  6. 前端报表筛选结果忽多忽少,最后发现是末尾空格或不可见字符造成的。

这些现象有一个共同点:问题不在“字符串能不能存进去”,而在“字符串被怎么比较、怎么分组、怎么关联”。

为什么这类问题在 GBase 8a 里容易被忽略

我自己更关注的是现场处理习惯。 很多团队会把字符集和排序规则当成安装时一次性选项,后面只要不出现乱码,就默认没有问题。但真正进入分析型场景后,字符串字段承担的职责通常比大家想得更重:

  • 作为业务主键参与关联;
  • 作为维度值参与分组;
  • 作为筛选项参与 where 条件;
  • 作为口径字段参与去重、排重、归一。

只要这些字段来源不一致,问题就不再只是显示问题,而是执行结果问题。

我最近整理下来比较认同的一个判断是: 在 GBase 8a 里,字符串相关问题最危险的地方不是报错,而是不报错但结果悄悄偏了。

我实际排查时一般先拆哪几层

真正落到现场时,我一般不会一开始就去改表结构,也不会先怀疑导数工具。我自己更倾向于先把问题拆成下面三层。

第一层:值本身到底一不一样

也就是先确认这个字段肉眼相同,到底是不是字节、空格、大小写、隐藏字符层面有差异。 这一层如果没确认,后面很多 SQL 讨论都容易跑偏。

select
    cust_code,
    length(cust_code) as len1,
    hex(cust_code) as hex1
from dwd_trade_order
where cust_code like 'AB12%';

如果两条看起来一样的值,length()hex() 明显不同,方向就已经很清楚了。

第二层:比较规则到底是什么

值本身一样,不代表比较行为一样。 有的现场问题不是脏数据,而是库、表、字段或 SQL 表达式落到不同排序规则后,大小写、空格、重音字符、全半角等处理方式不一致。

这一层我通常会先把表定义拉出来看:

show create table dim_customer;
show create table ods_customer_src;

再结合字段定义确认重点列的字符类型和长度定义,尤其是:

  • 是否混用了 charvarchar
  • 是否存在不同字符集字段直接比较
  • 是否在表达式里做了显式或隐式转换
  • 是否有 trim、upper、lower 这类函数参与比较

第三层:业务逻辑有没有把脏值放大

很多现场不是一开始就坏,而是在汇总和建模环节被放大。 比如原始层只是偶发大小写不统一,到了主题层拿它做主键关联、做分组、做去重,问题就会越来越明显。

这一步我一般会把出问题 SQL 拆成最小版本来跑,确认偏差到底出在过滤、关联还是聚合阶段。

一个比较接近现场的例子

我自己把一个常见场景做了下简化。 某零售业务从两套系统同步门店信息,一套是会员系统,一套是交易系统,最后在 GBase 8a 里做统一分析。两边都有门店编码,但写法不完全一致。

create table ods_store_member (
    store_code varchar(20),
    store_name varchar(100),
    city_name  varchar(50)
);

create table ods_store_trade (
    store_code varchar(20),
    trade_dt   date,
    sale_amt   decimal(18,2)
);

导入后表面看字段都正常,但实际数据像这样:

来源 store_code 示例 肉眼观感 实际风险
会员系统 BJ001 正常 大写
交易系统 bj001 正常 小写
交易系统 BJ001 正常 末尾带空格
外部文件 BJ001  正常 全角空格
某脚本修复后 BJ001 正常 前导空格

这时如果直接关联:

select
    a.store_code,
    a.store_name,
    b.trade_dt,
    b.sale_amt
from ods_store_member a
join ods_store_trade b
  on a.store_code = b.store_code;

很多人第一反应是“是不是有丢数”“是不是 join 条件不全”,但我自己更关注的是先把参与比较的值标准化后再看结果。

比如先验证差异:

select
    store_code,
    length(store_code) as code_len,
    hex(store_code) as code_hex
from ods_store_trade
where store_code like '%BJ001%';

只要把这一步做出来,通常就能很快分清到底是大小写问题、空格问题还是编码问题。

这类偏差最容易出现在哪几种 SQL 里

我最近整理下来觉得,有四类 SQL 特别容易把字符串比较问题放大。

1. 关联 SQL

只要字符串字段参与 join,比较行为就直接影响匹配率。 这类问题最常见于客户号、门店号、渠道编码、设备编号这类业务键。

2. 去重 SQL

如果字符串没有统一标准,distinctgroup by 的结果就不一定等于业务理解里的“同一个值”。

3. 条件过滤 SQL

where code = 'xxx' 这种最常见,也最容易让人误判成“数据没进来”。

4. 主题汇总 SQL

前面几层的小偏差,到了宽表、汇总表、报表层会被不断累积。 最后业务只看到结果不稳定,却很难第一时间意识到根源在字符串比较行为。

SQL 类型 现场常见表现 我优先检查的点
join 匹配率偏低 大小写、空格、隐藏字符
distinct 去重后仍有重复 标准化前后结果差异
where 明明有值却查不到 比较表达式、trim/upper 处理
group by 分组数量异常 原始值是否存在多种写法

我自己更倾向的排查顺序

从处理顺序看,我一般会先做三组对照,而不是直接改业务 SQL。

对照一:原始值与标准化值对比

select
    store_code as raw_code,
    trim(store_code) as trim_code,
    upper(trim(store_code)) as norm_code,
    length(store_code) as raw_len,
    length(trim(store_code)) as trim_len
from ods_store_trade
where store_code like '%001%'
limit 20;

这个对照最大的价值在于: 能快速看出问题到底能不能被 trim + upper 这类标准化动作明显改善。

对照二:标准化前后的关联命中率对比

-- 原始关联
select count(*) as raw_join_cnt
from ods_store_member a
join ods_store_trade b
  on a.store_code = b.store_code;

-- 标准化后关联
select count(*) as norm_join_cnt
from ods_store_member a
join ods_store_trade b
  on upper(trim(a.store_code)) = upper(trim(b.store_code));

如果两边命中率差异很大,问题基本就已经落到字符串处理语义上了。

对照三:标准化前后的分组结果对比

-- 原始分组
select store_code, count(*) as cnt
from ods_store_trade
group by store_code
order by cnt desc
limit 20;

-- 标准化后分组
select upper(trim(store_code)) as norm_code, count(*) as cnt
from ods_store_trade
group by upper(trim(store_code))
order by cnt desc
limit 20;

我自己实际排查时一般先看这组三对照。 因为只要标准化前后差异足够明显,后面是修 SQL、修数据还是补治理规则,方向就会清楚很多。

常见误区比我最初想的还多

误区一:只要不乱码就说明字符集没问题

这是我最常见到的误判。 不乱码只说明“能显示”,并不说明“比较结果一定正确”。

误区二:用 varchar 就天然安全

varchar 只是可变长度,不代表数据内容就规整。 前后空格、大小写、全半角、不可见字符仍然会带来比较偏差。

误区三:业务键是文本也无所谓

在分析型场景里,文本业务键很常见,但只要承担关联职责,就不能把它当普通说明字段对待。

误区四:在 SQL 里临时 trim 一下就彻底解决了

临时标准化可以救急,但不能替代源头治理。 如果所有 SQL 都靠运行时去 trim/upper,后续维护成本会越来越高,而且不同人写法不一致,口径也可能继续漂。

常见误区 现场后果 我个人更建议的做法
不乱码就当没问题 结果静默偏差 同时检查显示和比较行为
所有文本键直接 join 命中率偏低 关键键先做标准化
全靠查询时修正 规则不统一 在入库或汇总层固化规则
发现重复就直接删 误删有效值 先核对标准化映射关系

真正落地时,我更倾向于这么处理

1. 先确定哪些字段必须“强标准化”

不是所有字符串字段都要高强度治理。 我自己更关注的是这些列:

  • 业务主键;
  • 维度编码;
  • 常用筛选值;
  • 会参与 join / group by / distinct 的列;
  • 会作为下游接口输出键值的列。

这些字段如果不统一,后面的问题基本会反复出现。

2. 在入库层保留原值,在主题层生成标准化值

我个人更倾向于不要直接覆盖原始字段,而是同时保留原值和标准化值。这样做的好处是:

  • 出问题时还能追源;
  • 业务可以复核原始数据;
  • 主题层查询可以统一走标准化列。

示意写法可以像这样:

create table dwd_store_trade as
select
    store_code as store_code_raw,
    upper(trim(store_code)) as store_code_norm,
    trade_dt,
    sale_amt
from ods_store_trade;

对应维表也做同样处理:

create table dwd_store_member as
select
    store_code as store_code_raw,
    upper(trim(store_code)) as store_code_norm,
    store_name,
    city_name
from ods_store_member;

后续关联尽量统一使用标准化列:

select
    a.store_code_norm,
    a.store_name,
    b.trade_dt,
    sum(b.sale_amt) as sale_amt
from dwd_store_member a
join dwd_store_trade b
  on a.store_code_norm = b.store_code_norm
group by a.store_code_norm, a.store_name, b.trade_dt;

3. 把异常值做成常规检查项

我最近整理下来觉得,这类问题不能只靠事故后排查。 真正稳一点的做法,是把异常值检查纳入日常巡检或装载校验。

比如:

-- 检查前后空格
select count(*) as cnt_blank_edge
from ods_store_trade
where store_code <> trim(store_code);

-- 检查大小写混用
select
    sum(case when store_code = upper(store_code) then 1 else 0 end) as upper_cnt,
    sum(case when store_code = lower(store_code) then 1 else 0 end) as lower_cnt
from ods_store_trade;

-- 检查标准化后发生合并的编码
select
    upper(trim(store_code)) as norm_code,
    count(distinct store_code) as raw_variant_cnt
from ods_store_trade
group by upper(trim(store_code))
having count(distinct store_code) > 1;

这些 SQL 不复杂,但我自己更看重它们能不能提前暴露风险,而不是等报表口径出问题后再追。

一些更容易被忽略的边角问题

char 字段的补齐行为

如果某些老表用了 char 类型,末尾补齐和比较语义可能会让现场判断更复杂。 我自己排查时,一旦碰到字符串业务键定义成 char,会优先确认它是不是历史遗留设计。

手工补数带来的不可见字符

很多文本问题不是系统自动产生的,而是手工补数、Excel 中转、脚本拼接时带进来的。 特别是全角空格、制表符、换行符这类,肉眼很难第一时间发现。

统一标准后对下游的影响

标准化并不只是“把值改对”。 如果下游报表、接口、缓存、标签表也依赖这些字段,改动前最好先确认影响范围。 我自己更倾向于先新增标准化列,再逐步替换,而不是直接把原字段全部改掉。

一个更稳一点的治理思路

从落地角度看,我最近更倾向于把字符串治理分成三层:

层次 主要目标 处理方式
原始层 保留原值、便于追溯 不轻易覆盖原字段
明细层 构造可比较的标准化键 trim/upper 等规则固化
主题层 统一口径、减少重复逻辑 关联和分组统一走标准化列

再往下细一点,我自己更关注下面这几条:

关注点 我通常怎么做 原因
关键业务键 强制生成 norm 列 统一 join 和 group by 口径
说明类文本 保留原值即可 不必过度治理
异常值监控 每日校验 提前发现风险
SQL 规范 明确标准化写法 避免每个人各写各的

Shell 层面也最好留一点检查动作

如果数据是批量装载进来的,我自己更倾向于在装载后补一轮轻量校验,而不是只看行数和成功状态。

#!/bin/bash

DBHOST=192.0.2.31
DBPORT=5258
DBNAME=dw_retail
DBUSER=etl_user
LOGDIR=/data/gbase/log/string_check
DAYSTR=$(date +%F)

mkdir -p "${LOGDIR}"

gccli -h ${DBHOST} -P ${DBPORT} -u ${DBUSER} ${DBNAME} <<'SQL' >> "${LOGDIR}/string_check_${DAYSTR}.log" 2>&1
select now();

select count(*) as cnt_blank_edge
from ods_store_trade
where store_code <> trim(store_code);

select upper(trim(store_code)) as norm_code, count(distinct store_code) as raw_variant_cnt
from ods_store_trade
group by upper(trim(store_code))
having count(distinct store_code) > 1
limit 50;

select now();
SQL

这类脚本的价值不在复杂,而在于让问题尽量提前暴露。 我自己更关注的是:只要某个关键业务键已经出现多版本写法,就应该尽快把治理动作往前提,而不是继续让下游查询各自兜底。

实战里我更在意的几条建议

先救结果,再治源头

如果线上报表已经受到影响,我通常会先在汇总层用标准化列把结果兜住,再去推动源头修正。 因为真正到现场时,业务首先关心的是口径能不能先稳定下来。

关键键不要只留一个原始文本列

只留原始值,后面所有查询都得自己处理大小写、空格和异常字符,维护成本会越来越高。 我个人更倾向于关键文本键统一保留“原值列 + 标准化列”。

不要把所有修正逻辑都塞进报表 SQL

报表 SQL 应该尽量消费已经治理过的数据,而不是自己承担字符串清洗职责。 否则不同报表、不同开发人员很容易写出不同规则,最后口径又散掉。

用最小对照 SQL 固化排查经验

每次遇到这类问题,我都建议把最有用的几条检查 SQL 留下来。 比如长度检查、十六进制检查、标准化前后关联对照、标准化前后分组对照。这些东西下次再遇到类似故障时会非常省时间。

结尾

我最近回头看 GBase 8a 里这类问题时,一个很明显的感受是: 字符串相关故障最难受的地方,不是它多复杂,而是它太像“看起来没事”。

数据能进、SQL 能跑、页面也不报错,但结果就是慢慢偏掉。 从处理顺序看,我自己更关注的是先确认值本身有没有差异,再确认比较规则,再看这些差异是不是在关联、去重和汇总环节被放大。这样排查虽然细,但通常能比“凭感觉改 SQL”更快把问题收住。

如果后面还继续写 GBase 8a 这条线,我个人会一直优先关注这种“现场不一定炸,但结果容易歪”的问题。因为它们比显性报错更难发现,也更值得提前治理。

参考资料

[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/post/2018

评论

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