実数型の非現実的な機能、または REAL には注意してください

出版後 記事 PostgreSQL での入力機能についての最初のコメントは、実数を扱うことの難しさについてでした。 私は、利用可能な SQL クエリのコードをざっと調べて、どれくらいの頻度で REAL 型が使用されているかを確認することにしました。 これは非常に頻繁に使用されていることが判明しましたが、開発者はその背後にある危険性を必ずしも理解しているわけではありません。 実数をコンピュータのメモリに保存する機能とその操作について、インターネットやハブレに非常に多くの優れた記事があるにもかかわらず、これは事実です。 したがって、この記事では、そのような機能を 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 が分数でうまく動作しないことはわかっているので、そこに整数を入力すれば、間違いなくすべてうまくいくでしょう、と誰かが言うでしょう。 OK、数値 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、XNUMX など) を表す場合、負の累乗ではすべてがより複雑になります (XNUMX、XNUMX、XNUMX、XNUMX など)。 問題はそれです すべての小数が有限の XNUMX 進分数として表現できるわけではありません。 したがって、悪名高い 0.1 は、0.0 進数の分数の形式で周期値 0011(XNUMX) として表示されます。 したがって、コンピュータ メモリ内のこの数値の最終値は、ビット深度に応じて変化します。

実数がコンピューターのメモリにどのように保存されるかを思い出してください。 一般に、実数は、符号、仮数、指数という 23 つの主要な部分で構成されます。 符号はプラスまたはマイナスのいずれかであるため、1 ビットがそれに割り当てられます。 ただし、仮数と指数のビット数は実数型によって決まります。 したがって、REAL 型の場合、仮数の長さは 24 ビット (8 に等しい 32 ビットが暗黙的に仮数の先頭に追加され、結果は 4 になります)、指数は 52 ビットです。 合計は 11 ビット、つまり 64 バイトです。 また、DOUBLE PRECISION 型の場合、仮数の長さは 8 ビット、指数の長さは XNUMX ビットとなり、合計 XNUMX ビット、つまり XNUMX バイトになります。 PostgreSQL は、浮動小数点数の高精度をサポートしていません。

0.1 進数 754 を REAL 型と DOUBLE PRECISION 型の両方にパックしてみましょう。 指数の符号と値は同じであるため、仮数に焦点を当てます(指数の値とゼロの実数値を保存するという非自明な機能は、理解を複雑にし、本質から遠ざけるため、意図的に省略しています)問題の詳細については、興味があれば IEEE 1 標準を参照してください)。 何が得られるでしょうか? 上の行では REAL 型の「仮数」を指定します (最後のビットを 0.099999 で最も近い表現可能な数値に丸めることを考慮します。それ以外の場合は XNUMX... になります)。下の行では、次のようにします。 DOUBLE PRECISION タイプ:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

明らかに、これら 0.1 つはまったく異なる数値です。 したがって、比較する場合、最初の数値はゼロで埋められ、したがって XNUMX 番目の数値よりも大きくなります (丸めを考慮すると、太字でマークされています)。 これで、例からのあいまいさが説明されます。 XNUMX 番目の例では、明示的に指定された数値 XNUMX が 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 番目の例についても説明しています。 456という数字は単純です 仮数部を 24 ビットに収めることは不可能です (明示的な 23 個 + 暗黙的な 1 個)。 24 ビットに収まる最大の整数は 224-1 = 16 です。したがって、数値 777 は最も近い表現可能な 215 に丸められます。型を DOUBLE PRECISION に変更することで、このシナリオは発生しなくなります。

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

それだけです。 奇跡など存在しないことが判明した。 しかし、ここで説明したことはすべて、REAL 型が実際にどの程度必要なのかを考える十分な理由になります。 おそらく、これを使用する最大の利点は、精度が低下することはわかっていますが、計算が高速になることです。 しかし、これは、このタイプの頻繁な使用を正当化する普遍的なシナリオなのでしょうか? 考えないでください。

出所: habr.com

コメントを追加します