目 录CONTENT

文章目录

【GaussDB】真相比你想的更复杂--执行exp函数报错

DarkAthena
2026-09-01 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

【GaussDB】真相比你想的更复杂--执行exp函数报错

背景

客户有个正态分布计算的存储过程从ORACLE迁移到GaussDB 506.0.0SPC0500,运行时发现报错ERROR: value out of range: underflow,但相同的SQL和相同的数据在ORACLE没有报错。经过排查,发现是执行到exp函数时报的错。我大概搜了一下,了解到postgresql系的数据库exp函数入参的确是有个范围的,有人甚至利用这个报错来对应用进行SQL注入。我自己实际测试了一下,发现不报错的整数范围是 -745 到 709 ,大了小了都会报错。

ORACLE为什么不报错

为什么ORACLE不报错?我之前有分析过ORACLE的NUMBER类型存储算法(【ORACLE】详解oracle数据库UTL_RAW包各个函数的模拟算法),想着应该与这个存储算法有关系,就来推理一下。

ORACLE 的NUMBER类型最多38个有效数字 ,在数据字典dba_tab_cols里可以看到NUMBER类型的字段DATA_LENGTH为22,但ORACLE官方文档则说NUMBER是21个字节。

正数范围 1.0*power(10, −130)9.99...9*power(10,125) (38个9)
负数范围 -9.99...9×power(10,125) (38个9) 到 -1.0×power(10, −130)

对于power函数,当第一个参数为10时:
第二个参数大于125会报错;
第二个参数小于-130会返回0

这个边界和ORACLE官方文档一致。

对于exp函数,输入参数大于 290 时会报错,输入参数小于 -300 时会返回0。

从power函数和exp函数的测试结果来看,可以发现小数部分小到一定程度,就直接舍弃了;而正数部分超过一定程度会报错。

这是因为NUMBER类型的存储机制决定的:
最小正数的的NUMBER十六进制存储为8002,两个字节,
80是指数位,要减193,得-65,
02减1,得1,
则为 1*100^-65 ,即 1E-130

80则是表示0,所以大于0的最小正数就是8002了(不存在8001,因为那会和80算出来的结果一样)

而最大的一个有效数字的整数情况是FF5B,第一个字节最大FF,减193后,指数就为62了,5B减1的十进制是90, 即90*100^62 ,即 9E+125。(更大的FF651E126,已经无法使用了)

如果整数的38位有效数字填满情况,则接近于 power(10,125)*9.9999999999999999999999999999999999999 ,存储为 FF64646464646464646464646464646464646464
64减1就是十进制的99了,38个9,占了19个字节,算上前面的指数位FF,一共20个字节,但此时这个存储的值已经无法正常显示了,utl_raw.cast_to_number会变成1E126,再to_char会返回~,

如果再多一个9 (39个9),就存储为FF646464646464646464646464646464646464645B ,最后的5B就是90,此时占到了21个字节,已经逼近了NUMBER的22字节上限,如果再加一个9 (40个9),此时其实还是21个字节,但其实已经溢出。(其实手动解析是能解到最多40个有效数字的,但ORACLE做了限制,38个9和39个9和40个9都是1E126,不过并不相等,42个9已经超了范围,和40个9是相等的)

SQL> select 1 as r from dual where
  2  utl_raw.cast_to_number('FF64646464646464646464646464646464646464') --38个9
  3  =
  4  utl_raw.cast_to_number('FF6464646464646464646464646464646464646464'); --40个9

         R
----------


SQL> select 1 as r from dual where
  2  utl_raw.cast_to_number('FF646464646464646464646464646464646464646464') --42个9
  3  =
  4  utl_raw.cast_to_number('FF6464646464646464646464646464646464646464'); --40个9

         R
----------
         1

