숫자 타입

숫자 타입 (Numeric Types)

숫자 타입은 숫자를 저장하는 데 사용되며, 모양과 크기가 다양해요. 고정 너비 정수부터 부동소수점, 고정소수점 DECIMAL, UUID까지 DuckDB의 숫자 타입을 함께 정리해 볼까요?

출처: 문서

본문

고정 너비 정수 타입 (Fixed-Width Integer Types)

TINYINT, SMALLINT, INTEGER, BIGINT, HUGEINT 타입은 다양한 범위의 정수, 즉 분수 성분이 없는 숫자를 저장해요. 허용 범위를 벗어난 값을 저장하려 하면 오류가 발생해요. UTINYINT, USMALLINT, UINTEGER, UBIGINT, UHUGEINT 타입은 부호 없는 정수를 저장해요. 음수나 허용 범위를 벗어난 값을 저장하려 하면 오류가 발생해요.

이름 별칭 (Aliases) Min Max 바이트 크기 (Size in bytes)
TINYINT INT1 - 2^7 2^7 - 1 1
SMALLINT INT2, INT16, SHORT - 2^15 2^15 - 1 2
INTEGER INT4, INT32, INT, SIGNED - 2^31 2^31 - 1 4
BIGINT INT8, INT64, LONG - 2^63 2^63 - 1 8
HUGEINT INT128 - 2^127 2^127 - 1 16
UTINYINT UINT8 0 2^8 - 1 1
USMALLINT UINT16 0 2^16 - 1 2
UINTEGER UINT32 0 2^32 - 1 4
UBIGINT UINT64 0 2^64 - 1 8
UHUGEINT UINT128 0 2^128 - 1 16

INT8은 64비트 정수이고, 부호 없는 8비트 정수인 UINT8의 부호 있는 등가물이 아니에요. 부호 있는 정수의 타입 별칭 INT1, INT2, INT4, INT8은 PostgreSQL에서 물려받은 것으로, 그 이름 속의 숫자는 바이트 단위의 크기를 나타내요. 반면 부호 없는 등가물의 타입 별칭 UINT8, UINT16, UINT32, UINT64는 C/C++ 관례를 따라 비트 단위의 크기를 나타내요.

INTEGER 타입이 범위, 저장 크기, 성능 사이의 최적의 균형을 제공하므로 보통 선택돼요. SMALLINT 타입은 일반적으로 디스크 공간이 부족할 때만 사용돼요. BIGINTHUGEINT 타입은 INTEGER 타입의 범위가 충분하지 않을 때 사용하도록 설계되었어요.

가변 길이 정수 (Variable-Length Integers)

앞서 언급한 정수 타입들은 모두 최소/최대 범위 안의 숫자가 같은 저장 크기를 갖는다는 공통점이 있어요. UTINYINT는 1바이트, SMALLINT는 2바이트 등이에요. 하지만 가끔은 HUGEINT가 지원하는 것보다도 더 큰 숫자가 필요할 때가 있어요! 이런 상황에서는 BIGNUM 타입을 사용할 수 있어요. BIGNUM은 다른 정수 타입과 비슷하게 양수를 저장하지만, 필요한 크기와 부호 비트를 저장하기 위해 추가로 3바이트를 더 사용해요. N개의 십진 자릿수를 가진 숫자는 BIGNUM에 저장될 때 대략 0.415 * N + 3 바이트가 필요해요.

다른 시스템의 가변 길이 정수 구현과 달리 BIGNUM에는 한계가 있어요: 표현 가능한 최대/최소 값은 대략 ±4.27e20201778이에요. 이것은 20,201,779개의 십진 자릿수를 가진 숫자로, 그런 숫자 하나를 저장하는 데 8 메가바이트가 필요해요.

고정소수점 DECIMAL (Fixed-Point Decimals)

DECIMAL(WIDTH, SCALE) 데이터 타입(별칭 NUMERIC(WIDTH, SCALE)로도 사용 가능)은 정확한 고정소수점 십진값을 나타내요. DECIMAL 타입의 값을 만들 때 WIDTHSCALE을 지정해서 그 필드에 담을 수 있는 십진값의 크기를 정의할 수 있어요. WIDTH 필드는 몇 자릿수를 담을 수 있는지 결정하고, scale은 소수점 뒤의 자릿수를 결정해요. 예를 들어 DECIMAL(3, 2) 타입은 1.23을 담을 수 있지만 12.3이나 1.234는 담을 수 없어요. 아무것도 지정하지 않으면 기본 WIDTHSCALEDECIMAL(18, 3)이에요. WIDTH만 지정하면 SCALE0이 기본이에요 — 예를 들어 DECIMAL(18)DECIMAL(18, 0)과 같고 소수 자릿수를 버려요.

WIDTHSCALE 지정 (Specifying the WIDTH and the SCALE)

두 파라미터 모두 선택 사항이고, WIDTH 뒤의 후행 쉼표는 허용돼요. 따라서 다음 철자들은 모두 구체적인 십진 타입에 바인딩돼요:

SELECT
    typeof(1.5::DECIMAL)      AS "DECIMAL",
    typeof(1.5::DECIMAL())    AS "DECIMAL()",
    typeof(1.5::DECIMAL(10))  AS "DECIMAL(10)",
    typeof(1.5::DECIMAL(10,)) AS "DECIMAL(10,)";
DECIMAL DECIMAL() DECIMAL(10) DECIMAL(10,)
DECIMAL(18,3) DECIMAL(18,3) DECIMAL(10,0) DECIMAL(10,0)

NUMERIC 별칭도 같은 철자를 받아들이고 같은 타입을 산출해요.

주의: PostgreSQL과 Oracle에서처럼, 베어 DECIMAL은 무제약 십진 타입이 아니에요. DuckDB는 항상 그것을 구체적인 타입 DECIMAL(18, 3)에 바인딩하는데, 이는 소수 자릿수 3개로 반올림하고 소수점 앞에 최대 15자리를 담아요.

WIDTH는 1과 38 사이여야 하고, SCALE은 0과 38 사이어야 하며 WIDTH를 초과할 수 없어요. 이 범위를 벗어난 값은 클램프되는 대신 바인더가 거부해요:

SELECT 1::DECIMAL(39, 2);
Binder Error:
DECIMAL type width must be between 1 and 38
SELECT 1::DECIMAL(18, 20);
Binder Error:
DECIMAL type scale cannot be greater than width

PostgreSQL과 Oracle과 달리 DuckDB는 음수 SCALE을 지원하지 않아요:

SELECT 1::DECIMAL(18, -1);
Binder Error:
DECIMAL type scale must be between 0 and 38

산술과 내부 표현 (Arithmetic and Internal Representation)

두 고정소수점 십진의 덧셈, 뺄셈, 곱셈은 정확한 결과를 담기에 필요한 WIDTHSCALE을 가진 또 다른 고정소수점 십진을 반환하거나, 필요한 WIDTH가 최대 지원 WIDTH(현재 38)를 초과하면 오류를 던져요.

고정소수점 십진의 나눗셈은 보통 유한한 십진 전개를 가진 숫자를 만들지 않아요. 따라서 DuckDB는 고정소수점 십진이 포함된 모든 나눗셈에 근사 부동소수점 연산을 사용하고, 그에 따라 부동소수점 데이터 타입을 반환해요.

내부적으로 십진은 지정된 WIDTH에 따라 정수로 표현돼요.

Width 내부 (Internal) 크기 (Size, bytes)
1-4 INT16 2
5-9 INT32 4
10-18 INT64 8
19-38 INT128 16

필요하지 않은데 너무 큰 십진을 사용하면 성능에 영향을 줄 수 있어요. 특히 width가 19를 넘는 십진 값은 느린데, INT128 타입을 포함한 산술이 INT32INT64 타입을 포함한 연산보다 훨씬 비싸기 때문이에요. 그래서 이것이 부족하다는 충분한 이유가 없다면 WIDTH 18 이하를 유지하는 것이 권장돼요.

부동소수점 타입 (Floating-Point Types)

FLOATDOUBLE 데이터 타입은 가변 정밀도 숫자 타입이에요. 실제로 이 타입들은 보통 기저 프로세서, 운영 체제, 컴파일러가 지원하는 범위 내에서 IEEE Standard 754 이진 부동소수점 산술을 구현한 것이에요 (각각 단정밀도와 배정밀도).

이름 별칭 (Aliases) 설명 (Description)
FLOAT FLOAT4, REAL 단정밀도 부동소수점 숫자 (4바이트)
DOUBLE FLOAT8 배정밀도 부동소수점 숫자 (8바이트)

고정소수점 데이터 타입처럼, 리터럴이나 다른 데이터 타입에서 부동소수점 타입으로의 변환은 정확히 표현할 수 없는 입력을 근사로 저장해요. 그러나 어떤 입력이 이 영향을 받는지 예측하기는 더 어려워요. 예를 들어 1.3::DECIMAL(1, 0) - 0.7::DECIMAL(1, 0) != 0.6::DECIMAL(1, 0)인 것은 놀랍지 않지만, 1.3::FLOAT - 0.7::FLOAT != 0.6::FLOAT인 것은 놀랄 수 있어요.

또한 고정소수점 십진 데이터 타입의 곱셈, 덧셈, 뺄셈은 정확한 반면, 부동소수점 이진 데이터 타입에서는 이 연산들이 근사일 뿐이에요.

하지만 더 복잡한 수학 연산에서는 내부적으로 부동소수점 산술이 사용되고, 중간 단계를 입출력과 같은 width의 고정소수점 형식으로 캐스팅하지 않으면 더 정확한 결과를 얻을 수 있어요. 예를 들어 (10::FLOAT / 3::FLOAT)::FLOAT * 3 = 10인 반면 (10::DECIMAL(18, 3) / 3::DECIMAL(18, 3))::DECIMAL(18, 3) * 3 = 9.999예요.

