메인 콘텐츠로 건너뛰기
입력출력별칭

설명

Native 형식은 컬럼을 행으로 변환하지 않는 진정한 “열 지향” 포맷이므로 ClickHouse에서 가장 효율적인 포맷입니다. 이 포맷에서는 데이터가 바이너리 형식으로 블록 단위로 기록되고 읽힙니다. 각 블록에는 행 수, 컬럼 수, 컬럼 이름과 타입, 그리고 블록 내 각 컬럼 데이터의 일부가 차례대로 기록됩니다. 이 포맷은 서버 간 상호작용을 위한 네이티브 인터페이스, command-line client 사용, 그리고 C++ 클라이언트에서 사용됩니다.
이 포맷을 사용하면 ClickHouse DBMS에서만 읽을 수 있는 덤프를 빠르게 생성할 수 있습니다. 다만 이 포맷을 직접 다루는 것은 실용적이지 않을 수 있습니다.

데이터 타입 wire 형식

데이터는 wire 상에서 열 지향 포맷으로 전송되며, 이는 각 컬럼이 개별적으로 전송되고 한 컬럼의 모든 값이 하나의 배열로 함께 전송된다는 뜻입니다. 블록의 각 컬럼에는 RowBinaryWithNamesAndTypes와 유사한 헤더가 포함됩니다.
네이티브 TCP 바이너리 프로토콜을 사용하거나(또는 HTTP 엔드포인트가 ?client_protocol_version=<n>을 받을 때), 컬럼 수와 행 수 앞에 BlockInfo 구조체가 기록됩니다. 이 섹션의 예시는 프로토콜 버전이 없는 일반 HTTP 인터페이스를 사용하므로 BlockInfo는 생략됩니다.

블록 구조

다음 쿼리는 numberstr 두 개의 컬럼과 3개의 행을 반환합니다:
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([
  // --- 블록 헤더 ---
  0x02,                   // 컬럼 2개
  0x03,                   // 행 3개
  // -- 컬럼 1 헤더 --
  0x06,                   // LEB128 - 컬럼명 'number'는 6바이트
  0x6e, 0x75, 0x6d,       
  0x62, 0x65, 0x72,       // 컬럼명: 'number'
  0x06,                   // LEB128 - 컬럼 타입 'UInt64'는 6바이트
  0x55, 0x49, 0x6e,
  0x74, 0x36, 0x34,       // 'UInt64'
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // UInt64 값 0
  0x01, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // UInt64 값 1
  0x02, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // UInt64 값 2
  0x03,                   // LEB128 - 컬럼명 'str'는 3바이트
  0x73, 0x74, 0x72,       // 컬럼명: 'str'
  0x06,                   // LEB128 - 컬럼 타입 'String'는 6바이트
  0x53, 0x74, 0x72, 
  0x69, 0x6e, 0x67,       // 'String'
  0x01,                   // LEB128 - 문자열 길이 1바이트
  0x30,                   // String 값 '0'
  0x01,                   // LEB128 - 문자열 길이 1바이트
  0x31,                   // String 값 '1'
  0x01,                   // LEB128 - 문자열 길이 1바이트
  0x32,                   // String 값 '2'
])

여러 블록

