ClickHouse 求和后 Decimal 为何变 Decimal128?
对一个 Decimal(18,8) 列执行 sum,拿出来的结果类型却变成了 Decimal(38,8)。
如果你用的是 clickhouse-go 老版本,紧接着就会看到一行报错:Decimal128 is not supported。
这事我自己踩过。
当时查的是库存金额,建表时字段明明写的 Decimal(18,8),聚合一下程序就崩了,盯着错误日志愣了好一会儿。
先说结论
ClickHouse 对 Decimal 列做 sum 聚合时,会把结果的存储位宽放大一档,小数位数 S 保持不变。
Decimal(18,8) 底层属于 Decimal64。
求和后结果变成 Decimal(38,8),也就是 Decimal128。
clickhouse-go 在 v2 之前不支持 Decimal128,所以驱动层直接抛出 Decimal128 is not supported。
Decimal 的 P 和 S 到底是什么
先把两个参数掰扯清楚,不然后面容易绕晕。
Decimal(P, S) 里,P 是 precision,指整个数字的总位数。
S 是 scale,指小数部分的位数。
ClickHouse 根据 P 的大小决定底层用哪种实现:
| P 范围 | 底层类型 | 数值范围 |
|---|---|---|
| 1 – 9 | Decimal32 | (-10^(9-S), 10^(9-S)) |
| 10 – 18 | Decimal64 | (-10^(18-S), 10^(18-S)) |
| 19 – 38 | Decimal128 | (-10^(38-S), 10^(38-S)) |
所以 Decimal(18,8) 是 Decimal64,Decimal(38,8) 是 Decimal128。
P 一旦跨进 19–38 这一档,存储就升级成了 128 位整数。
补充一句,原始数值范围数据来自 ClickHouse 官方文档的 Decimal 章节。
为什么 sum 之后会被放大
核心原因是防溢出。
假设一列有上千万行 Decimal(18,8) 的金额,每一行都顶着 18 位的上限,全加起来很容易超过 18 位能表示的范围。
ClickHouse 为了不让累加结果溢出,干脆在聚合时把结果位宽提到最大档 Decimal128(P=38),小数位 S 不动。
这就是 Decimal(18,8) 求和后变成 Decimal(38,8) 的来由。
这里要纠正一个常见的误传:sum 是对单列多行的聚合,不是"两个列相加"。
两者的精度规则不一样,别混为一谈。
顺带说下两列做二进制运算的规则
如果是两个 Decimal 列之间做加减乘除(不是 sum),规则是另一套,由 ClickHouse 官方文档明确给出:
- 加法、减法:
S = max(S1, S2) - 乘法:
S = S1 + S2 - 除法:
S = S1
结果的底层位宽取两个操作数里更宽的那个。
我得承认,sum 聚合放大到 Decimal128 这个行为,官方 Decimal 文档页里并没有逐字写明 P 的具体公式,我是结合实际查询结果和位宽规则推出来的。
不同 ClickHouse 版本表现可能略有差异,建议你在自己的环境用 toTypeName() 验证一下。
怎么解决
思路很直接:既然驱动不认 Decimal128,那就在 SQL 里把结果转回 Decimal64。
用 toDecimal64 把求和结果重新收回到 18 位以内即可。
-- amount 列类型为 Decimal(18,8),直接 sum 会得到 Decimal(38,8)
-- clickhouse-go v1 不支持 Decimal128,这一句会报错
select sum(amount) as amount from stock;
-- 解决:把结果显式转回 Decimal64(8)
select toDecimal64(sum(amount), 8) as amount from stock;toDecimal64 的第二个参数 8 就是小数位数,要和原列对齐,不然小数会被截断。
要确认类型有没有改对,可以用 toTypeName 看一眼:
select toTypeName(toDecimal64(sum(amount), 8)) from stock;
-- 输出 Decimal(18, 8) 就对了转换前要注意溢出
toDecimal64 能装下的总位数是 18 位。
如果你的求和结果本身就超过了 18 位(比如金额体量特别大),强行转回去会溢出报错。
这种情况下,要么保留 Decimal128 并升级 clickhouse-go 到 v2,要么改用 toDecimal128 配合支持它的驱动。
别为了消掉一个报错,又埋下一个数据被截断的坑。
常见问题
为什么只有 sum 会触发,普通查询不会?
因为普通查询直接返回原列类型 Decimal(18,8),底层是 Decimal64,驱动认得。
只有聚合放大到 Decimal128 时才会撞上不支持的问题。
clickhouse-go 从哪个版本开始支持 Decimal128?
v2 版本加入了 Decimal128 支持(对应社区 PR #411)。
如果项目允许升级,直接上 v2 是更彻底的方案,不用每条聚合语句都套一层 toDecimal64。
toDecimal64 和 toDecimal128 怎么选?
求和结果在 18 位以内,用 toDecimal64 配老驱动就够了。
数据体量大、可能超过 18 位的,用 toDecimal128 并确保驱动支持,避免截断。
改成 toDecimal64 会丢精度吗?
只要第二个 scale 参数和原列一致(这里是 8),小数部分不会丢。
需要担心的是整数部分超过 18 位时的溢出,而不是精度。
写在最后
这个坑本质上是驱动能力和 ClickHouse 类型系统对不上导致的。
服务端为了安全放大了精度,老版本客户端却接不住。
短期用 toDecimal64 顶一下,长期还是建议把驱动升到 v2。
如果你对 ClickHouse 的 Decimal 精度规则还有疑问,或者遇到了其他类型转换的坑,欢迎在评论区一起聊聊。
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/clickhouse-decimal-sum-cast-decimal128/
备用原文链接: https://blog.fiveyoboy.com/articles/clickhouse-decimal-sum-cast-decimal128/