首页 GIS基础理论 PostGIS矢量瓦片服务:生成MVT和矢量瓦片性能

PostGIS矢量瓦片服务:生成MVT和矢量瓦片性能

作者: GIS研习社 更新时间:2026-05-25 11:03:36 分类:GIS基础理论

PostGIS矢量瓦片服务:生成MVT和矢量瓦片性能

很多 WebGIS 项目一开始用 GeoJSON 接口返回地块、道路、POI 或网格数据,小范围调试很顺利,一旦缩放到城市级别就开始卡顿。此时可以把数据发布方式改成 PostGIS矢量瓦片:数据库按 z/x/y 瓦片范围裁剪数据,返回 MVT 二进制瓦片,前端按需加载和渲染。

本文以一个“从 PostGIS 表动态发布矢量瓦片”的项目为例,讲清楚 PostGIS矢量瓦片服务 的基本结构、PostGIS生成MVT 的 SQL 写法,以及影响 PostGIS矢量瓦片性能 的关键检查点。重点不是堆参数,而是让你能写出可调试、可上线、可排错的瓦片接口。

问题背景:为什么 GeoJSON 接口会变慢

常见场景是这样的:数据库里有几十万到几百万个面要素或线要素,前端使用 Leaflet、OpenLayers 或 MapLibre 加载数据。开发阶段只查一个行政区,GeoJSON 返回几百条记录,页面很流畅;上线后用户缩放到全市范围,接口一次返回几十 MB,浏览器解析 JSON、投影坐标、绘制图形都变慢。

GeoJSON 的优点是直观、容易调试,但它并不适合把大范围、高密度矢量数据一次性塞给浏览器。矢量瓦片的思路是把地图按缩放级别和瓦片行列号切成小块,每次只返回当前视窗需要的瓦片。这样服务端、网络和浏览器都能分摊压力。

PostGIS矢量瓦片与PostGIS生成MVT服务流程示意图
一条完整的矢量瓦片链路应包含瓦片范围计算、空间索引过滤、MVT 几何转换、二进制响应和缓存策略。

因此,本文解决的问题不是“怎样把数据查出来”,而是“怎样按瓦片范围查出刚好需要的数据,并以浏览器更容易消费的格式返回”。这正是 PostGIS矢量瓦片 在 WebGIS 后端中的核心价值。

核心原理:PostGIS矢量瓦片如何从 z/x/y 变成 MVT

一次瓦片请求通常包含三个参数:z 是缩放级别,x 是瓦片列号,y 是瓦片行号。服务端拿到这三个参数后,需要把它们转换成 Web Mercator 坐标系下的瓦片边界,再用这个边界过滤空间表。

在 PostGIS 中,常用三个函数完成这条链路。

  • ST_TileEnvelope:根据 z/x/y 生成瓦片范围,默认用于 Web Mercator 瓦片体系。
  • ST_AsMVTGeom:把真实世界坐标中的几何对象裁剪并转换到 MVT 瓦片坐标空间。
  • ST_AsMVT:把一组属性和 MVT 几何聚合成一块 Mapbox Vector Tile 二进制数据。

这里容易误解的一点是:MVT 不是普通 JSON,也不是直接把数据库几何字段原样发出去。生成 MVT 时,几何对象会被裁剪、缩放到瓦片内部坐标,并和属性字段一起编码成二进制瓦片。前端看到的是一个图层名和一组要素,样式由客户端地图样式控制。

理解 PostGIS 瓦片性能时,可以把流程拆成四步:先算瓦片范围,再用索引筛候选数据,然后把几何转换成瓦片坐标,最后聚合成 MVT 返回。

数据准备:让空间表适合动态切瓦片

假设项目中有一张地块表 public.parcels,字段包括 idlandusenamegeom。如果 geom 使用 EPSG:4326,经纬度适合存储和交换,但瓦片计算通常需要 Web Mercator 坐标。因此第一步要确认 SRID、几何有效性和空间索引。

