설명
RowBinary 포맷은 데이터를 바이너리 형식으로 행 단위로 파싱합니다.
행과 값은 구분자 없이 연속적으로 나열됩니다.
데이터가 바이너리 형식이기 때문에 FORMAT RowBinary 뒤에 오는 구분자는 다음과 같이 엄격하게 지정됩니다.
- 공백 문자는 개수 제한 없이 올 수 있습니다:
' '(공백 - 코드0x20)'\t'(탭 - 코드0x09)'\f'(폼 피드 - 코드0x0C)
- 그 뒤에는 정확히 하나의 줄바꿈 시퀀스가 와야 합니다:
- Windows 형식
"\r\n" - 또는 Unix 형식
'\n'
- Windows 형식
- 그리고 그 직후에 바이너리 데이터가 와야 합니다.
이 포맷은 행 기반이므로 Native 포맷보다 효율이 떨어집니다.
데이터 타입의 wire 형식
Unsigned LEB128 (리틀 엔디언 베이스 128)
String, Array, Map과 같은 가변 크기 데이터 타입의 길이를 인코딩하는 데 사용되는 부호 없는 리틀 엔디언 방식의 가변 길이 정수 인코딩입니다. 샘플 구현은 LEB128 위키 페이지에서 확인할 수 있습니다.
(U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
Int8부터 Int256까지)은 2의 보수 표현을 사용합니다. 대부분의 언어에서는 내장 도구 또는 널리 사용되는 라이브러리를 사용해 바이트 배열에서 이러한 정수를 추출할 수 있습니다. Int128/Int256 및 UInt128/UInt256는 대부분의 언어에서 지원하는 네이티브 정수 크기를 초과하므로, 사용자 정의 역직렬화가 필요할 수 있습니다.
Bool
UInt8와 유사한 방식으로 역직렬화할 수 있습니다.
0은false입니다1은true입니다
Float32, Float64
Float32는 4바이트, Float64는 8바이트로 인코딩되는 리틀 엔디언 부동소수점 값입니다. 정수와 마찬가지로 대부분의 언어는 이러한 값을 역직렬화할 수 있는 적절한 도구를 제공합니다.
BFloat16
BFloat16의 내부 값 예시:
Decimal32, Decimal64, Decimal128, Decimal256
Decimal32- 4바이트 또는Int32Decimal64- 8바이트 또는Int64Decimal128- 16바이트 또는Int128Decimal256- 32바이트 또는Int256
trunc는 0을 향해 버림을 수행하며(음수 값에서 결과가 달라지는 내림 나눗셈이 아닙니다), scale은 소수점 이하 자릿수입니다. 예를 들어 Decimal(10, 2)(Decimal32(2)와 동일함)의 경우 scale은 2이고, 값 12345는 (123, 45)로 표현됩니다.
직렬화에는 이의 역연산이 필요합니다:
String
- 문자열의 바이트 길이를 나타내는 가변 길이 정수(LEB128)
- 문자열의 실제 바이트 값
foobar는 다음과 같이 7바이트로 인코딩됩니다:
FixedString
String과 달리 FixedString은 스키마(schema)에 정의된 고정 길이를 가집니다. 바이트 시퀀스로 인코딩되며, 값의 길이가 N보다 짧으면 뒤쪽을 0 바이트로 채웁니다.
FixedString을 읽을 때 뒤쪽의 0 바이트는 패딩일 수도 있고 데이터에 포함된 실제 \0 문자일 수도 있으며, wire 상에서는 이를 구분할 수 없습니다. ClickHouse는 N 바이트 전체를 있는 그대로 보존합니다.FixedString(3)에는 패딩용 0만 포함됩니다:
hi가 들어 있는 비어 있지 않은 FixedString(3):
bar가 들어 있는 비어 있지 않은 FixedString(3):
Date
1970-01-01 이후 경과한 일수를 나타내는 UInt16(2바이트)로 저장됩니다.
지원되는 값 범위: [1970-01-01, 2149-06-06].
Date의 예시 내부 값:
Date32
1970-01-01을 기준으로 이전 또는 이후의 날짜 수를 나타내는 Int32(4바이트)로 저장됩니다.
지원되는 값 범위: [1900-01-01, 2299-12-31].
Date32의 내부 저장 값 예시:
DateTime
1970-01-01 00:00:00 UTC 이후부터의 초 수를 나타내는 UInt32(4바이트)로 저장됩니다.
구문:
DateTime 또는 DateTime('UTC')입니다.
바이너리 값은 항상 UTC epoch 오프셋입니다. 시간대는 인코딩을 바꾸지 않습니다. 하지만 시간대는 문자열 값을 삽입할 때 그 문자열이 해석되는 방식에 실제로 영향을 줍니다. 즉,
'2024-01-15 10:30:00'을 DateTime('America/New_York') 컬럼에 삽입하면 같은 문자열을 DateTime('UTC') 컬럼에 삽입할 때와는 다른 epoch 값이 저장됩니다. 이는 해당 문자열이 컬럼의 시간대에 따른 현지 시간으로 해석되기 때문입니다. 전송 시에는 둘 다 단순히 UInt32 epoch 초입니다.[1970-01-01 00:00:00, 2106-02-07 06:28:15].
DateTime의 내부 저장 값 예시:
DateTime64
1970-01-01 00:00:00 UTC를 기준으로 이전 또는 이후의 틱 수를 나타내는 Int64(8바이트)로 저장됩니다. 틱의 해상도는 precision 매개변수로 정의되며, 아래 구문을 참조하십시오:
precision은 0부터 9까지의 정수입니다. 일반적으로는 다음 값만 사용합니다: 3(밀리초), 6(마이크로초),
9(나노초).
유효한 DateTime64 정의 예시는 다음과 같습니다: DateTime64(0), DateTime64(3), DateTime64(6, 'UTC'), DateTime64(9, 'Europe/Amsterdam').
DateTime과 마찬가지로 바이너리 값은 항상 UTC epoch 기준 오프셋입니다. 시간대는 문자열 값을 삽입할 때 어떻게 해석할지에 영향을 주지만(DateTime 참고), 인코딩 자체는 항상 UTC epoch 이후의 Int64 틱입니다.DateTime64 유형의 내부 Int64 값은 UNIX epoch 이전 또는 이후의 다음 단위 수로 해석할 수 있습니다:
DateTime64(0)- 초.DateTime64(3)- 밀리초.DateTime64(6)- 마이크로초.DateTime64(9)- 나노초.
[0000-01-01 00:00:00, 9999-12-31 23:59:59.999999999] (precision이 7 이하인 경우이며, precision 8과 9는 더 좁습니다. 아래 참고 사항을 참조하십시오).
DateTime64의 내부 값 예시는 다음과 같습니다:
DateTime64(3): 값1546300800000은2019-01-01 00:00:00 UTC를 나타냅니다.DateTime64(6): 값1705314600123456은2024-01-15 10:30:00.123456 UTC를 나타냅니다.DateTime64(9): 값1705314600123456789은2024-01-15 10:30:00.123456789 UTC를 나타냅니다.
내부
Int64 틱 범위는 정밀도가 높을수록 더 좁아지므로 지원되는 최댓값도 줄어듭니다. precision 8에서는 4892-10-07이고, precision 9(나노초)에서는 UTC 기준 2262-04-11 23:47:16입니다.Time
Int32로 저장됩니다. 음수 값도 유효합니다.
지원되는 값 범위는 [-999:59:59, 999:59:59](즉, [-3599999, 3599999]초)입니다.
현재
Time 또는 Time64를 사용하려면 enable_time_time64_type 설정을 1로 지정해야 합니다.Time의 내부 저장 값 예시:
Time64
Decimal64(Int64로 저장됨)로 저장되며, 소수 초를 포함하는 시간 값을 나타냅니다. 정밀도는 구성할 수 있으며, 음수 값도 유효합니다.
구문:
precision은 0부터 9까지의 정수입니다. 일반적으로는 3(밀리초), 6(마이크로초), 9(나노초)를 사용합니다.
지원되는 값 범위: [-999:59:59.xxxxxxxxx, 999:59:59.xxxxxxxxx].
현재
Time 또는 Time64를 사용하려면 enable_time_time64_type 설정을 1로 지정해야 합니다.Int64 값은 10^precision 배로 스케일된 초의 소수 부분을 나타냅니다.
Time64의 내부 값 예시:
인터벌 타입
Int64(8바이트, 리틀 엔디언)로 저장됩니다. 값은 해당 시간 단위의 개수를 나타냅니다. 음수도 유효합니다.
인터벌 타입은 다음과 같습니다: IntervalNanosecond, IntervalMicrosecond, IntervalMillisecond, IntervalSecond, IntervalMinute, IntervalHour, IntervalDay, IntervalWeek, IntervalMonth, IntervalQuarter, IntervalYear.
인터벌 타입 이름(예:
IntervalSecond 또는 IntervalDay)이 저장된 값의 단위를 결정합니다. wire 인코딩은 항상 동일합니다.Enum8, Enum16
Enum8 == Int8) 또는 2바이트(Enum16 == Int16)로 저장됩니다. 저장 유형은 signed이므로 enum 값은 음수일 수 있습니다(예: Enum8('a' = -128, 'b' = 0)).
Enum은 다음과 같이 간단하게 정의할 수 있습니다:
\'와 같은 이스케이프된 기호와, 따옴표로 묶인 문자열 내부에 나타날 수 있는 =와 같은 특수 기호를 정확히 식별하는 것입니다.
UUID
UInt64 값으로 저장됩니다. 즉, 표준 UUID 표현에서 처음 8바이트는 바이트 순서가 반전되어 저장되며, 다음 8바이트도 별도로 바이트 순서가 반전되어 저장됩니다.
예를 들어 UUID 61f0c404-5cb3-11e7-907b-a6006ad3dba0가 주어졌을 때:
- 표준 바이트 표현:
61 f0 c4 04 5c b3 11 e7|90 7b a6 00 6a d3 db a0 - 앞쪽 절반 반전(LE UInt64):
e7 11 b3 5c 04 c4 f0 61 - 뒤쪽 절반 반전(LE UInt64):
a0 db d3 6a 00 a6 7b 90
UUID의 예시 내부 값:
61f0c404-5cb3-11e7-907b-a6006ad3dba0는 다음과 같이 표현됩니다:
- 기본 UUID
00000000-0000-0000-0000-000000000000은 0으로 채워진 16바이트로 표현됩니다:
IPv4
UInt32로 리틀 엔디언 바이트 순서로 저장됩니다. 이는 IP 주소에 일반적으로 사용되는 기존 네트워크 바이트 순서(빅 엔디언)와 다릅니다. IPv4의 예시 내부 값은 다음과 같습니다:
IPv6
IPv6의 예시 내부 값은 다음과 같습니다:
널 허용
- 값이
NULL인지 여부를 나타내는 1바이트:0x00은 값이NULL이 아님을 의미합니다.0x01은 값이NULL임을 의미합니다.
- 값이
NULL이 아니면 기반 데이터 타입이 평소와 같이 인코딩됩니다. 값이NULL이면 기반 타입에는 추가 바이트를 전혀 기록하지 않습니다.
Nullable(UInt32) 값은 다음과 같습니다.
LowCardinality
LowCardinality(String)은 일반 String과 동일한 방식으로 인코딩됩니다.
컬럼은
LowCardinality(Nullable(T))로 정의할 수 있지만 Nullable(LowCardinality(T))로는 정의할 수 없습니다. 이렇게 정의하면 항상 서버에서 오류가 발생합니다.1로 설정하여 LowCardinality 내부에서 대부분의 데이터 타입을 허용할 수 있습니다.
배열
- 배열의 요소 개수를 나타내는 가변 길이 정수(LEB128)
- 기반 데이터 타입과 동일한 방식으로 인코딩된 배열의 요소
UInt32 값으로 이루어진 배열은 다음과 같습니다:
배열에는 널 허용 값을 포함할 수 있지만, 배열 자체는 널 허용일 수 없습니다.
Tuple
맵
Array(Tuple(K, V))로 볼 수 있으며, 여기서 K는 키 타입이고 V는 값 타입입니다. 맵은 다음과 같이 인코딩됩니다.
- 맵의 요소 개수를 나타내는 가변 길이 정수(LEB128)
- 각 타입에 따라 인코딩된 key-value 쌍 형태의 맵 요소
String이고 값이 UInt32인 맵은 다음과 같습니다.
Map(String, Map(Int32, Array(Nullable(String))))와 같이 깊이 중첩된 구조의 맵도 가능하며, 위에서 설명한 것과 유사한 방식으로 인코딩됩니다.Variant
Variant(T1, T2, ..., TN)은 이 타입의 각 행이 T1, T2, …, TN 중 하나의 타입 값을 가지거나, 어느 타입에도 속하지 않는 값(NULL 값)을 가질 수 있음을 의미합니다.
다음 예시를 살펴보겠습니다.
NULL 값은 0xFF 판별자 바이트로 인코딩됩니다:
Variant 타입을 더 철저하게 테스트할 수 있습니다.
Dynamic
Dynamic 타입은 런타임에 결정되는 임의의 타입의 값을 담을 수 있습니다. RowBinary format에서는 각 값이 자체적으로 타입 정보를 포함합니다. 즉, 첫 번째 부분에는 이 포맷으로 표현된 타입 지정이 들어갑니다. 그 뒤에는 이 문서에 설명된 방식으로 인코딩된 값의 내용이 이어집니다. 따라서 값을 파싱하려면 타입 인덱스를 사용해 적절한 parser를 결정한 다음, 이미 다른 곳에서 사용 중인 RowBinary 파싱 로직을 재사용하면 됩니다.
BinaryTypeIndex는 타입을 식별하는 1바이트 값입니다. 타입 인덱스와 매개변수는 여기의 참고 문서를 확인하십시오.
NULL Dynamic 값은 추가 바이트 없이 BinaryTypeIndex 0x00(Nothing 타입)로 인코딩됩니다:
JSON
- 타입이 명시된 경로 - 스키마에 타입을 명시해 선언한 경로(예:
JSON(user_id UInt32, name String)) - 동적 경로 한도를 초과했을 때의 Dynamic 경로/오버플로우 경로 - 런타임에 발견되어
Dynamic유형으로 저장되는 경로입니다. 값 인코딩 앞에는 유형 정의가 먼저 옵니다.
경로는 세 개의 그룹으로 직렬화되어 순차적으로 기록됩니다: 타입이 지정된 경로(typed paths), 동적 경로(dynamic paths), 공유 데이터(shared data) 오버플로우 경로 순입니다. 타입이 지정된 경로와 동적 경로는 구현 정의 순서(내부 해시맵 반복에 의해 결정)로 기록되며, 공유 데이터 경로는 알파벳 순으로 기록됩니다. 특정 경로 순서에 의존해서는 안 됩니다. 역직렬화기는 각 경로를 위치가 아닌 이름을 기준으로 처리합니다.
RowBinary 포맷에서 각 JSON 행은 다음과 같이 직렬화됩니다:
JSON(user_id UInt32, active Bool)
행: {"user_id": 42, "active": true}
바이너리 인코딩(어노테이션이 포함된 16진수):
JSON(user_id UInt32, active Bool)
행: {"user_id": 42, "active": true, "name": "Alice"}
바이너리 인코딩 (어노테이션이 포함된 16진수):
JSON(score Nullable(Int32))
행: {"score": null }
바이너리 인코딩(어노테이션이 포함된 16진수):
JSON(name String)
행: {"name": null}
바이너리 인코딩:
JSON(id UInt64)
행: {"id": 100, "metadata": null}
바이너리 인코딩:
metadata 경로는 동적 경로가 null이 아닐 때만 직렬화되기 때문에 포함되지 않습니다. 이는 타입이 지정된 경로와의 핵심적인 차이점입니다.
4. 중첩된 JSON 객체(Nested JSON objects):
스키마: JSON()
행: {"user": {"name": "Bob", "age": 30}}
바이너리 인코딩(어노테이션 포함 16진수):
user.name)로 평탄화됩니다.
대안: JSON을 String으로 사용하는 모드
설정 output_format_binary_write_json_as_string=1을 사용하면 JSON 컬럼은 구조화된 바이너리 형식 대신 하나의 JSON 텍스트 문자열로 직렬화됩니다. JSON 컬럼에 쓸 때 사용하는 대응 설정 input_format_binary_read_json_as_string도 있습니다. 여기서 어떤 설정을 사용할지는 JSON을 클라이언트에서 파싱할지, 서버에서 파싱할지에 따라 달라집니다.
Geo 타입
Point-Tuple(Float64, Float64)로 표현됩니다.Ring-Array(Point)또는Array(Tuple(Float64, Float64))로 표현됩니다.Polygon-Array(Ring)또는Array(Array(Tuple(Float64, Float64)))로 표현됩니다.MultiPolygon-Array(Polygon)또는Array(Array(Array(Tuple(Float64, Float64))))로 표현됩니다.LineString-Array(Point)또는Array(Tuple(Float64, Float64))로 표현됩니다.MultiLineString-Array(LineString)또는Array(Array(Tuple(Float64, Float64)))로 표현됩니다.
RowBinaryWithNamesAndTypes 포맷 헤더에는 예를 들어 Point, Ring, Polygon, MultiPolygon, LineString, MultiLineString과 같은 이들 타입의 별칭이 포함됩니다.
Geometry
Geometry는 위에 나열된 모든 Geo 타입을 담을 수 있는 Variant 타입입니다. wire 형식에서는 뒤에 오는 geo 타입을 나타내는 판별자 바이트를 포함해 Variant와 완전히 동일한 방식으로 인코딩됩니다.
Geometry의 판별자 인덱스는 다음과 같습니다:
wire 형식 구조:
Point를 Geometry로 인코딩한 예:
Ring을 Geometry로 인코딩한 예입니다:
Nested
Nested의 wire 형식은 flatten_nested 설정에 따라 달라집니다.
flatten_nested = 1 (기본값)
Nested가 개별 배열로 평탄화됩니다. 각 하위 컬럼은 점(.)으로 구분된 이름의 별도 Array 컬럼이 됩니다:
DESCRIBE TABLE foo는 평탄화된 컬럼을 표시합니다:
flatten_nested = 0
flatten_nested = 0으로 설정하면 Nested는 Array(Tuple(...)) 유형의 단일 컬럼으로 유지됩니다. 컬럼 이름은 점으로 구분하지 않습니다:
DESCRIBE TABLE foo는 컬럼 하나를 표시합니다:
Array(Tuple(String, Int32))입니다: 먼저 배열 길이 접두사가 오고, 그다음 각 요소의 튜플 필드가 순서대로 옵니다:
SimpleAggregateFunction
SimpleAggregateFunction(func, T)는 기반 데이터 타입인 T와 동일한 방식으로 인코딩됩니다. 집계 함수 이름은 wire 형식에 영향을 주지 않습니다.
예를 들어, SimpleAggregateFunction(max, UInt32)는 일반 UInt32와 동일한 방식으로 인코딩됩니다:
SimpleAggregateFunction(max, UInt32)로 보고하지만, 실제 wire 형식의 값은 그냥 UInt32입니다:
AggregateFunction
AggregateFunction(func, T)는 집계 함수의 전체 중간 상태를 저장합니다. 중간 상태를 저장하되 이를 기본 데이터 타입과 동일한 방식으로 인코딩하는 SimpleAggregateFunction과 달리, AggregateFunction은 각 집계 함수마다 포맷이 다른 불투명한 바이너리 blob을 저장합니다.
내부 포맷은 함수마다 다릅니다. 몇 가지 간단한 예시는 다음과 같습니다.
countState — count를 VarUInt(LEB128)로 저장합니다:
sumState — 누적 합계를 고정 크기 정수에 저장합니다. 너비는 인수 유형에 따라 달라지며(정수 인수의 경우 UInt64):
minState / maxState — 먼저 플래그 바이트를 저장한 다음, 기본 데이터 유형의 값을 저장합니다. 플래그는 빈 state(값이 없음)일 때 0x00, 값이 있을 때 0x01입니다:
uniq, quantile, groupArray와 같은 더 복잡한 함수는 구현별 포맷을 사용합니다. 이러한 상태를 읽거나 써야 한다면, 해당 함수의 ClickHouse 소스 코드를 참조하십시오.QBit
QBit은 다양한 수준의 정밀도로 효율적인 조회를 지원하는 벡터 타입입니다. 내부적으로는 전치된 포맷으로 저장됩니다. 전송 시 QBit은 단순히 기반 요소 타입(Int8, Float32, Float64 또는 BFloat16)의 배열입니다. 저장을 위한 비트 전치 최적화는 RowBinary 프로토콜이 아니라 server 측에서 수행됩니다.
구문:
element_type은 Int8, Float32, Float64 또는 BFloat16이고, dimension은 고정된 벡터 차원입니다. 선택적 stride는 비트 평면이 server 측에서 저장 스트림으로 그룹화되는 방식만 제어하며, 항상 dimension개 원소로 이루어진 전체 배열인 RowBinary wire 형식에는 영향을 주지 않습니다.
wire 형식: Array(element_type)와 동일합니다:
QBit(Float32, 4)로 [1.0, 2.0, 3.0, 4.0]를 인코딩한 예시는 다음과 같습니다:
포맷 설정
RowBinary 계열 포맷에 공통으로 적용됩니다.