SQL> select
  2  utl_raw.cast_to_number('FF64646464646464646464646464646464646464') as c1,--38个9
  3  utl_raw.cast_to_number('FF646464646464646464646464646464646464645B') as c2,--39个9
  4  utl_raw.cast_to_number('FF6464646464646464646464646464646464646464') as c3,--40个9
  5  utl_raw.cast_to_number('FF646464646464646464646464646464646464646464') as c4 --42个9
  6  from dual;

        C1         C2         C3         C4
---------- ---------- ---------- ----------
     1E126      1E126      1E126      1E126

SQL> 
SQL> select
  2  to_char(utl_raw.cast_to_number('FF64646464646464646464646464646464646464')) as c1, --38个9
  3  to_char(utl_raw.cast_to_number('FF646464646464646464646464646464646464645B')) as c2, --39个9
  4  to_char(utl_raw.cast_to_number('FF6464646464646464646464646464646464646464')) as c3, --40个9
  5  to_char(utl_raw.cast_to_number('FF646464646464646464646464646464646464646464')) as c4 --42个9
  6  from dual;

C1   C2   C3   C4
---- ---- ---- ----
~    ~    ~    ~

ORACLE官方文档有说

Oracle stores values of the NUMBER datatype in a variable-length format. The first byte is the exponent and is followed by 1 to 20 mantissa bytes.
Oracle 以变长格式存储 NUMBER 数据类型的值。第一个字节为指数,其后跟随 1 至 20 个尾数字节。

Up to 20 data bytes can represent the mantissa. However, only 19 are guaranteed to be accurate.
最多 20 个数据字节可表示尾数。但只有 19 个字节保证精确。

即,数据字典22字节,官方文档21字节,表示数字的20字节,其中实际19字节保证精确。计算时(不限于),小数小到一定程度则忽略,而大数大到一定程度则报错。

PG为什么"报错"

回头看看为什么PG系会报错。我先在GaussDB上测试

power(10,-324) 报错
power(10,-323) = 9.88131291682493088e-324
power(10,308) = 9.99999999999999909e-309
power(10,309) 报错

power函数第一个参数为10时:
第二个参数的整数范围为-323 到 308 ,大了小了都会报错

让AI捞了下PG的源码,说是PG直接用的标准C库里的pow函数,IEEE 754标准的确就有这个限制。

而对于exp函数的限制,也是设计如此,在openGauss的测试用例集里已经包含了exp的边界测试:
https://gitcode.com/opengauss/Yat/blob/6.0.2/openGaussBase/testcase/SQL/INNERFUNC/numeric/Opengauss_Function_Innerfunc_Exp_Case0006.sql

-- @testpoint: exp函数入参范围校验,合理报错
select exp(709);
select exp(710);
select exp(-745);
select exp(-746);

不过这个时候我注意到了,AI给出来的源码引用并不是numeric类型的,而是float.c里的。而且我在numeric的power函数里,并没有看到使用标准的pow函数,于是乎我做了个测试select power(10,-324::numeric) 并没有报错,numeric会不会有新的边界呢?

我又分别针对numeric的power和exp函数做了下测试

  • power
  power(10,131072::numeric) 报错
  power(10,131071::numeric) 131072位数
  power(10,-16::numeric) = 0.0000000000000001
  power(10,-17::numeric) = 0
  power(10,-1000000::numeric) 0
  --numeric的有效范围内,继续小都一直返回0了
 
  • exp
  exp(6000::numeric) 报错
  exp(5999::numeric) 不报错,2606位精确数
  exp(-2303::numeric) = 1E-1000
  exp(-2304::numeric) = 0
  exp(-5999::numeric) = 0
  exp(-6000::numeric)  报错

很明显的不对劲,power第二个参数小那么多都不报错,而exp在-6000时就报错了。

