一、问题背景
在一次测试中,发现下面这条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
c1 | c2 | c3 | c4 | c5 | c6 |
---|---|---|---|---|---|
0 | 1 | 2 | 2 | 4 | 4 |
从这组数据中发现一个规律,当小数尾数为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;
castsource | casttarget | castfunc | castcontext | castmethod | castowner |
---|---|---|---|---|---|
701 | 20 | 483 | i | f |
查到了是有的,这里castmethod为f,表示这个转换是使用某个函数进行转换的,一般是二进制存储结构有变更才会需要使用到函数。然后castfunc即为这个转换函数的oid,接下来我们去查这是用的哪个函数
select proname,prosrc from pg_proc where oid=483;
proname | prosrc |
---|---|
int8 | dtoi8 |
可以看到使用的是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进行测试
declare
x binary_integer;
begin
select 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中测试
declare
x integer;
begin
select 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的权威性陡然下降。
为了避免这种问题,建议无论在用什么数据库时,对于类型转换,还是慎重一些为好
点击阅读原文跳转作者文章