객체 수준 접근 방식을 적용하세요같은 스키마 안에서도 객체마다 서로 다른 기법을 적용할 수 있습니다. 예를 들어, 일부 객체는
String 타입이 가장 적합하고 다른 객체는 Map 타입이 더 적합할 수 있습니다. String 타입을 사용하면 추가로 스키마를 결정할 필요가 없다는 점에 유의하십시오. 반면 아래에서 보듯이 Map 키 안에 하위 객체를 중첩할 수도 있으며, 여기에 JSON을 나타내는 String을 포함할 수도 있습니다:String 타입 사용하기
String로 그대로 저장한 뒤, 필요에 따라 함수를 사용해 필드를 추출하면 됩니다. 이는 JSON을 구조화된 객체로 처리하는 방식의 정반대에 있는 극단적인 접근입니다. 하지만 이러한 유연성에는 비용이 따르며, 특히 쿼리 구문이 더 복잡해지고 성능이 저하된다는 단점이 있습니다.
앞서 언급했듯이, 원래의 person 객체에서는 tags 컬럼의 구조를 보장할 수 없습니다. 원래 행을 삽입하고(지금은 무시하는 company.labels 포함), Tags 컬럼을 String으로 선언합니다:
tags 컬럼을 선택하면 JSON이 문자열로 삽입되었음을 확인할 수 있습니다:
JSONExtract 함수를 사용할 수 있습니다. 아래의 간단한 예시를 살펴보겠습니다:
String 컬럼 tags에 대한 참조가 모두 필요하다는 점에 유의하십시오. 중첩 경로를 추출하려면 함수도 중첩해야 합니다. 예를 들어 JSONExtractUInt(JSONExtractString(tags, 'car'), 'year')는 tags.car.year 컬럼을 추출합니다. 중첩 경로 추출은 JSON_QUERY 및 JSON_VALUE 함수를 사용하면 더 간단해집니다.
전체 본문을 String으로 간주하는 arxiv 데이터셋의 극단적인 사례를 살펴보겠습니다.
JSONAsString 포맷을 사용해야 합니다:
JSON_VALUE(body, '$.versions[0].created')입니다.
String 함수는 인덱스를 사용하는 명시적 형 변환보다 현저히 느립니다(> 10배). 위 쿼리는 항상 전체 테이블을 스캔하고 모든 행을 처리해야 합니다. 이와 같은 작은 데이터셋에서는 이러한 쿼리도 여전히 빠르지만, 더 큰 데이터셋에서는 성능이 저하됩니다.
이 접근 방식은 유연한 대신 성능과 구문 측면에서 분명한 대가가 따르므로, 스키마에서 매우 동적인 객체에만 사용해야 합니다.
단순 JSON 함수
simpleJSON* 함수는 주로 JSON의 구조와 포맷에 대해 엄격한 가정을 적용하여 더 나은 성능을 제공할 수 있습니다. 구체적으로는 다음과 같습니다.
- field 이름은 상수여야 합니다
-
field 이름의 인코딩은 일관되어야 합니다. 예:
simpleJSONHas('{"abc":"def"}', 'abc') = 1, 하지만visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0 - field 이름은 모든 중첩 구조 전체에서 고유해야 합니다. 중첩 수준은 구분하지 않으며, 일치는 구분 없이 처리됩니다. 일치하는 field가 여러 개이면 첫 번째 항목이 사용됩니다.
-
문자열 리터럴 밖에는 특수 문자가 있으면 안 됩니다. 여기에는 공백도 포함됩니다. 다음 예시는 올바르지 않으므로 파싱되지 않습니다.
simpleJSONExtractString으로 created 키를 추출합니다. 이 경우 성능상 이점을 고려하면 simpleJSON* 함수의 제한 사항은 감수할 만합니다.
맵(Map) 타입 사용하기
객체를 주로 하나의 타입으로 된 임의의 키를 저장하는 데 사용한다면Map 타입을 고려하십시오. 이상적으로는 고유 키 수가 수백 개를 넘지 않는 것이 좋습니다. Map 타입은 하위 객체가 있는 객체에도 사용할 수 있으며, 이 경우 하위 객체들의 타입이 일관되어야 합니다. 일반적으로는 레이블과 태그에 Map 타입을 사용하는 것을 권장합니다. 예를 들어 로그 데이터의 Kubernetes 파드 레이블이 이에 해당합니다.
Map은 중첩 구조를 표현하는 간단한 방법이지만, 몇 가지 중요한 제약이 있습니다:
- 필드는 모두 동일한 타입이어야 합니다.
- 필드는 컬럼으로 존재하지 않으므로 서브컬럼에 접근하려면 별도의 맵 구문이 필요합니다. 객체 전체 자체가 하나의 컬럼입니다.
- 서브컬럼에 접근하면 전체
Map값, 즉 모든 형제 요소와 그 값까지 함께 로드됩니다. 맵이 큰 경우 이는 상당한 성능 저하로 이어질 수 있습니다.
String 키객체를
Map으로 모델링할 때는 JSON 키 이름을 저장하기 위해 String 키를 사용합니다. 따라서 맵은 항상 Map(String, T) 형태이며, 여기서 T는 데이터에 따라 달라집니다.기본형 값
Map의 가장 단순한 활용 방식은 객체의 값이 모두 동일한 기본형 유형인 경우입니다. 대부분은 값 T로 String 타입을 사용합니다.
company.labels 객체가 동적이라고 판단된 앞서의 person JSON을 살펴보겠습니다. 여기서 중요한 점은 이 객체에 String 타입의 key-value 쌍만 추가된다고 예상한다는 것입니다. 따라서 이를 Map(String, String)으로 선언할 수 있습니다:
Map 함수 세트가 제공되며, 여기에 설명되어 있습니다. 데이터 유형이 일관되지 않은 경우 필요한 타입 강제 변환을 수행하는 함수도 제공됩니다.
객체 값
하위 객체를 가진 객체에도Map 타입을 사용할 수 있습니다. 단, 하위 객체의 타입은 일관되어야 합니다.
예를 들어, persons 객체의 tags 키에 일관된 구조가 필요하다고 가정해 보겠습니다. 즉, 각 tag의 하위 객체는 name 및 time 컬럼을 가져야 합니다. 이러한 JSON 문서의 단순화된 예시는 다음과 같습니다.
Map(String, Tuple(name String, time DateTime))으로 표현할 수 있습니다:
Array(Tuple(key String, name String, time DateTime))를 사용할 수 있도록 다음과 같이 재모델링할 수 있습니다.
Nested 타입 사용
Nested 타입은 거의 변경되지 않는 정적 객체를 모델링하는 데 사용할 수 있으며,Tuple 및 Array(Tuple)의 대안이 될 수 있습니다. 일반적으로 이 타입은 동작 방식이 종종 혼란스러울 수 있으므로 JSON에는 사용하지 않는 것을 권장합니다. Nested의 주요 장점은 서브컬럼을 정렬 키에 사용할 수 있다는 점입니다.
아래에서는 정적 객체를 모델링할 때 Nested 타입을 사용하는 예시를 제공합니다. 다음과 같은 간단한 JSON 로그 항목을 살펴보겠습니다:
request 키는 Nested로 선언할 수 있습니다. Tuple과 마찬가지로 하위 컬럼을 명시해야 합니다.
flatten_nested
설정flatten_nested는 Nested의 동작 방식을 제어합니다.
flatten_nested=1
1 값(기본값)에서는 임의 깊이의 중첩을 지원하지 않습니다. 이 값을 사용할 때는 중첩 데이터 구조를 길이가 같은 여러 개의 배열 컬럼으로 생각하는 것이 가장 이해하기 쉽습니다. 이 경우 method, path, version 필드는 사실상 각각 별도의 Array(Type) 컬럼이며, 한 가지 중요한 제약이 있습니다. method, path, version 필드의 길이는 반드시 같아야 합니다. 이는 SHOW CREATE TABLE을 사용하면 확인할 수 있습니다:
-
JSON을 중첩 구조로 삽입하려면
input_format_import_nested_json설정을 사용해야 합니다. 이 설정을 사용하지 않으면 JSON을 평탄화해야 합니다. 즉, -
중첩 필드
method,path,version은 JSON 배열 형태로 전달해야 합니다. 즉,
Array를 사용하면 배열 함수를 폭넓게 활용할 수 있으며, 여기에는 ARRAY JOIN 절도 포함됩니다 - 컬럼에 여러 값이 있는 경우 특히 유용합니다.
flatten_nested=0
이 설정을 사용하면 중첩을 임의의 깊이까지 허용할 수 있으며, 중첩된 컬럼은Tuple의 단일 배열로 유지됩니다. 즉, 사실상 Array(Tuple)와 동일해집니다.
이는 Nested와 함께 JSON을 사용할 때 권장되는 방식이며, 대체로 가장 간단한 방법이기도 합니다. 아래에서 보듯이 모든 객체가 리스트이기만 하면 됩니다.
아래에서는 테이블을 다시 생성하고 행 하나를 다시 삽입합니다:
-
input_format_import_nested_json은 삽입할 때 필요하지 않습니다. -
Nested유형은SHOW CREATE TABLE에서도 유지됩니다. 실제로 이 컬럼의 내부 표현은Array(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String))))입니다. -
따라서
request는 배열 형태로 삽입해야 합니다. 즉,
예시
위 데이터의 더 큰 예시는s3://datasets-documentation/http/에 있는 S3 공개 버킷에서 확인할 수 있습니다.
flatten_nested=0으로 설정합니다.
다음 문은 1,000만 개의 행을 삽입하므로 실행에 몇 분 정도 걸릴 수 있습니다. 필요하면 LIMIT를 적용하십시오:
쌍별 배열 사용
쌍별 배열은 JSON을 String으로 표현할 때의 유연성과, 더 구조화된 접근 방식이 제공하는 성능 사이에서 균형을 이룹니다. 이 스키마는 새로운 필드를 루트에 추가할 수 있어 유연합니다. 하지만 이 방식은 훨씬 더 복잡한 쿼리 구문이 필요하며, 중첩 구조와는 호환되지 않습니다. 예시로, 다음 테이블을 살펴보겠습니다:JSONExtractKeysAndValues를 사용하는 예를 보여줍니다.
indexOf 함수를 사용해야 합니다(이 인덱스는 값의 순서와 일치해야 합니다). 이를 통해 values 배열 컬럼, 즉 values[indexOf(keys, 'status')]에 접근할 수 있습니다. 또한 request 컬럼에는 여전히 JSON 파싱 메서드가 필요하며, 여기서는 simpleJSONExtractString을 사용합니다.