Skip to main content

概述

Mapbox 矢量瓦片 (MVT) 是采用 protobuf 编码的瓦片,MapLibre 和 Mapbox GL 等 Web 地图 客户端可原生渲染这类瓦片。ClickHouse 可通过一对相互配合的函数,完全使用 SQL 构建此类瓦片:
  • MVTEncodeGeom — 一个标量函数,用于将几何对象投影到 slippy-map 瓦片的局部像素空间中,并 将其裁剪到该瓦片范围内。
  • MVTEncode — 一个聚合函数,用于将某个分组中的已投影几何对象聚合为单图层瓦片的 二进制字节。
两个辅助函数 MVTBoundingBoxMVTBoundingBoxMercator 会返回瓦片的边界框,以便在 WHERE 子句中使用索引将行 限制到该边界框内。 支持 Point、line 和 polygon 几何对象,包括 Geometry 类型以及具体的 geo types (PointLineStringMultiLineStringRingPolygonMultiPolygon) 。 生成的字节是一个完整的瓦片,可通过 HTTP interface 结合 FORMAT RawBLOB 直接返回。 这些函数对应 PostGIS 的工作流,同时也提供了使用 PostGIS 名称的别名:ST_AsMVTGeom 对应 MVTEncodeGeomST_AsMVT 对应 MVTEncode

MVTEncodeGeom

将以地理坐标 (经度/纬度) 表示的几何对象投影到由 zoomtile_xtile_y 标识的 slippy-map 瓦片局部像素空间中,将其裁剪到瓦片范围内,对齐到整数像素网格, 并返回瓦片空间中的几何对象。 该投影采用覆盖完整 UInt32 坐标范围的 Web Mercator。返回坐标的原点位于瓦片 左上角,y 轴向下,这正是 Mapbox Vector Tile 格式采用的坐标约定,因此结果可直接传给 MVTEncode。坐标会被舍入到整像素,因此按 MVTEncodeGeom 分组时,会将落在同一网格上的几何对象归并为一个聚类。 启用 clip 时 (默认值) ,几何对象会被裁剪到按 buffer 像素扩展后的瓦片范围内 (每个轴上的范围为 [-buffer, extent + buffer]) ;完全落在该范围之外的几何对象会变为 NULL。这相当于 PostGIS 的 ST_AsMVTGeom 输出几何类型取决于输入:Point 返回 PointLineStringMultiLineString 返回 MultiLineStringRingPolygonMultiPolygon 返回 MultiPolygon (裁剪可能会将一个几何对象分成 多个部分) 。 语法
MVTEncodeGeom(geometry, zoom, tile_x, tile_y[, extent[, buffer[, clip]]])
参数
  • geometry — 以经纬度 (度) 表示的 Geometry。经度会被限制在 [-180, 180],纬度会被限制在 Web Mercator 的范围 [-85.05112878, 85.05112878] 内。Point / LineString / MultiLineString / Ring / Polygon / MultiPolygon / Geometry
  • zoom — Slippy 地图的缩放级别,范围为 [0, 32]UInt8
  • tile_x — 瓦片列索引,范围为 [0, 2^zoom - 1]UInt32
  • tile_y — 瓦片行索引,范围为 [0, 2^zoom - 1]UInt32
  • extent — 可选的瓦片边长 (每边像素数) ,范围为 [1, 2147483647]。默认值为 4096,即 Mapbox Vector Tile 的默认值。UInt32
  • buffer — 可选的裁剪缓冲区 (像素) ,范围为 [0, 2147483647]。默认值为 1UInt32
  • clip — 可选标志;当其为非零值时 (默认如此) ,几何对象会被裁剪到瓦片加缓冲区的范围内。UInt8
返回值 返回瓦片空间中的几何对象;如果被完全裁剪掉,则返回 NULLGeometry 示例
SELECT MVTEncodeGeom((13.37, 52.52)::Point, 10, 550, 335) AS pixel
┌─pixel──────┐
│ (124,3384) │
└────────────┘

MVTEncode

