임의 정밀도 숫자 데이터 저장

Spanner는 십진수 정밀도 숫자를 정확히 저장할 수 있는 NUMERIC 유형을 제공합니다. Spanner에서 NUMERIC 유형의 시맨틱스는 특히 규모 및 정밀도 한도에 대해 두 SQL 언어(GoogleSQL 및 PostgreSQL)에 따라 다릅니다.

  • PostgreSQL 언어의 NUMERIC임의 십진수 정밀도 숫자 유형(소수 자릿수 또는 정밀도가 지원되는 범위 내의 모든 숫자일 수 있음)이므로 임의 정밀도 숫자 데이터를 저장하는 데 적합합니다.

  • GoogleSQL의 NUMERIC고정 정밀도 숫자 유형(정밀도=38 및 규모=9)이며 임의 정밀도 숫자 데이터를 저장하는 데 사용할 수 없습니다. 임의 정밀도 숫자를 GoogleSQL 언어 데이터베이스에 저장해야 하는 경우 문자열로 저장하는 것이 좋습니다.

Spanner 숫자 유형 정밀도

정밀도는 숫자의 자릿수입니다. 소수 자릿수는 숫자의 소수점 오른쪽에 있는 자릿수입니다. 예를 들어 숫자 123.456의 정밀도는 6이고 소수 자릿수는 3입니다. Spanner에는 세 가지 숫자 유형이 있습니다.

  • GoogleSQL 언어에서 INT64, PostgreSQL 언어에서는 INT8이라고 부르는 64비트의 부호 있는 정수 유형
  • GoogleSQL 언어에서 FLOAT64, PostgreSQL 언어에서는 FLOAT8이라고 부르는 IEEE 64비트 배정밀도 바이너리 부동 소수점 유형
  • 십진수 정밀도 NUMERIC 유형

정밀도와 소수 자릿수에 대해 각각 살펴보겠습니다.

INT64 / INT8는 소수 구성요소가 없는 숫자 값을 나타냅니다. 이 데이터 유형은 소수 자릿수가 0인 18자리 정밀도를 제공합니다.

FLOAT64 / FLOAT8은 소수 구성요소가 있는 대략적인 십진수 숫자 값만 나타낼 수 있으며 십진수 정밀도에 15~17자리의 유효 자릿수(모든 후행 0이 삭제된 숫자의 자릿수)를 제공합니다. Spanner가 사용하는 IEEE 64비트 부동 소수점 바이너리 표현은 십진수(base-10) 소수를 정밀하게 표현할 수 없기 때문에(base-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 열 값을 손실 없이 변환하게 됩니다.

대부분의 임의 정밀도 라이브러리에는 이 무손실 변환을 수행하기 위한 내장 메소드가 있습니다. 예를 들어 자바에서는 BigDecimal.toPlainString() 메서드와 BigDecimal(String) 생성자를 사용할 수 있습니다.

숫자를 문자열로 저장하면 값이 정확한 정밀도(최대 STRING/VARCHAR 열 길이 제한까지)로 저장되고 인간이 읽을 수 있다는 이점이 있습니다.

정확한 집계 및 계산 수행

임의 정밀도 숫자의 문자열 표현에 대한 정확한 집계 및 계산을 수행하려면 애플리케이션이 이러한 계산을 수행해야 합니다. SQL 집계 함수를 사용할 수 없습니다.

예를 들어 행 범위에 대해 SQL SUM(value)을 수행하려면 애플리케이션이 행의 문자열 값을 쿼리한 다음 앱에서 애플리케이션에서 내부적으로 변환하고 합해야 합니다.

대략적인 집계, 정렬, 계산 수행

SQL 쿼리를 사용하면 값을 FLOAT64/FLOAT8로 Cast 변환하여 대략적인 집계 계산을 수행할 수 있습니다.

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자리로 제한됩니다.

소수점 이하 다섯 자릿수의 정확도로 저장해야 하는 숫자를 예로 들어 보겠습니다. 애플리케이션은 100,000을 곱하여 값을 정수로 변환합니다(소수점을 다섯 자리 오른쪽으로 이동). 그러면 값 12.54321이 1254321로 저장됩니다.

이 방법은 시간 단위를 밀리초로 저장하는 것과 마찬가지로 달러 값을 밀리센트의 배수로 저장하는 것과 유사합니다.

애플리케이션은 고정된 소수 자릿수 계수를 결정합니다. 소수 자릿수 계수를 변경하는 경우 데이터베이스에서 이전에 조정된 값을 모두 변환해야 합니다.

이 방식은 사람이 소수 자릿수 계수를 알고 있다면 값을 인간이 읽을 수 있는 형식으로 저장합니다. 또한 결과 값의 소수 자릿수가 올바르게 조정되고 오버플로되지 않는 한 SQL 쿼리를 사용하여 데이터베이스에 저장된 값으로 직접 계산을 수행할 수 있습니다.

소수 자릿수가 없는 정수 값 및 소수 자릿수를 별도의 열에 저장

다음 두 요소를 사용하여 임의 정밀도 숫자를 Spanner에 저장할 수도 있습니다.

  • 바이트 배열에 저장된 소수 자릿수가 없는 정수 값
  • 소수 자릿수 계수를 지정하는 정수

먼저 애플리케이션이 임의 정밀도 십진수를 소수 자릿수가 없는 정수 값으로 변환합니다. 예를 들어 애플리케이션은 12.543211254321로 변환합니다. 이 예시의 소수 자릿수는 5입니다.

그런 다음 애플리케이션이 포팅 가능한 표준 바이너리 표현(예: 빅 엔디언 2의 보수)을 사용하여 소수 자릿수가 없는 정수 값을 바이트 배열로 변환합니다.

그런 다음 데이터베이스가 바이트 배열(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);

이 방식을 사용하면 임의 정밀도 및 포팅 가능한 표현과 함께 값을 저장하지만 값은 데이터베이스에서 사람이 읽기 어렵고 모든 계산은 애플리케이션을 통해 수행해야 합니다.

애플리케이션 내부 표현을 바이트로 저장

또 다른 옵션은 애플리케이션의 내부 표현을 사용하여 임의 정밀도 십진수 값을 바이트 배열로 직렬화한 후 데이터베이스에 직접 저장하는 것입니다.

저장된 데이터베이스 값은 사람이 읽기 어렵고 애플리케이션을 통해 모든 계산을 수행해야 합니다.

이 방식에는 이동성 문제가 있습니다. 처음 작성된 것과 다른 프로그래밍 언어 또는 라이브러리로 값을 읽으려는 경우 작동하지 않을 수 있습니다. 임의 정밀도 라이브러리마다 바이트 배열로 직렬화된 표현이 다를 수 있으므로 값을 다시 읽지 못할 수 있습니다.

다음 단계