PostGIS矢量瓦片服务:生成MVT和矢量瓦片性能
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矢量瓦片 在 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,字段包括 id、landuse、name 和 geom。如果 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,函数输入为 z、x、y,输出为 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 的关键点有四个。
- 先用瓦片范围过滤。
p.geom && b.tile_4326用边界框做粗筛,配合 GiST 索引可以减少候选行。 - 再做精确相交判断。
ST_Intersects避免把只在外包矩形上碰到瓦片、实际几何不相交的数据送进转换流程。 - 转换到 3857 后再进入 MVT 坐标。
ST_AsMVTGeom使用的边界和几何应在同一坐标系中。 - 只返回必要字段。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。实际排查时,接口性能更常慢在候选集过大、几何太复杂、索引没有生效、每次请求重复投影、或者高并发下没有缓存。
建议先用 EXPLAIN 或 EXPLAIN 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 中常见的两个参数是 extent 和 buffer。extent 可以理解为瓦片内部坐标网格大小,常用值是 4096;buffer 用于在瓦片边界外保留少量几何,减少线和面在瓦片边界处被硬切造成的视觉问题。
如果你发现瓦片边缘的线符号断裂,或者跨瓦片的面标注显示不稳定,可以适当增加 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 函数,然后用执行计划检查慢瓦片,最后补上缓存、缩放级别控制和监控。这样搭出来的瓦片服务更容易上线,也更容易在数据增长后继续优化。
-
QGIS虚拟图层SQL查询:连接表和空间筛选 2026-06-13 01:55:21
-
DEM流向:水文分析和流域划分前处理 2026-06-13 01:50:34
-
无人机正射影像:航测正射和影像正射流程 2026-06-12 22:19:43
-
无人机航测精度:像控点布设和飞行高度计算 2026-06-12 20:49:03
-
OpenLayers点击事件:图层点击事件和坐标拾取 2026-06-12 01:38:49
-
QGIS Processing报错:Processing错误和处理工具箱打不开 2026-06-11 20:55:46
-
Sentinel2云掩膜:大气校正、GEE去云和NDVI检查 2026-06-11 13:42:34
-
ArcGIS Pro字段计算器:数值涵义和顺序编号 2026-06-11 11:39:27
-
ArcPy栅格计算:arcpy.sa和栅格计算器排查 2026-06-11 10:48:22
-
ArcPy字段计算:AddField、字段映射和更新游标 2026-06-11 09:49:34
-
Leaflet加载WMTS:瓦片地图和离线地图配置 2026-06-11 03:40:08
-
ArcPy投影转换:定义投影、重投影和空间参考 2026-06-10 20:51:20
-
OpenLayers图层不显示:WMTS、TIF加载和原因排查 2026-06-10 19:22:44
-
ArcPy批量裁剪:批处理栅格处理和输出检查 2026-06-10 18:47:40
-
GeoPandas裁剪:clip、读取SHP和GeoJSON裁剪流程 2026-06-10 08:45:06
-
ArcPy批量出图:arcpy.mp导出PDF和批量制图 2026-06-10 08:40:05
-
QGIS修复无效几何:修复几何和几何修复流程 2026-06-10 03:48:19
-
遥感监督分类:遥感图像监督分类步骤和精度验证 2026-06-09 18:16:55
-
无人机航线规划软件:规划方法和规划步骤 2026-06-09 15:16:34
-
无人机测绘流程:软件有哪些、数据处理和精度 2026-06-09 13:32:14