任意精度の数値データの保存

Cloud Spanner にはさまざまな列データ型がありますが、任意精度の数値用のデータ型はありません。Cloud Spanner に任意精度の数値を保存する場合は、文字列として保存することをおすすめめします。

数値型の概要

ほとんどのデータベースでは、任意精度の数値は NUMERIC(p,s) として表されます。ここで、p は小数点以下の精度(合計桁数)、s はスケール(小数点以下の桁数)です。たとえば、NUMERIC(8,3) は -99999.999~+99999.999 までを表すことができます。

Cloud Spanner には任意精度の数値のデータ型がありません。Cloud Spanner には、INT64FLOAT64 の 2 つの数値タイプがあります。これらの型はどちらも、一般に 30 桁以上の精度が必要とされる金融、科学、工学のための計算に適した精度を持っていません。 FLOAT64 は 15 桁の精度(-294~+ 294 のスケール)を提供し、INT64 は 18 桁の精度(ゼロのスケール)を提供します。浮動小数点精度の計算方法について詳しくは、倍精度浮動小数点形式をご覧ください。

任意精度の数値型は通常、値を保存するときは、値をスケーリングして整数として保存します。たとえば、精度が 5、スケールが 2 の数値型の場合、値 0.3 を 100 の倍数(10 2)でスケーリングし、00030 として保存します。このようにして値を保存すると、小数部のエラーや精度の低下を防ぐことができます。

それとは反対に、Cloud Spanner は FLOAT64 値を 2 進数として保存します。Cloud Spanner が使用する IEEE 64 ビット浮動小数点バイナリ表現は、小数を正確に表現できません。この精度の低下により、一部の小数点以下の桁数に丸め誤差が生じます。

たとえば、FLOAT64 データ型を使用して 10 進値 0.2 を保存すると、バイナリ表現は 10 進値 0.20000000000000001(精度 18 桁まで)に変換されます。同様に(1.4 * 165)は 230.999999999999971 に変換され、(0.1 + 0.2)は 0.30000000000000004 に変換されます。このため、64 ビット浮動小数点は 15 桁の精度しか持たないことになります。

推奨事項: 任意精度の数値を文字列として保存する

Cloud Spanner データベースに任意精度の数値を保存するときに、FLOAT64 が提供するよりも高い精度が必要な場合は、その値を 10 進表現として STRING 列に保存することをおすすめします。たとえば、数値 123.4 を文字列 "123.4" として保存します。

このアプローチでは、アプリケーションでアプリケーション内部の数値の表現と STRING 列の値とのロスレス変換を行う必要があります。これにより、データベースでの読み書きを行います。

ほとんどの任意精度ライブラリには、このロスレス変換を行うための組み込みメソッドがあります。たとえば、Java では、BigDecimal.toPlainString() メソッドと BigDecimal(String) コンストラクタを使用できます。

数値を文字列として保存すると、値が正確な精度(STRING 列の長さの上限まで)で保存され、値が判読可能なままであるという利点がもたらされます。

正確な集計と計算を行う

任意精度の数値の文字列表現に対して正確な集計と計算を行うには、アプリケーションでこれらの計算を行う必要があります。SQL 集計関数は使用できません。

たとえば、ある範囲の行に対して SQL SUM(value) と同等の処理を行うには、アプリケーションで行の文字列値をクエリしてから、アプリ内部で変換して合計する必要があります。

近似集計、ソート、計算を行う

SQL クエリを使用して、値を FLOAT64 にキャストすることで、近似集計計算を行うことができます。

SELECT SUM(CAST(value AS FLOAT64)) FROM my_table

同様に、キャストによって数値でソートしたり、特定の範囲で値を制限したりできます。

SELECT value FROM my_table ORDER BY CAST(value AS FLOAT64)
SELECT value FROM my_table WHERE CAST(value AS FLOAT64) > 100.0

これらの計算は、FLOAT64 データ型の制限に近似しています。

代替:

Cloud Spanner に任意精度の数値を保存する他の方法もあります。任意精度の数値を文字列として保存してもアプリケーションで機能しない場合は、代わりに、次の方法を検討してください。

アプリケーション スケールの INT64 値を保存する

任意精度の数値を保存するには、書き込む前に値をプリスケーリングして、数値が常に整数として保存されるようにし、読み取り後に値を再スケーリングします。アプリケーションが固定されたスケール係数を保存すると、精度は INT64 データ型によって提供される 18 桁に制限されます。

たとえば、小数点以下 5 桁の精度で保存する必要がある数を考えます。アプリケーションは、値を 100,000 倍して(小数点を 5 桁右にシフトして)整数に変換するので、値 12.54321 は 1254321 として保存されます。

通貨に関して言えば、このアプローチはドルの値をミリ秒の倍数で保存するのに似ています。時間単位をミリ秒で保存するのと同じです。

スケール係数は、アプリケーションによって決まります。スケール係数を変更する場合は、データベース内の以前にスケーリングしたすべての値を変換する必要があります。

このアプローチでは、判読可能な値が保存されます(スケール係数が既知であるという前提)。また、SQL クエリを使用して、データベースに保存されている値に対して直接計算を行うことができます。ただし、結果が正しくスケーリングされ、オーバーフローしていない場合に限ります。

スケールなしの整数値とスケールを別々の列に保存する

以下の 2 つの要素を使用して、Cloud Spanner に任意精度の数値を保存することもできます。

  • バイト配列に保存されているスケールなしの整数値。
  • スケール係数を指定する整数。

最初に、アプリケーションで任意精度の 10 進数をスケールなしの整数値に変換します。たとえば、12.543211254321 に変換します。この例では、スケールは 5 です。

次に、アプリケーションで標準のポータブル バイナリ表現(たとえば、ビッグエンディアンの 2 の補数)を使用して、スケールなしの整数値をバイト配列に変換します。

次いで、データベースにバイト配列(BYTES)と整数スケール(INT64)を 2 つの別々の列に保存し、リード上に戻って変換します。

Java では、BigDecimalBigInteger を使用してこれらの計算を行うことができます。

byte[] storedUnscaledBytes = bigDecimal.unscaledValue().toByteArray();
int storedScale = bigDecimal.scale();

次のコードを使用して、Java BigDecimal 読み返すことができます。

BigDecimal bigDecimal = new BigDecimal(
    new BigInteger(storedUnscaledBytes),
    storedScale);

このアプローチでは、任意精度とポータブル表現で値を保存します。しかし、値はデータベース内で判読不可能のため、すべての計算はアプリケーションで行う必要があります。

アプリケーションの内部表現をバイトとして保存する

もう 1 つの選択肢は、アプリケーションの内部表現を使用して、任意精度の 10 進数値をバイト配列にシリアル化してから、データベースに直接保存することです。

データベースに保存される値は判読不可能なため、アプリケーションですべての計算を行う必要があります。

このアプローチには移植性の問題があります。最初に書いたものとは異なるプログラミング言語やライブラリで値を読もうとする場合、うまくいかない可能性があります。値を読み戻してもうまくいかない場合の理由として、任意精度ライブラリが異なると、バイト配列のシリアル化表現が異なるということが考えられます。

次のステップ