일반적으로:

대부분의 플랫폼에서 FLOAT 타입은 최소 6자리 십진수 정밀도로 적어도 1E-37에서 1E+37 범위를 가져요. DOUBLE 타입은 보통 최소 15자리 정밀도로 대략 1E-307에서 1E+308 범위를 가져요. 이 범위 밖의 양수(및 대칭 범위 밖의 음수)는 일부 플랫폼에서 오류를 일으킬 수 있지만 보통 각각 0이나 무한대로 변환돼요.

일반적인 숫자 값 외에도 부동소수점 타입은 IEEE 754 특수 값을 나타내는 몇 가지 특수 값이 있어요:

  • Infinity: 무한대
  • -Infinity: 음의 무한대
  • NaN: 숫자가 아님 (not a number)

필요한 CPU/FPU 지원이 있는 머신에서 DuckDB는 두 가지 예외를 제외하고 이 특수 값들에 대해 IEEE 754 사양을 따르지 않아요:

  • NaNNaN과 같다고 비교되고 다른 어떤 부동소수점 숫자보다 크다고 비교돼요.
  • sqrt/sin/asin 같은 일부 부동소수점 함수는 정의 범위 밖의 값에 대해 NaN을 반환하는 대신 오류를 던져요.

이 값들을 SQL 명령에서 리터럴로 삽입하려면 따옴표로 감싸야 하고, InfinityInf로 줄여 쓸 수 있으며 대문자/소문자는 아무거나 사용할 수 있어요. 예를 들어:

SELECT
    sqrt(2) > '-inf',
    'nan' > sqrt(2);
(sqrt(2) > '-inf') ('nan' > sqrt(2))
true true

범용 고유 식별자 (UUID)

DuckDB는 UUID 타입을 통해 범용 고유 식별자 (UUID)를 지원해요. 이들은 128비트를 사용하며 내부적으로 HUGEINT 값으로 표현돼요. 출력될 때는 소문자 16진수 문자로, 대시로 구분되어 다음과 같이 표시돼요: ⟨12345678⟩-⟨1234⟩-⟨1234⟩-⟨1234⟩-⟨1234567890ab⟩{:.language-sql .highlight} (대시 포함 총 36문자). 예를 들어 4ac7a9e9-607c-4c8a-84f3-843f0191e3fd는 유효한 UUID예요.

DuckDB는 UUIDv4와 UUIDv7 식별자 생성을 지원해요. UUID 값의 버전을 얻으려면 [uuid_extract_version 함수]({% link docs/current/sql/functions/utility.md %}#uuid_extract_versionuuid)를 사용하세요.

UUIDv4

UUIDv4 값을 생성하려면 [uuid() 함수]({% link docs/current/sql/functions/utility.md %}#uuid)나 그 별칭인 [uuidv4()]({% link docs/current/sql/functions/utility.md %}#uuidv4)와 [gen_random_uuid()]({% link docs/current/sql/functions/utility.md %}#gen_random_uuid) 함수를 사용하세요.

UUIDv7

UUIDv7 값을 생성하려면 [uuidv7()]({% link docs/current/sql/functions/utility.md %}#uuidv7) 함수를 사용하세요. UUIDv7 값에서 타임스탬프를 얻으려면 [uuid_extract_timestamp 함수]({% link docs/current/sql/functions/utility.md %}#uuid_extract_timestampuuidv7)를 사용하세요:

SELECT uuid_extract_timestamp(uuidv7()) AS ts;
ts
2025-04-19 15:51:20.07+00

반올림 (Rounding)

부동소수점이나 고정소수점 타입에서 정수로의 캐스팅은 원본 숫자를 반올림하는 것을 의미해요.

다음 기본 반올림 로직이 적용돼요:

  • FLOATDOUBLE에서 어떤 크기의 정수로의 캐스팅: 가장 가까운 정수로 반올림하며, 절반(ties)은 가장 가까운 짝수로 반올림해요 (banker's rounding).

    SELECT 2.5::FLOAT::INTEGER AS result;  -- round to even
    
    ┌────────┐
    │ result │
    │ int32  │
    ├────────┤
    │      2 │
    └────────┘
    
  • DECIMAL에서 어떤 크기의 정수로의 캐스팅: 가장 가까운 정수로 반올림하며, 절반은 0에서 멀어지는 방향으로 반올림해요.

    SELECT 2.5::DECIMAL::INTEGER AS result;  -- round away from zero
    SELECT 2.5::INTEGER AS result;           -- number literals are treated as DECIMALs
    
    ┌────────┐
    │ result │
    │ int32  │
    ├────────┤
    │      3 │
    └────────┘
    

비기본 반올림 로직이 필요하다면 ceil, floor, round, round_even 같은 함수를 사용해야 해요.

함수 (Functions)

[Numeric 함수와 연산자]({% link docs/current/sql/functions/numeric.md %})를 참고하세요.

더 알아보기 (Learn more)