做地理空间数据库相关工作,有一组能力你躲不掉:PostGIS 的空间函数,加上 pgRouting 的最短路径和距离计算。准备地理空间数据库的笔试、面试,或者要在项目里做路径分析、范围检索、可达性评估,翻来覆去考的其实就是这两块。刚上手的人看到几十个函数常常头皮发麻,其实没那么复杂,关键是把函数分好类,再把 pgRouting 的完整链路跑通。这篇内容就围绕 30 个 PostGIS 核心空间函数和 pgRouting 最短路径实操展开,按场景讲清楚每个函数解决什么问题、SQL 怎么写、里面积了什么坑。
不管你是在复习地理空间数据库的必考点,还是已经接到一个需要“算最短路径”的需求,这套内容都可以直接参考。PostGIS 不是给 PostgreSQL 加了一堆工具函数,而是彻底引入了几何类型、空间索引和空间计算体系;pgRouting 则是把路网数据变成可计算的拓扑网络,两者合起来,数据库自己就能回答“哪两个点最近”“哪条路最省时间”“哪些点在范围内”。
1. 先认清 PostGIS 到底在算什么
1.1 一个扩展,补上了数据库缺失的空间计算能力
PostgreSQL 本身擅长存数据、做事务、写 SQL,但它的原生类型里没有“点”“线”“面”这种东西。你当然可以用两个字段存经纬度,但那样做不了“判断两条线是否相交”“算两个商圈的重叠面积”这类操作。PostGIS 干的事,就是给 PostgreSQL 增加了 geometry 和 geography 两种空间数据类型,以及配套的 GiST 空间索引和海量空间函数。
我见过不少同学疯狂背函数列表,但忽略了一个更本质的东西:空间索引。没有 GiST 索引,哪怕你会写 ST_Buffer、ST_Intersects,数据量一大查询就全表扫描,几十万条记录能跑出分钟级。而 Analyzing 空间数据时,大量函数都是“索引友好型”的,比如 ST_DWithin、ST_Intersects、<->距离排序。学 PostGIS 的第一步,不是会背函数,而是知道哪些函数能用上索引,哪些不能。
1.2 30 个函数怎么拆,pgRouting 怎么定位
PostGIS 现在有几百个函数,但高频使用的核心函数集中在四类:构造与读写、几何编辑与空间分析、空间关系判断、距离与度量。下面这张表就是我对“必考 30 个”的整理方式。每个函数我都标了它最常见的用途,你可以把它当索引来用,面试前刷一遍,项目里遇到对应场景直接回头看本文。
pgRouting 则单独成一块。它不是 PostGIS 的一部分,而是 PostgreSQL 的另一个扩展,专门做图论计算:把道路表抽象成“点”和“边”,然后跑 Dijkstra、A* 这类最短路径算法。它和距离函数是配套的关系:distance 是基础度量,pgRouting 里的 cost 通常由距离、时间决定。
2. 30 个核心空间函数分类盘点
2.1 构造与读写:从数据到几何,再从几何到数据
| 函数 | 作用 | 典型用法 |
|---|---|---|
| ST_GeomFromText | 从 WKT 文本构造几何 | ST_GeomFromText('POINT(116.4 39.9)') |
| ST_GeomFromGeoJSON | 从 GeoJSON 构造几何 | 前端上传 GeoJSON 直接入库 |
| ST_AsText | 把几何转成 WKT 文本 | 调试、导出 |
| ST_AsGeoJSON | 把几何转成 GeoJSON | Web 地图前端渲染 |
| ST_Point | 由经纬度坐标生成点 | ST_Point(116.4, 39.9) |
| ST_MakeLine | 将多个点连成线 | 轨迹点排序后聚合 |
| ST_SetSRID | 设置几何的坐标系编号 | 给裸坐标补 4326 |
| ST_Transform | 执行坐标转换 | 4326 转 3857 或地方投影坐标 |
这一组是入门的命脉。实际开发中,数据从外部系统过来大多是 CSV、Excel、GeoJSON、WKT 文本,第一步就是把这些文本转成 PostGIS 能计算的几何对象。ST_GeomFromText 和 ST_GeomFromGeoJSON 是两条最常见的入库通道;ST_AsText 和 ST_AsGeoJSON 则是输出通道,尤其是 ST_AsGeoJSON,后端把查询结果直接喂给 Leaflet、OpenLayers 非常方便。
这一组里最容易翻车的是 ST_SetSRID 和 ST_Transform 的区别。很多新手拿到一份带经纬度的表,顺手ST_SetSRID(geom, 4326)就以为坐标“转好了”,其实 ST_SetSRID 只是给几何贴了一个坐标系标签,坐标数值没动。ST_Transform 才是真正做投影换算。举个例子:原始数据是 WGS84 经纬度,你如果想算米制距离,得先用 ST_Transform 转到 3857 或本地投影,再计算;中途只贴标签,后续运算结果一定不对。
如果你要处理的是 GPS 轨迹、配送路径这类数据,ST_MakeLine 也很重要。它通常配合GROUP BY和ORDER BY使用,把同一辆车的轨迹点按时间排好序,再聚合成一条线:
SELECT vehicle_id, ST_MakeLine(geom ORDER BY record_time) AS track_line FROM vehicle_log GROUP BY vehicle_id;这种写法在轨迹还原、里程统计里几乎是标配。
2.2 几何编辑与空间分析:缓冲区、交集、合并与抽稀
| 函数 | 作用 | 典型用法 |
|---|---|---|
| ST_Centroid | 求面要素的几何中心 | 计算区域中心点 |
| ST_Buffer | 生成缓冲区 | 周边 500 米范围 |
| ST_ConvexHull | 求点/线/面的凸包 | 覆盖范围分析 |
| ST_Intersection | 求两个几何的交集部分 | 裁剪、叠置分析 |
| ST_Union | 合并几何对象 | 多个面合成一个面 |
| ST_Difference | 求差集 | 扣除重叠区域 |
| ST_Simplify | 几何抽稀,保留主要轮廓 | 减少数据量,加速显示 |
| ST_Envelope | 求几何的最小外接矩形 | 相交预判断 |
这里面的高频操作是 ST_Buffer、ST_Intersection 和 ST_Union。ST_Buffer 最容易踩单位坑:如果你直接用 4326 经纬度坐标,ST_Buffer(geom, 1000)出来的“1000”不是米,而是 1000 度(那会变成一个覆盖半个地球的圆)。要生成以米为单位的缓冲区,首先得保证几何处于米制投影坐标下,比如 3857,或中国常用的 4547、4529 等地方坐标系。如果只有 4326,可以这样处理:
SELECT ST_Buffer( ST_Transform(ST_SetSRID(ST_Point(116.4, 39.9), 4326), 3857), 1000 );ST_Intersection 和 ST_Union 是叠置分析的重点。ST_Union 常用于把分散的行政区合并成一个大区,或者把路网里互相打断的小线段合并成完整路径;ST_Intersection 则常用于计算两个图层重叠的区域,比如“这个缓冲区和哪些地块相交”。笔试里经常问的“求两个图层相交面积”,其实就是ST_Intersection+ST_Area的组合。
ST_Translate、ST_Simplify 这类“编辑型”函数我建议也眼熟。尤其拿到一批高精度路网或海岸线数据时,几百万个点会让前端卡死,调用一次 ST_Simplify 把冗余顶点抽掉,数据量能下降一个数量级,而且视觉上几乎看不出差异。
2.3 空间关系判断:相交、包含、邻接、穿越
| 函数 | 作用 | 典型用法 |
|---|---|---|
| ST_Intersects | 两个几何是否相交 | 空间连接、过滤 |
| ST_Contains | A 是否包含 B | 找出商圈内的门店 |
| ST_Within | A 是否在 B 内部 | 点和面的包含判断 |
| ST_Touches | 两个几何是否边缘邻接 | 找相邻地块 |
| ST_Crosses | 线是否穿越面/线 | 公路穿越保护区 |
| ST_Overlaps | 两个几何是否有重叠部分 | 行政区边界重叠检查 |
| ST_Disjoint | 两个几何是否完全分离 | 排除相交记录 |
空间关系判断函数在 SQL 里主要出现在 JOIN 条件和 WHERE 条件中。它们的输出都是布尔值,可以无缝配合普通 SQL 逻辑。用起来也很直观:要判断点是否落在面内,可以用 ST_Within 或 ST_Contains;要判断路网里哪些线路穿过了某个保护区,用 ST_Crosses 或 ST_Intersects。
这组里要特别理解 ST_Contains 和 ST_Within 是互逆关系:ST_Contains(A, B)等价于ST_Within(B, A)。做题时容易绕,记住“Contains 是前者包含后者,Within 是前者位于后者内部”就好。ST_Touches 则要求两个几何只在边界上接触,不能有内部相交,比如相邻地块只共墙,就是典型的 Touches 关系。ST_Overlaps 常用于检查两个面是否存在部分重叠,比如规划用地和保护区范围有重叠区域。
一个实战中非常高频的查询是“找出落在某个区域内的所有 POI”,我通常这样写:
SELECT p.id, p.name FROM poi p JOIN district d ON ST_Contains(d.geom, p.geom) WHERE d.name = '核心商圈';这条 SQL 背后的执行计划,如果没有 GiST 索引,会先把两表所有几何读出来逐对判断;建了索引之后,PostGIS 会先用外接矩形做粗略过滤,再做精确几何判断,速度可以提升几十倍。
2.4 距离与度量:算长度、面积和最近关系
| 函数 | 作用 | 典型用法 |
|---|---|---|
| ST_Distance | 两个几何间最小距离 | 点与面最近距离 |
| ST_DWithin | 距离是否小于给定阈值 | 周边范围内的点 |
| ST_ClosestPoint | 几何上距离另一个几何最近的点 | 求最近接入点 |
| ST_ShortestLine | 两个几何之间的最短连线 | 可视化最近路径 |
| ST_Length | 线要素长度 | 道路里程 |
| ST_Area | 面要素面积 | 面积统计 |
| ST_LineLocatePoint | 点在线上投影位置(0~1) | 计算里程桩号 |
ST_Distance 是距离计算的核心,但它有个容易被忽略的问题:如果几何是 4326 经纬度,ST_Distance 返回的是“度”,不是米。1 度纬度约等于 111 公里,1 度经度则随纬度变化,所以直接拿经纬度算出来的数值很难解读。解决办法是转成 geometry 的投影坐标系,或者用::geography类型强制按球面算米:
SELECT ST_Distance( ST_SetSRID(ST_Point(116.4, 39.9), 4326)::geography, ST_SetSRID(ST_Point(116.5, 39.95), 4326)::geography ) AS distance_m;ST_DWithin 是距离阈值过滤的最佳选择。做“周边 500 米”类需求时,很多人习惯写ST_Distance(geom, target) < 500,这个写法不一定错,但它没法充分利用空间索引,数据量大时很吃亏。换成ST_DWithin(geom, target, 500),PostGIS 能先通过索引快速筛出候选集,再精确判断,性能和表达方式都更好。
ST_ClosestPoint 和 ST_ShortestLine 在路径规划里很有用。比如一个车辆点位可能不在道路线上,你需要找到它距离路网最近的那个点,再从这个点开始进入路网寻路。这就用 ST_ClosestPoint 或者ST_ShortestLine先求出接入点。ST_LineLocatePoint 则更偏测量方向,比如把 GPS 点投影到某条道路上,返回 0 到 1 的比例位置,乘以道路长度就能换算成桩号。
3. 距离函数背后:从“能算”到“会算”
3.1 三个距离函数的选型差异
距离函数并不是越多越好,关键是分清楚用哪个。ST_Distance 返回两个几何之间的最近距离;ST_DWithin 是一个“距离阈值判断”,适合过滤;ST_ShortestLine 则把最近距离对应的那条连线以几何形式输出,适合画图。如果只想判断“是否在 5 公里内”,用 ST_DWithin;如果要算“最近有多远”,用 ST_Distance;如果想把“最近点之间的连线”画在地图上,用 ST_ShortestLine 最直观。
ST_DWithin 还有一个变体:它支持 geometry 和 geography 两种类型。当参数是 geography 时,第三个参数的单位就是米;当参数是 geometry 且坐标系是 4326 时,第三个参数是度。所以很多人写 500 想表示 500 米,结果查出来的范围却是搞笑级别的错误。解决这个问题看你的数据情况:最好在建表时就设计好投影坐标系,或者统一用 geography。
3.2 坐标系与单位陷阱:geometry 还是 geography
这里值得展开说。geometry 类型是平面计算,速度快,所有函数都是基于坐标数值直接算;geography 类型是球面计算,理解上更像“真实地球”,但它内部要处理大量三角函数,性能比 geometry 慢不少。
项目里常见做法是:数据入库用 4326 geometry 做存储和通用交换,真正做米制空间分析时就转投影坐标系,比如 3857 是 Web 墨卡托,适合全球尺度展示但不适合精确测量;局部区域用国家或地方坐标系更准。做距离、缓冲区、面积计算时,如果没转投影,可以考虑直接用::geography分支算,代价是慢一点,但对于几十万的数据量通常也能接受。
还有一条很重要的经验:不要对比两个坐标系不同的 geometry 调用距离函数。PostGIS 会直接报错,或给出一个莫名其妙的值。每次使用 ST_Transform 之前,想清楚源坐标系和目标坐标系是什么。
3.3 距离衰减函数:从“算距离”到“分析影响力”
距离衰减函数是地理信息分析里的经典模型,它不是一个写死的 PostGIS 内置函数,而是一类分析表达式。做商业选址、可达性评价、公共服务覆盖分析时,经常会遇到“距离越远,影响力越小”这种需求。常见的衰减模型有三种:线性衰减、指数衰减、幂函数衰减。它们都可以用 SQL 结合 ST_Distance 现算。
举个例子,要计算某个点周围一圈小区的出行便利度,可以用高斯衰减模型给每个小区打分:
SELECT b.id, b.name, ST_Distance(b.geom, ST_SetSRID(ST_Point(116.4, 39.9), 4326)::geography) AS dist_m, EXP(-1.0 * ST_Distance(b.geom, ST_SetSRID(ST_Point(116.4, 39.9), 4326)::geography) / 2000.0) AS decay_score FROM building b WHERE ST_DWithin(b.geom::geography, ST_SetSRID(ST_Point(116.4, 39.9), 4326)::geography, 5000);这里的衰减参数 2000 是衰减半径,数值越大,距离影响减小得越慢。实际分析时你可以根据场景调整模型:比如食堂选址,5 公里外影响可以忽略;外卖配送,衰减半径可能只需要 1 公里。理解了距离衰减函数,等于把 PostGIS 从“算距离”的工具升级成了“做空间分析”的建模工具。
4. pgRouting 最短路径:把路段表变成可计算路网
4.1 核心思路:边表、顶点表和拓扑
pgRouting 本身不负责“找路”,它只负责“在已经建好的拓扑网络里找最短路径”。什么是拓扑?通俗讲,就是把现实中的道路抽象成一张图:道路的转弯点是顶点,两段道路之间的连接关系由顶点定义,每条道路(边)带有通行成本,比如长度或行驶时间。
要跑 pgRouting,你首先需要一张包含字段id、source、target、cost(以及可选reverse_cost)的边表。其中source是这条边的起点顶点编号,target是终点顶点编号,cost是正方向通行成本,reverse_cost是反方向通行成本。如果现实道路是双向通行的,两个 cost 设成一样;如果是一条单行线,就把禁止方向设成 -1。
很多人第一次接触时卡在:原始道路表只有id和geom,没有source和target。这个不用担心,pgRoutiong 提供了自动构建拓扑的工具。
4.2 用 pgr_createTopology 自动构建拓扑
我以一张常见的道路表road为例,字段有id、geom。执行下面的 SQL,它会自动给道路的端点编号,找出互相连接的端点,并创建一张顶点表:
SELECT pgr_createTopology('road', 0.00001, 'geom', 'id');几个参数说明一下:第一个参数是表名;第二个是容差,判断两个端点是否应该“粘在一起”。容差的值需要根据你的坐标系定,如果坐标是经纬度,0.00001 大约对应 1 米左右;如果坐标是米制投影,通常用 0.01 或 0.5。容差设大了,会把本该分开的路口连起来;设小了,又会把实际上相交的道路断开,所以这个值需要反复实验。
执行成功后,road表会自动多出source和target两个字段,同时生成一张名为road_vertices_pgr的顶点表。这张顶点表记录了网络里每一个节点,后续查询起点和终点,都要从这里面找节点 id。
4.3 pgr_dijkstra 最短路径 SQL 拆解
拓扑建好后,最核心的查询就长这样:
SELECT * FROM pgr_dijkstra( 'SELECT id, source, target, cost, reverse_cost FROM road', start_vid, end_vid, directed := true );第一参数是一个返回边表的 SQL 子查询;第二个参数是起点顶点 ID;第三个参数是终点顶点 ID;第四个参数directed := true表示按有向图计算,单行线、禁行方向都能正确处理。
这个函数的返回结果是这样一张表:
| 字段 | 含义 |
|---|---|
| seq | 结果行的顺序号 |
| path_seq | 路径内部的节点顺序 |
| node | 当前顶点 ID |
| edge | 经过的边 ID,-1 表示路径结束 |
| cost | 这条边的通行成本 |
| agg_cost | 从起点到当前节点的累计成本 |
实际使用时,我们通常并不关心 vertex 本身,而是想把路径对应的道路几何画出来。所以一般会这样写:
SELECT pq.seq, pq.node, pq.edge, r.geom FROM pgr_dijkstra( 'SELECT id, source, target, cost, reverse_cost FROM road', 123, 456, directed := true ) pq LEFT JOIN road r ON pq.edge = r.id ORDER BY pq.seq;这里有个常见错误:看到pq.edge = -1的结束行,它并不是一条道路,而是 Dijkstra 算法的终止标记。如果直接用这个 edge 去 JOIN road 表,JOIN 不到,也没关系,因为最后一行本来就只是为了标记“到达终点”。绘制路径时,把 JOIN 到的所有线按 seq 顺序合并显示即可。
如果你只想算最短路径长度,而不需要几何,可以直接用pgr_dijkstraCost。它返回每个目标节点的累计成本,适合做“多点可达性矩阵”,比如计算某个地铁站到全城所有站点的乘车时间。
4.4 真实路网里,起终点不是路网节点怎么办
pgRouting 的算法入口是节点 ID,但真实业务里,用户输入的多半是经纬度坐标。处理思路很固定:先把经纬度转换成几何对象,然后找距离它最近的顶点,把它作为路径起点。
查找最近顶点时,我强烈推荐用<->操作符而不是 ST_Distance 加 ORDER BY:
SELECT id FROM road_vertices_pgr ORDER BY geom <-> ST_SetSRID(ST_Point(116.4, 39.9), 4326) LIMIT 1;<->是 PostGIS 里专门用于“KNN 最近邻搜索”的距离算子,配合 GiST 索引可以快速返回最近结果,而 ST_Distance 做排序会导致全表计算,数据量一大就崩。找到最近顶点后,再把它传给 pgr_dijkstra。
5. 高频踩坑与排查速查
5.1 PostGIS 安装失败和 CREATE EXTENSION 报错
这个坑我在新环境里踩过很多次。最常见的现象是:CREATE EXTENSION postgis;执行后报错“无法加载库 postgis-3”或“could not open extension control file”。
原因基本是版本不匹配,比如 PostGIS 3.4 对应 PostgreSQL 16,但你的 PostgreSQL 是 15,两者不配套;或者 Windows 安装时 bin 目录没加入 PATH,数据库进程找不到 DLL 文件。解决办法是:卸载后重新安装同一套版本,Windows 上最好用 Stack Builder 安装 PostGIS Bundle,Linux 上用系统包管理器安装对应 PostgreSQL 版本的 postgis 包。装完后执行SELECT postgis_version();验证是否成功。
pgRouting 的安装也类似,很多人在 PostGIS 正常后,执行CREATE EXTENSION pgRouting;发现没有这个控制文件。这说明系统里根本没装 pgRouting 库。Windows 的 PostGIS Bundle 有时候默认不勾选 pgRouting,重新安装时记得勾上;Linux 则要单独装postgresql-XX-pgrouting包。
5.2 source/target 为 0、路径算不出来的原因
生成拓扑后,常见的异常是道路表里的source或target出现 0。0 代表这条边的某个端点没有连接上任何顶点,通常是因为容差设置太小,本应相交的路口被当成断开处理。解决方法不是去改数据,而是回头调整pgr_createTopology的容差参数重新生成。
还有一种情况是道路线段之间没有精确相交,视觉上看似连着,实际端点并不重合。这时候需要先运行pgr_nodeNetwork,把相交的线段打断,再重新构建拓扑。这个函数是路网预处理的常用工具,很多新手不知道它,导致拓扑总是残缺。
5.3 查询性能问题:两个索引就够了
最短路径查询慢,绝大多数不是算法问题,而是表结构缺索引。道路表的source和target字段要建普通 B-tree 索引,顶点表的geom字段要建 GiST 索引。两句话就能解决:
CREATE INDEX idx_road_source ON road(source); CREATE INDEX idx_road_target ON road(target); CREATE INDEX idx_vertex_geom ON road_vertices_pgr USING GIST(geom);另外提醒一点:边表 SQL 子查询里的cost和reverse_cost不要存成文本类型,一定要是数值类型。很多从 Excel 导入的数据会把数字变成文本,pgRouting 执行时会因为类型问题报错或者强制转换失败。
6. 一点个人经验总结
如果这些内容按优先级拉一条线,我的习惯是:先把 30 个函数压缩成 10 个常驻脑子里的:ST_GeomFromText、ST_SetSRID、ST_Transform、ST_Intersects、ST_Intersection、ST_Buffer、ST_Distance、ST_DWithin、ST_Simplify、ST_AsGeoJSON。日常 80% 的场景,这几个就够用。剩余函数遇到再查,反复查几次就成了肌肉记忆。
pgRouting 整条链路更关键,装好扩展、准备道路表、调好容差生成拓扑、写 pgr_dijkstra、JOIN 取几何,这是个成套流程,任何一个环节断了,结果都不对。第一次跑通这条链路,相当于把地理空间数据库最硬核的一环啃下来了。
最后分享一个小技巧:调试路径查询时,不要把目标路径一次算完再检查,先看road_vertices_pgr顶点分布是否合理,再对单条道路执行SELECT source, target FROM road WHERE id = 某条边确认连接关系。顶点图对了,最短路径一般不会错。