JSON 적재
JSON 적재 (Loading JSON)
다음 예시들은 구조화·반구조화된 JSON 데이터를 적재하는 아주 간단한 예시예요. 중첩 구조를 포함한 더 복잡한 JSON은 JSON 스키마 설계 가이드를 참고하세요.
출처: 문서
본문
구조화된 JSON 적재
이 섹션에서는 JSON 데이터가 NDJSON(Newline delimited JSON) 포맷 — ClickHouse에서는 JSONEachRow로 알려져 있어요 — 이고, 컬럼 이름과 타입이 고정된 잘 구조화된 형태라고 가정해요. NDJSON은 간결하고 공간 효율적이기 때문에 JSON 적재에 선호되는 포맷이지만, 입력과 출력 모두에서 다른 포맷도 지원돼요. Python PyPI 데이터셋의 한 행을 나타내는 다음 JSON 샘플을 살펴볼게요.
{
"date": "2022-11-15",
"country_code": "ES",
"project": "clickhouse-connect",
"type": "bdist_wheel",
"installer": "pip",
"python_minor": "3.9",
"system": "Linux",
"version": "0.3.0"
}
이 JSON 객체를 ClickHouse에 적재하려면 테이블 스키마를 정의해야 해요. 이 단순한 경우에는 구조가 정적이고 컬럼 이름을 알고 있으며 타입도 잘 정의되어 있어요. ClickHouse는 키 이름과 타입이 동적일 수 있는 JSON 타입을 통해 반구조화 데이터를 지원하지만, 여기서는 그게 필요하지 않아요. 가능하면 정적 스키마를 선호하세요 컬럼의 이름과 타입이 고정되어 있고 새 컬럼이 예상되지 않는 경우에는 프로덕션에서 항상 정적으로 정의된 스키마를 선호하세요. JSON 타입은 컬럼의 이름과 타입이 바필 수 있는 매우 동적인 데이터에 적합해요. 이 타입은 프로토타이핑과 데이터 탐색에도 유용해요. 다음은 이에 대한 단순한 스키마예요. JSON 키가 컬럼 이름에 매핑돼요.
CREATE TABLE pypi (
`date` Date,
`country_code` String,
`project` String,
`type` String,
`installer` String,
`python_minor` String,
`system` String,
`version` String
)
ENGINE = MergeTree
ORDER BY (project, date)
정렬 키(Ordering keys) 여기서 ORDER BY 절로 정렬 키를 선택했어요. 정렬 키와 선택 방법에 대한 자세한 내용은 여기를 참고하세요.
ClickHouse는 몇 가지 포맷으로 JSON 데이터를 적재할 수 있으며, 확장자와 내용에서 타입을 자동으로 추론해요. 위 테이블에 대해 S3 함수로 JSON 파일을 읽을 수 있어요.
SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/json/*.json.gz', NOSIGN)
LIMIT 1
┌───────date─┬─country_code─┬─project────────────┬─type────────┬─installer────┬─python_minor─┬─system─┬─version─┐
│ 2022-11-15 │ CN │ clickhouse-connect │ bdist_wheel │ bandersnatch │ │ │ 0.2.8 │
└────────────┴──────────────┴────────────────────┴─────────────┴──────────────┴──────────────┴────────┴─────────┘
1 row in set. Elapsed: 1.232 sec.
파일 포맷을 명시할 필요가 없다는 점을 눈여겨보세요. 대신 glob 패턴을 사용해 버킷의 모든 *.json.gz 파일을 읽어요. ClickHouse는 파일 확장자와 내용에서 포맷이 JSONEachRow(ndjson)임을 자동으로 추론해요. ClickHouse가 포맷을 감지하지 못하는 경우 파라미터 함수로 포맷을 수동 지정할 수 있어요.
SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/json/*.json.gz', NOSIGN, JSONEachRow)
압축 파일 위 파일은 압축되어 있기도 해요. ClickHouse가 자동으로 감지하고 처리해요.
이 파일들의 행을 적재하려면 INSERT INTO SELECT를 사용할 수 있어요.
INSERT INTO pypi SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/json/*.json.gz', NOSIGN)
Ok.
0 rows in set. Elapsed: 10.445 sec. Processed 19.49 million rows, 35.71 MB (1.87 million rows/s., 3.42 MB/s.)
SELECT * FROM pypi LIMIT 2
┌───────date─┬─country_code─┬─project────────────┬─type──┬─installer────┬─python_minor─┬─system─┬─version─┐
│ 2022-05-26 │ CN │ clickhouse-connect │ sdist │ bandersnatch │ │ │ 0.0.7 │
│ 2022-05-26 │ CN │ clickhouse-connect │ sdist │ bandersnatch │ │ │ 0.0.7 │
└────────────┴──────────────┴────────────────────┴───────┴──────────────┴──────────────┴────────┴─────────┘
2 rows in set. Elapsed: 0.005 sec. Processed 8.19 thousand rows, 908.03 KB (1.63 million rows/s., 180.38 MB/s.)
행은 FORMAT 절을 사용해 인라인으로도 적재할 수 있어요. 예:
INSERT INTO pypi
FORMAT JSONEachRow
{"date":"2022-11-15","country_code":"CN","project":"clickhouse-connect","type":"bdist_wheel","installer":"bandersnatch","python_minor":"","system":"","version":"0.2.8"}
이 예시들은 JSONEachRow 포맷을 사용한다고 가정해요. 다른 일반적인 JSON 포맷도 지원되며, 적재 예시는 여기에서 확인할 수 있어요.
반구조화된 JSON 적재
앞선 예시는 키 이름과 타입이 잘 알려진 정적 JSON을 적재했어요. 하지만 실제로는 그렇지 않은 경우가 많아요. 키가 추가되거나 타입이 바뀔 수 있거든요. 이런 경우는 Observability 데이터 같은 사용 사례에서 흔해요. ClickHouse는 전용 JSON 타입으로 이를 처리해요. 위 Python PyPI 데이터셋의 확장 버전에서 온 다음 예시를 살펴볼게요. 여기서는 임의의 키-값 쌍을 가진 tags 컬럼을 추가했어요.
{
"date": "2022-09-22",
"country_code": "IN",
"project": "clickhouse-connect",
"type": "bdist_wheel",
"installer": "bandersnatch",
"python_minor": "",
"system": "",
"version": "0.2.8",
"tags": {
"5gTux": "f3to*PMvaTYZsz!*rtzX1",
"nD8CV": "value"
}
}
여기서 tags 컬럼은 예측할 수 없어서 모델링하는 게 불가능해요. 이 데이터를 적재하려면 이전 스키마를 쓰되 JSON 타입의 tags 컬럼을 추가하면 돼요.
SET enable_json_type = 1;
CREATE TABLE pypi_with_tags
(
`date` Date,
`country_code` String,
`project` String,
`type` String,
`installer` String,
`python_minor` String,
`system` String,
`version` String,
`tags` JSON
)
ENGINE = MergeTree
ORDER BY (project, date);
원본 데이터셋과 같은 방식으로 테이블을 채워요.
INSERT INTO pypi_with_tags SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/pypi_with_tags/sample.json.gz', NOSIGN)
INSERT INTO pypi_with_tags SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/pypi_with_tags/sample.json.gz', NOSIGN)
Ok.
0 rows in set. Elapsed: 255.679 sec. Processed 1.00 million rows, 29.00 MB (3.91 thousand rows/s., 113.43 KB/s.)
Peak memory usage: 2.00 GiB.
SELECT *
FROM pypi_with_tags
LIMIT 2
┌───────date─┬─country_code─┬─project────────────┬─type──┬─installer────┬─python_minor─┬─system─┬─version─┬─tags─────────────────────────────────────────────────────┐
│ 2022-05-26 │ CN │ clickhouse-connect │ sdist │ bandersnatch │ │ │ 0.0.7 │ {"nsBM":"5194603446944555691"} │
│ 2022-05-26 │ CN │ clickhouse-connect │ sdist │ bandersnatch │ │ │ 0.0.7 │ {"4zD5MYQz4JkP1QqsJIS":"0","name":"8881321089124243208"} │
└────────────┴──────────────┴────────────────────┴───────┴──────────────┴──────────────┴────────┴─────────┴──────────────────────────────────────────────────────────┘
2 rows in set. Elapsed: 0.149 sec.
데이터 적재 시 성능 차이를 눈여겨보세요. JSON 컬럼은 insert 시점에 타입 추론이 필요하고, 하나 이상의 타입을 가진 컬럼이 존재하면 추가 저장 공간도 필요해요. JSON 타입은 컬럼을 명시적으로 선언하는 것과 동등한 성능을 내도록 구성할 수 있지만(참고: JSON 스키마 설계), 기본값은 의도적으로 유연하게 만들어졌어요. 하지만 이 유연성에는 약간의 비용이 따르죠.
JSON 타입을 언제 사용할까
다음과 같은 데이터라면 JSON 타입을 사용하세요.
- 시간이 지나면서 바뀔 수 있는 예측 불가능한 키가 있다.
- 타입이 다양한 값을 포함한다 (예: 경로가 어떤 때는 문자열, 어떤 때는 숫자일 수 있음).
- 엄격한 타이핑이 실용적이지 않은 스키마 유연성이 필요하다.
데이터 구조를 알고 있고 일관적이라면, 데이터가 JSON 포맷이라도 JSON 타입이 필요한 경우는 거의 없어요. 구체적으로 데이터가 다음과 같다면:
- 알려진 키를 가진 플랫 구조: String 같은 표준 컬럼 타입을 사용하세요.
- 예측 가능한 중첩: 그런 구조에는 Tuple, Array, Nested 타입을 사용하세요.
- 타입이 다양한 예측 가능한 구조: 대신 Dynamic이나 Variant 타입을 고려하세요.
위 예시에서처럼 접근 방식을 섞을 수도 있어요. 예측 가능한 최상위 키에는 정적 컬럼을, 페이로드의 동적 부분에는 단일 JSON 컬럼을 사용하는 식이에요.