ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PostGIS实战指南:从空间数据管理到性能优化

PostGIS实战指南:从空间数据管理到性能优化 写这篇东西的起因很简单前阵子项目里接到一个需求要把几万条商户的经纬度坐标跟一批行政区划边界数据做叠加分析判断“哪些店在哪个商圈里”。数据量不算夸张但用传统的关系型数据库处理空间关系SQL里写起来极其折磨。后来把库从普通PostgreSQL升级到PostGIS扩展问题一下变得清爽了。这篇就把PostGIS这套地理信息处理工具的完整玩法、坑点和实战思路从头到尾捋一遍。如果你手里有经纬度坐标、有地图边界数据或者正在做LBS相关的东西这篇文章应该能帮你少走不少弯路。我尽量按“从0到1”的顺序写先讲清楚PostGIS到底能干什么、为什么选它再带你把环境搭起来接着展开常用函数和实操案例最后说一下优化的路子和我踩过的坑。1. 地理信息处理的本质PostGIS解决了什么问题1.1 空间数据的特殊性不只是多了一个字段普通业务数据比如订单表、用户表核心就是数字、字符串、时间的组合查询时靠B-tree索引就能跑得飞快。但地理信息数据完全不同一条记录里存的往往是一个坐标点、一条折线或者一个多边形这类数据天然是二维甚至更高维的。你不能简单地用“大于等于”“小于等于”去判断两个多边形是否相交也没有办法用普通的等值条件查出“距离某点3公里以内的所有POI”。这就需要数据库能理解“空间关系”这个概念——两个几何体之间的距离、相交、包含、相邻等等。传统SQL完全不具备这套语义所以地理信息处理在过去很长一段时间里要靠ArcGIS这类专业桌面软件或者自己写计算脚本把数据导出来算完再导回去。整个过程麻烦不说还很慢。PostGIS就是用来改变这个局面的。它是PostgreSQL的一个空间扩展把几何类型、空间索引、空间函数全部塞进了数据库内部让SQL语句可以直接写“找出与我相交的多边形”“按距离排序”这类逻辑。数据不导出、不复制直接在原库上做空间分析这一点在实际工程里价值极大。1.2 为什么是PostGIS而不是MySQL或其他方案做技术选型时团队里也讨论过要不要直接用MySQL的空间功能或者干脆用Elasticsearch的Geo查询。我的结论是PostGIS在空间分析能力上目前仍然是开源界的头号选择。MySQL从5.7开始支持InnoDB的空间索引但它的空间函数集相比PostGIS来说单薄很多不少联级空间计算比如多个多边形合并、缓冲区叠加在MySQL里写起来非常吃力性能也有差距。Elasticsearch的Geo-point、Geo-shape更适合“按距离检索POI”这类简单场景一旦涉及复杂的空间分析比如路网拓扑、栅格数据处理就力不从心了。PostGIS背后是GEOS、GDAL、PROJ这些老牌空间计算库从点线面操作、坐标投影变换到栅格数据分析、拓扑网络构建几乎覆盖了地理信息系统的全部基础能力。再加上PostgreSQL本身的可靠性用来做业务系统的空间数据底座非常稳。2. 快速搭起一套可用的PostGIS环境2.1 安装PostgreSQL与PostGIS扩展Windows Linux Mac三种方式先说最简单的方式如果你只是想快速体验直接装一个PostgreSQL的完整安装包。Windows下官方安装器EDB Installer很省心一路Next在“Select Components”那一步一定要勾上PostGIS否则后面还得去找单独的安装包。Linux下用apt的话Ubuntu系统一行命令就能搞定sudo apt install postgresql postgis postgresql-16-postgis-3包名里面的版本号需要和你的PostgreSQL版本对应比如PostgreSQL 16对应的PostGIS 3.x它不会自动帮你匹配所以一定要查一下当前可用的包名。我自己最常用的是Docker方式因为它能把整个环境隔离起来项目里其他人拉起来就能跑不需要各自折腾本机环境。一个最小可用的docker-compose配置如下version: 3.8 services: postgis: image: postgis/postgis:16-3.4 container_name: postgis_demo environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: gis_demo ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata:这个镜像自带PostGIS扩展不需要额外安装启动后直接创建数据库就行。实测下来postgis/postgis官方镜像比普通的postgres镜像多了几十兆的大小多出来的全是空间扩展的依赖后面你建库的时候只需一条SQL启用扩展所有功能就都出来了。注意如果你用的是云端数据库比如RDS PostgreSQL一般实例参数里有个“PostGIS”选项开启后同样执行CREATE EXTENSION即可不需要关心底层安装细节。2.2 启用扩展与验证基础环境安装好之后第一步不是建表而是先在数据库里启用PostGIS扩展CREATE EXTENSION IF NOT EXISTS postgis;如果你想做栅格数据分析还需要加上postgis_raster旧版本叫postgis_raster新版本要单独启用CREATE EXTENSION IF NOT EXISTS postgis_raster;启用完之后可以查一下版本确认环境没问题SELECT postgis_version();输出大概是3.4 USE_GEOS1 USE_PROJ1 USE_STATS1这里有个小点值得说明USE_GEOS1表示编译时集成了GEOS库如果没有这个很多空间函数你是用不了的。比如ST_Intersection、ST_Buffer这类核心分析函数都需要GEOS支持。所以在你拿到一个PostGIS环境时第一步先跑这个命令确认GEOS、PROJ这些子模块都处于启用状态否则后续很容易出现“函数明明存在但怎么都调不通”的怪问题。2.3 一张图理解PostGIS的目录结构很多第一次接触PostGIS的人会被它几十个扩展表搞懵。其实你只需要关注几个核心的spatial_ref_sys坐标系信息全球几千种坐标系的定义都在里面SRID 4326是GPS用的WGS84经纬度SRID 3857是Web墨卡托投影这两个最常用。geometry_columns所有几何字段的注册信息。你在任意表里建了geometry字段后PostGIS会自动往这张视图里登记一条。geography_columnsgeography类型字段的注册信息这个后面会细说。这三张表搞明白了整个空间数据框架就清晰了。其他表基本不用管。3. 核心概念geometry与geography怎么选3.1 geometry类型下的空间计算逻辑先来看最基础的geometry类型。它是基于平面直角坐标系来设计的所有的距离、面积、相交判断都在二维平面上进行计算。也就是说当你往geometry字段里塞一个经纬度坐标SRID 4326时PostGIS会忽略地球是圆的这个事实直接在平面上算。这样做的好处是计算快因为平面三角比球面三角简单得多误差在短距离时也可以接受。但是问题也很明显如果你用SRID 4326的geometry直接算两个点之间的距离得到的结果单位是“度”不是米。比如北京的经度是116.4上海是121.5两点经度差5度但这个“5度”在地球表面实际长度可不是简单的5乘以某个公里数因为纬度的长度在不同位置完全不一样。想解决这个问题有两个思路一是用geography类型它直接按球面计算距离返回单位是米二是做投影变换把数据转到合适的投影坐标系再用geometry计算。后者的计算速度快很多但需要你对投影规则有一定了解。3.2 geography类型适用的场景geography类型是PostGIS对geometry的一个补充它内部把你输入的数据当作地球球面上的地理坐标来处理。好处很明显距离算出来就是米单位统一不用你做投影变换。坏处是计算开销比geometry大很多因为球面计算涉及三角函数CPU消耗高一个量级。那什么时候用geography呢我个人的经验是如果应用的核心操作是“算距离”比如查出离我最近的门店、计算用户到商家的实际距离而且数据量在百万级以内直接用geography最省事。如果你的数据量到了千万级又全都是点Geohash或者干脆提前用程序算好距离存起来可能比数据库硬算更划算。3.3 类型选择建议一张表帮你做决定对比项geometrygeography计算模型平面球面距离返回单位取决于坐标系米面积计算平面面积球面面积性能快慢约数倍支持函数非常丰富较丰富适合场景投影坐标系数据、复杂空间分析全球经纬度直接算距离实际项目中如果数据本来就是经纬度GPS、高德、百度地图我推荐用geography做距离查询用geometry做空间关系判断。比如判断点是否在多边形内geometry的ST_Contains效率明显更好而算两点之间距离geography的ST_DWithin不会让你因为坐标系问题翻车。两种类型可以在SQL里通过显式转换无缝配合。4. 实操入门常用的PostGIS函数与SQL示例4.1 数据准备如何把经纬度变成几何对象从文件导入数据之前最常见的方式是在SQL里用ST_GeomFromText、ST_GeogFromText这些函数把WKT格式的文本转成几何对象。WKT格式其实不复杂就是一个文本规范类似这样-- 点 SELECT ST_GeomFromText(POINT(116.4074 39.9042), 4326); -- 线 SELECT ST_GeomFromText(LINESTRING(116.40 39.90, 116.41 39.91), 4326); -- 面 SELECT ST_GeomFromText(POLYGON((116.40 39.90, 116.41 39.90, 116.41 39.91, 116.40 39.91, 116.40 39.90)), 4326);如果你手里的数据是GeoJSON格式可以用ST_GeomFromGeoJSON如果是一个.shp文件那就得靠shp2pgsql命令行工具导入了。shp2pgsql是PostGIS自带的转换工具用法也比较直接shp2pgsql -s 4326 -I -W UTF-8 ./admin_boundary.shp admin_boundary | psql -U postgres -d gis_demo这里的参数简单解释下-s指定源文件的SRID一定要先搞清楚你的shp文件是什么坐标系不然导进去坐标全是错的-I表示导入后自动创建空间索引-W是指定shp文件里属性字段的编码国内的数据大部分是UTF-8或者GBK你根据实际情况填。4.2 空间关系判断点在面内、线与面相交数据准备好之后最常用的就是空间关系判断。假设有一张商户表stores一个商圈表districts要查出每个商户落在哪个商圈里SELECT s.name, d.name AS district_name FROM stores s JOIN districts d ON ST_Contains(d.geom, s.geom);ST_Contains(A, B)的含义是“A是否完全包含B”。注意参数顺序第一个是容器第二个是被包含对象搞反了结果会不一样。除ST_Contains之外还有ST_Intersects、ST_Within、ST_Disjoint、ST_Touches、ST_Crosses等等它们的含义或多或少容易记混我的经验是先搞清楚业务要判断什么关系再看函数的参数说明不要靠猜。比如“判断两点是否在500米内”我一般直接用ST_DWithin它是性能最好的一种方式因为它在判断距离的同时充分利用了空间索引SELECT a.name, b.name FROM stores a, stores b WHERE a.id b.id AND ST_DWithin(a.geog, b.geog, 500);这里有个技巧使用ST_DWithin时如果两个字段都是geography类型第三个参数500的单位就是米非常直观。如果两个字段是geometry类型第三个参数的单位取决于坐标系用SRID 4326是度那500度的范围就大到离谱了。4.3 空间计算距离、面积、长度与缓冲区再来看一组非常常用的计算函数。求两点之间的距离SELECT ST_Distance( ST_GeogFromText(SRID4326;POINT(116.4074 39.9042)), ST_GeogFromText(SRID4326;POINT(121.4737 31.2304)) );返回结果的单位是米北京到上海的距离大概是1067公里左右。如果你用geometry来计算的话得到的结果会是5度左右单位是度几乎没有任何参考价值。这就是前面说过的geography的好处。计算面积和长度也差不多但这里提醒一句直接对4326的geometry做面积计算得到的是“平方度”不是平方米意义不大。正确做法是先把数据投影到适合的平面坐标系或者用geographySELECT ST_Area(geog) AS area_sqm FROM districts WHERE name 朝阳区;缓冲区Buffer分析是另一个常用操作比如“找出某个地铁站1公里范围内的小区”SELECT h.name FROM houses h WHERE ST_Intersects( h.geom, ST_Buffer(ST_GeomFromText(POINT(116.397 39.909), 4326)::geography, 1000)::geometry );这段SQL里我把geography的缓冲区结果又转回了geometry因为ST_Intersects在geometry类型上性能更好。如果你直接拿geography去比对也不是不行但性能会有明显差距——数据量大的时候这个差距会从几百毫秒变成好几秒。5. 空间索引与性能优化从“能用”到“好用”5.1 GIST索引原理与创建方法空间查询如果没有索引就意味着每一行数据都要做一次完整的空间计算复杂度是O(n)。当表里数据量过万后一条简单的“点是否在面内”查询就可能跑出几十秒。PostGIS解决这个问题的方案是GIST索引它是一种通用搜索树能把空间上相邻的几何对象分到同一个叶子节点查询时快速剪枝避免扫描整表。创建GIST索引很简单CREATE INDEX idx_stores_geom ON stores USING GIST (geom);我见过很多人在导入第一批数据之后不建索引结果越跑越慢最后才想起来其实这个习惯很不好。建议建表后数据一导入就直接建索引别等出问题再回头补。5.2 常见慢查询排查思路当一条空间查询慢得出奇时第一步别急着优化SQL先用EXPLAIN ANALYZE看执行计划EXPLAIN ANALYZE SELECT s.name, d.name FROM stores s JOIN districts d ON ST_Contains(d.geom, s.geom);重点看输出的计划里有没有“Seq Scan”如果有说明索引没被用上。索引没被用上的原因通常有三个一是表上没有GIST索引二是SQL里的条件写得太灵活导致优化器判断走索引成本更高三是函数的使用方式阻挡了索引比如你在字段外面套了一层函数PostgreSQL就无法用它来加速了。5.3 一定要避开的性能陷阱第一个陷阱是“在WHERE子句里对字段做无谓的函数转换”。比如你想查一个点的持久化对象却写成SELECT * FROM stores WHERE ST_AsText(geom) LIKE %116.4%;这个查询索引完全失效因为它是在结果文本上做模糊匹配而不是在空间范围上做过滤。正确的写法是用ST_DWithin或者其他空间操作符。第二个陷阱是空间JOIN时驱动表选反了。比如两张表一张100万行一张只有1000行你要做包含判断正确姿势是先让大表关联小表小表作为驱动表。优化器通常能自动调好但如果你发现执行计划异常可以手动通过JOIN顺序控制。第三个陷阱是数据导入时没有先建索引再导入。如果你先导入几百万行再建索引索引构建时会把所有数据重新组织一遍速度慢不说还会产生大量的临时文件很容易就把磁盘空间打满。我的习惯是导入数据前先建好表结构导入完成后一次性创建索引这样索引只有一次构建成本。6. 坐标系与投影最容易出错的隐形深坑6.1 SRID、WKT与坐标系之间的关系坐标系是这个领域里最容易埋雷的地方。相同的一组经纬度在不同的坐标系下代表的实际位置可能相差几百米。比如国内最常见的WGS84GPS原始坐标、GCJ02火星坐标高德和腾讯地图用、BD09百度坐标三者之间是不能直接互换的。PostGIS里SRID用来标识坐标系4326就是WGS844490是CGCS20003857是Web墨卡托。你往表里存数据时必须知道源数据是在哪个坐标系下采集的并填对SRID。如果你把一个BD09坐标标成4326存进去后面所有空间分析的结果都会偏移。6.2 ST_Transform投影转换的正确打开方式不同坐标系之间的转换用ST_Transform实现。比如把Web墨卡托的geometry转成WGS84SELECT ST_AsText(ST_Transform(geom, 4326)) FROM some_table WHERE SRID(geom) 3857;这里的关键是ST_Transform只能用于geometry类型而且要求目标SRID和源SRID都已经在spatial_ref_sys表里有定义。geography类型则没有投影转换的概念它内部就假定你是全球经纬度坐标。6.3 同一张表里SRID不统一怎么办这是新手经常踩的坑之一从外部拿来的数据源文件本身坐标系是乱的有人标4326有人标3857还有人干脆填0。入库时如果不校验后面算距离、做比对全是乱的。我通常会在导入后立刻做一个SRID的体检SELECT ST_SRID(geom) AS srid, count(*) FROM imported_data GROUP BY srid;如果发现混了多个SRID先统一成同一个再入库。统一操作可以用ST_Transform如果你不确定某个数据集到底是什么坐标系宁可先去问数据提供方也不要瞎猜。坐标转换一旦做错后面所有的分析都等于白做。7. 常见问题排查与避坑清单7.1 ST_Transform报“Geometry SRID (4490) is unknown”错误出现这个报错的原因是目标坐标系在spatial_ref_sys表里没有定义。比如你写ST_Transform(geom, 4490)但4490记录缺失。解决办法是先确认PostGIS的proj相关的数据有没有装全很多精简版安装包不会包含所有坐标系定义。可以用下面的SQL手动查一下SELECT * FROM spatial_ref_sys WHERE srid 4490;如果结果为空最简单的方法是把SRID换成已经存在的或者重新安装完整版PostGIS。千万别手工往spatial_ref_sys里塞自定义数据那是最后手段非专业人士很容易填错参数。7.2 ST_AsText输出坐标顺序为什么会变化PostGIS里用ST_AsText输出点时默认格式是“经度 纬度”也就是X YX在前。但如果你的数据来自某些特定的数据源它可能是“纬度 经度”的顺序。输出结果和你预期的顺序不同十有八九是源数据本身录入时顺序就反了而不是PostGIS输出格式的问题。排查方法很简单找几个已知坐标的样本点用高德/谷歌地图打点验证一下。7.3 空间数据量太大把表撑爆了怎么办我记得有一次导入了全量行政区划边界和几百万个POI点直接把数据库空间撑到几十GB。PostGIS往表里存几何对象比存普通文本要占用更多空间。解决方案主要有两个一是把不需要经常做空间分析的数据归档到历史表只保留活跃数据二是可以用ST_SnapToGrid来简化几何对象的精度比如UPDATE some_table SET geom ST_SnapToGrid(geom, 0.0001);0.0001度大概相当于10米级别适合大部分LBS场景。如果不需要那么高的精度可以放宽到0.001数据体积能缩小好几倍。8. 写在最后用好PostGIS的几个小建议一个成熟的PostGIS项目不能只停留在“会造表、会写两个函数”的程度。从我个人的经验来看下面这几点是决定项目能否长期健康跑下去的关键第一一定要在建表之初就明确坐标系的规范。所有入库数据先统一SRID不要等数据堆到几百万后再做转换到那时基本上已经动不起了。第二空间索引要当成普通索引一样管理。新增表、新增字段后第一时间补索引而不是等到查询慢得没法用的时候再来加。第三数据模型设计时想清楚用geometry还是geography。我的建议是所有经纬度坐标源数据都存成geography格式涉及复杂空间分析时再cast成geometry这样既保证了距离计算的正确性又能在必要的时候获得性能优势。如果你把PostGIS当压制空间大数据分析的核心组件甚至还可以把它接上QGIS做可视化调试。比如在QGIS里直接连PostGIS数据库把表拖出来查看几何形状是否正常排查数据问题时比在SQL里看文本要直观得多。这也是我日常调试空间数据最喜欢用的组合PostGIS当数据仓库和计算引擎QGIS当眼睛两边一配合绝大多数空间数据问题都能在半小时内定位到根因。
返回列表