真实类型的虚幻特征,或者小心真实类型

出版后 文章 关于 PostgreSQL 中输入的功能,第一个评论是关于处理实数的困难。 我决定快速浏览一下可用的 SQL 查询的代码,看看它们使用 REAL 类型的频率。 事实证明,它的使用频率很高,而开发人员并不总是了解其背后的危险。 尽管事实上互联网上和 Habré 上有很多关于在计算机内存中存储实数的功能以及如何使用它们的好文章。 因此,在本文中,我将尝试将这些功能应用到 PostgreSQL 中,并尝试快速了解与它们相关的麻烦,以便 SQL 查询开发人员更容易避免它们。

PostgreSQL 文档简洁地指出:“此类错误的管理及其在计算过程中的传播是数学和计算机科学整个分支的主题,这里不予讨论”(同时明智地向读者推荐 IEEE 754 标准)。 这里指的是什么样的错误? 我们按顺序讨论一下,很快就会明白我为什么再次提笔了。

我们以一个简单的请求为例:

********* ЗАПРОС *********
SELECT 0.1::REAL;
**************************
float4
--------
    0.1
(1 строка)

结果,我们不会看到任何特别的东西——我们会得到预期的 0.1。 但现在让我们将其与 0.1 进行比较:

********* ЗАПРОС *********
SELECT 0.1::REAL = 0.1;
**************************
?column?
----------
f
(1 строка)

不平等! 多么奇迹啊! 但更进一步,更多。 有人会说,我知道 REAL 对分数的表现很糟糕,所以我会在那里输入整数,并且一切都会好起来的。 好的,让我们将数字 123 转换为 REAL:

********* ЗАПРОС *********
SELECT 123456789::REAL::INT;
**************************
   int4   
-----------
123456792
(1 строка)

结果又多了3个! 就是这样,数据库终于忘记怎么算了! 或者我们误解了什么? 让我们弄清楚一下。

首先,让我们记住材料。 如您所知,任何十进制数都可以扩展到十的幂。 因此,数字 123.456 将等于 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + 6*10-3。 但计算机以二进制形式运行数字,因此它们必须以 5.625 的幂展开的形式表示。 因此,二进制数 101.101 表示为 1,等于 22*0 + 21*1 + 20*1 + 2*1-0 + 2*2-1 + 2*3-1。 如果 2 的正幂总是给出整数十进制数(4、8、16、0.5、0.25 等),那么负数一切就更复杂了(0.125、0,0625、XNUMX、XNUMX 等)。 问题是 并非所有小数都可以表示为有限二进制分数。 因此,我们臭名昭著的 0.1 以二进制分数的形式显示为周期值 0.0(0011)。 因此,计算机内存中该数字的最终值将根据位深度而变化。

现在是时候记住实数是如何存储在计算机内存中的了。 一般来说,实数由三个主要部分组成——符号、尾数和指数。 符号可以是正号或负号,因此为其分配一位。 但尾数和指数的位数是由实数类型决定的。 所以,对于 REAL 类型,尾数的长度为 23 位(等于 1 的一位隐式添加到尾数的开头,结果为 24),指数为 8 位。 总共是 32 位,即 4 个字节。 对于 DOUBLE PRECISION 类型,尾数的长度为 52 位,指数为 11 位,总共 64 位,即 8 个字节。 PostgreSQL 不支持更高精度的浮点数。

让我们将十进制数 0.1 打包为 REAL 和 DOUBLE PRECISION 类型。 由于指数的符号和值相同,因此我们将重点关注尾数(我故意省略了存储指数值和零实数值的非明显特征,因为它们使理解复杂化并分散了对本质的注意力如果有兴趣,请参阅 IEEE 754 标准)。 我们会得到什么? 在顶行中,我将给出 REAL 类型的“尾数”(考虑到最后一位四舍五入到最接近的可表示数字,否则它将是 1...),在底行中 - 对于双精度型:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

显然这是两个完全不同的数字! 因此,在比较时,第一个数字将用零填充,因此将大于第二个数字(考虑到四舍五入 - 以粗体标记的数字)。 这解释了我们的例子中的歧义。 在第二个示例中,显式指定的数字 0.1 被转换为 DOUBLE PRECISION 类型,然后与 REAL 类型的数字进行比较。 两者都被简化为相同的类型,我们得到的正是上面所看到的。 让我们修改查询以使一切就位:

********* ЗАПРОС *********
SELECT 0.1::REAL > 0.1::DOUBLE PRECISION;
**************************
?column?
----------
t
(1 строка)

事实上,通过将数字 0.1 双重归约到 REAL 和 DOUBLE PRECISION,我们得到了谜语的答案:

********* ЗАПРОС *********
SELECT 0.1::REAL::DOUBLE PRECISION;
**************************

      float8       
-------------------
0.100000001490116
(1 строка)

这也解释了上面第三个例子。 123这个数字很简单 不可能将尾数装入 24 位 (23 个显式 + 1 个隐含)。 24 位可以容纳的最大整数是 224-1 = 16。因此,我们的数字 777 被四舍五入到最接近的可表示的 215。通过将类型更改为 DOUBLE PRECISION,我们不再看到这种情况:

********* ЗАПРОС *********
SELECT 123456789::DOUBLE PRECISION::INT;
**************************
   int4   
-----------
123456789
(1 строка)

就这样。 事实证明,没有奇迹。 但所描述的一切都是思考您到底有多么需要 REAL 类型的一个很好的理由。 也许使用它的最大优点是计算速度快,但精度有已知损失。 但这是一个普遍的场景,可以证明这种类型的频繁使用是合理的吗? 别想了。

来源: habr.com

添加评论