以上都是在GaussDB上进行的测试,然而当我把相同的用例搬到多个PG版本上时( https://dbfiddle.uk/XSOhWTce ),测试结果又发生了变化:

从pg9.6起 ,exp(-6000::numeric) 不报错了,让AI去查了下

Commit: 7d9a4737c268f61fb8800957631f12d3f13be218
Author: Dean Rasheed
Date: 2015-11-14
Message: Improve type numeric's calculations for ln(), log(), exp(), pow().

这个 commit 彻底重写了 exp_var() 的实现逻辑:

In exp_var(), get rid entirely of the logic that separated the calculation into integer and fractional parts: that was neither accurate nor particularly fast. The existing range-reduction method of dividing by 2^n can be applied across the full input range instead of only 0..1, as long as we are careful to set an appropriate rscale for each step.

旧实现把参数拆成整数部分和小数部分分别计算,对大负数会溢出报错。新实现改用统一的 range-reduction(除以 2^n)方式处理整个输入范围,exp(-6000) 这种情况就不会再报错了。

影响版本:

  • 首次发布于 PostgreSQL 9.6(2016-09-29)
  • 没有回移到 9.5 及更早版本。Tom Lane 原话:"This is arguably a bug fix, but in view of the lack of field complaints, it does not seem worth the risk of back-patching."

当年没有"complaints",现在有了,在GaussDB上。

也就是说,GaussDB上exp函数报错,可以认为的确是个BUG。

另外,虽然说PG9.5和GaussDB 都报错,但是报错不一样

  • PG9.5是ERROR: argument for function "exp" too big
  • GaussDB是ERROR: value overflows numeric format

所以我又让AI去对比了一下PG和openGauss的源码


三家 exp_var() 核心溢出检查对比

PG 9.5 — 旧算法(拆整数/小数)

// 把 |x| 的整数部分逐位累加到 xintval
while (x.weight >= 0)
{
   xintval *= NBASE;
   if (x.ndigits > 0)
       xintval += x.digits[0];
   x.digits++;
   x.ndigits--;
   x.weight--;

   if (xintval >= NUMERIC_MAX_RESULT_SCALE * 3)   // ← 只检查正方向
       ereport(ERROR,
               (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
                errmsg("argument for function \"exp\" too big")));
}
  • 取绝对值后逐位提取整数部分,一旦超限就报错
  • 不区分正负exp(-6000) 取绝对值后 xintval=6000,超限 → 报错
  • 错误信息是 exp 专用的:argument for function "exp" too big

PG 9.6 — 新算法(range-reduction + Taylor)✅ 有 underflow 处理

val = numericvar_to_double_no_overflow(&x);

/* Guard against overflow/underflow */
if (Abs(val) >= NUMERIC_MAX_RESULT_SCALE * 3)
{
   if (val > 0)                                    // 正数 → 溢出报错
       ereport(ERROR,
               (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
                errmsg("value overflows numeric format")));
   zero_var(result);                               // 负数 → 返回 0
   result->dscale = rscale;
   return;
}
  • 先转 double 估算范围
  • 区分正负val > 0 报溢出,val < 0 直接 zero_var(result) 返回 0
  • exp(-6000) → val ≈ -6000,走 zero_var返回 0,不报错

openGauss — 基于 9.6 算法 ❌ 缺少 underflow 处理

val = numericvar_to_double_no_overflow(&x);

/* Guard against overflow */                        // ← 注释只写了 overflow
if (Abs(val) >= NUMERIC_MAX_RESULT_SCALE * 3)
   ereport(ERROR,                                  // ← 无差别报错
           (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
            errmsg("value overflows numeric format")));
  • 算法框架和 PG 9.6 一样(range-reduction + Taylor)
  • 但漏掉了 val <= 0 的分支处理,不管正负一律报错
  • exp(-6000) → val ≈ -6000,直接走 ereport → 报错 value overflows numeric format

一图总结

输入: exp(-6000::numeric)

PG 9.5:  旧算法,xintval=6000 超限
        → ERROR: argument for function "exp" too big

PG 9.6:  新算法,val≈-6000 < 0,走 zero_var()
        → 返回 0  ✅

openGauss: 新算法,val≈-6000,但无 val>0/val≤0 分支
          → ERROR: value overflows numeric format

根因: openGauss 从 PG 9.6 搬代码时,只搬了溢出检查的"壳"(Abs(val) >= 阈值 → 报错),没搬 PG 9.6 里区分 overflow/underflow 的分支逻辑(if (val > 0) ... else zero_var())。看起来是合入时的遗漏。

是不是感觉已经真相了?不,基于我理性的判断,openGauss不可能在搬代码的时候漏了这个逻辑。我猜测它搬的时候,原生PG里的代码就长这样,于是继续让AI查


时间线

时间事件exp_var() 溢出检查
2015-11-147d9a4737c2 — 重写 exp_var,进入 PG 9.6 开发分支if (Abs(val) >= 阈值) 报错 "value overflows numeric format"不区分正负
2016-09-29PG 9.6.0 发布同上,没有 underflow 处理
2021-07-315cf350ce02 — backport 到 REL9_6_STABLE加上 if (val > 0) 分支,负数走 zero_var() 返回 0
2021-11-11PG 9.6.24 发布(含上述 backport)exp(-6000) → 返回 0 ✅
openGauss fork基于 PG 9.6 代码(2021 年 backport 之前)和 9.6.0 一样,不区分正负

关键 diff(5cf350ce02,REL9_6_STABLE 的 backport)

-	/* Guard against overflow */
+	/* Guard against overflow/underflow */
	if (Abs(val) >= NUMERIC_MAX_RESULT_SCALE * 3)
-		ereport(ERROR,
-				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
-				 errmsg("value overflows numeric format")));
+	{
+		if (val > 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("value overflows numeric format")));
+		zero_var(result);
+		result->dscale = rscale;
+		return;
+	}

