真實類型的虛幻特徵,或小心真實類型

發表後 文章 關於 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 類型的一個很好的理由。 也許使用它的最大優點是計算速度快,但精度有已知損失。 但這是一個普遍的場景,可以證明這種類型的頻繁使用是合理的嗎? 別想了。

來源: www.habr.com

添加評論