하지만 많은 경우 데이터가 단일 블록 하나에 담기지 않으므로, ClickHouse는 데이터를 여러 블록으로 나누어 전송합니다. 다음 쿼리는 블록 크기를 줄여 데이터가 블록당 한 행씩 분할되도록 강제한 상태에서 2개의 행을 가져오는 예입니다:
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([
 
  // ----- 블록 1 ----- 
  0x02,                   // 컬럼 2개
  0x01,                   // 행 1개
  0x06,                   // LEB128 - 컬럼명 'number'는 6바이트
  0x6E, 0x75, 0x6D, 
  0x62, 0x65, 0x72,       // 컬럼명: 'number' 
  0x06,                   // LEB128 - 컬럼 유형 'UInt64'는 6바이트
  0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34,       // 'UInt64' 
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // UInt64로 표현한 0
  0x03,                   // LEB128 - 컬럼명 'str'는 3바이트
  0x73, 0x74, 0x72,       // 컬럼명: 'str'
  0x06,                   // LEB128 - 컬럼 유형 'String'은 6바이트
  0x53, 0x74, 0x72, 
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - 문자열은 1바이트
  0x30,                   // String으로 표현한 '0'
  
  // ----- 블록 2 -----
  0x02,                   // 컬럼 2개
  0x01,                   // 행 1개
  0x06,                   // LEB128 - 컬럼명 'number'는 6바이트
  0x6E, 0x75, 0x6D,  
  0x62, 0x65, 0x72,       // 컬럼명: 'number'
  0x06,                   // LEB128 - 컬럼 유형 'UInt64'는 6바이트
  0x55, 0x49, 0x6E,  
  0x74, 0x36, 0x34,       // 'UInt64'
  0x01, 0x00, 0x00, 0x00,  
  0x00, 0x00, 0x00, 0x00, // UInt64로 표현한 1
  0x03,                   // LEB128 - 컬럼명 'str'는 3바이트
  0x73, 0x74, 0x72,       // 컬럼명: 'str'
  0x06,                   // LEB128 - 컬럼 유형 'String'은 6바이트
  0x53, 0x74, 0x72,  
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - 문자열은 1바이트
  0x31,                   // String으로 표현한 '1'
]);

단순 데이터 타입

보다 단순한 데이터 타입에 속하는 개별 값의 wire 형식은 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 데이터 타입의 wire 형식”에 있는 각 타입 설명을 참조하십시오.

복합 데이터 타입

다음 데이터 타입의 인코딩은 RowBinaryRowBinaryWithNamesAndTypes와 다릅니다.
  • 널 허용
  • LowCardinality
  • 배열
  • Variant
  • Dynamic
  • JSON

널 허용

Native 형식에서는 널 허용 컬럼의 실제 데이터 앞에 블록의 행 수와 동일한 바이트 수가 옵니다. 각 바이트는 해당 값이 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([
  // --- 블록 헤더 ---
  0x01,                         // LEB128 - 컬럼 1개
  0x05,                         // LEB128 - 행 5개
  
  // -- 컬럼 헤더 --
  0x0A,                         // LEB128 - 컬럼 이름 10바이트
  0x6D, 0x61, 0x79, 0x62, 0x65, 
  0x5F, 0x6E, 0x75, 0x6C, 0x6C, // 컬럼 이름: 'maybe_null'
  
  0x10,                         // LEB128 - 컬럼 유형 16바이트
  0x4E, 0x75, 0x6C, 0x6C, 
  0x61, 0x62, 0x6C, 0x65, 
  0x28, 0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34, 0x29,       // 컬럼 유형: 'Nullable(UInt64)'
  
  // -- 널 허용 마스크 --
  0x00,                         // 행 0: NOT NULL
  0x01,                         // 행 1: NULL
  0x00,                         // 행 2: NOT NULL
  0x01,                         // 행 3: NULL
  0x00,                         // 행 4: NOT NULL
  
  // -- UInt64 값 --
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // 행 0: UInt64 값 0

  // 블록 내에 해당 숫자의 실제 값이 존재하더라도,
  // 사용자에게는 NULL로 반환됩니다!
  0x01, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // 행 #1: NULL
  
  0x02, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // 행 #2: UInt64 값 2
  
  0x03, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // 행 #3: NULL (행 #1과 동일)
  
  0x04, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // 행 #4: UInt64 값 4
]);
Nullable(String)도 비슷한 방식으로 작동합니다. null 표시자는 항상 널 허용 마스크 바이트에서 오며 — 마스크 값이 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([
  // --- 블록 헤더 ---
  0x01, // LEB128 - 컬럼 1개
  0x05, // LEB128 - 행 5개

  // -- 컬럼 헤더 --
  0x09, // LEB128 - 컬럼 이름 9바이트
  0x6d,
  0x61,
  0x79,
  0x62,
  0x65,
  0x5f,
  0x73,
  0x74,
  0x72, // 컬럼 이름: 'maybe_str'

  0x10, // LEB128 - 컬럼 유형 16바이트
  0x4e,
  0x75,
  0x6c,
  0x6c,
  0x61,
  0x62,
  0x6c,
  0x65,
  0x28,
  0x53,
  0x74,
  0x72,
  0x69,
  0x6e,
  0x67,
  0x29, // 컬럼 유형: 'Nullable(String)'

  // -- 널 허용 마스크 --
  0x00, // 행 0: NOT NULL
  0x01, // 행 1: NULL
  0x00, // 행 2: NOT NULL
  0x01, // 행 3: NULL
  0x00, // 행 4: NOT NULL

  // -- String 값 --
  0x01,
  0x30, // 행 0: LEB128 == 1, String '0'
  0x00, // 행 1: LEB128 == 0, NULL
  0x01,
  0x32, // 행 2: LEB128 == 1, String '2'
  0x00, // 행 3: LEB128 == 0, NULL
  0x01,
  0x34, // 행 4: LEB128 == 1, String '4'
])