结论

openGauss 的代码没有遗漏,它就是 PG 9.6.0 的原版逻辑。

underflow → 0 的处理是 2021 年才 backport 到 REL9_6_STABLE 的(9.6.24),openGauss fork 的时间点在这个 backport 之前,所以它的 exp_var() 和 PG 9.6.0 完全一致——Abs(val) 超限就无差别报错。

三方代码实际对应的是:

PG 9.5       → 旧算法,专用报错 "argument for function \"exp\" too big"
PG 9.6.0     → 新算法,通用报错 "value overflows numeric format"  ← openGauss 就是这个
PG 9.6.24+   → 新算法 + underflow 分支,负数返回 0

看了下openGauss的这段代码,从首次开源起就没改过,时间点是在 2020-06-30 ,刚好在2015-11-14(7d9a4737c2) 到 2021-07-31(5cf350ce02) 之间,即使用了新算法,但是没特殊处理小于0的分支。但众所周知,openGauss的PG内核版本锁在了9.2.4,但开源时的这段代码却长得和9.6.0一样。

  lishifu   6 years ago (June 30th, 2020 5:38 PM) 

first commit for openGauss server

 815a977         |        |    Explain   |      |   

+     /* Guard against overflow */
Changes added in  815a977   |   


所以我直接去找了PG9.2.4的代码

https://github.com/postgres/postgres/blob/REL9_2_4/src/backend/utils/adt/numeric.c

PG9.2.4的确没有合入来自PG9.6.0的这个新算法,所以openGauss虽然是基于9.2.4版本,但是首次开源的时候,就已经包含了一些新版PG的代码了。

到此,真相终于大白。

总结

ORACLE 和最新的PG,在exp的入参是个绝对值很大的负NUMBER/NUMERIC值时(-6000),都不会报错;而PG9.6.24之前(不含)和openGauss/GaussDB会报错。

追寻真相的过程中,AI的确帮我节省了不少时间,但是一开始AI生成的看上去很靠谱的分析报告,却并不是真相。AI的幻觉问题,后续通过大模型能力提升肯定是可以减轻的。但是当下阶段使用AI,最好还是保持清醒的认知,不要让AI做凌驾于你认知之上的事,哪天被AI忽悠瘸了还不知道;如果要这么做,也要有确保可信的手段去核实真假。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论