Almacena datos numéricos arbitrarios de precisión

Spanner proporciona el tipo NUMERIC que puede almacenar con exactitud los números de precisión decimales. 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 pueden 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 (precisión=38 y escala=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 las bases de datos del dialecto GoogleSQL, te recomendamos que los almacenas 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 de 64 bits firmado llamado INT64 en el dialecto GoogleSQL y INT8 en el dialecto PostgreSQL.
  • Tipo de punto flotante de precisión binaria IEEE de 64 bits (doble) llamado FLOAT64 en el dialecto GoogleSQL y FLOAT8 en el dialecto PostgreSQL.
  • Tipo de NUMERIC de precisión decimal.

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

INT64 / INT8 representan 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 pueden representar valores numéricos decimales aproximados con componentes fraccionarios y proporciona entre 15 y 17 dígitos significativos (recuento de dígitos de un número sin los ceros finales) de precisión decimal. Decimos que este tipo representa valores numéricos decimales aproximados porque la representación binaria de punto flotante IEEE de 64 bits que usa Spanner no puede representar con precisión fracciones decimales (de base 10) (puede representar solo fracciones de base 2 de manera exacta). Esta pérdida de precisión genera 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 un valor decimal de 0.20000000000000001 (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. Por eso, 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úmero de punto flotante de 64 bits sin redondeo). Para obtener más información sobre cómo se calcula la precisión del punto flotante, consulta Formato de punto flotante de doble precisión.

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

El tipo de datos NUMERIC es adecuado para esas aplicaciones, ya que puede representar valores numéricos de precisión decimal exactas con 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 -99999999999999999999999999999.999999999 a 9999999999999999999999999999999.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, tu 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 lecturas y escrituras 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.

Realizar 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.

Realizar agregaciones, clasificaciones y cálculos aproximados

Puedes usar las consultas de SQL para realizar cálculos agregados aproximados mediante la conversión de los valores a FLOAT64 / 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 enteros ajustados a escala de la 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. Tu 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 los vuelve a leer.

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?