SELECT ST_SRID(geom) AS srid, GeometryType(geom) AS geom_type, COUNT(*) AS feature_count
FROM public.parcels
GROUP BY ST_SRID(geom), GeometryType(geom);

如果 SRID 为空或混乱,应先修正数据,而不是在瓦片函数里临时猜坐标系。正式服务中,空间索引是基本要求。

CREATE INDEX IF NOT EXISTS parcels_geom_gix
ON public.parcels
USING GIST (geom);

ANALYZE public.parcels;

几何特别复杂的面要素会放大 MVT 生成成本。县界、宗地、道路缓冲面、河道面这类数据如果节点过多,建议在入库阶段准备简化版本或按比例尺分层,而不是每次瓦片请求都对原始高精度几何做昂贵计算。

PostGIS生成MVT的基础 SQL 写法

下面的示例适合初次搭建动态瓦片服务。假设表的 geom 是 EPSG:4326,函数输入为 zxy,输出为 bytea。API 层只需要把这个二进制结果作为 .pbf 返回即可。

CREATE OR REPLACE FUNCTION public.tile_parcels(
  z integer,
  x integer,
  y integer
)
RETURNS bytea
LANGUAGE sql
STABLE
AS $$
WITH bounds AS (
  SELECT
    ST_TileEnvelope(z, x, y) AS tile_3857,
    ST_Transform(ST_TileEnvelope(z, x, y), 4326) AS tile_4326
),
mvtgeom AS (
  SELECT
    p.id,
    p.landuse,
    p.name,
    ST_AsMVTGeom(
      ST_Transform(p.geom, 3857),
      b.tile_3857,
      4096,
      64,
      true
    ) AS geom
  FROM public.parcels AS p
  CROSS JOIN bounds AS b
  WHERE p.geom && b.tile_4326
    AND ST_Intersects(p.geom, b.tile_4326)
)
SELECT ST_AsMVT(mvtgeom, 'parcels', 4096, 'geom', 'id')
FROM mvtgeom
WHERE geom IS NOT NULL;
$$;

这段 SQL 的关键点有四个。

  1. 先用瓦片范围过滤。p.geom && b.tile_4326 用边界框做粗筛,配合 GiST 索引可以减少候选行。
  2. 再做精确相交判断。ST_Intersects 避免把只在外包矩形上碰到瓦片、实际几何不相交的数据送进转换流程。
  3. 转换到 3857 后再进入 MVT 坐标。ST_AsMVTGeom 使用的边界和几何应在同一坐标系中。
  4. 只返回必要字段。MVT 属性不是越多越好,前端不用的字段不要放进瓦片。

如果你的表已经维护了 EPSG:3857 的几何列,可以避免每次请求都执行 ST_Transform。这通常是提升瓦片接口性能的第一批优化动作之一。

搭建 PostGIS矢量瓦片服务的接口层

SQL 函数只是数据库部分。要形成真正可用的瓦片服务,还需要一个 HTTP 接口,例如 /tiles/parcels/{z}/{x}/{y}.pbf。接口层负责解析 z/x/y,调用 SQL 函数,把返回的 bytea 作为二进制响应发送给前端。

接口实现可以使用 Node.js、Python、Go、Java 或现成瓦片服务框架。无论用哪种语言,都要注意三件事。

  • 不要把 MVT 包成 JSON。接口应该直接返回二进制瓦片,而不是返回 Base64 字符串或 JSON 字段。
  • 设置正确的响应类型。常见做法是返回 application/vnd.mapbox-vector-tile,并让 Web 服务器或网关处理压缩。
  • 限制参数范围。后端应校验 z/x/y 是否在允许范围内,避免异常请求扫描大范围数据或打满数据库。

前端侧使用 MapLibre、OpenLayers 或其他支持 MVT 的客户端时,图层 URL 指向这个接口即可。样式、颜色、线宽、标签字段通常不写在 MVT 里,而是在前端样式配置中控制。

