儲存任意精度的數字資料

Spanner 提供 NUMERIC 型別,可精確儲存十進位精確度數字。Spanner 中 NUMERIC 類型的語意會因兩種 SQL 方言 (GoogleSQL 和 PostgreSQL) 而異,特別是規模和精確度的限制:

  • PostgreSQL 方言中的 NUMERIC任意十進位數精確度的數值型別 (比例或精確度可以是支援範圍內的任何數字),因此非常適合儲存任意精確度的數值資料。

  • GoogleSQL 中的 NUMERIC固定精確度的數字類型 (精確度為 38,小數位數為 9),無法用於儲存任意精確度的數字資料。如果您需要在 GoogleSQL 方言資料庫中儲存任意精度數字,建議您將其儲存為字串

Spanner 數值型別的精確度

「總位數」代表數字的位數。「小數位數」是指數字中小數點右側的位數。舉例來說,數字 123.456 的總位數為 6,小數位數為 3。Spanner 具備三種數字類型:

  • GoogleSQL 方言中稱為 INT64,PostgreSQL 方言中稱為 INT8
  • IEEE 64 位元 (雙精度) 二進位精確度浮點類型,在 GoogleSQL 方言中稱為 FLOAT64,在 PostgreSQL 方言中則稱為 FLOAT8
  • 小數點精確度 NUMERIC 類型。

接著就來看看各項指標的精確度和規模。

INT64 / INT8 代表沒有小數的數值。這個資料類型提供的精度為 18 位數,小數位數為 0。

FLOAT64/FLOAT8 只能表示具有小數部分的概略十進位數值,並提供 15 至 17 個有效位數 (移除所有尾隨零的數字位數計數) 的十進位精確度。我們說這個型別代表「近似」十進位數值,是因為 Spanner 使用的 IEEE 64 位元浮點二進位表示法無法精確表示十進位 (以 10 為底) 分數 (只能精確表示以 2 為底的分數)。這種失去精度的情形可能會讓某些小數在四捨五入時出現錯誤。

舉例來說,當您使用 FLOAT64 / FLOAT8 資料類型儲存小數值 0.2 時,二進位表示法會將其轉換回小數值 0.20000000000000001 (精度為 18 位數)。同樣地,(1.4 * 165) 會轉換回 230.999999999999971,而 (0.1 + 0.2) 則轉換回 0.30000000000000004。這就是為什麼會說 64 位元浮點數的精度只有 15 到 17 個有效位數 (只有部分超過 15 個十進位數字的數字,可以表示為 64 位元浮點數,而不需四捨五入)。如要進一步瞭解如何計算浮點位數,請參閱雙位數浮點格式

INT64 / INT8FLOAT64 / FLOAT8 的精度皆不適合用於金融、科學或工程計算,因為這類計算的精度通常需要至少 30 位數。

NUMERIC 資料類型適合這類應用程式,因為它能夠表示精確的小數精度數值,且精度超過 30 個小數位數。

GoogleSQL NUMERIC 資料類型可表示小數點後固定有 38 位數的數字,以及小數點後固定有 9 位數的數字。GoogleSQL NUMERIC 的範圍是 -99999999999999999999999999999.999999999 到 99999999999999999999999999999.999999999。

PostgreSQL 方言 NUMERIC 型別可表示小數點後最多 147,455 位數的數字,以及最多 16,383 位數的比例。

如果您需要儲存的數字大於 NUMERIC 提供的精確度和比例,請參閱下列各節,瞭解一些建議的解決方案。

建議:將任意精度數字儲存為字串

如果您要在 Spanner 資料庫中儲存任意精度數字,而且您需要的精度超過 NUMERIC 能夠提供的位數,我們建議您將值以十進位表示法儲存於 STRING / VARCHAR 資料欄中。舉例來說,數字 123.4 會儲存為字串 "123.4"

如果使用此方法,您的應用程式必須在應用程式內部的數字表示法與 STRING/VARCHAR 資料欄值之間執行無損轉換,才能進行資料庫讀取和寫入作業。

大部分的任意精度程式庫均內建可執行這項無損轉換作業的方法。舉例來說,在 Java 中,您可以使用 BigDecimal.toPlainString() 方法和 BigDecimal(String) 建構函式。

將數字儲存為字串的優點如下:儲存的值仍保有相同的精度 (但不能超過 STRING / VARCHAR 資料欄長度限制) 且維持使用者可理解的格式。

執行準確匯總和計算

如要針對任意精度數字的字串表示法執行「準確」exact的匯總和計算,您必須使用應用程式來執行這類計算。 而無法透過 SQL 匯總函式執行。

舉例來說,如要針對某個資料列範圍執行對應的 SQL SUM(value) 作業,則應用程式必須先查詢資料列的字串值,然後在應用程式內部進行轉換和加總。

執行近似的匯總、排序和計算

您可以將值轉換為 FLOAT64 / FLOAT8,以便使用 SQL 查詢來執行「近似」匯總計算。

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 查詢,對資料庫中儲存的值直接執行計算。

在個別的資料欄儲存整數值和小數位數

您也可以使用下列兩項元素在 Spanner 中儲存任意精度數字:

  • 儲存在位元組陣列中的整數值。
  • 指定比例因數的整數。

首先,您的應用程式會將任意精度小數轉換為整數值。舉例來說,應用程式會將 12.54321 轉換為 1254321。此範例的小數位數即為 5

然後,應用程式會使用標準的可攜式二進位表示法 (例如大端序 (big-endian) 二補數) 將整數值轉換為位元組陣列。

接著,資料庫就會將位元組陣列 (BYTES / BYTEA) 和整數小數位數 (INT64 / INT8) 儲存至兩個單獨的資料欄中,並於讀取時轉換回來。

在 Java 中,您可以使用 BigDecimalBigInteger 執行這些計算:

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

您可以使用下列程式碼回讀到 Java BigDecimal

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

此方法會以任意精度和可攜式表示法來儲存值,但是使用者無法理解資料庫中的值,而且所有計算均必須透過應用程式執行。

將應用程式內部表示法儲存為位元組

另一個選項是使用應用程式的內部表示法,將任意精度小數值序列化為位元組陣列,然後直接儲存到資料庫中。

如此一來,使用者將無法理解儲存在資料庫中的值,而且所有計算都必須由應用程式執行。

這種方法有可攜性的問題。如果您在嘗試讀取值時使用的程式設計語言或程式庫與原先編寫時的語言不同,您可能無法進行這項操作。不同的任意精度程式庫所採用的位元組陣列序列化表示法可能不同,因此可能無法回讀值。

後續步驟