GBase 8a 字符集、排序规则和字符串比较结果偏差
GBase 8a 字符集、排序规则和字符串比较结果偏差
我最近看资料和整理现场问题时,越来越觉得 GBase 8a 里很多“查出来不对”的问题,并不是表没导对,也不是 SQL 逻辑写错了,而是字符集、排序规则、大小写处理和字符串比较语义没有统一。
真正落到现场时,这类问题经常表现得很隐蔽:同一条 SQL 在测试环境和生产环境结果不一致,join 能跑但匹配行数偏少,按业务键去重时发现还有重复,group by 看着正常但汇总结果总对不上,甚至连 where 条件里一个很普通的字符串过滤都会出现“明明有数据却查不到”的情况。
我自己理解下来,这类问题最容易被归到“数据质量不行”或者“应用写入不规范”上,但从排查顺序看,如果库里同时存在不同来源、不同编码、不同大小写习惯的数据,而对象定义和 SQL 习惯又不够统一,GBase 8a 最后暴露出来的就不只是显示乱码,更多是匹配结果偏差、聚合口径漂移、去重判断失真和下游报表不稳定。
这条线和常见的慢 SQL、大表查询、分布键、导数吞吐并不是一回事。我最近整理下来觉得,它更接近 SQL 行为差异和对象治理问题:平时不一定报错,但一旦进入宽表、主题汇总、跨系统对接这些场景,影响会持续放大。
现场里常见的几个现象
我自己排查过几类比较典型的情况,表面都不像字符集和比较规则问题,但往回追时又常常能落到这里。
- 两张表业务主键看起来一样,
join后匹配率却明显偏低。 group by user_code后分组数量偏多,人工看又像是同一个值。where shop_name = 'Beijing_01'在测试能查到,生产查不到。- 同一个客户号既有大写版本又有小写版本,下游去重后仍然重复。
- 从不同系统导入的文本字段肉眼一致,但比较时就是不相等。
- 前端报表筛选结果忽多忽少,最后发现是末尾空格或不可见字符造成的。
这些现象有一个共同点:问题不在“字符串能不能存进去”,而在“字符串被怎么比较、怎么分组、怎么关联”。
为什么这类问题在 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;
再结合字段定义确认重点列的字符类型和长度定义,尤其是:
- 是否混用了
char和varchar - 是否存在不同字符集字段直接比较
- 是否在表达式里做了显式或隐式转换
- 是否有 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
如果字符串没有统一标准,distinct 和 group 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
评论
热门帖子
- 12025-12-01浏览数:183290
- 22023-05-09浏览数:26063
- 42023-09-25浏览数:19708
- 52020-05-11浏览数:18352