将一组要素编码为二进制的 Mapbox Vector Tile layer。这是标量 函数 MVTEncodeGeom 的聚合版本。每个输入行都会成为一个要素;支持点、线和多边形几何。 geometry 参数是一个使用瓦片空间坐标的 Geometry,通常由 MVTEncodeGeom 生成。几何为 NULL 的行 (例如被 MVTEncodeGeom 裁剪掉的行) 会被跳过。可选的 properties 参数是一个 命名元组,其元素名称会成为要素属性的键,其元素类型则决定矢量切片中的值类型。 结果是单layer切片的原始字节数据。空分组会生成空切片。这对应于 PostGIS ST_AsMVT 语法
MVTEncode(layer_name[, extent[, feature_id_name[, stringify_unsupported]]])(geometry[, properties])
参数
  • layer_name — 矢量瓦片layer名称。String
  • extent — 瓦片每条边的像素范围,取值区间为 [1, 2147483647]。默认值为 4096UInt32
  • feature_id_name — 可选,用于指定 properties 元组中某个无符号整数字段的名称,将其作为 MVT 要素 的 id (UInt64) 输出,而不是作为标签输出。有符号整数会被拒绝。若 idNULL,则该 要素 不包含 id。参数按位置传递,因此要使用它,必须提供 extentString
  • stringify_unsupported — 可选标志 (0/1,默认值为 0) ;当设为 1 时,不受直接支持的属性类型 (如大整数、UUIDDecimal) 会被编码为其文本 string_value,而不是报错。UInt8
参数
  • geometry — 瓦片空间中的几何数据,例如来自 MVTEncodeGeomGeometry
  • properties — 可选的要素属性命名元组。元素名称会成为属性键。Tuple
返回值 返回单layer Mapbox Vector Tile 的二进制内容。String

属性类型

每个属性元素都会编码为与其 ClickHouse 类型相匹配的 Mapbox Vector Tile Value Variant:
ClickHouse 类型矢量切片值类型
String / FixedStringstring_value
Float32 / BFloat16float_value
Float64double_value
Boolbool_value
Int8 / Int16 / Int32 / Int64 / Date32sint_value
UInt8 / UInt16 / UInt32 / UInt64 / Date / DateTimeuint_value
类型可以包裹在 Nullable 和/或 LowCardinality 中。对于 NULL 值,会省略该要素的这个 attribute,因为 矢量切片格式不支持 null。其他任何属性类型都会引发异常,除非设置了 stringify_unsupported,此时 它会被编码为其文本形式的 string_value 相同的属性值会被归并到该 layer 的共享值池中,因此某个在许多要素中出现的值 只会存储一次。

为 properties Tuple 命名

properties Tuple 必须具有明确的元素名称。tuple(...) 内部的列别名不会传递给 Tuple 的元素名称,因此请使用类型转换为这些元素命名:
tuple(count(), any(id))::Tuple(cluster_count UInt64, id String)

聚类

聚类逻辑是在 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,因为默认情况下, 不允许按基于 VariantGeometry 类型分组。省略内部的 GROUP BY (以及 count()) 即可为每个输入行输出一个要素, 而不是输出聚类后的要素。

MVTBoundingBox

返回由 zoomtile_xtile_y 标识的 slippy-map 瓦片的地理边界框,以元组 (min_lon, min_lat, max_lon, max_lat) 表示,单位为度。 可在直接按 longitude/latitude 列过滤时,使用它将行限制在某个瓦片范围内——这样就可以利用这些列上的主键或 索引——而无需对每一行重新计算 Web Mercator 投影。可选参数 margin 会按瓦片大小的该比例向边界框四周扩展;将其设置为 buffer / extent,即可覆盖 MVTEncodeGeom 的裁剪缓冲区。 语法
MVTBoundingBox(zoom, tile_x, tile_y[, margin])
参数
  • zoom — Slippy 地图的缩放级别,范围为 [0, 32]UInt8
  • tile_x — 瓦片列索引,范围为 [0, 2^zoom - 1]UInt32
  • tile_y — 瓦片行索引,范围为 [0, 2^zoom - 1]UInt32
  • margin — 可选的瓦片尺寸比例,用于将边界框向四周扩展。默认为 0Float64
返回值 返回以度为单位的瓦片边界框,表示为元组 (min_lon, min_lat, max_lon, max_lat)Tuple(Float64, Float64, Float64, Float64) 示例
SELECT MVTBoundingBox(0, 0, 0) AS bbox
┌─bbox────────────────────────────────────────────┐
│ (-180,-85.05112877980659,180,85.05112877980659)  │
└──────────────────────────────────────────────────┘

