PostgreSQL에서 FLOAT 타입 제대로 알고 사용하기

문제

우리 회사는 PostgreSQL에 실수 값이 `real(float4)` 타입으로 저장되어 있으며, 이 데이터를 JavaScript에서 `Math.round()`를 사용하여 반올림하여 통계를 제공하고 있었다.

새로운 통계 방식을 위해 Superset을 도입했고, 이 과정에서 SQL의 `ROUND()` 함수를 사용하여 반올림했다.

하지만 통계 수치나 비교 결과가 불일치하는 문제가 발생했다.

 

원인

부동소수점의 한계

앞서 얘기했듯, PostgreSQL에는 실수가 `real` 타입으로 저장되고 있었다.

`real`과 `double precision` 같은 부동소수점 타입은 이진수로 표현할 수 있는 숫자만 정확히 저장할 수 있어서, 대부분의 10진 실수는 근사값으로 저장된다.

SELECT 0.1::real;             -- 출력: 0.1 (처럼 보임)
SELECT 0.1::real::numeric(20,18);
-- numeric을 사용하여 정확한 수치 출력: 0.100000001490116119

SELECT 0.1::real = 0.1::numeric;  -- false

이러한 차이로 인해 `real` 타입으로 저장된 실수에 `ROUND` 함수를 적용하면 오차가 발생할 수 있다.

서로 다른 반올림 방식

JavaScript에서 사용하는 `Math.round()` 함수는 사사오입 방식을 채택하고 있고, PostgreSQL은 Banker’s rounding 방식을 채택하고 있다.

  • 사사오입 방식 - 0.5 이상이면 무조건 올림
  • Banker’s rounding 방식 - .5일 때는 가장 가까운 짝수로 반올림

예)

Math.round() ROUND()
1.5 2 2
2.5 3 2
3.5 4 4
4.5 5 4

PostgreSQL이 Banker’s rounding 방식을 채택한 이유는 한쪽으로 쏠리는 누적 오차를 줄이기 위함이라고 한다.

 

해결

먼저 부동소수점의 한계를 해결하기 위해 데이터를 `text`로 변환한 후에 `numeric`으로 변환했다.

그 후에 SQL의 `ROUND` 함수를 사용하지 않고, JavaScript에서 사용하는 사사오입 방식을 그대로 구현했다.

FLOOR(실수::text::numeric * 1000 + 0.5) / 1000