openGauss/POSTGRESQL 中float类型到int类型的隐式转换

文摘   2024-11-18 17:30   广东  

一、问题背景

在一次测试中,发现下面这条sql在oracle和POSTGRESQL/OPENGAUSS中的查询结果不一致

select  cast(cast(0.5 as float) as integer);

在oracle中返回1,在openGauss中返回0,咋一看好像是openGauss中使用了截断的方式,但是如果执行

select  cast(cast(1.5 as float) as integer);

则会发现oracle和openGauss中同样返回了2,这个时候就会猜想是不是这个float类型在两个数据库中实现并不一样导致的,因为ORACLE的float是按number格式存储的,不是标准定义中的浮点类型,而是一种精确类型;而openGauss中的float符合标准定义的浮点类型,是一种非精确类型。
但我们并不能妄下这种判断,多测几组数据:

select  cast(cast(0.5 as float) as integer) c1 , cast(cast(0.6 as float) as integer) c2 , cast(cast(1.5 as float) as integer) c3, cast(cast(2.5 as float) as integer) c4, cast(cast(3.5 as float) as integer) c5,  cast(cast(4.5 as float) as integer) c6
c1c2c3c4c5c6
012244

从这组数据中发现一个规律,当小数尾数为5进行舍入时,前一位总是偶数,这自然联想到了另一种四舍五入算法,即银行家算法,对应ORACLE12C新增的一个函数round_ties_to_even。

openGauss数据库是开源的,我们完全可以通过查看源码来验证我们的猜想。

二、分析

接下来开始就是说明如何通过这种问题来找到对应的源码逻辑。

首先要知道一些openGauss的元数据基础知识。
openGauss的元数据基本继承postgresql,所以这个知识可以复用。
在postgresql中,几乎所有的类型、函数、操作符、规则等,都可以在元数据表中查到。对于类型转换,即cast,也有一张对应的表,即pg_cast,下面这个SQL就是查询从float8到int8是否存在直接的类型转换规则

select * from pg_cast h where h.castsource ='float8'::regtype and h.casttarget ='int8'::regtype;
castsourcecasttargetcastfunccastcontextcastmethodcastowner
70120483if

查到了是有的,这里castmethod为f,表示这个转换是使用某个函数进行转换的,一般是二进制存储结构有变更才会需要使用到函数。然后castfunc即为这个转换函数的oid,接下来我们去查这是用的哪个函数

select proname,prosrc from pg_proc where oid=483;
pronameprosrc
int8dtoi8

可以看到使用的是int8函数,然后int8的函数源码是dtoi8,此时我们就可以去openGauss源码中搜索dtoi8了,

找到了
.\openGauss-server\src\common\backend\utils\adt\int8.cpp

/* dtoi8() * Convert float8 to 8-byte integer. */Datum dtoi8(PG_FUNCTION_ARGS){    float8 num = PG_GETARG_FLOAT8(0);
/* * Get rid of any fractional part in the input. This is so we don't fail * on just-out-of-range values that would round into range. Note * assumption that rint() will pass through a NaN or Inf unchanged. */ num = rint(num);
/* * Range check. We must be careful here that the boundary values are * expressed exactly in the float domain. We expect PG_INT64_MIN to be an * exact power of 2, so it will be represented exactly; but PG_INT64_MAX * isn't, and might get rounded off, so avoid using it. */ if (num < (float8)PG_INT64_MIN || num >= -((float8)PG_INT64_MIN) || isnan(num)) { if (fcinfo->can_ignore && !isnan(num)) { ereport(WARNING, (errmsg("bigint out of range"))); PG_RETURN_INT64(num < (float8)PG_INT64_MIN ? LONG_MIN : LONG_MAX); } ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("bigint out of range"))); }
PG_RETURN_INT64((int64)num);}

这段代码注释加警告比实际逻辑还多,但真正要看的只有 num = rint(num);这一行,这里又引用到了rint函数,点进去

double rint(double x){    return (x >= 0.0) ? floor(x + 0.5) : ceil(x - 0.5);}

如果不是开发人员,对这种计算看不懂,可以借助AI

这段代码定义了一个名为 rint 的函数,用于将一个双精度浮点数 x 四舍五入到最接近的整数。具体来说:
如果 x 大于或等于 0.0,则返回 floor(x + 0.5),即将 x 加上 0.5 后向下取整。
如果 x 小于 0.0,则返回 ceil(x - 0.5),即将 x 减去 0.5 后向上取整。
这样可以实现对正数和负数的四舍五入。

这里是按照常规的算法进行四舍五入的,但结果竟然和银行家算法一样。

看到这里我一下犯了迷糊,理所当然的认为一定会走到这个rint,但后来根据openGauss社区专家的指点,实际上这里调用的是std::rint(float),c++内置的一个标准函数,而这个函数就是使用的银行家算法!

三、扩展

既然已经定位到了原理,于是我们回到ORACLE看看,不精确的浮点类型在ORACLE的表现是怎样的?其实ORACLE也有这种浮点类型,叫binary_float和binary_double,这两个类型不能在sql中使用,只能在plsql中使用,我们写个简单的例子在ORACLE进行测试

declarex binary_integer;beginselect  cast(0.5 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(0.6 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(1.5 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(2.5 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(3.5 as binary_double) into x from dual;dbms_output.put_line(x);end;

0
0
1
2
3

竟然出现了第三种结果,即全部都是按trunc

再把binary_integer改成integer,再在ORACLE中测试

declarex integer;beginselect  cast(0.5 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(0.6 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(1.5 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(2.5 as binary_double) into x from dual;dbms_output.put_line(x);select  cast(3.5 as binary_double) into x from dual;dbms_output.put_line(x);end;

.5
.59999999999999998
1.5
2.5
3.5

出现了第四种结果!ORACLE的integer竟然能显示出小数!

我们理解不同的数据类型可以有不同的表现行为,也知道oracle中的integer其实是按number类型进行的实现,但是这明晃晃的声明了integer类型竟然还能打印出小数,这对于应用开发者来说如何能够接受?
当然,我们可以认为这个用例的用法是不合理的,正常情况下小数转换成整数应该使用函数来处理,而不是通过类型转换。也可能之前从来没有人这么用过,所以ORACLE一直没发现存在这个问题。

于是回到最开始的问题上,openGauss/postgresql和ORACLE表现不一致,究竟谁是对的,或许我们永远都无法找到准确答案,这只能算是一种行为差异,并且都可以解释得通。但在看到ORACLE换个方式出现了第三种结果,甚至还出现了整数类型显示小数这种奇葩BUG时,ORACLE的权威性陡然下降。

为了避免这种问题,建议无论在用什么数据库时,对于类型转换,还是慎重一些为好

点击阅读原文跳转作者文章

openGauss
开源关系型数据库
 最新文章