PostGIS矢量瓦片性能:先优化候选集再优化函数

很多人看到瓦片接口慢,会直接怀疑 ST_AsMVT。实际排查时,接口性能更常慢在候选集过大、几何太复杂、索引没有生效、每次请求重复投影、或者高并发下没有缓存。

建议先用 EXPLAINEXPLAIN ANALYZE 查看单个慢瓦片的执行计划。测试时选一个真实的 z/x/y,不要只查空瓦片。

EXPLAIN ANALYZE
WITH bounds AS (
  SELECT
    ST_TileEnvelope(13, 6743, 3102) AS tile_3857,
    ST_Transform(ST_TileEnvelope(13, 6743, 3102), 4326) AS tile_4326
)
SELECT COUNT(*)
FROM public.parcels AS p
CROSS JOIN bounds AS b
WHERE p.geom && b.tile_4326
  AND ST_Intersects(p.geom, b.tile_4326);

如果执行计划显示大量顺序扫描,先检查索引和统计信息。如果候选行数量本身就很大,说明瓦片范围内数据密度高,需要按缩放级别控制图层、简化几何或使用聚合图层。

慢点 典型表现 处理方式
空间索引没有生效 执行计划出现大范围顺序扫描,CPU 和 IO 都高 确认 GiST 索引、SRID、统计信息和 WHERE 条件,不要在索引列外包一层运行时函数
每次请求都投影 候选行多时 ST_Transform 成本明显 维护 geom_3857 派生列并建立索引,或按业务坐标系预处理瓦片图层
几何节点过多 面瓦片生成慢,输出瓦片也很大 准备简化图层,按比例尺切换,必要时对大面做切分
属性字段过多 瓦片体积偏大,前端解析和网络传输变慢 只输出渲染和交互需要的字段,详情信息另走查询接口
没有缓存 热门区域反复打数据库,高并发下响应波动 对稳定图层缓存 z/x/y 结果,更新频率高的数据设置短 TTL

一个实用原则是:低缩放级别不要返回过细数据。全国、省、市尺度下直接返回每个宗地或每条支路,既不利于读图,也会拖慢服务。低 zoom 可以用行政区汇总、中心点聚合或简化边界,高 zoom 再展示详细对象。

优化写法:为 MVT 准备 3857 几何列

如果项目长期使用 Web Mercator 瓦片,建议在表中准备一个 geom_3857 字段,并单独建空间索引。这样生成 MVT 时就不必对候选几何反复投影。

ALTER TABLE public.parcels
ADD COLUMN IF NOT EXISTS geom_3857 geometry(MultiPolygon, 3857);

UPDATE public.parcels
SET geom_3857 = ST_Transform(geom, 3857)
WHERE geom_3857 IS NULL;

CREATE INDEX IF NOT EXISTS parcels_geom_3857_gix
ON public.parcels
USING GIST (geom_3857);

ANALYZE public.parcels;

然后瓦片函数可以改成直接用 geom_3857 过滤和转换。

CREATE OR REPLACE FUNCTION public.tile_parcels_fast(
  z integer,
  x integer,
  y integer
)
RETURNS bytea
LANGUAGE sql
STABLE
AS $$
WITH bounds AS (
  SELECT
    ST_TileEnvelope(z, x, y) AS tile_3857
),
mvtgeom AS (
  SELECT
    p.id,
    p.landuse,
    ST_AsMVTGeom(
      p.geom_3857,
      b.tile_3857,
      4096,
      64,
      true
    ) AS geom
  FROM public.parcels AS p
  CROSS JOIN bounds AS b
  WHERE p.geom_3857 && b.tile_3857
)
SELECT ST_AsMVT(mvtgeom, 'parcels', 4096, 'geom', 'id')
FROM mvtgeom
WHERE geom IS NOT NULL;
$$;