MVTBoundingBoxMercator

MVTBoundingBox 的 Web Mercator 版本。返回 MVTEncodeGeom 在内部使用的完整 UInt32 Web Mercator 坐标空间中某个瓦片的边界框,形式为 Tuple (min_x, min_y, max_x, max_y)。y 轴向下增大 (顶部为北方) 。适用于将 Mercator 坐标列物化并为其创建索引,而不是为 longitude/latitude 创建索引的表。 语法
MVTBoundingBoxMercator(zoom, tile_x, tile_y[, margin])
参数 MVTBoundingBox 相同。 返回值 返回瓦片边界框,对应 Web Mercator 坐标中的 tuple (min_x, min_y, max_x, max_y)Tuple(Float64, Float64, Float64, Float64) 示例
SELECT MVTBoundingBoxMercator(1, 0, 0) AS bbox
┌─bbox────────────────────────┐
│ (0,0,2147483648,2147483648)  │
└──────────────────────────────┘

将行限制在单个瓦片内

一个瓦片只能包含属于它的几何对象。最好的做法是让两个步骤协同配合:在 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 的裁剪操作来保证。向 MVTEncodeGeom 传入 clip => false (第七个参数) 可禁用裁剪,并且仅依赖 WHERE 谓词。

通过 HTTP 提供瓦片

默认情况下,ClickHouse 不会提供瓦片端点:HTTP interface 只接受发送到 / 的查询。简洁的 /tile/{z}/{x}/{y} URL 由 operator 通过 预定义查询处理程序 添加到 服务器配置中。该处理程序的 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 会返回编码后的瓦片。 对于点数据,这种方法同样适用于普通的 longitude/latitude 列,只需通过 MVTEncodeGeom((lon, lat)::Point, …) 直接内联构造点即可。若要对重合的要素进行聚类,或为大型表添加利用索引的边界框预过滤, 请按 ClusteringRestricting rows to a tile 中所示扩展内部查询。

局限性

  • Web Mercator 投影会将纬度限制在 ±85.05112878°,且不处理跨越反经线的输入。
  • Polygon 裁剪不保证输出是有效的 MVT。 裁剪会修正环的方向和闭合性,但不会修复自相交。因此,自相交的 (“蝴蝶结形”) 环不会被修复:根据它与瓦片的相交情况,要么原样输出 (仍然无效) ,要么被丢弃为 NULL。例如,完全位于瓦片内的蝴蝶结形会被丢弃,而将相同的四个角按简单环的顺序连接时则会被保留:
-- self-intersecting ring -> dropped (NULL)
SELECT MVTEncodeGeom([[(40.0, 40.0), (50.0, 50.0), (50.0, 40.0), (40.0, 50.0), (40.0, 40.0)]]::Polygon, 2, 2, 1) IS NULL;  -- 1
-- simple ring, same four corners -> kept
SELECT MVTEncodeGeom([[(40.0, 40.0), (50.0, 40.0), (50.0, 50.0), (40.0, 50.0), (40.0, 40.0)]]::Polygon, 2, 2, 1) IS NULL;  -- 0
  • Geometry 会先被裁剪,再舍入到整数像素网格。 PostGIS 会先将几何对象贴合到整数像素网格上,再进行裁剪;MVTEncodeGeom 则是先裁剪 (在浮点投影坐标上) ,再进行舍入。在靠近瓦片边缘时,这可能会丢掉原本会被舍入到边界像素上的坐标。例如,当 buffer = 0 时,位于瓦片边缘正东侧的一个点会被裁剪掉,尽管它会被舍入到边缘像素 4096,而先舍入的方法会保留它:
-- floating-point x ~= 4096.23 is just past the east edge (extent = 4096) -> clipped
SELECT MVTEncodeGeom((90.005, 30.0)::Point, 2, 2, 1, 4096, 0) IS NULL;          -- 1
-- the same point projected without clipping rounds onto the edge pixel:
SELECT MVTEncodeGeom((90.005, 30.0)::Point, 2, 2, 1, 4096, 0, false);           -- (4096,2664)
Last modified on June 29, 2026