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

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

Cloud Spanner の数値型の精度

精度とは、数値の桁数のことです。スケールは、数値の小数点以下の桁数です。たとえば、数値 123.456 の精度は 6、スケールは 3 です。Cloud Spanner には、INT64FLOAT64NUMERIC の 3 つの数値型があります。それぞれを精度とスケールの点から見てみましょう。

INT64 は、小数部分を含まない数値を表します。このデータ型では精度が 18 桁、スケールが 0 になります。

FLOAT64 は小数部分の近似値を表し、精度が 15 桁、スケールが -294~+294 になります。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 桁の精度しか持たないことになります。浮動小数点精度の計算方法について詳しくは、倍精度浮動小数点形式をご覧ください。

INT64FLOAT64 はどちらも、一般に 30 桁以上の精度が必要とされる金融、科学、工学のための計算に適した精度を持っていません。

NUMERIC データ型は、精度が 38、スケールが 9 で正確な数値を表すことができるため、それらのアプリケーションに適しています。NUMERIC の範囲は -99999999999999999999999999999.999999999~99999999999999999999999999999.999999999 です。

NUMERIC での精度とスケールよりも大きい数値を保存する必要がある場合は、次のセクションでおすすめの対策について説明します。

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

Cloud Spanner データベースに任意精度の数値を保存するときに、NUMERIC が提供するよりも高い精度が必要な場合は、その値を 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 進数値をバイト配列にシリアル化してから、データベースに直接保存することです。

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

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

次のステップ