儲存任意精度數字資料

Spanner 提供 NUMERIC 類型,可精確儲存小數精確度數字。Spanner 中的 NUMERIC 類型語意會因兩種 SQL 方言 (GoogleSQL 和 PostgreSQL) 而異,尤其是在 scale 和 precision 限制方面:

  • PostgreSQL 方言中的 NUMERIC任意小數精確度數值類型 (比例或精確度可為支援範圍內的任何數字),因此是儲存任意精確度數值資料的理想選擇。

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

Spanner 數值類型的精確度

精確度是指數字中的位數,小數位數是指數字中小數點右側的數字數量。舉例來說,數字 123.456 的精度為 6,小數位數為 3。Spanner 有三種數值類型:

  • 64 位元帶正負號整數類型,在 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 位數的原因。只有少數超過 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 資料欄長度限制) 且維持使用者可理解的格式。

執行準確匯總和計算

如要針對任意精度數字的字串表示法執行「準確」的匯總和計算,您必須使用應用程式來執行這類計算,而無法透過 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);

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

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

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

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

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

後續步驟