这个版本更简洁,但前提是 geom_3857 与原始几何保持同步。对于频繁更新的数据,可以用触发器、ETL 流程或定时任务维护派生列。不要让原始坐标和派生坐标长期不一致,否则瓦片会出现位置偏移或漏数据。

缓冲区、extent 和空瓦片排查

ST_AsMVTGeom 中常见的两个参数是 extentbufferextent 可以理解为瓦片内部坐标网格大小,常用值是 4096buffer 用于在瓦片边界外保留少量几何,减少线和面在瓦片边界处被硬切造成的视觉问题。

如果你发现瓦片边缘的线符号断裂,或者跨瓦片的面标注显示不稳定,可以适当增加 buffer。但 buffer 不是越大越好,过大会增加每块瓦片参与编码的几何范围。真正需要大范围标注避让时,前端样式、标注规则和图层层级也要一起调整。

空瓦片要分情况排查。

  • 数据库查询为空。先用同一个 z/x/y 的瓦片范围做 COUNT(*),确认范围内是否真的有数据。
  • 坐标系不一致。如果表是 4326,但过滤边界是 3857,或者反过来,索引过滤会得到错误结果。
  • 图层名不一致。前端样式中的 source-layer 必须和 ST_AsMVT 中的图层名一致。
  • 瓦片坐标方案不一致。前端、网关和后端要使用同一套 z/x/y 规则,尤其要注意 TMS 与 XYZ 的 y 方向差异。
  • 返回格式被改坏。如果接口把 bytea 当文本、JSON 或 HTML 返回,前端会认为瓦片不可解析。

常见坑点:这些问题会拖慢 PostGIS矢量瓦片

  • 在 WHERE 中对索引列直接 ST_Transform。例如用 ST_Transform(p.geom, 3857) && tile 过滤 4326 原始列,可能让已有索引难以发挥作用。更稳的是把瓦片范围转换到表的 SRID,或维护 3857 派生列。
  • 一个瓦片层塞太多业务字段。MVT 适合携带渲染和点击识别所需字段,不适合把详情页全部属性都放进去。
  • 低缩放级别返回明细数据。z 很小时,单块瓦片覆盖范围很大,明细要素数量可能暴涨。应使用概化层、聚合层或直接隐藏该图层。
  • 没有区分静态图层和动态过滤。边界、道路底图等相对稳定的数据适合缓存或预生成;按用户权限、时间、条件过滤的数据更适合动态 SQL。
  • 把瓦片服务当作属性查询接口。地图渲染走 MVT,详情面板可以通过要素 id 再查数据库,两个接口职责分开会更稳定。
  • 忽略数据库连接池。瓦片请求数量通常很密集,没有连接池或连接数配置不合理,会导致接口层排队或数据库连接耗尽。

工具和方法对比:动态 MVT、预生成瓦片和 GeoJSON

同一个 WebGIS 项目不一定全部使用动态 MVT。方法选择取决于数据更新频率、查询条件复杂度、并发规模和运维能力。

方法 适合场景 注意事项
PostGIS 动态 MVT 数据来自数据库,更新较频繁,需要按权限、时间或属性动态过滤 SQL、索引、缓存和连接池都要设计好,复杂几何需要预处理
预生成 MBTiles 或 PMTiles 底图、行政边界、道路、兴趣点等相对稳定图层 查询灵活性较弱,但并发和分发能力更强,适合 CDN 或对象存储
GeoJSON 接口 小数据量、后台管理、单要素编辑、调试接口 大范围明细数据会导致传输和浏览器解析压力过大
专用瓦片服务框架 团队希望减少接口层代码,并统一管理图层配置 仍然需要理解 SQL、空间索引和瓦片参数,否则只是把问题移到配置文件中

实际项目中常见组合是:基础底图和低频更新图层预生成,业务实时图层使用 PostGIS矢量瓦片 动态发布,点选详情和编辑仍使用普通 API。这样既保留交互灵活性,又能控制数据库压力。

实战检查清单:上线前逐项确认

