存储任意精度数值数据

Spanner 提供可以存储十进制精度数字的 NUMERIC 类型 。 Spanner 中 NUMERIC 类型的语义因其两个 SQL 而异 方言(GoogleSQL 和 PostgreSQL),尤其是 规模和精度方面的限制:

  • PostgreSQL 方言中的 NUMERIC任意十进制精度 数字类型(比例或精度可以是支持范围内的任何数字) 因此非常适合存储任意精度数值数据。

  • GoogleSQL 中的 NUMERIC固定精度 数值类型(精度=38 和 scale=9),不能用于存储任意精度数字数据。 如果您需要在 GoogleSQL 方言数据库中存储任意精度数字, 建议您将它们存储为字符串

Spanner 数值类型的精度

精度是指数字位数。标度是数字中小数点右边的位数。例如,数字 123.456 的精度为 6,标度为 3。Spanner 有三种数值类型:

  • 64 位有符号整数类型(在 GoogleSQL 方言中称为 INT64),在 PostgreSQL 方言中称为 INT8
  • IEEE 64 位(双精度)二进制精度浮点类型,在 GoogleSQL 方言中称为 FLOAT64,在 PostgreSQL 方言中称为 FLOAT8
  • 小数精度 NUMERIC 类型。

我们分别来看看精度和标度。

INT64 / INT8 表示不含小数部分的数值。此数据类型提供 18 位精度,标度为零。

FLOAT64 / FLOAT8 只能表示包含小数部分的近似十进制数值, 15 到 17 个有效位(数字中带有尾随零的数位) 小数精度。我们说,此类型表示近似十进制数字 值,因为 IEEE 64 位浮点 Spanner 使用的二进制表示法无法精确地表示 十进制(以 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 数字不少于 1 位。

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) 的同样的效果,应用必须查询行的字符串值,然后在应用内部进行转换和求和。

执行近似聚合、排序和计算

您可以使用 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 位小数的精度存储。应用会将该值乘以 100000(将小数点向左移 5 位),使其转换为整数,因此值 12.54321 会存储为 1254321

从货币角度来看,这种方法就像将美元金额存储为千分之一美分的倍数,类似于以毫秒为单位存储时间单位。

应用会确定固定的比例因素。如果您要更改比例因数,必须转换数据库中所有先前扩缩的值。

此方法可存储直观易懂的值(假设您知道比例因数)。此外,只要结果正确扩缩且不溢出,您就可以使用 SQL 查询直接对存储在数据库中的值执行计算。

将未扩缩的整数值和比例存储在不同的列中

您还可以使用 元素:

  • 存储在字节数组中的未扩缩整数值。
  • 一个指定了比例因数的整数。

首先,您的应用将任意精度小数转换为未扩缩的整数值。例如,应用将 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);

此方法以任意精度和可移植表示法存储值,但这些值在数据库中不是直观易懂的,并且所有计算必须由应用执行。

将应用内部表示存储为字节

您还可以使用应用的内部表示将任意精度小数值序列化为字节数组,然后将它们直接存储在数据库中。

存储的数据库值不是直观易懂的,并且需要由应用执行所有计算。

这种方法具有可移植性问题。如果您尝试使用与最初编写它的编程语言或库不同的编程语言或库来读取值,可能无法正常读取。重新读取回值可能失败,因为不同的任意精度库的字节数组的序列化表示法可能有所不同。

后续步骤