LowCardinality

LowCardinality가 투명하게 처리되는 RowBinary와 달리, Native 형식은 딕셔너리 기반의 열 지향 인코딩을 사용합니다. 컬럼은 버전 접두사로 시작하고, 이어서 고유 값 딕셔너리와 해당 딕셔너리를 참조하는 정수 인덱스 배열로 인코딩됩니다.
컬럼은 LowCardinality(Nullable(T))로 정의할 수 있지만, Nullable(LowCardinality(T))로 정의할 수는 없습니다 — 이렇게 정의하면 항상 서버에서 오류가 발생합니다.
버전 접두사는 값이 1UInt64(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)은 다음과 같습니다:
// 버전 접두사
01 00 00 00 00 00 00 00    // UInt64(LE) = 1

// IndexesSerializationType: UInt8 인덱스, 키 있음, 딕셔너리 업데이트
00 06 00 00 00 00 00 00    // UInt64(LE) = 0x0600

04 00 00 00 00 00 00 00    // 딕셔너리 키 4개
00                          // key 0: "" (기본값)
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행
01 02 03 01 02              // 인덱스 → "foo", "bar", "baz", "foo", "bar"
LowCardinality(Nullable(String))에서는 인덱스 0은 NULL입니다:
01 00 00 00 00 00 00 00    // 버전
00 06 00 00 00 00 00 00    // IndexesSerializationType
03 00 00 00 00 00 00 00    // 키 3개
00                          // 키 0: NULL
00                          // 키 1: "" (기본값)
03 79 65 73                 // 키 2: "yes"
05 00 00 00 00 00 00 00    // 행 5개
02 00 02 00 02              // 인덱스 → "yes", NULL, "yes", NULL, "yes"

배열

각 배열 앞에 LEB128 요소 개수가 붙는 RowBinary와 달리, Native 형식에서는 배열을 2개의 열 지향 하위 스트림으로 인코딩합니다:
  • 누적 UInt64 오프셋 N개(리틀 엔디언, 각각 8바이트). i번째 행에는 offset[i] - offset[i-1]개의 요소가 있으며, offset[-1]은 암묵적으로 0입니다.
  • 모든 행의 중첩 요소를 하나로 이어서 연속적으로 일괄 직렬화합니다.
예를 들어, 3개의 행 [[0, 10], [1, 11], [2, 12]]를 가진 Array(UInt32)는 다음과 같습니다:
// 오프셋
02 00 00 00 00 00 00 00    // 2 (행 0: 요소 2개)
04 00 00 00 00 00 00 00    // 4 (행 1: 요소 2개)
06 00 00 00 00 00 00 00    // 6 (행 2: 요소 2개)

