빌드노트
PostgreSQL에서 (1 / 2)::numeric이 0인 이유
PostgreSQL에서 (1 / 2)::numeric은 타입만 numeric인 0이다. 실제 값과 pg_typeof를 비교해 나눗셈 전에 변환할 위치를 찾고, NULLIF로 0 분모를 처리해도 정상 분모의 정수 나눗셈은 그대로라는 점을 확인한다.
PostgreSQL에서 정수끼리 나눈 결과를 numeric으로 바꿨는데도 소수가 나오지 않는다면, 캐스팅 위치를 확인해야 한다. (1 / 2)::numeric은 이미 정수 나눗셈으로 잘린 0의 타입만 바꾼다. 소수 비율이 필요하면 1::numeric / 2처럼 나눗셈에 들어가는 값을 먼저 바꾼다.
결과 타입이 numeric이어도 값은 이미 잘렸을 수 있다
PostgreSQL에 연결할 수 있는 psql이나 SQL 편집기가 있으면 다음 조회를 실행할 수 있다. 테이블은 필요 없다. 아래는 전용 PostgreSQL 18.6의 새 연결에서 합성 값만 조회한 결과다.
먼저 캐스팅 위치에 따른 값과 실제 결과 타입을 함께 비교한다.
SELECT 1 / 2 AS divided,
(1 / 2)::numeric AS cast_after,
1::numeric / 2 AS cast_before;
SELECT pg_typeof(1 / 2) AS divided_type,
pg_typeof((1 / 2)::numeric) AS after_type,
pg_typeof(1::numeric / 2) AS before_type;
첫 조회에서는 마지막 식만 소수 부분을 남겼다. 두 번째 조회에서는 뒤늦게 변환한 식도 numeric으로 나왔다.
divided | cast_after | cast_before
---------+------------+------------------------
0 | 0 | 0.50000000000000000000
(1 row)
divided_type | after_type | before_type
--------------+------------+-------------
integer | numeric | numeric
(1 row)
pg_typeof는 식의 실제 타입을 확인하는 함수다. 최종 결과가 numeric이라는 사실만으로 나눗셈도 numeric으로 수행됐다고 판단할 수는 없다. 자신의 조회에서는 / 바로 양쪽에 들어가는 식도 각각 pg_typeof로 확인한다. 분모가 계산식이나 집계식이면 원래 열이 아니라 그 식의 타입을 본다. PostgreSQL 18의 pg_typeof 설명 (새 탭에서 열림)
PostgreSQL은 입력 타입에 맞춰 연산자를 선택한다. 이 실험에서 1과 2는 모두 integer였고, 1 / 2의 결과도 integer였다. (1 / 2)::numeric은 이 정수 결과를 변환하므로 없어진 소수 부분을 되살리지 못한다. 반면 1::numeric / 2는 나눗셈의 한쪽 입력부터 numeric이다. 기본 수치 타입끼리의 연산에서 integer와 numeric을 함께 쓰면 numeric으로 계산한다. 연산자 선택 규칙 (새 탭에서 열림), 수치 연산자와 혼합 타입 규칙 (새 탭에서 열림)
정수 나눗셈은 반올림이 아니라 소수 부분을 0 방향으로 잘라 내는 연산이다. 별도로 실행한 (-5) / 2도 -2였다. 1 / 2::numeric처럼 분모를 먼저 바꾼 결과 역시 0.50000000000000000000이었다. 어느 쪽을 변환하든 바깥 결과가 아니라 해당 나눗셈의 피연산자를 바꾸는 것이 핵심이다. 정수 범위만 넓히는 1::bigint / 2::bigint는 여전히 0으로 나왔다. 정수 나눗셈 규칙 (새 탭에서 열림)
NULLIF는 0 분모를 처리하고, 캐스팅은 나눗셈 타입을 정한다
완료 건수 done을 전체 건수 total로 나눌 때는 소수 보존과 0 분모 처리를 따로 정해야 한다. 보호 없이 SELECT 1 / 0;을 실행한 별도 연결에서는 division by zero 오류와 SQLSTATE 22012가 나왔다.
전체 건수가 0일 때 비율을 계산 불가인 NULL로 남기려면 NULLIF(total, 0)을 분모에 쓸 수 있다. NULLIF는 두 값이 같으면 NULL, 다르면 첫 값을 반환한다. 여기처럼 두 인자가 모두 integer이면 그 결과 타입도 integer이므로, 이것만 추가해서는 정상 분모의 소수 비율까지 보존되지 않는다. NULLIF의 반환값과 타입 (새 탭에서 열림)
다음 조회로 정상 분모, 0, 누락된 분모를 비교하고 분자 캐스팅이 필요한 위치를 확인할 수 있다.
SELECT done, total,
done / NULLIF(total, 0) AS still_integer,
done::numeric / NULLIF(total, 0) AS ratio
FROM (VALUES (1, 2), (1, 0), (1, NULL::integer)) AS counts(done, total);
분모가 2일 때는 두 식의 값이 달랐고, 0이거나 NULL일 때는 두 식 모두 NULL이었다. 아래는 psql의 NULL 표시를 NULL로 둔 출력이다. 도구에서 빈칸으로 보이면 NULL 표시 설정도 확인한다.
done | total | still_integer | ratio
------+-------+---------------+------------------------
1 | 2 | 0 | 0.50000000000000000000
1 | 0 | NULL | NULL
1 | NULL | NULL | NULL
(3 rows)
자신의 조회에서는 done과 total 자리에 실제 분자·분모 식을 넣는다. 소수 비율을 원한다면 done::numeric / NULLIF(total, 0)처럼 나누기 전에 변환한다. 여기서 반환한 NULL은 비율 0과 의미가 다르다. 이를 화면에서 0으로 대체할지는 별도의 업무 규칙으로 정해야 한다.
입력 타입이 이미 numeric인데도 기대와 다르면 실제 분자·분모 값과 그 안의 앞선 정수 나눗셈을 확인한다. SQL 조회에서는 소수가 나오는데 화면에서만 정수로 보인다면, 이 캐스팅 위치 문제와 구분해 표시 형식을 확인한다.
직접 확인한 범위는 PostgreSQL 18.6의 기본 정수·numeric 연산자와 합성 SELECT다. 실제 테이블 변경은 하지 않았다. 저장 열의 numeric(p, s) 제약, 드라이버의 매개변수 타입과 애플리케이션 표시 형식은 이 실험에서 검증하지 않았다.