在把瓦片接口接入正式地图前,可以按下面清单检查。它比单纯调 SQL 更接近真实上线流程。

  • 坐标系。确认原始表 SRID、瓦片范围 SRID、ST_AsMVTGeom 输入几何处于同一逻辑链路。
  • 索引。空间列已有 GiST 索引,常用属性过滤字段也有合适索引,并执行过 ANALYZE
  • 字段。瓦片只输出 id、分类、名称、样式条件等必要属性。
  • 缩放级别。不同 zoom 返回不同层级的数据,不在小比例尺返回全量明细。
  • 单瓦片大小。抽查热点区域瓦片体积,过大的瓦片要减少字段、简化几何或调整图层显示级别。
  • 空瓦片。确认没有数据的瓦片能快速返回,不要让空瓦片也进行昂贵扫描。
  • 缓存。稳定瓦片设置缓存策略,动态瓦片至少设置合理的短缓存或接口限流。
  • 前端样式。source-layer 名称、字段名、过滤条件和图层可见级别与后端输出一致。
  • 监控。记录慢瓦片的 z/x/y、SQL 耗时、返回体积和数据库连接使用情况。

FAQ:瓦片服务、性能和 MVT 生成

PostGIS生成MVT一定要把数据先转成 3857 吗?

不一定必须先落表成 3857,但 MVT 几何转换时要保证输入几何和瓦片边界在同一坐标系。小数据量可以在查询中使用 ST_Transform,数据量大或并发高时,建议维护 3857 派生列并建空间索引,这通常更有利于整体响应速度。

PostGIS矢量瓦片性能慢,应该先查哪里?

先查单个慢瓦片的执行计划和候选行数量。确认空间索引是否生效、瓦片范围是否正确、输出字段是否过多、几何是否过复杂。不要一开始就调数据库全局参数,先证明瓶颈发生在过滤、投影、几何转换还是 MVT 聚合。

PostGIS矢量瓦片服务可以直接给 OpenLayers 或 MapLibre 用吗?

可以。接口需要返回 MVT 二进制数据,前端的 source-layer 名称要与 ST_AsMVT 中的图层名一致。样式规则通常写在前端地图配置里,后端瓦片只负责返回几何和必要属性。

ST_AsMVTGeom 的 extent 和 buffer 应该怎么设置?

extent 常用 4096,能满足多数渲染精度需求。buffer 用来减少瓦片边缘裁剪造成的断裂,线和面图层可以从 64 这类小缓冲开始测试。最终值应结合瓦片大小、符号样式和边缘显示效果决定。

MVT 返回空结果,是 SQL 错了吗?

不一定。先用相同 z/x/y 计算瓦片范围,并对原表执行 COUNT(*) 检查是否有候选要素。如果数据库有数据但前端不显示,再检查坐标系、图层名、返回的 Content-Type、bytea 是否被错误编码,以及 XYZ/TMS 的 y 坐标是否混用。

PostGIS矢量瓦片适合替代所有空间接口吗?

不适合。MVT 适合地图渲染和轻量交互,不适合承载完整属性编辑、复杂空间分析或详情页数据。更稳的设计是地图用瓦片,点选详情用普通 API,后台编辑和空间处理仍走专门的数据库或业务接口。

结论:把 PostGIS矢量瓦片做成可调试的服务

PostGIS矢量瓦片 的优势不只是“文件更小”,而是把大范围矢量数据拆成可缓存、可按需加载、可分层控制的瓦片请求。真正的难点在于:SQL 要让索引先缩小候选集,几何要按比例尺控制复杂度,接口层要正确返回二进制瓦片,前端样式要和图层名、字段名保持一致。

如果你正在从 GeoJSON 迁移到 MVT,可以按本文顺序做:先确认数据和索引,再写最小可用 SQL 函数,然后用执行计划检查慢瓦片,最后补上缓存、缩放级别控制和监控。这样搭出来的瓦片服务更容易上线,也更容易在数据增长后继续优化。

相关文章