- ClickHouse는 JSON 문서를 문자열로 넣고 매번 파싱하는 병목을 피하려고, JSON 경로별 값을 진짜 컬럼형 저장소에 배치하는 새 JSON 타입을 도입함
- 구현의 핵심은 Variant와 Dynamic 타입이며, 같은 JSON 경로에 정수·문자열·배열처럼 서로 다른 타입이 들어와도 최소 공통 타입으로 억지 통합하지 않음
max_dynamic_paths 기본값 1024, max_dynamic_types 기본값 32로 서브컬럼과 타입별 파일 수를 제한해 파일 디스크립터와 merge 비용 증가를 제어함
- 타입 힌트,
SKIP, SKIP REGEXP로 경로별 저장 방식을 조정할 수 있고, 값은 C.a.b 같은 서브컬럼 문법으로 읽을 수 있음
- 새 타입은 deprecated된
Object('json') 대체를 목표로 하며, JSON 키 경로를 기본 키나 data-skipping index에 쓰는 개선도 로드맵에 남아 있음
JSON을 컬럼형 저장소에 맞추는 과제
- JSON은 로그, 관측성, 실시간 데이터 스트리밍, 모바일 앱 저장소, 머신러닝 파이프라인에서 반정형·비정형 데이터를 다루는 공통 형식으로 쓰임
- ClickHouse는 진짜 컬럼 지향 데이터베이스로, 테이블을 디스크의 컬럼 데이터 파일 모음으로 저장해 압축과 벡터화된 필터·집계를 수행함
- JSON에서도 같은 성능을 내려면 문서를 문자열 컬럼에 저장한 뒤 나중에 파싱하는 대신, 각 고유 JSON 경로의 값을 컬럼처럼 저장해야 함
새 JSON 타입이 다루는 네 가지 제약
-
경로별 컬럼형 저장
- JSON 경로별 값도 숫자형 같은 일반 컬럼처럼 압축하고, 벡터화된 방식으로 필터링·집계할 수 있어야 함
-
동적으로 바뀌는 타입
- 같은 JSON 경로
a에 정수, 실수, 배열처럼 서로 다른 타입이 들어올 수 있음
- ClickHouse는 이를 미리 알 수 없고 타입끼리 호환되지 않을 수도 있어, 최소 공통 타입으로 합치면 정보가 손실될 수 있음
-
컬럼 파일 폭증 방지
- 모든 새로운 JSON 경로마다 새 컬럼 파일을 만들면 고유 키가 많은 데이터에서 디스크 파일 수가 급증함
- 파일 디스크립터는 메모리를 쓰고, 처리해야 할 파일이 많아지면 merge 성능에도 영향을 줌
-
희소 키의 밀집 저장
- 고유하지만 희소한 JSON 키가 많을 때, 값이 없는 행마다
NULL이나 기본값을 반복 저장하지 않아야 함
- 실제 값만 밀집 저장해야 PB 규모 분석에서도 확장 가능함
Variant 타입: 타입을 억지로 통합하지 않는 기반
- Variant 데이터 타입은 JSON과 별개로 쓸 수 있는 독립 기능이며, 하나의 테이블 컬럼 안에 서로 다른 데이터 타입 값을 저장하고 읽을 수 있음
- 기존 ClickHouse 컬럼은 고정 타입을 가지며, 삽입 값은 해당 타입이어야 하거나 암묵적으로 변환됨
Nullable 컬럼은 값 파일 외에 NULL 마스크 파일을 사용함
Array는 배열 크기를 별도 파일에 저장하고, 이를 통해 오프셋을 계산함
- Variant 컬럼은 같은 구체 타입의 값을 타입별 서브컬럼에 저장함
- 예: 모든
Int64 값은 C.Int64.bin, 모든 String 값은 C.String.bin에 저장됨
- 각 행이 어떤 타입을 쓰는지는
UInt8 discriminator 컬럼으로 추적함
- discriminator 값은 정렬된 타입 이름 목록의 인덱스임
- discriminator
255는 NULL 예약값임
- 이 설계 때문에 Variant는 최대 255개 구체 타입을 가질 수 있음
- 타입별 데이터 파일은 값이 있는 행만 담는 밀집 저장 구조임
- 타입별 파일에는
NULL 값을 저장하지 않음
- discriminator 행에서 실제 타입 파일의 행 위치를 찾기 위해 메모리상의
UInt64 오프셋 컬럼을 사용함
- 이 오프셋은 디스크에 저장되지 않고 discriminator 컬럼 파일에서 즉석 생성될 수 있음
- Variant는 임의 중첩을 지원함
Variant(T1, T2)와 Variant(T2, T1)의 타입 순서는 의미가 같음
- Variant 내부에 다시 Variant를 넣을 수 있음
- 특정 중첩 타입 값은 타입 이름을 서브컬럼처럼 붙여 읽음
Dynamic 타입: 타입 목록을 미리 몰라도 저장
- Dynamic 타입은 Variant 위에 구현된 독립 기능이며, JSON 문맥 밖에서도 사용할 수 있음
- Dynamic은 Variant에 두 가지 기능을 더함
- 하나의 컬럼 안에 임의 타입 값을 저장하되 타입 목록을 미리 지정하지 않아도 됨
- 별도 컬럼 데이터 파일로 저장할 타입 수를 제한할 수 있음
- 내부 저장 방식은 Variant와 같지만
C.dynamic_structure.bin 파일이 추가됨
- 이 파일은 서브컬럼으로 저장된 타입 목록과 타입별 컬럼 데이터 파일 크기 통계를 담음
- 해당 메타데이터는 서브컬럼 읽기와 데이터 파트 merge에 사용됨
Dynamic(max_types=N)은 별도 파일로 저장할 타입 수를 제한함
- 제한에 도달하면 나머지 타입 값은
C.SharedVariant.bin 같은 단일 컬럼 파일에 저장됨
- 이 파일의 타입은
String
- 각 행은
<binary_encoded_data_type><binary_value> 구조의 문자열 값을 담음
- 여러 타입 값을 하나의 컬럼 파일 안에 저장하고 다시 읽을 수 있음
- Dynamic도 Variant처럼 타입 이름을 서브컬럼으로 사용해 특정 타입 값을 읽을 수 있음
JSON 타입 선언과 저장 구조
- 새 JSON 타입은 임의 구조의 JSON 객체를 저장하고, 각 JSON 값을 경로 기반 서브컬럼으로 읽을 수 있게 함
- 타입 선언은 선택 파라미터와 힌트를 가질 수 있음
<column_name> JSON(
max_dynamic_paths=N,
max_dynamic_types=M,
some.path TypeName,
SKIP path.to.skip,
SKIP REGEXP 'paths_regexp')
max_dynamic_paths
- 기본값은 1024
- 별도 서브컬럼으로 저장할 JSON 키 경로 수를 지정함
- 제한을 넘는 경로는 특수 구조의 단일 서브컬럼에 함께 저장됨
max_dynamic_types
- 기본값은 32
- 값 범위는
0부터 254
- 하나의 JSON 키 경로 컬럼에서 별도 컬럼 데이터 파일로 저장할 데이터 타입 수를 지정함
- 제한을 넘는 새 타입은 특수 구조의 단일 컬럼 데이터 파일에 함께 저장됨
some.path TypeName
- 특정 JSON 경로에 대한 타입 힌트임
- 해당 경로는 지정된 타입의 서브컬럼으로 항상 저장되어 성능 보장을 제공함
SKIP path.to.skip
- 특정 JSON 경로를 파싱 중 건너뜀
- 해당 경로는 JSON 컬럼에 저장되지 않음
- 지정 경로가 중첩 JSON 객체이면 전체 중첩 객체가 건너뛰어짐
SKIP REGEXP 'path_regexp'
- 정규식에 매칭되는 경로를 JSON 파싱 중 건너뜀
- 매칭 경로는 JSON 컬럼에 저장되지 않음
JSON 경로를 컬럼처럼 읽는 방식
- JSON 컬럼의 각 고유 leaf 경로 값은 디스크에 두 방식 중 하나로 저장됨
- 타입 힌트가 있는 경로는 일반 컬럼 데이터 파일로 저장됨
- 타입이 동적으로 바뀔 수 있는 경로는 Dynamic 서브컬럼으로 저장됨
- JSON 타입은
object_structure라는 특수 파일을 사용함
- 동적 경로에 대한 메타데이터를 담음
- 각 동적 경로의 non-null 값 통계를 담음
- 서브컬럼 읽기와 데이터 파트 merge에 사용됨
- 컬럼 파일 폭증은 두 단계 제한으로 제어됨
max_dynamic_types는 하나의 JSON 키 경로 안에서 별도 파일로 저장할 타입 수를 제한함
max_dynamic_paths는 별도 서브컬럼으로 저장할 JSON 키 경로 수를 제한함
max_dynamic_paths 제한을 넘는 추가 동적 JSON 경로는 shared data로 저장됨
- 예시 파일은
C.object_shared_data.size0.bin, C.object_shared_data.paths.bin, C.object_shared_data.values.bin
object_shared_data.values는 String 타입임
- 각 엔트리는
<binary_encoded_data_type><binary_value> 구조를 가짐
- shared data에 대해서도
object_structure.bin에 추가 통계를 저장함
- 현재 shared data 컬럼에 저장된 경로 중 처음 10000개 경로에 대해 non-null 값 통계를 저장함
JSON 경로 문법과 중첩 객체
- JSON 타입은 각 경로의 leaf 값을 경로명 기반 서브컬럼으로 읽을 수 있음
- 타입 힌트가 지정되지 않은 경로의 값은 항상 Dynamic 타입을 가짐
- Dynamic 타입의 하위 타입 서브컬럼은 특수 JSON 문법으로 읽음
- 중첩 JSON 객체는
JSON_column.^some.path 문법으로 JSON 타입 서브컬럼처럼 읽을 수 있음
- 현재 dot 문법은 성능 이유로 중첩 객체를 읽지 않음
- 경로별 리터럴 값 읽기에는 현재 저장 구조가 효율적임
- 경로별 전체 하위 객체를 읽으려면 더 많은 데이터를 읽어야 해서 느려질 수 있음
- 객체 반환에는
.^ 문법이 필요함
- ClickHouse는 두 가지
. 문법을 통합할 계획임
Compact discriminator 직렬화
- 많은 동적 JSON 경로는 값 타입이 대부분 같을 수 있음
- 고유하지만 희소한 JSON 경로가 많으면 각 경로의 discriminator 파일은 주로
255, 즉 NULL 값을 담게 됨
- 이런 파일은 압축은 잘 되지만, 모든 행의 값이 같으면 여전히 중복이 많음
- ClickHouse는 discriminator 직렬화를 위한 compact format을 구현함
- 일반적으로
UInt8 discriminator 값을 모두 쓰는 대신, 대상 granule의 discriminator가 모두 같으면 3개 값만 직렬화함
- compact granule format 표시자
- 해당 granule의 값 개수 표시자
- discriminator 값
- 이 최적화는 MergeTree 설정
use_compact_variant_discriminators_serialization으로 제어됨
- 기본값은 활성화임
- 일반 index granularity 기준 8192개 값 대신 3개 값만 저장하는 경우가 생김
릴리스 상태와 다음 단계
- 새 JSON 타입은 deprecated된 Object('json') 타입을 대체하도록 설계됨
- 구현은 ClickHouse 24.08 릴리스에서 테스트 목적의 experimental 기능으로 제공됨
- JSON 로드맵에는 JSON 키 경로를 테이블 기본 키나 데이터 스키핑 인덱스(data-skipping index) 안에서 사용하는 개선이 포함됨
- Variant와 Dynamic 같은 구성요소는 JSON 외에도 XML, YAML 같은 추가 반정형 타입 지원의 기반이 됨
- ClickHouse Cloud 사용자가 새 JSON 데이터 타입을 테스트하려면 ClickHouse 지원팀에 연락해 private preview 접근을 요청해야 함