Native 형식
Native 형식
Native 형식은 ClickHouse에서 가장 효율적인 형식이에요. 컬럼을 행으로 변환하지 않는 진정한 "컬럼형(columnar)" 형식이라서, 블록 단위로 바이너리 형식으로 데이터를 쓰고 읽어요. 서버 간 상호작용, 커맨드라인 클라이언트, C++ 클라이언트를 위한 네이티브 인터페이스에서 사용되는 형식입니다.
출처: 문서
본문
| Input | Output | Alias |
|---|---|---|
| ✔ | ✔ |
설명 (Description)
Native 형식의 전체 공식 사양은 여기에서, 이를 운반하는 TCP 와이어 프로토콜인 Native 프로토콜의 동반 사양은 여기에서 확인할 수 있어요. 두 사양 모두 ClickHouse 소스 코드에서 LLM으로 생성되었습니다. 코드가 여전히 주요 진실의 원천입니다. 사양과 코드가 불일치하면 코드가 올바릅니다.
Native 형식은 컬럼을 행으로 변환하지 않는다는 점에서 진정한 "컬럼형"이기 때문에 ClickHouse에서 가장 효율적인 형식입니다.
이 형식에서는 데이터가 블록 단위로 바이너리 형식으로 쓰고 읽습니다. 각 블록에 대해 블록의 행 수, 컬럼 수, 컬럼 이름과 타입, 컬럼의 일부가 차례로 기록됩니다.
이것은 서버 간 상호작용, 커맨드라인 클라이언트 사용, C++ 클라이언트를 위한 네이티브 인터페이스에서 사용되는 형식입니다. 이 형식을 사용하여 ClickHouse DBMS에서만 읽을 수 있는 덤프를 빠르게 생성할 수 있어요. 이 형식을 직접 다루는 것은 실용적이지 않을 수 있습니다.
데이터 타입 와이어 형식 (Data types wire format)
데이터는 컬럼형 형식으로 와이어로 전송되며, 이는 각 컬럼이 별도로 전송되고 한 컬럼의 모든 값이 단일 배열로 함께 전송된다는 뜻입니다.
블록의 각 컬럼에는 RowBinaryWithNamesAndTypes와 유사한 헤더가 포함됩니다. 네이티브 TCP 바이너리 프로토콜을 사용할 때(또는 HTTP 엔드포인트가 ?client_protocol_version=
블록 구조 (Block structure)
다음 쿼리는 number와 str 두 컬럼, 세 개의 행을 반환합니다:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str FROM system.numbers LIMIT 3" > out.bin
출력 데이터는 단일 ClickHouse 블록에 맞으며, 다음과 같이 보입니다:
const data = new Uint8Array([
// --- Block Header ---
0x02, // 2 columns
0x03, // 3 rows
// -- Column 1 Header --
0x06, // LEB128 - column name 'number' has 6 bytes
0x6e, 0x75, 0x6d,
0x62, 0x65, 0x72, // column name: 'number'
0x06, // LEB128 - column type 'UInt64' has 6 bytes
0x55, 0x49, 0x6e,
0x74, 0x36, 0x34, // 'UInt64'
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 0 as UInt64
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 1 as UInt64
0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 2 as UInt64
0x03, // LEB128 - column name 'str' has 3 bytes
0x73, 0x74, 0x72, // column name: 'str'
0x06, // LEB128 - column type 'String' has 6 bytes
0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, // 'String'
0x01, // LEB128 - the string has 1 byte
0x30, // '0' as String
0x01, // LEB128 - the string has 1 byte
0x31, // '1' as String
0x01, // LEB128 - the string has 1 byte
0x32, // '2' as String
])
여러 블록 (Multiple blocks)
그러나 많은 경우 데이터가 단일 블록에 맞지 않으며, ClickHouse는 데이터를 여러 블록으로 보냅니다. 블록 크기를 줄여 행 하나당 블록 하나로 데이터를 강제 분할하는 두 행을 가져오는 다음 쿼리를 생각해 보세요:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str FROM system.numbers LIMIT 2 SETTINGS max_block_size=1" \ > out.bin
출력:
const data = new Uint8Array([
// ----- Block 1 -----
0x02, // 2 columns
0x01, // 1 row
0x06, // LEB128 - column name 'number' has 6 bytes
0x6E, 0x75, 0x6D,
0x62, 0x65, 0x72, // column name: 'number'
0x06, // LEB128 - column type 'UInt64' has 6 bytes
0x55, 0x49, 0x6E,
0x74, 0x36, 0x34, // 'UInt64'
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 0 as UInt64
0x03, // LEB128 - column name 'str' has 3 bytes
0x73, 0x74, 0x72, // column name: 'str'
0x06, // LEB128 - column type 'String' has 6 bytes
0x53, 0x74, 0x72,
0x69, 0x6E, 0x67, // 'String'
0x01, // LEB128 - the string has 1 byte
0x30, // '0' as String
// ----- Block 2 -----
0x02, // 2 columns
0x01, // 1 row
0x06, // LEB128 - column name 'number' has 6 bytes
0x6E, 0x75, 0x6D,
0x62, 0x65, 0x72, // column name: 'number'
0x06, // LEB128 - column type 'UInt64' has 6 bytes
0x55, 0x49, 0x6E,
0x74, 0x36, 0x34, // 'UInt64'
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 1 as UInt64
0x03, // LEB128 - column name 'str' has 3 bytes
0x73, 0x74, 0x72, // column name: 'str'
0x06, // LEB128 - column type 'String' has 6 bytes
0x53, 0x74, 0x72,
0x69, 0x6E, 0x67, // 'String'
0x01, // LEB128 - the string has 1 byte
0x31, // '1' as String
]);
단순 데이터 타입 (Simple data types)
더 단순한 데이터 타입 중 하나의 개별 값에 대한 와이어 형식은 RowBinary/RowBinaryWithNamesAndTypes와 유사합니다.
이 설명에 맞는 타입의 전체 목록은 다음과 같습니다:
- (U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
- Float32, Float64
- Bool
- String
- FixedString(N)
- Date
- Date32
- DateTime
- DateTime64
- IPv4
- IPv6
- UUID
자세한 내용은 "RowBinary 데이터 타입 와이어 형식"의 위 타입 설명을 참조하세요.
복잡한 데이터 타입 (Complex data types)
다음 타입의 인코딩은 RowBinary 및 RowBinaryWithNamesAndTypes와 다릅니다.
- Nullable
- LowCardinality
- Array
- Map
- Variant
- Dynamic
- JSON
Nullable
Native 형식에서 nullable 컬럼은 실제 데이터 앞에 블록의 행 수와 같은 바이트 수를 갖습니다. 각 바이트는 값이 NULL인지 여부를 나타냅니다. 예를 들어 이 쿼리를 사용하면 각 홀수가 대신 NULL이 됩니다:
curl -XPOST "http://localhost:8123?default_format=Native" \ --data-binary "SELECT if(number % 2 = 0, number, NULL) :: Nullable(UInt64) AS maybe_null FROM system.numbers LIMIT 5" \ > out.bin
출력은 다음과 같습니다:
const data = new Uint8Array([
// --- Block Header ---
0x01, // LEB128 - 1 column
0x05, // LEB128 - 5 rows
// -- Column Header --
0x0A, // LEB128 - column name has 10 bytes
0x6D, 0x61, 0x79, 0x62, 0x65,
0x5F, 0x6E, 0x75, 0x6C, 0x6C, // column name: 'maybe_null'
0x10, // LEB128 - column type has 16 bytes
0x4E, 0x75, 0x6C, 0x6C,
0x61, 0x62, 0x6C, 0x65,
0x28, 0x55, 0x49, 0x6E,
0x74, 0x36, 0x34, 0x29, // column type: 'Nullable(UInt64)'
// -- Nullable mask --
0x00, // Row 0 is NOT NULL
0x01, // Row 1 is NULL
0x00, // Row 2 is NOT NULL
0x01, // Row 3 is NULL
0x00, // Row 4 is NOT NULL
// -- UInt64 values --
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row 0: 0 as UInt64
// even though we still might have a proper value for this number
// in the block, it should be still returned as NULL to the user!
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #1: NULL
0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #2: 2 as UInt64
0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #3: NULL, similar to Row #1
0x04, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #4: 4 as UInt64
]);
Nullable(String)에서도 유사하게 작동합니다. null 표시기는 항상 nullable 마스크 바이트에서 나옵니다 — 마스크 값 0x01은 문자열 내용과 관계없이 행이 NULL임을 뜻합니다. NULL 행의 경우 기본 문자열은 빈 문자열(LEB128 길이 0)로 저장됩니다. 비-NULL 빈 문자열도 LEB128 길이 0을 가지므로 두 경우를 구분하는 것은 마스크 바이트뿐이라는 점에 유의하세요. 예를 들어 다음 쿼리:
curl -XPOST "http://localhost:8123?default_format=Native" \ --data-binary "SELECT if(number % 2 = 0, toString(number), NULL) :: Nullable(String) AS maybe_str FROM system.numbers LIMIT 5" \ > out.bin
출력은 다음과 같습니다:
const data = new Uint8Array([
// --- Block Header ---
0x01, // LEB128 - 1 column
0x05, // LEB128 - 5 rows
// -- Column Header --
0x09, // LEB128 - column name has 9 bytes
0x6d,
0x61,
0x79,
0x62,
0x65,
0x5f,
0x73,
0x74,
0x72, // column name: 'maybe_str'
0x10, // LEB128 - column type has 16 bytes
0x4e,
0x75,
0x6c,
0x6c,
0x61,
0x62,
0x6c,
0x65,
0x28,
0x53,
0x74,
0x72,
0x69,
0x6e,
0x67,
0x29, // column type: 'Nullable(String)'
// -- Nullable mask --
0x00, // Row 0 is NOT NULL
0x01, // Row 1 is NULL
0x00, // Row 2 is NOT NULL
0x01, // Row 3 is NULL
0x00, // Row 4 is NOT NULL
// -- String values --
0x01,
0x30, // Row 0: LEB128 == 1, '0' as String
0x00, // Row 1: LEB128 == 0, NULL
0x01,
0x32, // Row 2: LEB128 == 1, '2' as String
0x00, // Row 3: LEB128 == 0, NULL
0x01,
0x34, // Row 4: LEB128 == 1, '4' as String
])
LowCardinality
LowCardinality가 투명한 RowBinary와 달리 Native 형식은 딕셔너리 기반 컬럼형 인코딩을 사용합니다. 컬럼은 버전 접두사, 그 다음에 고유 값의 딕셔너리, 그리고 그 딕셔너리에 대한 정수 인덱스 배열로 인코딩됩니다.
컬럼은 LowCardinality(Nullable(T))로 정의할 수 있지만, Nullable(LowCardinality(T))로 정의할 수는 없습니다 — 항상 서버에서 오류가 발생합니다.
버전 접두사는 값 1을 가진 UInt64(LE)로, 컬럼당 한 번 기록됩니다. 그런 다음 블록마다 다음이 기록됩니다:
UInt64(LE)—IndexesSerializationType비트필드. 비트 0–7은 인덱스 너비를 인코딩합니다(0 = UInt8, 1 = UInt16, 2 = UInt32, 3 = UInt64). 비트 8 (NeedGlobalDictionaryBit)은 Native 형식에서 설정되지 않습니다(만나면 서버가 예외를 발생시킵니다). 비트 9는 추가 딕셔너리 키가 있음을 나타냅니다. 비트 10은 딕셔너리를 재설정해야 함을 나타냅니다.UInt64(LE)— 딕셔너리 키 수, 그 다음 내부 타입 인코딩을 사용하여 키를 일괄 직렬화한 값.UInt64(LE)— 행 수, 그 다음 적절한 UInt 너비를 사용하여 인덱스 값을 일괄 직렬화한 값.
딕셔너리는 항상 인덱스 0에 기본값을 포함합니다(예: String의 경우 빈 문자열, 숫자 타입의 경우 0). LowCardinality(Nullable(T))의 경우 인덱스 0은 NULL을 나타내고, 키는 Nullable 래퍼 없이 직렬화됩니다.
예를 들어 5개 행 ['foo', 'bar', 'baz', 'foo', 'bar']의 LowCardinality(String):
// Version prefix
01 00 00 00 00 00 00 00 // UInt64(LE) = 1
// IndexesSerializationType: UInt8 indexes, has keys, update dictionary
00 06 00 00 00 00 00 00 // UInt64(LE) = 0x0600
04 00 00 00 00 00 00 00 // 4 dictionary keys
00 // key 0: "" (default)
03 66 6f 6f // key 1: "foo"
03 62 61 72 // key 2: "bar"
03 62 61 7a // key 3: "baz"
05 00 00 00 00 00 00 00 // 5 rows
01 02 03 01 02 // indexes → "foo", "bar", "baz", "foo", "bar"
LowCardinality(Nullable(String))의 경우 인덱스 0은 NULL입니다:
01 00 00 00 00 00 00 00 // version
00 06 00 00 00 00 00 00 // IndexesSerializationType
03 00 00 00 00 00 00 00 // 3 keys
00 // key 0: NULL
00 // key 1: "" (default)
03 79 65 73 // key 2: "yes"
05 00 00 00 00 00 00 00 // 5 rows
02 00 02 00 02 // indexes → "yes", NULL, "yes", NULL, "yes"
Array
각 배열이 LEB128 요소 수로 접두사가 붙는 RowBinary와 달리 Native 형식은 배열을 두 개의 컬럼형 하위 스트림으로 인코딩합니다:
- N개의 누적
UInt64오프셋(리틀엔디언, 각 8바이트). 행i는offset[i] - offset[i-1]개의 요소를 가지며,offset[-1]은 암시적으로 0입니다. - 모든 행에 걸친 모든 중첩 요소를 연속적으로 일괄 직렬화한 값.
예를 들어 3개 행 [[0, 10], [1, 11], [2, 12]]의 Array(UInt32):
// Offsets
02 00 00 00 00 00 00 00 // 2 (row 0: 2 elements)
04 00 00 00 00 00 00 00 // 4 (row 1: 2 elements)
06 00 00 00 00 00 00 00 // 6 (row 2: 2 elements)
// Nested UInt32 values (6 total)
00 00 00 00 // 0
0a 00 00 00 // 10
01 00 00 00 // 1
0b 00 00 00 // 11
02 00 00 00 // 2
0c 00 00 00 // 12
빈 배열은 이전 행과 같은 오프셋을 갖습니다. 예를 들어 4개 행 [[], ['0'], ['0','1'], ['0','1','2']]의 Array(String):
00 00 00 00 00 00 00 00 // 0 (empty)
01 00 00 00 00 00 00 00 // 1
03 00 00 00 00 00 00 00 // 3
06 00 00 00 00 00 00 00 // 6
01 30 // "0"
01 30 // "0"
01 31 // "1"
01 30 // "0"
01 31 // "1"
01 32 // "2"
Map
Map(K, V)은 Array(Tuple(K, V))로 인코딩됩니다 — 배열 오프셋 다음에 모든 키, 그 다음에 모든 값. 키와 값이 항목별로 인터리브되는 RowBinary와 다릅니다.
예를 들어 3개 행 [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}]의 Map(String, UInt64):
// Array offsets
02 00 00 00 00 00 00 00 // 2
04 00 00 00 00 00 00 00 // 4
06 00 00 00 00 00 00 00 // 6
// All keys (6 Strings)
01 61 // "a"
01 62 // "b"
01 61 // "a"
01 62 // "b"
01 61 // "a"
01 62 // "b"
// All values (6 UInt64s)
00 00 00 00 00 00 00 00 // 0
0a 00 00 00 00 00 00 00 // 10
01 00 00 00 00 00 00 00 // 1
0b 00 00 00 00 00 00 00 // 11
02 00 00 00 00 00 00 00 // 2
0c 00 00 00 00 00 00 00 // 12
Variant
각 행이 자체 판별(discriminant) 바이트와 그 뒤에 인라인 값이 오는 RowBinary와 달리 Native 형식은 판별자와 데이터를 분리합니다.
RowBinary와 마찬가지로 정의의 타입은 항상 알파벳순으로 정렬되며, 판별자는 그 정렬된 목록의 인덱스입니다. 0xFF (255)는 NULL을 나타냅니다.
Variant 컬럼은 다음과 같이 인코딩됩니다:
UInt64(LE)판별자 모드 접두사(0= BASIC,1= COMPACT). Native 형식 출력은 일반적으로 BASIC(0)을 사용합니다. COMPACT 모드는 use_compact_variant_discriminators_serialization이 활성화된 상태로 저장된 데이터를 읽을 때 나타날 수 있습니다.- 행마다 하나씩 N개의
UInt8판별자. - 각 variant 타입의 데이터를 판별자 순서로 일치하는 행만 포함하는 별도의 일괄 컬럼으로.
예를 들어 5개 행 [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']의 Variant(String, UInt32)(정렬: String = 0, UInt32 = 1):
00 00 00 00 00 00 00 00 // discriminators mode = BASIC
01 00 ff 01 00 // UInt32, String, NULL, UInt32, String
// String (2 values, rows 1 and 4)
05 68 65 6c 6c 6f // "hello"
05 68 65 6c 6c 6f // "hello"
// UInt32 (2 values, rows 0 and 3)
00 00 00 00 // 0
03 00 00 00 // 3
Dynamic
각 값이 자체 기술(self-describing) 형식(타입 접두사 + 값)인 RowBinary와 달리 Native 형식은 Dynamic을 구조 접두사 다음에 Variant 컬럼으로 직렬화합니다.
구조 접두사는 UInt64(LE) 직렬화 버전, 그 다음 동적 타입 수(VarUInt), 그 다음 문자열로 된 타입 이름을 포함합니다. 버전 V1에서 타입 수는 호환성을 위해 두 번 기록됩니다. 뒤따르는 데이터는 동적 타입에 내부 SharedVariant 타입을 더한 목록을 알파벳순으로 정렬한 Variant 컬럼입니다.
예를 들어 5개 행 [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']의 Dynamic:
// Structure prefix (V1)
01 00 00 00 00 00 00 00 // version = V1
02 // num types (V1 writes twice)
02 // num types
06 53 74 72 69 6e 67 // "String"
06 55 49 6e 74 33 32 // "UInt32"
// Variant data: Variant(SharedVariant, String, UInt32)
// discriminants: SharedVariant=0, String=1, UInt32=2
00 00 00 00 00 00 00 00 // discriminators mode = BASIC
02 01 ff 02 01 // UInt32, String, NULL, UInt32, String
// SharedVariant: 0 values
05 68 65 6c 6c 6f // String: "hello"
05 68 65 6c 6c 6f // String: "hello"
00 00 00 00 // UInt32: 0
03 00 00 00 // UInt32: 3
JSON
각 행이 경로 이름과 값을 가진 자체 기술 형식인 RowBinary와 달리 Native 형식은 JSON을 컬럼형 구조로 직렬화합니다. 인코딩은 복잡하고 버전에 의존합니다. 직렬화 버전이 있는 구조 접두사, 동적 경로 이름, 공유 데이터 레이아웃, 그 다음에 타입 있는 경로(각각 일괄 컬럼), 동적 경로(각각 Dynamic 컬럼), 그리고 오버플로우 경로를 위한 공유 데이터로 구성됩니다.
더 간단한 상호운용을 위해 output_format_native_write_json_as_string=1 설정을 사용하는 것을 고려하세요. 이 설정은 JSON 컬럼을 일반 JSON 텍스트 문자열(행당 String 하나)로 직렬화합니다.