GeoJSON 형식
GeoJSON 형식
GeoJSON 형식은 단일 FeatureCollection 문서로 데이터를 주고받아요. ClickHouse는 이 문서를 Feature당 하나씩 세 개의 컬럼 — id, geometry, properties로 매핑합니다. 읽기는 Feature마다 행 하나를, 쓰기는 행마다 Feature 하나를 생성해요.
출처: 문서
본문
| Input | Output | Alias |
|---|---|---|
| ✔ | ✔ |
설명 (Description)
GeoJSON 데이터는 단일 FeatureCollection 문서로 교환되며, ClickHouse는 이것을 Feature당 하나씩 세 개의 컬럼 — id, geometry, properties로 매핑합니다. 문서를 읽으면 Feature마다 행 하나가 생성되고, 쓰면 행마다 Feature 하나가 생성됩니다.
데이터 읽기 (Reading data)
FeatureCollection을 읽으면 Feature마다 행 하나와 다음 고정 스키마가 생성됩니다:
| Column | Type | Description |
|---|---|---|
| id | Nullable(String) | Feature의 id 멤버(JSON 문자열 또는 숫자)를 텍스트로 저장. id가 없거나 null 이면 NULL, 명시적 빈 문자열 id는 '' 로 유지. |
| geometry | Geometry | Feature의 geometry를 Geometry variant 타입으로 저장. |
| properties | Nullable(JSON) | Feature의 properties 객체를 반구조적 JSON 컬럼으로 저장. 명시적 "properties": null 은 NULL로 보존. |
각 geometry는 ClickHouse의 Geometry 타입(Variant)에 저장됩니다. 지원되는 GeoJSON geometry 타입은 Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon입니다. 나머지 GeoJSON geometry 타입인 GeometryCollection은 Geometry 타입으로 표현할 수 없습니다. 하나를 geometry 컬럼으로 읽으면 기본적으로 예외가 발생하며, 대신 NULL을 삽입하도록 변경할 수 있어요(아래 "지원되지 않는 geometry 타입 처리" 참조). 기본적으로 geometry 컬럼은 Feature의 geometry가 명시적인 JSON null일 때만 NULL이며, input_format_geojson_unsupported_geometry_handling = 'null'에서는 지원되지 않는 geometry 타입에도 NULL입니다.
문서 구조는 검증됩니다. 최상위 타입은 FeatureCollection이어야 하고 features의 각 요소는 Feature 타입이어야 합니다. 기본적으로 좌표는 GeoJSON 모양 불변량을 만족해야 합니다 — LineString(및 MultiLineString의 각 라인)은 최소 2개의 점을, Polygon 링(및 MultiPolygon의 각 링)은 닫혀 있고 최소 4개의 점을 가져야 합니다(Geometry validation 참조). 잘못된 문서는 조용히 로드되는 대신 거부됩니다.
키 순서는 유연합니다. 최상위 type은 features 배열 앞이나 뒤에 올 수 있고, geometry 객체 안에서 coordinates는 type 앞이나 뒤에 올 수 있어요.
스키마 추론은 위의 고정 스키마를 반환하므로 테이블 정의 없이도 DESCRIBE 및 SELECT ... FROM format(...)이 작동합니다.
다음 GeoJSON 파일 london.geojson이 geometry 타입을 혼합해 포함한다고 가정해 볼게요:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "1",
"geometry": {"type": "Point", "coordinates": [-0.0761, 51.5081]},
"properties": {"name": "Tower of London", "feature_type": "landmark", "year_built": 1078}
},
{
"type": "Feature",
"id": "2",
"geometry": {
"type": "LineString",
"coordinates": [[-0.2500, 51.4700], [-0.1800, 51.4900], [-0.1200, 51.5060], [-0.0700, 51.5050], [0.0000, 51.5100]]
},
"properties": {"name": "River Thames", "feature_type": "river", "length_km": 346}
},
{
"type": "Feature",
"id": "3",
"geometry": {
"type": "Polygon",
"coordinates": [[[-0.1880, 51.5074], [-0.1533, 51.5074], [-0.1533, 51.5153], [-0.1880, 51.5153], [-0.1880, 51.5074]]]
},
"properties": {"name": "Hyde Park", "feature_type": "park", "area_km2": 1.42}
}
]
}
파일을 쿼리하고 geometry 타입을 검사할 수 있어요:
쿼리
SELECT id, properties.name AS name, variantType(geometry) AS geo_type
FROM file('london.geojson', GeoJSON);
응답
┌─id─┬─name────────────┬─geo_type───┐
│ 1 │ Tower of London │ Point │
│ 2 │ River Thames │ LineString │
│ 3 │ Hyde Park │ Polygon │
└────┴─────────────────┴────────────┘
파일 확장자 .geojson이 자동 감지되므로 형식 인자를 생략할 수 있어요:
쿼리
SELECT id, properties.name AS name, variantType(geometry) AS geo_type
FROM file('london.geojson');
variantType을 사용하여 각 Geometry 객체의 기본 타입을 확인할 수 있어요:
쿼리
SELECT properties.name AS name, geometry, variantType(geometry)
FROM file('london.geojson', GeoJSON);
응답
Row 1:
──────
name: Tower of London
geometry: (-0.0761,51.5081)
variantType(geometry): Point
Row 2:
──────
name: River Thames
geometry: [(-0.25,51.47),(-0.18,51.49),(-0.12,51.506),(-0.07,51.505),(0,51.51)]
variantType(geometry): LineString
Row 3:
──────
name: Hyde Park
geometry: [[(-0.188,51.5074),(-0.1533,51.5074),(-0.1533,51.5153),(-0.188,51.5153),(-0.188,51.5074)]]
variantType(geometry): Polygon
그리고 다음과 같이 기본 데이터를 추출할 수 있어요:
쿼리
SELECT properties.name AS name, variantType(geometry), geometry.Point, geometry.LineString, geometry.Polygon
FROM file('london.geojson', GeoJSON);
응답
Row 1:
──────
name: Tower of London
variantType(geometry): Point
geometry.Point: (-0.0761,51.5081)
geometry.LineString: []
geometry.Polygon: []
Row 2:
──────
name: River Thames
variantType(geometry): LineString
geometry.Point: (0,0)
geometry.LineString: [(-0.25,51.47),(-0.18,51.49),(-0.12,51.506),(-0.07,51.505),(0,51.51)]
geometry.Polygon: []
Row 3:
──────
name: Hyde Park
variantType(geometry): Polygon
geometry.Point: (0,0)
geometry.LineString: []
geometry.Polygon: [[(-0.188,51.5074),(-0.1533,51.5074),(-0.1533,51.5153),(-0.188,51.5153),(-0.188,51.5074)]]
Geometry 하위 컬럼에 접근하면 행이 해당 타입을 가질 때 값을, 그렇지 않으면 타입의 기본값 — Point의 경우 (0,0), 배열 기반 타입의 경우 [] — 을 반환합니다. 따라서 어떤 것이 설정되었는지 알려면 variantType(geometry)를 사용하세요.
GeoJSON 데이터를 테이블로 수집할 수도 있어요:
쿼리
CREATE TABLE london
(
id String,
geometry Geometry,
properties Nullable(JSON),
name String MATERIALIZED properties.name,
feature_type String MATERIALIZED properties.feature_type
)
ENGINE = MergeTree
ORDER BY id;
INSERT INTO london
SELECT id, geometry, properties
FROM file('london.geojson', GeoJSON);
그런 다음 feature type으로 쿼리합니다:
쿼리
SELECT name, feature_type, variantType(geometry) AS geo_type
FROM london
ORDER BY id;
응답
┌─name────────────┬─feature_type─┬─geo_type───┐
│ Tower of London │ landmark │ Point │
│ River Thames │ river │ LineString │
│ Hyde Park │ park │ Polygon │
└─────────────────┴──────────────┴────────────┘
테이블 정의 없이 GeoJSON 데이터의 스키마를 추론할 수도 있어요:
쿼리
DESCRIBE format(GeoJSON, '{"type":"FeatureCollection","features":[]}');
응답
┌─name───────┬─type─────────────┐
│ id │ Nullable(String) │
│ geometry │ Geometry │
│ properties │ Nullable(JSON) │
└────────────┴──────────────────┘
지원되지 않는 geometry 타입 처리 (Handling unsupported geometry types)
GeometryCollection과 같은 일부 유효한 GeoJSON geometry 타입은 ClickHouse의 Geometry 타입으로 표현할 수 없어요. 그러한 geometry가 geometry 컬럼에 저장되어야 할 때 무슨 일이 일어날지 input_format_geojson_unsupported_geometry_handling 설정으로 제어할 수 있습니다. 가능한 값:
'throw'— 예외 발생 (기본값)'null'—geometry컬럼에NULL값을 삽입하고 파싱 계속
이 처리는 geometry 컬럼이 읽힐 때만 적용됩니다. geometry가 요청된 출력 컬럼이 아닐 때(예: SELECT id FROM ...) 지원되지 않는 geometry는 여전히 잘 형성되었는지 검증되지만, 처리를 트리거하지는 않습니다. geometry 값이 실체화되지 않기 때문에 예외를 던지지도 NULL을 삽입하지도 않습니다.
제한 사항 (Limitations)
읽기는 고정 스키마에 맞는 것만 반영하므로 일부 GeoJSON 정보는 보존되지 않습니다:
id,geometry,properties만 생성되며, 다른 문서 구조는 컬럼으로 노출되지 않습니다.- 위치의 세 번째(고도) 좌표와 그 이후는 버려집니다 — 위치는
[longitude, latitude]가 됩니다. bbox와 외부 멤버(최상위name또는crs,Feature내부의 추가 멤버 등)는 무시됩니다.- 숫자
id는 텍스트로 저장되므로 문자열과 숫자의 구분이 손실됩니다. 없거나null인id는NULL이 됩니다. GeometryCollection은 표현할 수 없습니다 — 지원되지 않는 geometry 타입 처리 참조.
데이터 쓰기 (Writing data)
결과 집합을 쓰면 단일 GeoJSON FeatureCollection이 생성되며 Feature는 행마다 하나씩입니다.
결과의 컬럼은 각 Feature에 다음과 같이 매핑됩니다:
| Feature member | Built from | Notes |
|---|---|---|
| type | — | 항상 "Feature" . |
| geometry | 단일 geometry 타입 컬럼 | 정확히 하나의 geometry 타입 컬럼이 필요하며, 그렇지 않으면 쿼리가 거부됩니다. NULL geometry는 null 로 기록됩니다. |
| id | id라는 이름의 컬럼 | 값이 NULL이면 생략. String 컬럼은 JSON 문자열로, 숫자 컬럼은 JSON 숫자로 기록. |
| properties | 나머지 모든 컬럼 | 이름이 properties이고 타입이 객체와 유사한( JSON , Map , 또는 명명된 Tuple ) 단일 컬럼은 properties 키 아래에 중첩되는 대신 properties 객체로 직접 기록. 그 외에는 각 나머지 컬럼이 자신의 이름을 키로 하는 하나의 property가 됩니다(없으면 빈 객체). |
geometry 타입 컬럼은 Geometry variant이거나 특정 geo 타입일 수 있으며, 각각 GeoJSON geometry 타입으로 매핑됩니다:
| ClickHouse type | GeoJSON "type" |
|---|---|
| Point | Point |
| MultiPoint | MultiPoint |
| LineString | LineString |
| MultiLineString | MultiLineString |
| Polygon | Polygon |
| MultiPolygon | MultiPolygon |
| Ring | Polygon (a single ring) |
| Geometry | the active variant’s type (or null ) |
Ring은 GeoJSON geometry 타입이 아닙니다 — 선형 링은 Polygon의 구성요소이므로 — Ring 값은 단일 링 Polygon으로 기록됩니다.
예시 (Examples)
위에서 만든 london 테이블을 계속 사용하여, 일반 속성 컬럼을 내보내면 id와 geometry를 제외한 모든 컬럼이 property가 됩니다:
쿼리
SELECT id, geometry, name, feature_type
FROM london
ORDER BY id
FORMAT GeoJSON;
응답
{"type":"FeatureCollection","features":[{"type":"Feature","id":"1","geometry":{"type":"Point","coordinates":[-0.0761,51.5081]},"properties":{"name":"Tower of London","feature_type":"landmark"}},{"type":"Feature","id":"2","geometry":{"type":"LineString","coordinates":[[-0.25,51.47],[-0.18,51.49],[-0.12,51.506],[-0.07,51.505],[0,51.51]]},"properties":{"name":"River Thames","feature_type":"river"}},{"type":"Feature","id":"3","geometry":{"type":"Polygon","coordinates":[[[-0.188,51.5074],[-0.1533,51.5074],[-0.1533,51.5153],[-0.188,51.5153],[-0.188,51.5074]]]},"properties":{"name":"Hyde Park","feature_type":"park"}}]}
이름이 properties인 단독 객체 타입 컬럼이 직접 기록되기 때문에, GeoJSON 파일을 읽고 바로 다시 쓰면 문서를 재현합니다(id, geometry, properties 컬럼이 파일에 대해 추론된 것이므로):
쿼리
SELECT * FROM file('london.geojson', GeoJSON) FORMAT GeoJSON;
응답
{"type":"FeatureCollection","features":[{"type":"Feature","id":"1","geometry":{"type":"Point","coordinates":[-0.0761,51.5081]},"properties":{"feature_type":"landmark","name":"Tower of London","year_built":1078}},{"type":"Feature","id":"2","geometry":{"type":"LineString","coordinates":[[-0.25,51.47],[-0.18,51.49],[-0.12,51.506],[-0.07,51.505],[0,51.51]]},"properties":{"feature_type":"river","length_km":346,"name":"River Thames"}},{"type":"Feature","id":"3","geometry":{"type":"Polygon","coordinates":[[[-0.188,51.5074],[-0.1533,51.5074],[-0.1533,51.5153],[-0.188,51.5153],[-0.188,51.5074]]]},"properties":{"area_km2":1.42,"feature_type":"park","name":"Hyde Park"}}]}
숫자 id 컬럼은 JSON 숫자로 기록됩니다(NULL인 Nullable id는 완전히 생략됨):
쿼리
SELECT 42 AS id, (-0.1276, 51.5072)::Point AS geometry FORMAT GeoJSON;
응답
{"type":"FeatureCollection","features":[{"type":"Feature","id":42,"geometry":{"type":"Point","coordinates":[-0.1276,51.5072]},"properties":{}}]}
Ring은 단일 링 Polygon으로 기록됩니다:
쿼리
SELECT [(0., 0.), (10., 0.), (10., 10.), (0., 0.)]::Ring AS geometry FORMAT GeoJSON;
응답
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,0]]]},"properties":{}}]}
파일에 쓰기 (Writing to a file)
INTO OUTFILE을 사용하여 클라이언트에서 GeoJSON 파일을 기록합니다:
쿼리
SELECT id, geometry, properties
FROM london
ORDER BY id
INTO OUTFILE 'london_export.geojson'
FORMAT GeoJSON;
서버는 file 테이블 함수로 스스로 파일을 쓸 수 있어요(.geojson 확장자가 형식을 자동 선택):
쿼리
INSERT INTO FUNCTION file('london_export.geojson', GeoJSON)
SELECT id, geometry, properties FROM london;
제한 사항 (Limitations)
ClickHouse의 geo 타입에는 좌표 참조 시스템이 없으므로, 출력은 좌표가 RFC 7946이 요구하는 WGS84 경도/위도, [longitude, latitude] 순서라고 가정합니다. 재투영이나 축 교환은 수행되지 않으므로 투영된 좌표 — 또는 (latitude, longitude)로 저장된 데이터 — 는 구조적으로 유효하지만 표준을 따르지 않는 GeoJSON을 생성합니다.
출력은 ClickHouse가 저장하는 것만 반영합니다:
- 읽을 때 버려진 정보 — 위치의 고도,
bbox, 외부 멤버,id의 문자열-숫자 구분 — 는 재현할 수 없습니다. 읽기 제한 사항 참조. - 좌표는
Float64값에서 가장 짧은 왕복 가능(round-trippable) 표현을 사용하여 기록됩니다. JSON컬럼에서 직접 가져온properties객체는JSON타입의 표준 키 순서로 출력되는데, 입력과 다를 수 있어요.
Geometry는 저장된 그대로 정확히 기록됩니다 — 좌표 순서와 와인딩(winding)이 보존됩니다. 기본적으로 GeoJSON 모양 유효성은 쓰기 시에도 강제됩니다(Geometry validation 참조). 단일 점의 LineString이나 닫히지 않은 Polygon 링 같은 유효하지 않은 GeoJSON 모양의 geometry는 기록된 문서가 다시 읽히도록 거부됩니다. format_geojson_validate_geometry = 0으로 설정하면 그러한 geometry를 있는 그대로 출력해서 구조적으로 유효하지만 표준을 따르지 않는 GeoJSON을 생성합니다. 오른손 규칙(와인딩) 불변량은 어느 쪽도 강제하지 않으며, null과 빈 properties 객체의 구분은 보존됩니다.
Geometry 검증 (Geometry validation)
format_geojson_validate_geometry 설정은 형식이 양방향으로 RFC 7946 geometry 모양 규칙을 강제할지 제어합니다. 기본적으로 활성화되어 있습니다.
활성화되면 GeoJSON 모양 규칙을 위반하는 geometry는 거부됩니다: 점이 2개 미만인 LineString(또는 MultiLineString의 라인); 점이 4개 미만이거나 첫 점과 마지막 점이 다른(닫히지 않은 링) Polygon 또는 MultiPolygon 링; 또는 빈 MultiLineString, Polygon, MultiPolygon. 동일한 규칙이 그러한 문서를 읽을 때와 그러한 ClickHouse 값을 쓸 때 적용되므로, 기록된 문서는 항상 다시 읽힙니다.
비활성화되면 이러한 모양 규칙은 어느 방향으로도 강제되지 않습니다. 변질된 geometry는 있는 그대로 읽고 씁니다. 이는 유효한 GeoJSON geometry가 아닌 ClickHouse geometry 값이 형식을 통해 왕복할 수 있게 하지만, 유효한 GeoJSON이 아닌 문서를 생성하는 대가가 따릅니다.
검증은 구조적인 것만 있습니다: 점 수와 링 폐쇄를 검사합니다. 모양의 기하학적 정확성은 검사하지 않으므로 구조적으로 유효하지만 기하학적으로 변질된 geometry는 어느 방향에서든 허용됩니다 — 예를 들어 면적이 0인 폴리곤, 자체 교차하는 링, 또는 구멍(내부 링)이 외부 링 밖에 있는 폴리곤처럼요. 폴리곤 링의 오른손 규칙(와인딩) 방향도 마찬가지로 강제되지 않습니다.
설정과 무관한 검사가 하나 있습니다: 유한하지 않은 좌표(NaN, Inf)는 JSON 숫자로 표현할 수 없으므로 항상 거부됩니다.