Almacena datos numéricos de precisión arbitrarios

Spanner proporciona el tipo NUMERIC, que puede almacenar números de precisión decimal con exactitud. La semántica del tipo NUMERIC en Spanner varía entre sus dos dialectos de SQL (GoogleSQL y PostgreSQL), en especial cerca de los límites de escalamiento y precisión:

  • NUMERIC en el dialecto PostgreSQL es un tipo numérico de precisión decimal arbitraria (la escala o la precisión puede ser cualquier número dentro del rango admitido) y, por lo tanto, es una opción ideal para almacenar datos numéricos de precisión arbitrarios.

  • NUMERIC en GoogleSQL es un tipo numérico de precisión fija (precision=38 y scale=9) y no se puede usar para almacenar datos numéricos de precisión arbitraria. Cuando necesites almacenar números de precisión arbitrarios en bases de datos de dialecto de GoogleSQL, te recomendamos que los almacenes como cadenas.

Precisión de los tipos numéricos de Spanner

La precisión es el número de dígitos que hay en un número. La escala es la cantidad de dígitos a la derecha del punto decimal de un número. Por ejemplo, el número 123.456 tiene una precisión de 6 y una escala de 3. Spanner tiene tres tipos numéricos:

  • Tipo de número entero firmado de 64 bits llamado INT64 en el dialecto GoogleSQL y INT8 en el dialecto PostgreSQL.
  • Tipo de punto flotante de precisión binaria de 64 bits (doble) llamado FLOAT64 en el dialecto GoogleSQL y FLOAT8 en el dialecto PostgreSQL.
  • Tipo NUMERIC con precisión decimal.

Analicemos cada uno en términos de precisión y escalamiento.

INT64 / INT8 representa valores numéricos que no tienen un componente fraccionario. Este tipo de datos proporciona 18 dígitos de precisión, con una escala de cero.

FLOAT64 / FLOAT8 solo puede representar valores numéricos decimales aproximados con componentes fraccionarios y proporciona entre 15 y 17 dígitos significativos (recuento de dígitos en un número sin todos los ceros finales) de precisión decimal. Decimos que este tipo representa valores numéricos decimales aproximados porque la representación binaria IEEE de punto flotante de 64 bits que usa Spanner no puede representar con precisión fracciones decimales (de base 10) (puede representar solo las fracciones de base 2 con exactitud). Esta pérdida de precisión introduce errores de redondeo para algunas fracciones decimales.

Por ejemplo, cuando almacenas el valor decimal 0.2 con el tipo de datos FLOAT64 / FLOAT8, la representación binaria vuelve a la conversión a un valor decimal de 0.2000000000000001 (con 18 dígitos de precisión). De manera similar, (1.4 * 165) vuelve a ser 230.999999999999971 y (0.1 + 0.2) vuelve a ser 0.30000000000000004. Esta es la razón por la que los números de punto flotante de 64 bits se describen como que solo tienen entre 15 y 17 dígitos significativos de precisión (solo algunos números con más de 15 dígitos decimales se pueden representar como números de punto flotante de 64 bits sin redondeo). Para obtener más detalles sobre cómo se calcula la precisión de punto flotante, consulta Formato de punto flotante de precisión doble.

Ni INT64 / INT8 ni FLOAT64 / FLOAT8 tienen la precisión ideal para cálculos financieros, científicos o de ingeniería, en los que a menudo se requiere una precisión de 30 dígitos o más.

El tipo de datos NUMERIC es adecuado para esas aplicaciones, ya que es capaz de representar valores numéricos con precisión decimal exacta que tienen una precisión de más de 30 dígitos decimales.

El tipo de datos NUMERIC de GoogleSQL puede representar números con una precisión decimal fija de 38 y una escala fija de 9. El rango de NUMERIC de GoogleSQL es de -999999999999999999999999999.999999999.999999999 a 999999999999999999999999999.999999999.

El tipo NUMERIC del dialecto PostgreSQL puede representar números con una precisión decimal máxima de 147,455 y una escala máxima de 16,383.

Si necesitas almacenar números que sean más grandes que la precisión y el escalamiento que ofrece NUMERIC, en las siguientes secciones se describen algunas soluciones recomendadas.

Recomendación: almacena números de precisión arbitrarios como strings

Cuando necesites almacenar un número de precisión arbitrario en una base de datos de Spanner y necesites más precisión que la que proporciona NUMERIC, te recomendamos que almacenes el valor como su representación decimal en una columna STRING / VARCHAR. Por ejemplo, el número 123.4 se almacena como la cadena "123.4".

Con este enfoque, la aplicación debe realizar una conversión sin pérdidas entre la representación interna de la aplicación del número y el valor de la columna STRING / VARCHAR para las operaciones de lectura y escritura de la base de datos.

