做 GIS 开发这几年,我越来越觉得 PostGIS 就是空间数据处理的地基。你可以在 MySQL 里存几个坐标点,但只要一碰到“路网分析”“缓冲区计算”“最近邻查找”“最短路径规划”这类真需求,最后基本都会回到地理空间数据库这套体系里来。尤其 PostGIS 配合 pgRouting,一套 PostgreSQL 就能把空间存储、空间计算、路径规划全包了。这篇文章我不讲概念性的空话,直接按我自己的使用经验,把 30 个最常用的 PostGIS 核心空间函数拆开讲清楚,再穿插 pgRouting 的最短路径和距离函数实战,不管是应付考试、准备面试,还是真要在项目里落地,都能直接拿去参考。
适合看这篇文章的人,我猜大致有三类:一是刚接触空间数据库、被各种函数搞晕的初学者;二是已经用 PostGIS 做点查表业务,但没深入做过路径分析的人;三是需要在简历或项目里体现空间计算能力,想把最短路径这块补上的开发者。无论哪类,核心思路都是一样的:先理解函数解决什么问题,再记住使用场景,最后上手跑一遍。
1. 环境与安装:把 PostGIS 装明白才算拿到入场券
1.1 快速安装路线
先说我个人最推荐的方式。除非你有特殊的离线部署要求,否则本地开发一律用 Docker,省掉一多半环境折腾。
docker run -d --name gis-db \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=gis \ -p 5432:5432 \ postgis/postgis:16-3.4这个镜像默认已经启用了 PostGIS 扩展,进去之后直接执行:
CREATE EXTENSION IF NOT EXISTS postgis;如果是生产环境用 Ubuntu 部署,通常用包管理器装:
sudo apt install postgresql-16 postgresql-16-postgis-3 postgresql-16-pgrouting装完同样先建库再启用扩展。注意 Windows 上别图省事,一定要用 EnterpriseDB 的安装包配合 Stack Builder 选 PostGIS,版本要和你安装的 PostgreSQL 大版本一致,否则后面创建扩展时大概率会栽跟头。
1.2 postgis 安装失败的几种典型原因
这个坑太常见了,我几乎每隔一阵就能在社区看到有人问“为什么 CREATE EXTENSION postgis 报错”。我自己踩过和帮人排查过的案例里,无外乎下面几种:
- 版本不匹配。PostGIS 是跟着 PostgreSQL 大版本走的,PostgreSQL 16 需要对应版本号的 PostGIS 3.4+。用 apt 装的时候如果 PostgreSQL 的 apt 源没更新,可能装到旧版 PostGIS,然后扩展文件目录对不上。
- 缺依赖库。PostGIS 编译安装需要 GEOS、GDAL、Proj 这些库,少一个都会导致扩展无法加载。用官方包管理器一般会自动处理依赖,但编译安装就要格外小心。
- 扩展目录不对。报错信息里如果出现
could not open extension control file,十有八九是 PostgreSQL 的extension目录里没有 PostGIS 的控制文件。可以先检查SHOW shared_preload_libraries;,再看扩展目录是否存在。 - 在错误的数据库中执行。
CREATE EXTENSION是在具体库上做的,不是全局操作。有次同事在postgres默认库里建好了扩展,结果连业务库之后一直提示找不到函数,就是这个问题。
解决之后可以用下面这段 SQL 做环境验证:
SELECT PostGIS_Full_Version(); SELECT name, default_version FROM pg_available_extensions WHERE name IN ('postgis', 'pgrouting');如果能正常返回版本信息,环境基本就绪。我习惯顺手跑一个最小测试:
SELECT ST_Distance( ST_SetSRID(ST_MakePoint(116.3, 39.9), 4326), ST_SetSRID(ST_MakePoint(116.4, 39.9), 4326) );这个结果会是一个以“度”为单位的数值,不是米。刚接触的人很容易在这里被误导。真正的米制距离,要么把坐标转投影坐标,要么用geography类型。这个细节后面讲距离函数时会重点展开。
2. 30 个核心空间函数:一表读懂,随查随用
2.1 函数分类与完整清单
我按自己的使用频率,把 30 个函数分成四组:构造与转换、空间关系判断、几何处理与聚合、测量计算。先给一张速查表:
| 分类 | 函数 | 一句话作用 |
|---|---|---|
| 构造与转换 | ST_Point | 从经纬度构造点 |
| 构造与转换 | ST_MakeLine | 把点集串成线 |
| 构造与转换 | ST_GeomFromText | 从 WKT 文本构造几何 |
| 构造与转换 | ST_SetSRID | 给几何设置坐标系编号 |
| 构造与转换 | ST_Transform | 在不同坐标系间转换 |
| 构造与转换 | ST_AsText | 把几何输出为 WKT 文本 |
| 构造与转换 | ST_AsGeoJSON | 把几何输出为 GeoJSON |
| 构造与转换 | ST_Envelope | 获取几何外包矩形 |
| 空间关系判断 | ST_Intersects | 判断两个几何是否相交 |
| 空间关系判断 | ST_Contains | 判断是否包含 |
| 空间关系判断 | ST_Within | 判断是否被包含 |
| 空间关系判断 | ST_Touches | 判断是否仅边界接触 |
| 空间关系判断 | ST_Crosses | 判断是否交叉穿过 |
| 空间关系判断 | ST_Overlaps | 判断是否部分重叠 |
| 空间关系判断 | ST_Covers | 判断是否覆盖(比 Contains 更宽松) |
| 空间关系判断 | ST_DWithin | 判断距离是否在某阈值内 |
| 几何处理与聚合 | ST_Buffer | 生成缓冲区 |
| 几何处理与聚合 | ST_Centroid | 求几何中心点 |
| 几何处理与聚合 | ST_ConvexHull | 求凸包 |
| 几何处理与聚合 | ST_Simplify | 简化几何顶点 |
| 几何处理与聚合 | ST_Intersection | 求两个几何的交集 |
| 几何处理与聚合 | ST_Difference | 求两个几何的差集 |
| 几何处理与聚合 | ST_Union | 合并几何(可聚合多行) |
| 几何处理与聚合 | ST_Collect | 收集多行几何为一个集合 |
| 测量计算 | ST_Distance | 求两个几何距离 |
| 测量计算 | ST_Length | 求线的长度 |
| 测量计算 | ST_Area | 求面的面积 |
| 测量计算 | ST_DistanceSphere | 球面距离计算 |
| 测量计算 | ST_ClosestPoint | 求两几何最近点 |
| 测量计算 | ST_ShortestLine | 求两几何最短连线 |
表里看着多,其实真正要记的是每个函数“解决什么问题”。函数名本身已经很语义化了,用一次比背十遍都管用。
2.2 构造与转换类:先让数据变成“空间数据”
最基本的操作是从无到有造出几何对象,或把已有坐标文本转成几何。其中最常用的是:
SELECT ST_Point(116.3, 39.9); SELECT ST_GeomFromText('POINT(116.3 39.9)', 4326); SELECT ST_MakeLine(ARRAY[ ST_MakePoint(116.3, 39.9), ST_MakePoint(116.4, 40.0) ]::geometry[]);很多人容易忽略一个关键点:ST_Point(x, y)的参数顺序是先经度后纬度,也就是先 x 后 y。业务系统里如果一直用“纬度,经度”的字段排序,到这儿就很容易把点写反,画出来的位置完全不对。
ST_SetSRID和ST_Transform两个函数经常被搞混。我打个比方:ST_SetSRID只是给一个几何对象“贴标签”,告诉数据库这些数字属于哪个坐标系,并不会改变坐标数值;ST_Transform才是真正的“坐标换算”,会把一组坐标系下的数字转成另一组坐标系下的数字。
-- 只设置 SRID,不改变坐标值 SELECT ST_SetSRID(ST_MakePoint(116.3, 39.9), 4326); -- 从 WGS84 转 Web Mercator SELECT ST_Transform( ST_SetSRID(ST_MakePoint(116.3, 39.9), 4326), 3857 );输出格式也是日常非常常用的能力。尤其是前后端交互时,GeoJSON 基本是事实标准:
SELECT ST_AsGeoJSON(ST_Transform(geom, 4326)) FROM buildings WHERE id = 1;如果你只需要数据的“大概范围”,ST_Envelope能快速拿到外包矩形,很多地图缩放到某个要素集合的场景都会用到。
2.3 空间关系判断类:空间 SQL 的“WHERE 条件”
这类函数本质上就是空间查询的过滤器。比如找出一个商圈 500 米范围内的所有餐厅,或者判断某个小区是否在生态红线内。它们的返回值是布尔值,直接用在WHERE里:
SELECT poi.name FROM poi, boundary WHERE ST_Intersects(poi.geom, boundary.geom);几个拓扑关系的关系很容易混淆,我简单理一下:
ST_Contains(A, B):A 完全包裹 B,并且 B 的任何部分不在 A 之外。ST_Within(A, B):是ST_Contains(B, A)的镜像。ST_Touches(A, B):两个几何只在边界上接触,内部区域没有重叠。比如两个相邻地块共边,就符合ST_Touches。ST_Crosses(A, B):两条线交叉穿过,但交点没有内部重叠。常用于判断道路是否穿过一个区域。ST_Overlaps(A, B):两个几何有一部分内部重叠,但又不完全互相包含。ST_Covers(A, B):跟ST_Contains类似,但对边界处理更宽松,B 在 A 的边界上也判断为 True。
实际项目中我查“某个点是否落在面内”,经常直接写ST_Covers,因为它对边界情况的语义干扰最小。而ST_DWithin的实用性更强,它不等价于ST_Distance < N,但配合索引时性能有巨大优势,这个后续详细说。
2.4 几何处理与聚合类:从“单个对象”到“批量派生对象”
做空间数据分析最爽的场景,就是把一个复杂几何变成更有业务含义的几何对象。
ST_Buffer是最典型的例子。比如给某个加油站点生成一个 3 公里服务半径面:
SELECT ST_Buffer(geom, 0.03) FROM stations WHERE id = 10;注意:如果几何是 4326,0.03 就代表 0.03 度,不是 30 米。要做米制缓冲区,最好先把几何转成投影坐标系,或者使用geography。如果只需要知道“哪些点在多远以内”,优先用ST_DWithin,别动不动就ST_Buffer生成一个大面。
ST_Centroid求几何质心,常用来做点标注:
SELECT ST_Centroid(geom) FROM districts;ST_ConvexHull就是求一组点或几何的最小外凸多边形,很多“覆盖范围分析”的粗糙版本会用它。
ST_Simplify在数据瘦身时非常管用。路网数据几百万个顶点,直接渲染只会拖垮前端。适当简化既能保持形状,又能大幅降体积:
SELECT ST_Simplify(geom, 0.0001) FROM roads;容差参数取决于坐标系。如果坐标是度,0.0001 大约就是几十米级别的简化幅度,具体要根据比例尺反复试。
ST_Intersection、ST_Difference、ST_Union和ST_Collect是几何“加减乘除”的四件套:
-- 两个面的交集 SELECT ST_Intersection(a.geom, b.geom) FROM parcels a, flood_zone b WHERE ST_Intersects(a.geom, b.geom); -- 面 A 减去面 B SELECT ST_Difference(a.geom, b.geom) FROM parcels a, red_line b; -- 将多行几何聚合为一个集合 SELECT ST_Union(geom) FROM counties; SELECT ST_Collect(geom) FROM points;ST_Union在这里有两种用法:聚合函数写法会把多行合并成一个几何,而二元函数写法会把两个几何合并。ST_Collect只做简单收集,不做拓扑合并,所以速度更快;如果不需要消除内部边界,优先考虑ST_Collect。
2.5 测量计算类:距离、长度、面积一网打尽
测量类是日常查询里出镜率最高的。ST_Distance算距离,ST_Length算线长,ST_Area算面积,但最容易出问题的点在于单位。
直接对 4326 的geometry调用这些函数,计算结果以“度”为单位。于是在中国地区,一个“面积”算出来可能是零点几甚至更小的数字,看起来完全不对。两条常规做法:
- 转投影坐标系后再算。比如全国范围用 3857 或者适合本地的 UTM 分区。
- 直接使用
geography类型,让 PostGIS 自动按球面/椭球面计算。
SELECT ST_Distance( ST_SetSRID(ST_MakePoint(116.3, 39.9), 4326)::geography, ST_SetSRID(ST_MakePoint(116.4, 39.9), 4326)::geography );ST_DistanceSphere和ST_ShortestLine这类函数,更多用在“近似距离”“最近位置”的查询场景。比如判断两个多边形最近距离,或者找到两个面之间的最短连线,用于可视化或进一步分析。
ST_Area同理:
SELECT ST_Area(geom::geography) AS area_m2 FROM land_parcels;这一节我不想每个函数都给一个例子,因为后续“距离函数”章节会把ST_Distance、ST_DWithin、ST_DistanceSphere展开得更细。现在先把 30 个函数的地图建立起来,真正用的时候你对号入座就行。
3. 距离函数实战:从“长度”到“业务权重”
3.1 三种距离计算方式的取舍
距离函数是空间数据库里最容易被误用的一个类别。同一个词“距离”,在不同坐标系和类型下返回的单位、精度、性能都不同。
第一种:ST_Distance作用于geometry,返回的是投影平面上的欧氏距离。如果几何是 4326,返回单位是度;如果是 3857,返回单位是米。好处是计算快,坏处是跨投影带时会有精度损失,而且忘转投影就直接算的话,结果基本不能用。
第二种:ST_Distance作用于geography。PostGIS 会按球面模型计算大圆距离,返回单位是米。精度比平面计算高很多,而且不用考虑投影选择。缺点是计算开销更大,不适合在超大表上直接做全表两两距离。
第三种:ST_DistanceSphere。它绕过了椭球体精确计算,直接用球体近似,速度比geography快,精度略低。对于打车、外卖这种对距离精度不敏感的场景,完全够用。
我平时给团队定的规范是:数据入库统一存 4326geometry,距离查询视精度要求决定是否 cast 成geography,如果要做大量两两距离计算,先建 GiST 索引,再结合ST_DWithin做前置过滤。
3.2 距离衰减函数的业务玩法
很多人在 PostGIS 文档里找不到“distance decay function”这个词,因为它不是一个内置函数名,而是一种空间分析方法。简单说,就是让“地理距离”转化为“业务权重”时,距离越远权重越低,衰减速度由模型决定。
常见的三种衰减模型:
- 线性衰减:
weight = 1 - d / D,到达最大距离 D 时权重归零。 - 指数衰减:
weight = exp(-d / D),衰减速度先快后慢。 - 高斯衰减:
weight = exp(-0.5 * (d / D)^2),短距离内衰减缓慢,过了阈值后快速下降。
我在做商圈选址分析时常用这样的 SQL:
WITH dist AS ( SELECT poi.id, ST_Distance( shop.geom::geography, poi.geom::geography ) AS d FROM shop, poi WHERE ST_DWithin(shop.geom, poi.geom, 5000) ) SELECT id, CASE WHEN d <= 1000 THEN 1.0 WHEN d <= 5000 THEN 1.0 - (d - 1000) / 4000 * 0.5 ELSE 0.0 END AS linear_weight, EXP(-d / 2000.0) AS exp_weight FROM dist;注意:ST_DWithin第一个参数是几何,第二个也是几何,阈值单位跟随坐标系。为了能在 5000 米的场景中使用,我更习惯把几何 cast 成 geography 后比较,但这时要确认索引匹配。最简单稳妥的做法是给几何建 GiST 索引,然后查询里一开始就统一投影或统一成 geography。
距离衰减函数本身不是 PostGIS 的计算瓶颈,真正的瓶颈往往是你做了全表笛卡尔积式的距离计算。所以业务上一定要先圈定候选集,再做权重计算。这也是ST_DWithin最大的价值所在。
3.3 性能与索引:为什么 ST_DWithin 比 ST_Distance 更讨喜
很多新手写“5000 米内所有 POI”会写成:
SELECT * FROM poi WHERE ST_Distance(shop.geom, poi.geom) < 5000;这个写法在数据量小的时候没毛病,但在百万级 POI 表上会非常痛苦,因为它无法有效利用 GiST 索引做边界框过滤。正确姿势是用ST_DWithin:
SELECT * FROM poi WHERE ST_DWithin(shop.geom, poi.geom, 0.05);配合 GiST 索引后,PostGIS 会先做外包框相交判断,快速筛掉绝大多数记录,再精确计算少量候选。两者在业务效果上几乎等价,但性能差距可能是几十倍。
CREATE INDEX idx_poi_geom ON poi USING GIST(geom);这条规则记牢:能用 ST_DWithin 判断邻近关系,就别用 ST_Distance 比较阈值。
4. pgRouting:让路网真正“会算路”
4.1 安装、建拓扑和成本模型
pgRouting 是 PostGIS 的黄金搭档,专门解决图论路径问题。扩展安装很简单:
CREATE EXTENSION IF NOT EXISTS pgrouting;难的从来不是安装,而是把一份普通路网表变成可计算的拓扑图。pgRouting 的路径算法是基于“节点 + 边”的,所以路网表里必须有source和target字段,分别代表边的起点和终点顶点编号。
假设路网表叫roads,字段为id, geom,接下来是标准流程:
ALTER TABLE roads ADD COLUMN source integer; ALTER TABLE roads ADD COLUMN target integer; ALTER TABLE roads ADD COLUMN cost double precision; ALTER TABLE roads ADD COLUMN reverse_cost double precision;接着用pgr_createTopology根据几何上的端点自动生成节点并填充 source/target:
SELECT pgr_createTopology('roads', 0.0001, 'geom', 'id');这个 0.0001 是容差。如果路网坐标是度,这个值大约对应十余米量级;如果端点坐标因为数据采集问题有一点点缝隙,小于容差的端点会被认为是同一个节点。容差设得太小,拓扑会出现断头路;设得太大,又会把邻近但不相连的道路误连到一起。我在实际项目中一般先取数据精度的 2 到 3 倍,再通过抽几条路线目视验证。
最后填充成本:
UPDATE roads SET cost = ST_Length(geom::geography), reverse_cost = ST_Length(geom::geography);cost是正向行驶成本,reverse_cost是反向行驶成本。如果允许双向通行且成本对称,两个字段都设置同样的值。如果遇到单行道,把逆向成本设为 -1 表示不可通过:
UPDATE roads SET reverse_cost = -1 WHERE oneway = 'true';pgRouting 约定负值表示该方向不连通,这是新手最容易忽略的规则。
4.2 pgr_dijkstra:最短路径查询
Dijkstra 算法是最经典的单源最短路径算法。pgRouting 中的标准调用方式如下:
SELECT * FROM pgr_dijkstra( 'SELECT id, source, target, cost, reverse_cost FROM roads', 1, 50, directed := false );参数说明:第一个参数是边的 SQL,第二个参数是起点顶点编号,第三个参数是终点顶点编号,directed决定是否按有向图处理。返回结果里比较重要的列是:
seq:结果顺序编号。path_seq:路径上边的顺序。node:当前节点编号。edge:当前边编号。如果 edge 为 -1,表示已经是终点。cost:当前边的行驶成本。
只拿一条路径还不够,业务上通常要连回原始路网几何,把路径画出来:
SELECT r.seq, rd.geom FROM pgr_dijkstra( 'SELECT id, source, target, cost, reverse_cost FROM roads', 1, 50, directed := false ) r LEFT JOIN roads rd ON r.edge = rd.id WHERE r.edge <> -1 ORDER BY r.seq;很多人在调用最短路径前会卡在一个问题上:起点和终点是经纬度坐标,但算法要的是节点编号。怎么找到最近的拓扑节点?我用 KNN 查询:
SELECT id FROM roads_vertices_pgr ORDER BY geom <-> ST_SetSRID(ST_MakePoint(116.3, 39.9), 4326) LIMIT 1;<->是 PostGIS 的 KNN 距离算子,配合 GiST 索引可以快速找到最近节点。roads_vertices_pgr是pgr_createTopology自动创建的顶点表。
求整条路径总成本也简单:
SELECT SUM(cost) FROM pgr_dijkstra( 'SELECT id, source, target, cost, reverse_cost FROM roads', 1, 50, directed := false );4.3 A* 与 KSP:不同场景的差异化选型
Dijkstra 虽然稳定,但在大规模路网上计算较慢。pgr_aStar 在 Dijkstra 基础上加了启发式估算,理论上可以减少搜索范围。它的 SQL 需要额外提供起点和终点的坐标字段:
SELECT * FROM pgr_aStar( 'SELECT id, source, target, cost, reverse_cost, x1, y1, x2, y2 FROM roads', 1, 50, directed := false );这里的 x1、y1、x2、y2 会由pgr_createTopology生成到顶点表中,我们可以通过关联把坐标字段带进边数据里。实际项目中我通常默认用 Dijkstra,只有路网特别大、对响应时间敏感时才切换 A*;因为 A* 的启发式效果受数据质量影响很大,网格状路网表现优秀,但复杂路网可能并不比 Dijkstra 快多少。
pgr_ksp则用于“前 K 条最短路径”,适合需要给用户多个备选方案的场景:
SELECT * FROM pgr_ksp( 'SELECT id, source, target, cost, reverse_cost FROM roads', 1, 50, 3, directed := false );最后一个参数 3 表示返回前 3 条候选路径。返回结果里route_id可以区分不同备选路线。
4.4 pgr_drivingDistance:等时圈与可达范围
最短路径解决的是“点到点”,但很多业务要的是“从某点出发,成本不超过某值能到哪些地方”。比如外卖配送的 30 分钟可达范围分析。这类问题用pgr_drivingDistance很合适:
SELECT * FROM pgr_drivingDistance( 'SELECT id, source, target, cost, reverse_cost FROM roads', 1, 30, directed := false );第三个参数 30 表示最大行驶成本。返回的是可达节点以及到达该节点的最小成本。把节点连回顶点表几何,就能画出等时圈范围,这比用圆面缓冲区更贴近真实路网约束。
5. 高频踩坑与排查实录
5.1 安装与扩展类问题
| 现象 | 原因 | 解决办法 |
|---|---|---|
| CREATE EXTENSION 提示控制文件不存在 | PostGIS 包未安装或版本不匹配 | 检查 PostgreSQL 版本与 PostGIS 版本对应关系,重装扩展包 |
| 执行 SQL 时提示 function st_distance(geometry, geometry) does not exist | 扩展建到了其他库 | 在业务库执行 CREATE EXTENSION postgis; |
| PostgreSQL 升级后扩展报错 | 二进制不兼容 | 重新安装匹配版本的 PostGIS,并执行 ALTER EXTENSION ... UPDATE |
补充一个容易被忽略的问题:如果数据库服务由云平台托管,如 RDS 或托管 PostgreSQL,部分云厂商默认不给普通用户创建扩展的权限,需要到参数组或控制台开启postgis白名单。
5.2 数据与坐标系类问题
- 面积数值异常大或异常小。多半是坐标系问题。4326 直接算面积,单位是度,数值会异常;3857 算面积不是真实地面面积,在高纬度地区误差很大。最稳的办法是转
geography或本地 UTM 投影。 - 最短路径结果“绕远路”。先查拓扑是否完整,
source、target是否大量为空。如果pgr_createTopology容差设置偏小,道路断开导致绕路是常态。用如下 SQL 快速检查悬空边数量:
SELECT count(*) FROM roads WHERE source IS NULL OR target IS NULL;- cost 为度值导致路径结果失真。如果直接用
ST_Length(geom)计算 cost,而 geom 是 4326,成本会以“度”为单位,导致短距离路段和长距离路段在算法看来没有正确区分。务必用ST_Length(geom::geography)或者转换到投影坐标系后计算。
5.3 性能与距离衰减调参实战
做距离衰减分析时最常见的性能灾难是“两两全表扫描”。我见过一张 50 万行 POI 表和其他表做笛卡尔积距离计算,一条 SQL 跑十几分钟。排查后发现WHERE ST_DWithin(...)里用了函数表达式包住索引列,导致 GiST 索引完全失效。
解决方式有两种:一是保证参与索引的列是原始几何列,不要在外面套ST_Transform或ST_SetSRID;二是如果查询经常做投影转换,直接增加一个生成列,把转换后的几何存储下来并建索引。
距离衰减模型中的阈值参数也需要根据业务反复调。比如指数衰减里的常数 D,决定曲线在什么距离上衰减到约 36.8%。如果 D 设得太小,稍微远一点的 POI 权重就趋近于零;设得太大,则距离对业务几乎没有区分度。我的经验是先从业务实际的核心服务半径出发,比如外卖 3 公里、商场 5 公里,把 D 设成半径的一半左右,再用线上的转化率数据做 A/B 验证。
我在实际项目中最后沉淀下来一套工作流程:所有空间表先统一 SRID 并建 GiST 索引;所有“邻近判断”优先 ST_DWithin;所有距离成本计算统一转 geography 或投影坐标;路径分析前先跑拓扑检查和少量可视化验证。这套流程虽然看起来平淡,但能帮你在空间数据库这条路上少走很多弯路。我自己刚开始学空间 SQL 时也是先背函数名,后来才发现真正值钱的不是记住 30 个函数,而是知道在哪种业务场景下该用哪个函数、为什么用、单位是什么、索引怎么建。把这些底层逻辑搞清楚了,学 pgRouting 时你会觉得顺理成章,因为最短路和分析函数的组合应用,本质还是空间数据思维的问题。