Mapbox Vector Tiles
Mapbox Vector Tiles (MVT) 함수
Mapbox Vector Tiles (MVT)은 MapLibre, Mapbox GL 같은 웹 지도 클라이언트가 네이티브로 렌더링하는 protobuf 인코딩 타일이에요. ClickHouse는 협력하는 한 쌍의 함수로 이런 타일을 순수 SQL에서 만들 수 있어요.
출처: 문서
본문
개요 (Overview)
MVTEncodeGeom— 스칼라 함수로, 기하(geometry)를 slippy-map 타일의 타일-로컬 픽셀 공간으로 투영하고 타일에 클리핑해요.MVTEncode— 집계 함수로, 그룹의 투영된 기하들을 단일 레이어 타일의 이진 바이트로 모아요.
두 개의 보조 함수 MVTBoundingBox와 MVTBoundingBoxMercator는 타일의 경계 상자를 반환해서, WHERE 절에서 인덱스를 사용해 행을 그 범위로 제한할 수 있게 해줘요.
점·선·폴리곤 기하를 지원하며, Geometry 타입과 구체적인 geo 타입(Point, MultiPoint, LineString, MultiLineString, Ring, Polygon, MultiPolygon)을 모두 포함해요.
결과 바이트는 완전한 타일이어서, FORMAT RawBLOB으로 HTTP 인터페이스를 통해 직접 반환할 수 있어요.
이 함수들은 PostGIS 워크플로우를 그대로 반영하며, PostGIS 이름의 별칭으로도 사용할 수 있어요: MVTEncodeGeom의 별칭 ST_AsMVTGeom, MVTEncode의 별칭 ST_AsMVT.
MVTEncodeGeom
지리 좌표(경도/위도)로 주어진 기하를 zoom, tile_x, tile_y로 식별되는 slippy-map 타일의 타일-로컬 픽셀 공간으로 투영하고, 정수 픽셀 그리드에 스냅하고, 타일에 클리핑한 뒤 타일 공간의 기하를 반환해요.
투영은 전체 UInt32 좌표 범위에 걸친 웹 메르카토르(Web Mercator)예요. 반환 좌표는 타일의 왼쪽 위 모서리를 원점으로 하고 y축이 아래쪽을 가리켜요 — 이것이 Mapbox Vector Tile 포맷의 좌표 관례이므로 결과를 MVTEncode에 그대로 넣을 수 있어요. 좌표는 정수 픽셀로 반올림되므로, MVTEncodeGeom으로 그룹화하면 같은 그리드에 떨어지는 기하가 하나의 클러스터로 합쳐져요.
clip이 활성화(기본값)되면 기하가 buffer 픽셀로 확장된 타일에 클리핑돼요(각 축에서 범위 [-buffer, extent + buffer]). 완전히 밖에 떨어지는 기하는 NULL이 돼요. PostGIS ST_AsMVTGeom과 대응되는 동작이에요.
폴리곤 좌표는 검증 전에 2^30 창으로 제한돼요 — 정확히 zoom 18, extent 4096에서 전 세계의 픽셀 폭이에요 — 그래서 현실적인 타일에서는 기하가 검증만 되고 클리핑되지 않으며, 그 제한은 극단적인 zoom 또는 extent 값에 놓인 기하에만 영향을 줘요.
출력 기하 타입은 입력에 따라 달라져요: Point는 Point를, MultiPoint는 MultiPoint를, LineString 또는 MultiLineString은 MultiLineString을, Ring·Polygon·MultiPolygon은 MultiPolygon을 반환해요(클리핑이 기하를 여러 조각으로 나눌 수 있어요).
구문 (Syntax)
MVTEncodeGeom(geometry, zoom, tile_x, tile_y[, extent[, buffer[, clip]]])
인자 (Arguments)
geometry— 경도/위도 도(degree) 단위의 기하. 경도는[-180, 180]으로, 위도는 웹 메르카토르 범위[-85.05112878, 85.05112878]로 제한돼요.Point/MultiPoint/LineString/MultiLineString/Ring/Polygon/MultiPolygon/Geometry.zoom— Slippy-map 줌 레벨. 범위[0, 32].UInt8.tile_x— 타일 컬럼 인덱스. 범위[0, 2^zoom - 1].UInt32.tile_y— 타일 행 인덱스. 범위[0, 2^zoom - 1].UInt32.extent— 선택. 한 변당 픽셀의 타일 extent. 범위[1, 2147483647]. 기본값은4096(Mapbox Vector Tile 기본값).UInt32.buffer— 선택. 픽셀 단위 클립 버퍼. 범위[0, 2147483647]. 기본값은1.UInt32.clip— 선택 플래그. 0이 아니면(기본값) 기하가 타일+버퍼에 클리핑돼요.UInt8.
반환 값 (Returned value)
타일 공간의 기하를 반환하고, 완전히 클리핑되면 NULL을 반환해요. Geometry.
예시 (Example)
SELECT MVTEncodeGeom((13.37, 52.52)::Point, 10, 550, 335) AS pixel
┌─pixel──────┐
│ (124,3384) │
└────────────┘
MVTEncode
피처 그룹을 이진 Mapbox Vector Tile 레이어로 인코딩해요. 스칼라 함수 MVTEncodeGeom의 집계 대응 함수예요. 각 입력 행이 하나의 피처가 되며, 점·선·폴리곤 기하를 지원해요.
geometry 인자는 타일 공간 좌표의 Geometry로, 보통 MVTEncodeGeom이 생성해요. 기하가 NULL인 행(예: MVTEncodeGeom에 의해 클리핑된 행)은 건너뛰어요. 선택적인 properties 인자는 이름 있는 튜플로, 요소 이름이 피처 속성 키가 되고 요소 타입이 벡터 타일 값 타입을 결정해요.
결과는 단일 레이어 타일의 원시 바이트예요. 빈 그룹은 빈 타일을 만들어요. PostGIS ST_AsMVT와 대응되는 동작이에요.
구문 (Syntax)
MVTEncode(layer_name[, extent[, feature_id_name[, stringify_unsupported]]])(geometry[, properties])
매개변수 (Parameters)
layer_name— 벡터 타일 레이어의 이름.String.extent— 한 변당 픽셀의 타일 extent. 범위[1, 2147483647]. 기본값은4096.UInt32.feature_id_name— 선택.properties튜플의 부호 없는 정수 요소 이름으로, 태그가 아니라 MVT Featureid(UInt64)로 내보낼 이름이에요. 부호 있는 정수는 거부돼요.NULLid는 해당 피처에서 생략돼요. 매개변수는 위치 기반이라extent를 먼저 줘야 이걸 쓸 수 있어요.String.stringify_unsupported— 선택 플래그(0/1, 기본값0)예요.1이면 직접 지원하지 않는 속성 타입(예: 큰 정수,UUID,Decimal)이 오류를 내는 대신 텍스트string_value로 인코딩돼요.UInt8.
인자 (Arguments)
geometry— 타일 공간 기하예요. 예:MVTEncodeGeom의 결과.Geometry.properties— 선택. 피처 속성의 이름 있는 튜플. 요소 이름이 속성 키가 돼요.Tuple.
반환 값 (Returned value)
단일 레이어 Mapbox Vector Tile의 이진 내용을 반환해요. String.
속성 타입 (Property types)
각 속성 요소는 ClickHouse 타입에 대응하는 Mapbox Vector Tile Value 변형으로 인코딩돼요:
| ClickHouse 타입 | 벡터 타일 값 타입 |
|---|---|
| String / FixedString | string_value |
| Float32 / BFloat16 | float_value |
| Float64 | double_value |
| Bool | bool_value |
| Int8 / Int16 / Int32 / Int64 / Date32 | sint_value |
| UInt8 / UInt16 / UInt32 / UInt64 / Date / DateTime | uint_value |
타입은 Nullable 및/또는 LowCardinality로 감쌀 수 있어요. NULL 값은 벡터 타일 포맷에 null이 없으므로 해당 피처의 그 속성을 생략해요. 다른 속성 타입은 예외를 발생시키며, stringify_unsupported가 설정된 경우에만 텍스트 string_value로 인코딩돼요.
동일한 속성 값은 레이어의 공유 값 풀에 인터닝되어, 많은 피처에 나타나는 값은 한 번만 저장돼요.
properties 튜플 이름 짓기
properties 튜플은 명시적인 요소 이름을 가져야 해요. tuple(...) 안의 컬럼 별칭은 튜플 요소 이름으로 전파되지 않으므로, 캐스트로 요소 이름을 지어줘요:
tuple(count(), any(id))::Tuple(cluster_count UInt64, id String)
클러스터링 (Clustering)
클러스터링은 함수가 아니라 SQL로 표현돼요. MVTEncodeGeom이 정수 픽셀로 반올림하므로, 픽셀 기하로 그룹화하면 일치하는 기하가 합쳐져요. 서브쿼리에서 그룹을 집계한 뒤, 클러스터당 한 행씩 MVTEncode에 넘겨요:
SELECT MVTEncode('points')(geom, tuple(cluster_count)::Tuple(cluster_count UInt64)) AS tile
FROM
(
SELECT MVTEncodeGeom((lon, lat)::Point, 10, 550, 335) AS geom, count() AS cluster_count
FROM points
GROUP BY geom
)
SETTINGS allow_suspicious_types_in_group_by = 1;
Geometry 값으로 그룹화하려면 allow_suspicious_types_in_group_by = 1이 필요해요. Variant 기반 Geometry 타입으로 그룹화하는 것이 기본적으로 제한되기 때문이에요. 클러스터링된 피처 대신 입력 행마다 피처 하나를 내보내려면 안쪽 GROUP BY(와 count())를 생략해요.
MVTBoundingBox
zoom, tile_x, tile_y로 식별되는 slippy-map 타일의 지리 경계 상자를 도(degree) 단위 튜플 (min_lon, min_lat, max_lon, max_lat)로 반환해요.
행당 웹 메르카토르 투영을 다시 계산하는 대신, longitude/latitude 컬럼 필터링에서 인덱스를 쓰도록 — 그 컬럼의 기본 키나 인덱스를 사용할 수 있게 — 행을 타일로 제한할 때 사용해요. 선택적인 margin은 타일 크기 대비 해당 비율만큼 상자를 사방으로 확장해요. MVTEncodeGeom의 클립 버퍼를 커버하려면 buffer / extent로 설정해요.
구문 (Syntax)
MVTBoundingBox(zoom, tile_x, tile_y[, margin])
인자 (Arguments)
zoom— Slippy-map 줌 레벨. 범위[0, 32].UInt8.tile_x— 타일 컬럼 인덱스. 범위[0, 2^zoom - 1].UInt32.tile_y— 타일 행 인덱스. 범위[0, 2^zoom - 1].UInt32.margin— 선택. 타일 크기 대비 상자를 사방으로 확장할 비율. 기본값은0.Float64.
반환 값 (Returned value)
도(degree) 단위 튜플 (min_lon, min_lat, max_lon, max_lat)로 타일 경계 상자를 반환해요. Tuple(Float64, Float64, Float64, Float64).
예시 (Example)
SELECT MVTBoundingBox(0, 0, 0) AS bbox
┌─bbox────────────────────────────────────────────┐
│ (-180,-85.05112877980659,180,85.05112877980659) │
└──────────────────────────────────────────────────┘
MVTBoundingBoxMercator
MVTBoundingBox의 웹 메르카토르 대응 함수예요. MVTEncodeGeom이 내부적으로 사용하는 전체-UInt32 웹 메르카토르 좌표 공간에서 타일의 경계 상자를 튜플 (min_x, min_y, max_x, max_y)로 반환해요. y축은 아래쪽으로 커져요(북쪽이 위). 메르카토르 좌표 컬럼을 구체화하고 longitude/latitude 대신 그걸 인덱싱하는 테이블을 위한 함수예요.
구문 (Syntax)
MVTBoundingBoxMercator(zoom, tile_x, tile_y[, margin])
인자 (Arguments)
MVTBoundingBox와 동일해요.
반환 값 (Returned value)
웹 메르카토르 좌표 튜플 (min_x, min_y, max_x, max_y)로 타일 경계 상자를 반환해요. Tuple(Float64, Float64, Float64, Float64).
예시 (Example)
SELECT MVTBoundingBoxMercator(1, 0, 0) AS bbox
┌─bbox────────────────────────┐
│ (0,0,2147483648,2147483648) │
└──────────────────────────────┘
행을 타일로 제한하기 (Restricting rows to a tile)
타일은 자신에게 속한 기하만 담아야 해요. 이는 협력하는 두 단계로 표현하는 것이 가장 좋아요: WHERE 절의 인덱스를 쓰는 저비용 경계 상자 조건(성능)과, MVTEncodeGeom의 클립(정확성). 클립이 타일 밖의 기하를 버리므로, 느슨한 경계 상자 조건조차 타일 밖 기하가 결과로 새어나가지 못하게 해요.
WITH
1 AS buffer,
4096 AS extent,
MVTBoundingBox({z:UInt8}, {x:UInt32}, {y:UInt32}, buffer / extent) AS bounding_box -- margin matches the clip buffer
SELECT MVTEncode('points')(geom, tuple(cluster_count)::Tuple(cluster_count UInt64))
FROM
(
SELECT MVTEncodeGeom((lon, lat)::Point, {z:UInt8}, {x:UInt32}, {y:UInt32}) AS geom, count() AS cluster_count
FROM points
WHERE lon BETWEEN bounding_box.1 AND bounding_box.3 AND lat BETWEEN bounding_box.2 AND bounding_box.4 -- index-using prefilter
GROUP BY geom
)
SETTINGS allow_suspicious_types_in_group_by = 1
경계 상자 조건은 대략적인 사전 필터일 뿐이고, 정확한 타일 경계는 MVTEncodeGeom의 클립이 보장해요. 클리핑을 끄고 WHERE 조건만 믿으려면 MVTEncodeGeom에 clip => false(일곱 번째 인자)를 넘겨요.
HTTP로 타일 제공하기 (Serving tiles over HTTP)
ClickHouse는 기본적으로 타일 엔드포인트를 노출하지 않아요. HTTP 인터페이스는 /의 쿼리만 받아요. 깔끔한 /tile/{z}/{x}/{y} URL은 서버 구성의 사전 정의 쿼리 핸들러로 운영자가 추가해요. 핸들러의 url은 regex: 형태를 사용해 경로 세그먼트를 캡처하고, 쿼리 매개변수에 바인딩하며, FORMAT RawBLOB으로 바이트를 반환해요.
가장 단순한 경우 테이블에 Geometry 컬럼이 있고 핸들러가 행당 피처 하나를 제공해요 — MVTEncodeGeom이 각 기하를 요청된 타일에 투영하고 클리핑하므로, 타일 밖의 행은 자동으로 빠져요:
<http_handlers>
<rule>
<methods>GET</methods>
<url><![CDATA[regex:/tile/(?P<z>\d+)/(?P<x>\d+)/(?P<y>\d+)]]></url>
<handler>
<type>predefined_query_handler</type>
<query>
SELECT MVTEncode('shapes')(
MVTEncodeGeom(geom, {z:UInt8}, {x:UInt32}, {y:UInt32}),
tuple(id, name)::Tuple(id UInt32, name String))
FROM shapes
FORMAT RawBLOB
</query>
<content_type>application/vnd.mapbox-vector-tile</content_type>
</handler>
</rule>
<defaults/>
</http_handlers>
여기서 shapes는 geom Geometry 컬럼(점·선·폴리곤의 어떤 조합이든)을 가진 테이블이에요. GET /tile/10/550/335이 인코딩된 타일을 반환해요.
점 데이터의 경우 MVTEncodeGeom((lon, lat)::Point, …)로 점을 인라인으로 만들어 일반 longitude/latitude 컬럼에서도 동일하게 동작해요. 일치하는 피처를 클러스터링하거나, 큰 테이블에 인덱스를 쓰는 경계 상자 사전 필터를 추가하려면 위의 "클러스터링"과 "행을 타일로 제한하기"처럼 안쪽 쿼리를 확장해요.
제한 사항 (Limitations)
- 웹 메르카토르 투영은 위도를
±85.05112878°로 제한하고, 날짜변경선(antimeridian)을 가로지르는 입력은 처리하지 못해요.
더 알아보기 (Learn more)
- ClickHouse 함수 목록 전체는 함수 개요를 참고해요.