Mapbox Vector Tiles

Mapbox Vector Tiles (MVT) 함수

Mapbox Vector Tiles (MVT)은 MapLibre, Mapbox GL 같은 웹 지도 클라이언트가 네이티브로 렌더링하는 protobuf 인코딩 타일이에요. ClickHouse는 협력하는 한 쌍의 함수로 이런 타일을 순수 SQL에서 만들 수 있어요.

출처: 문서

본문

개요 (Overview)

  • MVTEncodeGeom — 스칼라 함수로, 기하(geometry)를 slippy-map 타일의 타일-로컬 픽셀 공간으로 투영하고 타일에 클리핑해요.
  • MVTEncode — 집계 함수로, 그룹의 투영된 기하들을 단일 레이어 타일의 이진 바이트로 모아요.

두 개의 보조 함수 MVTBoundingBoxMVTBoundingBoxMercator는 타일의 경계 상자를 반환해서, 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 값에 놓인 기하에만 영향을 줘요.

출력 기하 타입은 입력에 따라 달라져요: PointPoint를, MultiPointMultiPoint를, LineString 또는 MultiLineStringMultiLineString을, Ring·Polygon·MultiPolygonMultiPolygon을 반환해요(클리핑이 기하를 여러 조각으로 나눌 수 있어요).

구문 (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 Feature id(UInt64)로 내보낼 이름이에요. 부호 있는 정수는 거부돼요. NULL id는 해당 피처에서 생략돼요. 매개변수는 위치 기반이라 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 조건만 믿으려면 MVTEncodeGeomclip => false(일곱 번째 인자)를 넘겨요.

HTTP로 타일 제공하기 (Serving tiles over HTTP)

ClickHouse는 기본적으로 타일 엔드포인트를 노출하지 않아요. HTTP 인터페이스는 /의 쿼리만 받아요. 깔끔한 /tile/{z}/{x}/{y} URL은 서버 구성의 사전 정의 쿼리 핸들러로 운영자가 추가해요. 핸들러의 urlregex: 형태를 사용해 경로 세그먼트를 캡처하고, 쿼리 매개변수에 바인딩하며, 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>

여기서 shapesgeom Geometry 컬럼(점·선·폴리곤의 어떤 조합이든)을 가진 테이블이에요. GET /tile/10/550/335이 인코딩된 타일을 반환해요.

점 데이터의 경우 MVTEncodeGeom((lon, lat)::Point, …)로 점을 인라인으로 만들어 일반 longitude/latitude 컬럼에서도 동일하게 동작해요. 일치하는 피처를 클러스터링하거나, 큰 테이블에 인덱스를 쓰는 경계 상자 사전 필터를 추가하려면 위의 "클러스터링"과 "행을 타일로 제한하기"처럼 안쪽 쿼리를 확장해요.

제한 사항 (Limitations)

  • 웹 메르카토르 투영은 위도를 ±85.05112878°로 제한하고, 날짜변경선(antimeridian)을 가로지르는 입력은 처리하지 못해요.

더 알아보기 (Learn more)

  • ClickHouse 함수 목록 전체는 함수 개요를 참고해요.