La mayoría de las bibliotecas de precisión arbitraria tienen métodos integrados para realizar esta conversión sin pérdidas. En Java, por ejemplo, puedes usar el método BigDecimal.toPlainString() y el constructor BigDecimal(String).

Almacenar el número como una cadena tiene la ventaja de que el valor se almacena con precisión exacta (hasta el límite de longitud de columna STRING / VARCHAR) y el valor sigue siendo legible por humanos.

Realiza agregaciones y cálculos exactos

Para realizar agregaciones y cálculos exactos en representaciones de string de números de precisión arbitrarios, la aplicación debe realizar estos cálculos. No puedes usar las funciones de agregación de SQL.

Por ejemplo, para realizar el equivalente de SUM(value) de SQL sobre un rango de filas, la aplicación debe consultar los valores de string para las filas, y convertirlas y sumarlas internamente en la aplicación.

Realiza agregaciones, ordenaciones y cálculos aproximados

Puedes usar consultas en SQL para realizar cálculos agregados aproximados si conviertes los valores en FLOAT64 o FLOAT8.

GoogleSQL

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

PostgreSQL

SELECT SUM(value::FLOAT8) FROM my_table

Del mismo modo, puedes ordenar por valor numérico o valores límite por rango con conversión:

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;

Estos cálculos son aproximados a los límites del tipo de datos FLOAT64 / FLOAT8.

Alternativas

Existen otras formas de almacenar números de precisión arbitrarios en Spanner. Si el almacenamiento de números de precisión arbitrarios como strings no funciona para tu aplicación, considera las siguientes alternativas:

Almacena valores de número entero escalados por aplicación

Para almacenar números de precisión arbitrarios, puedes escalar previamente los valores antes de escribirlos, de modo que los números siempre se almacenen como números enteros, y volver a escalar los valores después de leerlos. La aplicación almacena un factor de escala fijo y la precisión se limita a los 18 dígitos que proporciona el tipo de datos INT64 / INT8.

Tomemos, por ejemplo, un número que debe almacenarse con una precisión de 5 decimales. La aplicación convierte el valor en un número entero cuando se multiplica por 100,000 (desplazando el punto decimal 5 lugares hacia la derecha), por lo que el valor 12.54321 se almacena como 1254321.

En términos monetarios, este enfoque es como almacenar los valores en dólares como múltiplos de milésima de centavo, similar a almacenar unidades de tiempo en milésimas de segundo.

La aplicación determina el factor de escalamiento fijo. Si cambia el factor de escalamiento, debes convertir todos los valores previamente escalados en tu base de datos.

Este enfoque almacena valores que son legibles (suponiendo que conoces el factor de escalamiento). Además, puedes usar consultas de SQL para realizar cálculos en valores almacenados en la base de datos, siempre que el resultado se escale correctamente y no se desborde.

Almacena el número entero sin escala y la escala en columnas separadas

También puedes almacenar números de precisión arbitrarios en Spanner con dos elementos:

  • Número entero sin escala almacenado en un arreglo de bytes
  • Número entero que especifica el factor de escalamiento

Primero, la aplicación convierte el decimal de precisión arbitrario en un número entero sin escala. Por ejemplo, la aplicación convierte 12.54321 a 1254321. La escala para este ejemplo es 5.

Luego, la aplicación convierte el número entero sin escala en un arreglo de bytes mediante una representación binaria portátil estándar (por ejemplo, complemento a dos de extremo grande).

Luego, la base de datos almacena el array de bytes (BYTES / BYTEA) y la escala de números enteros (INT64 / INT8) en dos columnas separadas, y las vuelve a convertir en lectura.

En Java, puedes usar BigDecimal y BigInteger para realizar estos cálculos:

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

Puedes volver a leer un BigDecimal de Java con el siguiente código:

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

Este enfoque almacena valores con precisión arbitraria y una representación portátil, pero los valores no son legibles en la base de datos, y la aplicación debe realizar todos los cálculos.

Almacena la representación interna de la aplicación como bytes

Otra opción es serializar los valores decimales de precisión arbitrarios en arreglos de bytes mediante la representación interna de la aplicación y, luego, almacenarlos directamente en la base de datos.

Los valores de base de datos almacenados no son legibles, y la aplicación debe realizar todos los cálculos.

Este enfoque tiene problemas de portabilidad. Si intentas leer los valores con un lenguaje de programación o una biblioteca diferente de la que se escribió originalmente, es posible que no funcione. Es posible que la lectura de los valores no funcione porque diferentes bibliotecas de precisión arbitraria pueden tener diferentes representaciones serializadas para arreglos de bytes.

¿Qué sigue?