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

Spanner には、10 進数の精度の数値を正確に格納できる NUMERIC 型が用意されています。 Spanner の NUMERIC 型のセマンティクスは、2 つの SQL 言語(GoogleSQL と PostgreSQL)で異なります。特に、スケールと精度に関する制限が異なります。

  • PostgreSQL 言語の NUMERIC任意精度演算の数値型であり(スケールまたは精度は、サポートされる範囲内の任意の数値にできます)、任意精度の数値データの保存に最適です。

  • GoogleSQL の NUMERIC固定精度の数値型(制度 = 38 およびスケール = 9)であり、任意精度の数値データの保存には使用できません。GoogleSQL 言語データベースに任意精度の数値を保存する場合は、文字列として保存することをおすすめします。

Spanner の数値型の精度

精度とは、数値の桁数のことです。スケールは、数値の小数点以下の桁数です。たとえば、数値 123.456 の精度は 6、スケールは 3 です。Spanner には 3 つの数値型があります。

  • GoogleSQL 言語の INT64 と PostgreSQL 言語の INT8 という 64 ビット符号付き整数型。
  • GoogleSQL 言語の FLOAT64 と PostgreSQL 言語の FLOAT8 という IEEE 64 ビット(倍精度)バイナリ浮動小数点型。
  • 10 進数精度の NUMERIC 型。

それぞれを精度とスケールの点から見てみましょう。

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

FLOAT64 / FLOAT8 は、小数部分の近似小数値のみを表し、15 ~ 17 桁の有効桁数(末尾のゼロがすべて削除された数値の桁数)になります。Spanner で使用する IEEE 64 ビット浮動小数点のバイナリ表現では小数(10 進数)を正確に表せないため(2 進数のみを正確に表すことができます)、この型では近似小数値を表します。この精度の低下により、一部の小数点以下の桁数に丸め誤差が生じます。

たとえば、FLOAT64 / FLOAT8 データ型を使用して 10 進数値 0.2 を保存すると、バイナリ表現は 10 進数値 0.20000000000000001(精度 18 桁まで)に変換されます。同様に(1.4 * 165)は 230.999999999999971 に変換され、(0.1 + 0.2)は 0.30000000000000004 に変換されます。このため、64 ビット浮動小数点は 15 ~ 17 桁の精度の有効桁数しか持たないとされています(10進数 15 桁より多い一部の数値のみを丸めずに 64 ビット浮動小数点として表せます)。浮動小数点精度の計算方法について詳しくは、倍精度浮動小数点の形式をご覧ください。

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

NUMERIC データ型は、10 進数 30 桁より多い精度を持つ 10 進数精度の数値を表すことができるため、それらのアプリケーションに適しています。

GoogleSQL の NUMERIC データ型は、固定の 10 進数精度 38 と固定スケール 9 の数値を表せます。GoogleSQL NUMERIC の範囲は -99999999999999999999999999999.999999999~99999999999999999999999999999.999999999 です。

PostgreSQL 言語 NUMERIC 型では、最大 10 進精度 147,455、最大スケール 16,383 の数値を表せます。

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

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

NUMERIC データ型で提供できるよりも高い精度の数値を Spanner データベースに保存する必要がある場合は、値を 10 進表記で STRING / VARCHAR 列に保存することをおすすめします。たとえば、数値 123.4 は文字列 "123.4" として保存されます。

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

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

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

正確な集計と計算を行う

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

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

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

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

GoogleSQL

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

PostgreSQL

SELECT SUM(value::FLOAT8) FROM my_table

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

GoogleSQL

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

PostgreSQL

SELECT value FROM my_table ORDER BY value::FLOAT8;
SELECT value FROM my_table WHERE value::FLOAT8 > 100.0;

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

別の方法

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

アプリケーションに合わせてスケーリングした整数値を保存する

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

次のステップ