또한 이 매개변수를 변경하지 않고도 동적 경로 수 제한을 변경할 수 있는 방법도 있습니다.
1024
max_dynamic_types
1과 255 사이의 선택적 매개변수로, Dynamic 타입의 단일 경로 컬럼에서 별도로 저장되는 단일 데이터 블록 전체에 걸쳐(예: MergeTree 테이블의 단일 데이터 파트(data part) 전체에서) 서로 다른 데이터 타입을 몇 개까지 별도로 저장할 수 있는지를 나타냅니다.
이 한도를 초과하면 모든 새로운 타입은 shared variant라는 단일 구조에 함께 저장됩니다.
32
some.path TypeName
JSON의 특정 경로에 대한 선택적 타입 힌트입니다. 이러한 경로는 항상 지정된 타입의 서브컬럼으로 저장됩니다.
SKIP path.to.skip
JSON 파싱 중 건너뛸 특정 경로에 대한 선택적 힌트입니다. 이러한 경로는 JSON 컬럼에 저장되지 않습니다. 지정한 경로가 중첩된 JSON 객체인 경우, 해당 중첩 객체 전체를 건너뜁니다.
SKIP REGEXP 'path_regexp'
JSON 파싱 중 경로를 건너뛰는 데 사용하는 정규식이 포함된 선택적 힌트입니다. 이 정규식과 일치하는 모든 경로는 JSON 컬럼에 저장되지 않습니다.
JSON 타입은 구조가 동적이거나 예측하기 어려운 JSON 객체 안의 특정 필드를 쿼리하고, 필터링하고, 집계할 수 있도록 설계되었습니다. 이를 위해 JSON 객체를 별도의 서브컬럼으로 분할하며, 그 결과 읽어야 하는 데이터 양이 크게 줄어들고 Map 또는 문자열 파싱 같은 대안보다 선택한 필드에 대한 쿼리 속도가 크게 빨라집니다.하지만 여기에는 중요한 절충점이 있습니다:
더 느린 INSERTs - JSON을 서브컬럼으로 분할하고, 타입을 추론하고, 유연한 저장 구조를 관리해야 하므로 JSON을 단순한 String 컬럼으로 저장하는 것보다 삽입이 더 느립니다.
전체 객체를 읽을 때 더 느림 - 특정 필드가 아니라 전체 JSON 문서를 가져와야 한다면 JSON 타입은 String 컬럼에서 읽는 것보다 더 느립니다. 필드 수준 쿼리를 수행하지 않는 경우에는 분리된 서브컬럼에서 객체를 다시 구성하는 오버헤드가 아무런 이점을 주지 않습니다.
스토리지 오버헤드 - 별도의 서브컬럼을 유지하면 JSON을 하나의 문자열 값으로 저장할 때보다 구조적 오버헤드가 추가됩니다.
데이터 구조가 이미 정해져 있고 일관적인 경우 - 이때는 일반 컬럼이나 Tuple, Array, Dynamic, Variant 타입을 대신 사용하십시오
JSON 문서를 필드 수준에서 분석하지 않고, 전체를 하나의 불투명한 blob으로 저장하고 그대로 다시 읽어오기만 하는 경우
데이터베이스 내에서 개별 JSON 필드에 대해 쿼리하거나 필터링할 필요가 없는 경우
JSON이 ClickHouse 내부에서 분석 대상이 아니라 단순한 전송/저장 포맷인 경우
JSON이 데이터베이스 내부에서 분석되지 않는 불투명한 문서이고, 단지 저장했다가 다시 읽어오기만 한다면 String 필드로 저장해야 합니다. JSON 타입의 장점은 동적인 JSON 구조 내 특정 필드에 대해 효율적으로 쿼리, 필터링, 집계해야 할 때에만 발휘됩니다.방식을 혼합해 사용할 수도 있습니다. 예측 가능한 최상위 필드에는 표준 컬럼을 사용하고, payload의 동적인 부분에는 JSON 컬럼을 사용할 수 있습니다.
JSON 경로는 평탄화된 형태로 저장됩니다. 즉, a.b.c와 같은 경로로부터 JSON 객체를 포맷할 때
이를 { "a.b.c" : ... }로 구성해야 하는지, 아니면 { "a": { "b": { "c": ... } } }로 구성해야 하는지 판단할 수 없습니다.
현재 구현에서는 항상 후자로 간주합니다.예시:
Dynamic 서브컬럼은 어떤 데이터 타입으로도 CAST할 수 있습니다. 이 경우 Dynamic 내부의 타입을 요청한 타입으로 CAST할 수 없으면 예외가 발생합니다:
Query
SELECT json.a.g::UInt64 AS uintFROM test;
Response
┌─uint─┐│ 42 ││ 0 ││ 43 │└──────┘
Query
SELECT json.a.g::UUID AS floatFROM test;
Response
Received exception from server:Code: 48. DB::Exception: Received from localhost:9000. DB::Exception:Conversion between numeric types and UUID is not supported.Probably the passed UUID is unquoted:while executing 'FUNCTION CAST(__table1.json.a.g :: 2, 'UUID'_String :: 1) -> CAST(__table1.json.a.g, 'UUID'_String) UUID : 0'.(NOT_IMPLEMENTED)
경로가 기본(map) 공유 데이터에 저장되어 있으면 전체 공유 데이터 구조를 스캔해야 하므로 하위 객체 서브컬럼을 읽는 작업이 비효율적일 수 있습니다. 반면 map_with_buckets 또는 advanced 공유 데이터 직렬화를 사용하면 공유 데이터에서 서브컬럼을 읽는 작업이 고도로 최적화됩니다.
JSON 타입은 특수 구문 json.@some.path를 사용해 경로를 combined 서브컬럼으로 읽을 수 있습니다.
지정한 경로의 combined 서브컬럼은 다음을 반환합니다:
해당 경로에 리터럴 값이 있으면, 그 경로에 저장된 리터럴 값을 Dynamic으로 반환합니다.
해당 경로에 리터럴 값은 없지만 중첩된 하위 경로가 있으면, 그 경로의 JSON 하위 객체를 Dynamic으로 반환합니다.
해당 경로에 리터럴 값도 하위 경로도 없으면 NULL을 반환합니다.
이 기능은 경로에 따라 서로 다른 행에서 스칼라 값이나 중첩 객체가 저장될 수 있을 때 유용하며, 리터럴 서브컬럼(json.a)과 하위 객체 서브컬럼(json.^a)을 각각 따로 쿼리하는 것보다 더 편리합니다.다음 예시는 경로 a에 대한 3가지 서브컬럼 타입을 모두 비교합니다:
Query
CREATE TABLE test (json JSON) ENGINE = Memory;INSERT INTO test VALUES ('{"a" : 42, "b" : {"c" : 1, "d" : "Hello"}}'), ('{"a" : {"x": 1, "y": 2}, "b" : {"c" : 1}}'), ('{"c" : "World"}');SELECT json FROM test;
Row 1: a에는 리터럴 42가 있습니다. json.a는 이를 Dynamic(Int64)로 반환하고, json.^a는 빈 하위 객체 {}를 반환하며(a 아래에 중첩된 키가 없음), json.@a는 리터럴 42를 반환합니다.
Row 2: a에는 중첩된 객체가 있습니다. json.a는 NULL을 반환하고(해당 경로에 리터럴 값이 없음), json.^a는 하위 객체를 JSON으로 반환하며, json.@a도 하위 객체를 Dynamic(JSON)로 반환합니다.
Row 3: a가 아예 없습니다. json.a와 json.@a는 모두 NULL을 반환하고, json.^a는 빈 {}를 반환합니다.
경로가 기본(map) 공유 데이터에 저장된 경우, 결합 서브컬럼을 읽으려면 전체 공유 데이터 구조를 스캔해야 하므로 비효율적일 수 있습니다. map_with_buckets 또는 advanced 공유 데이터 직렬화를 사용하면 공유 데이터에서 서브컬럼을 읽는 작업이 크게 최적화됩니다.
이미 눈치채셨겠지만, 중첩된 JSON 타입의 max_dynamic_types/max_dynamic_paths 매개변수는 기본값보다 작게 줄어들었습니다.
이는 JSON 객체의 중첩 배열에서 서브컬럼 수가 걷잡을 수 없이 늘어나는 것을 방지하기 위해 필요합니다.이제 중첩된 JSON 컬럼에서 서브컬럼을 읽어보겠습니다:
Query
SELECT json.a.b.:`Array(JSON)`.c, json.a.b.:`Array(JSON)`.f, json.a.b.:`Array(JSON)`.d FROM test;
보시는 것처럼 원래 JSON {"a.b" : 42}는 이제 {"a" : {"b" : 42}} 형태로 포맷됩니다.이러한 제한 때문에 다음과 같이 유효한 JSON 객체도 파싱에 실패합니다:
Query
SELECT '{"a.b" : 42, "a" : {"b" : "Hello World!"}}'::JSON AS json;
Response
Code: 117. DB::Exception: Cannot insert data into JSON column: Duplicate path found during parsing JSON object: a.b. You can enable setting type_json_skip_duplicated_paths to skip duplicated paths during insert: In scope SELECT CAST('{"a.b" : 42, "a" : {"b" : "Hello, World"}}', 'JSON') AS json. (INCORRECT_DATA)
점(.)이 포함된 키를 유지하고 이를 중첩 객체로 포맷하지 않으려면
설정 json_type_escape_dots_in_keys를 활성화할 수 있습니다(버전 25.8부터 사용 가능). 이 경우 파싱 시 JSON 키의 모든 점은
%2E로 이스케이프되며, 포맷 시 다시 원래대로 복원됩니다.
Query
SET json_type_escape_dots_in_keys=1;SELECT '{"a" : {"b" : 42}}'::JSON AS json1, '{"a.b" : 42}'::JSON AS json2, JSONAllPaths(json1), JSONAllPaths(json2);
JSON 데이터 타입은 내부적으로 제한된 수의 경로만 별도의 서브컬럼으로 저장할 수 있습니다.
기본적으로 이 한도는 1024이며, 타입 선언에서 max_dynamic_paths 매개변수를 사용해 변경할 수 있습니다.한도에 도달하면 JSON 컬럼에 새로 삽입되는 모든 경로는 하나의 공유 데이터 구조에 저장됩니다.
이러한 경로도 여전히 서브컬럼으로 읽을 수 있지만,
효율이 다소 떨어질 수 있습니다(공유 데이터 관련 섹션 참조).
이 한도는 서로 다른 서브컬럼 수가 과도하게 늘어나 테이블을 사용할 수 없게 되는 상황을 방지하기 위해 필요합니다.이제 몇 가지 시나리오에서 한도에 도달하면 어떤 일이 발생하는지 살펴보겠습니다.
MergeTree 테이블에서 여러 데이터 파트를 머지하는 동안, 결과 데이터 파트의 JSON 컬럼이 동적 경로 한도에 도달할 수 있으며
소스 파트의 모든 경로를 서브컬럼으로 저장하지 못할 수 있습니다.
이 경우 ClickHouse는 머지 후 어떤 경로를 서브컬럼으로 유지하고, 어떤 경로를 공유 데이터 구조에 저장할지 결정합니다.
대부분의 경우 ClickHouse는 null이 아닌 값을 가장 많이 포함한 경로를 유지하고, 가장 드문 경로를 공유 데이터 구조로 이동하려고 합니다. 다만 이는 구현에 따라 달라질 수 있습니다.이러한 머지의 예시를 살펴보겠습니다.
먼저 JSON 컬럼이 있는 테이블을 만들고, 동적 경로 한도를 3으로 설정한 다음, 서로 다른 5개의 경로를 가진 값을 삽입하겠습니다:
Query
CREATE TABLE test (id UInt64, json JSON(max_dynamic_paths=3)) ENGINE=MergeTree ORDER BY id;SYSTEM STOP MERGES test;INSERT INTO test SELECT number, formatRow('JSONEachRow', number as a) FROM numbers(5);INSERT INTO test SELECT number, formatRow('JSONEachRow', number as b) FROM numbers(4);INSERT INTO test SELECT number, formatRow('JSONEachRow', number as c) FROM numbers(3);INSERT INTO test SELECT number, formatRow('JSONEachRow', number as d) FROM numbers(2);INSERT INTO test SELECT number, formatRow('JSONEachRow', number as e) FROM numbers(1);
각 삽입 작업은 JSON 컬럼에 단일 경로만 포함된 별도의 데이터 파트를 생성합니다:
Query
SELECT count(), groupArrayArrayDistinct(JSONDynamicPaths(json)) AS dynamic_paths, groupArrayArrayDistinct(JSONSharedDataPaths(json)) AS shared_data_paths, _partFROM testGROUP BY _partORDER BY _part ASC
SELECT count(), groupArrayArrayDistinct(JSONDynamicPaths(json)) AS dynamic_paths, groupArrayArrayDistinct(JSONSharedDataPaths(json)) AS shared_data_paths, _partFROM testGROUP BY _partORDER BY _part ASC
이전 섹션에서 설명했듯이 max_dynamic_paths 제한에 도달하면 모든 새 경로는 하나의 공유 데이터 구조에 저장됩니다.
이 섹션에서는 공유 데이터 구조의 세부 사항과 이 구조에서 경로 서브컬럼을 읽는 방법을 살펴봅니다.JSON 컬럼의 내용을 검사하는 데 사용되는 함수에 관한 자세한 내용은 “인트로스펙션 함수” 섹션을 참조하십시오.
메모리상의 공유 데이터 구조는 평탄화된 JSON 경로를 바이너리로 인코딩된 값에 매핑해 저장하는 Map(String, String) 유형의 서브컬럼일 뿐입니다.
여기서 경로 서브컬럼을 추출하려면 이 Map 컬럼의 모든 행을 순회하면서 요청된 경로와 해당 값을 찾기만 하면 됩니다.
map 직렬화 버전에서는 공유 데이터가 메모리에 저장되는 것과 동일하게 Map(String, String) 타입의 단일 컬럼으로 직렬화됩니다. 이 직렬화 방식에서 경로 서브컬럼을 읽으려면 ClickHouse가 전체 Map 컬럼을 읽은 다음
메모리에서 요청된 경로를 추출합니다.이 직렬화 방식은 데이터를 쓰고 전체 JSON 컬럼을 읽을 때는 효율적이지만, 경로 서브컬럼을 읽을 때는 효율적이지 않습니다.
map_with_buckets serialization version에서는 shared data가 Map(String, String) 유형의 N개 컬럼(“버킷”)으로 직렬화됩니다.
각 버킷에는 경로의 부분 집합만 포함됩니다. 이 직렬화 유형에서 경로 서브컬럼을 읽으려면 ClickHouse는
단일 버킷에서 전체 Map 컬럼을 읽은 다음, 메모리에서 요청된 경로를 추출합니다.이 직렬화는 데이터를 쓰거나 전체 JSON 컬럼을 읽을 때는 효율이 떨어지지만, 경로 서브컬럼을 읽을 때는 더 효율적입니다.
필요한 버킷에서만 데이터를 읽기 때문입니다.버킷 수 N은 MergeTree 설정 object_shared_data_buckets_for_compact_part (기본값 8)
및 object_shared_data_buckets_for_wide_part (기본값 32)로 제어됩니다.
두 설정의 허용 최대값은 모두 256입니다.
advanced 직렬화 버전에서는 공유 데이터를, 요청한 경로의 데이터만 읽을 수 있도록 추가 정보를 저장해 경로 서브컬럼 읽기 성능을 극대화하는 특수한 데이터 구조로 직렬화합니다.
또한 이 직렬화는 버킷도 지원하므로 각 버킷에는 경로의 일부 집합만 포함됩니다.이 직렬화는 데이터 쓰기에는 상당히 비효율적이므로 zero-level 파트에는 사용을 권장하지 않습니다. 전체 JSON 컬럼을 읽는 효율도 map 직렬화보다 약간 떨어지지만, 경로 서브컬럼을 읽는 데에는 매우 효율적입니다.참고: 데이터 구조 내부에 추가 정보를 일부 저장하므로, 이 직렬화를 사용하면 map 및 map_with_buckets 직렬화보다 디스크 저장 크기가 더 커집니다.새로운 공유 데이터 직렬화에 대한 더 자세한 개요와 구현 세부 정보는 블로그 글을 참조하십시오.
JSON에서 동적 경로의 한도를 설정하는 주요 방법은 JSON 타입 선언에서 max_dynamic_paths 매개변수를 사용하는 것입니다.
하지만 기존 컬럼의 max_dynamic_paths를 변경하려면 ALTER TABLE <table> MODIFY COLUMN <column> JSON(max_dynamic_paths=K)를 실행해야 하며, 그러면 기존의 모든 파트를 재작성하는 백그라운드 뮤테이션이 시작됩니다.
이러한 뮤테이션은 매우 부담이 클 수 있으며 완료될 때까지 서버 성능에 영향을 줄 수 있습니다. 이를 피하려면 다음 3가지 설정을 사용해 새 데이터 파트에 대해 MergeTree 테이블의 동적 경로 한도를 변경할 수 있습니다.
merge_max_dynamic_subcolumns_in_wide_part - Wide 데이터 파트로 머지하는 동안 각 JSON 컬럼의 동적 서브컬럼 수를 제한하는 MergeTree 설정입니다.
merge_max_dynamic_subcolumns_in_compact_part - Compact 데이터 파트로 머지하는 동안 각 JSON 컬럼의 동적 서브컬럼 수를 제한하는 MergeTree 설정입니다.
max_dynamic_subcolumns_in_json_type_parsing - JSON 데이터를 JSON 컬럼으로 파싱하는 동안 각 JSON 컬럼의 동적 서브컬럼 수를 제한하는 세션 설정입니다.
참고: 설명한 설정 값이 더 크더라도 동적 경로 한도는 max_dynamic_paths 매개변수에 지정된 값을 초과할 수 없습니다.
이 기능은 실험 단계이며, 설정 allow_experimental_json_lazy_type_hints이 활성화되어 있어야 합니다.
ALTER TABLE ... MODIFY COLUMN을 사용해 JSON 컬럼에 type hint를 추가하거나 수정하면, ClickHouse는 일반적으로 새 type hint를 구체화하기 위해 모든 데이터 파트를 재작성합니다. 대규모 이력 데이터(수백 TB)를 보유한 테이블에서는 이 작업 비용이 매우 클 수 있습니다.Lazy type hints를 사용하면 기존 데이터를 재작성하지 않고 메타데이터만 변경하는 방식으로 type hint를 추가할 수 있습니다.
기존 파트: type hint는 Dynamic에서 힌트된 유형으로 CAST하여 쿼리 시점에 적용됩니다.
새 파트: type hint는 INSERT 작업 중에 구체화됩니다.
머지: 파트가 머지될 때 type hint가 구체화됩니다.
즉, type hint를 즉시 추가할 수 있으며, 이후 일반적인 백그라운드 머지가 진행되면서 데이터가 점진적으로 변환됩니다.
-- 테이블 생성 및 데이터 삽입CREATE TABLE test_lazy (json JSON) ENGINE = MergeTree ORDER BY tuple();INSERT INTO test_lazy VALUES ('{"user_id": "123", "score": "95.5"}');-- 실험적 설정 활성화SET allow_experimental_json_lazy_type_hints = 1;-- 타입 힌트 추가 - 뮤테이션 없이 즉시 완료됨ALTER TABLE test_lazy MODIFY COLUMN json JSON(user_id UInt64, score Float64);-- 데이터 쿼리 - 타입 힌트는 읽기 시점에 적용됨SELECT json.user_id, toTypeName(json.user_id), json.score, toTypeName(json.score) FROM test_lazy;
일반 컬럼에 사용하는 것과 동일한 구문으로 모든 JSON 서브컬럼에 스킵 인덱스를 생성할 수 있습니다.
지원되는 인덱스 유형은 모두 사용할 수 있습니다(minmax, set, bloom_filter, tokenbf_v1, ngrambf_v1 등).인덱스 표현식에서 JSON 서브컬럼을 참조하는 방법은 두 가지입니다:
JSON 타입 힌트에 선언된 타입이 지정된 경로 — 이름으로 직접 접근합니다: json.a.
명시적 캐스트를 사용하는 동적 경로 — :: 캐스트 구문을 사용합니다: json.b::String.
예를 들어 json.a || json.b::String처럼 여러 서브컬럼을 결합한 표현식도 사용할 수 있습니다.
CREATE TABLE sensor_data( data JSON(sensor_id UInt32), INDEX idx_sensor data.sensor_id TYPE minmax GRANULARITY 1, INDEX idx_location data.location::String TYPE bloom_filter GRANULARITY 1)ENGINE = MergeTreeORDER BY tuple()SETTINGS index_granularity = 1;INSERT INTO sensor_data SELECT toJSONString(map('sensor_id', number, 'location', 'room_' || toString(number))) FROM numbers(4);INSERT INTO sensor_data SELECT toJSONString(map('sensor_id', number, 'location', 'room_' || toString(number))) FROM numbers(4, 4);
형식이 지정된 서브컬럼 data.sensor_id의 minmax 인덱스는 스캔 범위를 일치하는 그래뉼로 좁힙니다:
Query
EXPLAIN indexes = 1 SELECT * FROM sensor_data WHERE data.sensor_id < 2;
JSONAllPaths 함수를 사용하면 JSON 컬럼에도 데이터 스키핑 인덱스를 생성할 수 있습니다.
이는 mapKeys를 사용해 Map 컬럼에 스킵 인덱스를 생성하는 방식과 유사합니다 — 인덱스는 각 그래뉼에 존재하는 JSON 경로 집합을 저장하고, 이를 사용해 쿼리한 경로를 포함할 수 없는 그래뉼을 건너뜁니다.
JSONAllPaths(json_column) 표현식은 JSON 값에 존재하는 모든 경로가 담긴 Array(String)을 생성합니다.
스킵 인덱스는 이러한 경로 문자열을 해당 데이터 구조(블룸 필터 또는 inverted index)에 저장합니다.
쿼리에서 json.some.path를 기준으로 필터링하면, 인덱스는 각 그래뉼마다 "some.path" 문자열이 인덱스에 있는지 확인하고, 없으면 해당 그래뉼을 건너뜁니다.
텍스트 인덱스는 JSONAllValues 함수를 통해 JSON 컬럼에 대한 전문 검색을 가속화하는 데 사용할 수 있습니다.
JSONAllValues는 JSON 컬럼의 모든 값을 Array(String)으로 반환하며, 이 값들은 텍스트 인덱스로 인덱싱할 수 있습니다.
JSONAllValues(json_column)에 단일 인덱스를 생성하면 모든 JSON 경로를 포괄할 수 있으므로, 각 경로마다 별도의 인덱스를 만들지 않고도 모든 서브컬럼에서 전문 검색을 수행할 수 있습니다.자세한 내용과 예시는 텍스트 인덱스 문서의 JSONAllValues를 사용한 값 기반 인덱스를 참조하십시오.
데이터를 살펴보고 가능한 한 많은 경로 힌트(path hint)와 타입을 지정하십시오. 이렇게 하면 저장 및 읽기 효율이 훨씬 높아집니다.
어떤 경로가 필요하고 어떤 경로가 전혀 필요하지 않은지 미리 판단하십시오. 필요하지 않은 경로는 SKIP 섹션에 지정하고, 필요하면 SKIP REGEXP 섹션에도 지정하십시오. 이렇게 하면 저장 효율이 향상됩니다.
max_dynamic_paths 매개변수를 지나치게 큰 값으로 설정하지 마십시오. 저장 및 읽기 효율이 떨어질 수 있습니다.
이는 메모리, CPU 등 시스템 매개변수에 크게 좌우되지만, 일반적인 경험칙으로는 로컬 파일 시스템 스토리지에서는 max_dynamic_paths를 10 000보다 크게 설정하지 않고, 원격 파일 시스템 스토리지에서는 1024보다 크게 설정하지 않는 것이 좋습니다.