// 중첩된 UInt32 값 (총 6개)
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개 행의 Array(String) 값이 [[], ['0'], ['0','1'], ['0','1','2']]인 경우는 다음과 같습니다:
00 00 00 00 00 00 00 00    // 0 (빈 배열)
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(K, V)Array(Tuple(K, V))로 인코딩됩니다. 즉, 배열 오프셋 다음에 모든 키가 나오고, 그 뒤에 모든 값이 나옵니다. 이는 각 항목마다 키와 값이 번갈아 저장되는 RowBinary와 다릅니다. 예시로, 3개의 행이 있는 Map(String, UInt64) [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}]는 다음과 같습니다:
// 배열 오프셋
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

// 모든 키 (6개의 String)
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"

// 모든 값 (6개의 UInt64)
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

각 행에 자체 판별자 바이트와 그 뒤의 값을 인라인으로 담는 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 type의 데이터는 판별자 순서대로, 해당하는 행만 포함하는 별도의 대량 컬럼으로 인코딩됩니다.
예를 들어, Variant(String, UInt32)에 5개의 행 [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']가 있는 경우(정렬 결과: String = 0, UInt32 = 1):
00 00 00 00 00 00 00 00    // 판별자 모드 = BASIC
01 00 ff 01 00              // UInt32, String, NULL, UInt32, String

// String (값 2개, 1번 및 4번 행)
05 68 65 6c 6c 6f          // "hello"
05 68 65 6c 6c 6f          // "hello"

// UInt32 (값 2개, 0번 및 3번 행)
00 00 00 00                 // 0
03 00 00 00                 // 3

Dynamic

각 값이 자체적으로 타입 정보를 포함하는(타입 접두사 + 값) RowBinary와 달리, Native 형식에서는 Dynamic을 구조체 접두사 다음에 Variant 컬럼이 이어지는 형태로 직렬화합니다. 구조체 접두사에는 UInt64(LE) 직렬화 버전, 동적 타입의 개수(VarUInt), 그리고 문자열 형태의 타입 이름이 포함됩니다. V1 버전에서는 호환성을 위해 타입 개수를 두 번 기록합니다. 그 뒤에 오는 데이터는 동적 타입과 내부 SharedVariant 타입을 합친 뒤 알파벳순으로 정렬한 타입 목록을 가지는 Variant 컬럼입니다. 예를 들어, 5개의 행 [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']이 있는 Dynamic은 다음과 같습니다:
// 구조 접두사 (V1)
01 00 00 00 00 00 00 00    // 버전 = V1
02                          // 타입 수 (V1은 두 번 기록)
02                          // 타입 수
06 53 74 72 69 6e 67       // "String"
06 55 49 6e 74 33 32       // "UInt32"

// Variant 데이터: Variant(SharedVariant, String, UInt32)
// 판별자: SharedVariant=0, String=1, UInt32=2
00 00 00 00 00 00 00 00    // 판별자 모드 = BASIC
02 01 ff 02 01              // UInt32, String, NULL, UInt32, String
// SharedVariant: 값 0개
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을 열 지향 구조로 직렬화합니다. 인코딩은 복잡하고 버전에 따라 달라집니다. 즉, 직렬화 버전, 동적 경로 이름, shared data 레이아웃을 포함하는 구조체 접두사로 구성되며, 그 뒤에 타입이 지정된 경로(각각 벌크 컬럼), 동적 경로(각각 Dynamic 컬럼), 그리고 오버플로우 경로용 shared data가 이어집니다. 더 간단한 상호 운용성을 위해 output_format_native_write_json_as_string=1 설정 사용을 고려하십시오. 이 설정은 JSON 컬럼을 일반 JSON 텍스트 문자열(각 행당 String 1개)로 직렬화합니다.)
마지막 수정일 2026년 6월 10일