简介:PostGIS是PostgreSQL中最著名的开源空间扩展,此安装包专为64位系统设计,对应PostgreSQL 17与PostGIS 3.5.0,面向GIS开发、时空数据分析与地理信息应用建设者。压缩包共1317个文件,约130.69MB,主体为933个sql脚本,覆盖空间对象创建、拓扑与地理编码等扩展逻辑,另有79个dll运行库、75个csv示例数据、50个tif栅格测试文件及pgrouting路径分析、地址标准化控制文件,安装后即可在PostgreSQL中存储查询点、线、面等空间数据。包体结构清晰,便于按模块对照学习或二次开发查阅。目前已有193人学习,适合需要在PostgreSQL中执行距离面积计算、空间索引与路径分析的中高级开发者或GIS工程师。
1. postgis-bundle-pg17-3.5.0x64.zip 是什么:Windows 上快速获得空间数据库能力的最小单元
第一次在 Windows 上把 PostGIS 3.5.0 和 PostgreSQL 17 凑到一起的人,多半会栽在版本匹配上。postgis-bundle-pg17-3.5.0x64.zip 这个命名已经把最关键的信息说清楚了:PostgreSQL 17、PostGIS 3.5.0、x64 架构,整体打包成一个 zip。它解决的正是空间扩展安装时最磨人的那部分——GEOS、GDAL、Proj 这些底层库的版本互相认不认,而不是让你手工去拼一堆 dll。适合 Windows 上的开发环境、离线环境、临时演示环境,或者只是想用真实空间数据库跑通一个原型的场景。它没有安装向导,部署方式更接近绿色解压版,整目录即实例,可复制、可带走,这一点在排障和交付上比官方安装器更省心。
2. 安装 postgis-bundle-pg17-3.5.0x64.zip:解压、初始化、启用扩展
2.1 解压和目录规划:bin 目录比想象中更关键
这个 zip 解压后,通常是一组以bin、lib、share为核心的结构。bin里是 PostgreSQL 17 的可执行文件,lib里放着 PostGIS 3.5.0 运行时要加载的扩展动态库,以及 GEOS、GDAL、Proj 的随附动态库。因为它不像安装包那样写注册表,解压路径可以随便选,但我建议固定在一个不带空格的路径下,比如D:\pg17_postgis。路径里有空格或中文时,后面用 shp2pgsql、GDAL 命令行这类工具,经常会出现引号处理不一致的玄学问题。
用 PowerShell 解压一次:
$zip = "D:\soft\postgis-bundle-pg17-3.5.0x64.zip" $dest = "D:\pg17_postgis" Expand-Archive -Path $zip -DestinationPath $dest -Force Get-ChildItem $dest看到bin、lib、share、doc之类目录后,我一般会先做两件事。第一,把bin目录加进系统环境变量 PATH,或者在后面用 psql、pg_ctl、shp2pgsql 时都写全路径。第二,确认解压目录所在的磁盘有足够空间,PostGIS 的 GIST 索引和空间数据写入都会产生不小的临时文件。PostGIS 的许多工具会调用同目录下的动态库,相对路径一旦找不到,就只会抛一句“无法加载 DLL”,不会告诉你到底是哪个文件缺失。把解压路径固定下来,后续升级、备份、换机器都方便不少。
2.2 初始化数据目录并启动 PostgreSQL 17
zip 里虽然有bin,但不会自动创建数据目录。第一次使用必须先执行 initdb。这里我建议显式指定编码和区域设置,尤其后面要存中文地址、中文名称时,否则极容易出现乱码。常用初始化命令如下:
D:\pg17_postgis\bin\initdb -D D:\pg17_postgis\data -U postgres -E UTF8 --locale=C-D指定数据目录,这个目录负责存放所有数据库文件和预写日志;-U指定初始超级用户名,这里用 postgres;-E UTF8把数据库默认编码设为 UTF-8;--locale=C把区域设为 C,避开 Windows 区域性排序规则给空间索引带来的不可预期行为。开发环境按这个组合通常不会出错。如果你确实需要中文排序,也应该在 initdb 之前想清楚,而不是建完库再改,因为数据目录初始化之后的编码和区域调整极麻烦。
初始化完成后,目录下会出现postgresql.conf、pg_hba.conf等文件。启动时我用 pg_ctl,而不是双击 exe。日志先落到固定文件,启动失败时能立刻在日志里看到原因:
D:\pg17_postgis\bin\pg_ctl -D D:\pg17_postgis\data -l D:\pg17_postgis\pg.log start D:\pg17_postgis\bin\pg_ctl -D D:\pg17_postgis\data status第一条命令的-D指向刚才初始化的数据目录,-l指定服务器日志位置,建议写绝对路径。启动成功后再跑 status 确认状态。如果本机 5432 端口已有别的 PostgreSQL,可以改postgresql.conf里的port,也可以在 psql 连接时用-p指定端口,避免两个实例冲突。日志里如果出现 “database system is ready to accept connections”,说明数据目录已经正常工作。
2.3 用 psql 启用 PostGIS 扩展及 SFCGAL
数据库服务起来后,默认只有一个名为 postgres 的数据库。PostGIS 不是解压完就能用的功能,需要先连接到目标库,再执行CREATE EXTENSION。这一步会把扩展的数据类型、函数、空间参考表spatial_ref_sys全部装载进去,操作对象是数据库,不是整个集群。我的做法是专门建一个业务库,而不是直接塞进 postgres 库,方便权限隔离和备份恢复。
D:\pg17_postgis\bin\psql -U postgres -d postgres -c "CREATE DATABASE gisdb ENCODING 'UTF8';" D:\pg17_postgis\bin\psql -U postgres -d gisdb -c "CREATE EXTENSION postgis;" D:\pg17_postgis\bin\psql -U postgres -d gisdb -c "CREATE EXTENSION postgis_sfcgal;"第一行创建 gisdb 数据库。第二行创建 postgis 扩展,包含绝大多数日常空间操作:geometry 类型、空间索引、ST_* 函数。第三行创建 postgis_sfcgal,用于更复杂的 3D 空间计算;如果业务用不到,可以不创建,减少扩展之间的耦合。注意spatial_ref_sys表会自动创建,它维护了常见 SRID 的定义,后面用 ST_Transform 做坐标系转换时依赖的就是这张表。
创建完后用一段验证脚本确认装载状态:
SELECT PostGIS_Full_Version(); SELECT version();PostGIS_Full_Version()会返回 PostGIS 3.5.0 以及 GEOS、GDAL、Proj 的版本组合;version()返回 PostgreSQL 17 的信息。两者同时正常返回,说明扩展和底层库没有冲突。此时数据库已经具备空间计算能力,可以建带 geometry 列的表,也可以用 ST_GeomFromText 直接构造临时数据。
2.4 用 shp2pgsql 导入第一批空间数据
扩展创建成功后,最常见的第一步是导入一批真实空间数据,shapefile 仍是当前最常遇到的交换格式。bundle 里自带 shp2pgsql,免去另外安装转换工具的麻烦:
D:\pg17_postgis\bin\shp2pgsql -s 4326 -I -W GBK D:\data\buildings.shp public.buildings | D:\pg17_postgis\bin\psql -U postgres -d gisdb-s指定 shapefile 的 SRID,这里按 WGS84 经纬度处理;-I表示导入完成后自动创建 GIST 索引;-W GBK指定属性表编码,当 dbf 里的中文字段来自国内数据源时非常实用。管道符把生成的 SQL 直接交给 psql 执行,中间不会产生临时文件。如果源数据不是 4326,这一步千万别硬填 4326,最好先用 QGIS 或 GDAL 确认原始投影;填错 SRID 是空间数据导入最隐蔽的坑之一。
3. 必调参数:让 PostGIS 3.5.0 在 Windows 上稳定的关键配置
3.1 postgresql.conf 里与空间查询强相关的三个参数
PostGIS 本身不依赖太多配置,真正影响空间查询体验的是 PostgreSQL 的内存与并行设置。ST_Intersects处理大范围面数据时,会把大量几何对象读入内存;ST_DWithin做缓冲查询时,也需要足够的内存承载中间结果,否则 PostgreSQL 会把临时结果写到磁盘,一次查询等好几秒。
这里列一份开箱后会改的参数表,适用于 8GB 内存的 Windows 开发机:
| 参数 | 默认值 | 建议值 | 影响 |
|---|---|---|---|
| shared_buffers | 128MB | 2GB | PostgreSQL 共享缓冲区,占物理内存约四分之一。空间数据底表越大,命中率提升越明显 |
| work_mem | 4MB | 64MB | 单次查询操作可用的内存,空间排序、聚合、ST_Intersects 的中间结果都在这里 |
| maintenance_work_mem | 64MB | 256MB | 创建 GIST 索引、VACUUM、重建索引时的内存,建大表索引卡不卡看它 |
| max_connections | 100 | 50 | 空间计算比 OLTP 更需要稳定连接,降低连接数给每个查询留更多内存 |
| max_parallel_workers_per_gather | 2 | 4 | 让大范围空间查询利用多个 CPU,但小查询反而变慢,先保持 2 或 4 观察 |
参数说明要算总账:work_mem是乘数参数,50 个并发连接同时跑空间排序,理论峰值是 50×64MB,设得过大反而可能把服务器内存耗尽;shared_buffers超过物理内存一半后,Windows 上容易出现 PostgreSQL 进程与系统争抢内存;maintenance_work_mem只在维护操作时占用,给大一点不影响正常查询。
改完参数后重启数据库生效。修改postgresql.conf时不要用带 BOM 的编辑器保存,Windows 上常见的问题是保存成“UTF-8 with BOM”,数据库启动时解析参数失败。我一般用编辑器把编码切成 UTF-8,保存后再重启:
D:\pg17_postgis\bin\pg_ctl -D D:\pg17_postgis\data restart -l D:\pg17_postgis\pg.log3.2 空间扩展自身的开关:栅格、并行与连接池
PostGIS 3.5.0 还有一个容易被忽略的参数:postgis.gdal_enabled_drivers。默认情况下,GDAL 驱动是关闭的,这是为了避免数据库任意读取服务器文件路径,埋下安全隐患。如果需要直接查询 GeoTIFF 栅格,我建议只打开必需的驱动:
postgis.gdal_enabled_drivers = 'GTiff' postgis.enable_outdb_rasters = on这里GTiff只放行 GeoTIFF;enable_outdb_rasters表示允许查询存储在数据库外部的栅格。如果没有栅格需求,保持默认关闭,不要为了“全功能”而把所有驱动全部放开。
连接池的选择也直接影响 PostGIS 稳定性。空间查询的 SQL 常常比常规 OLTP 重,单条查询占用work_mem的时间更长;如果应用层连接池把连接数压得太高,数据库大概率出现“内存不足”或“无法申请共享内存”之类的报错。常见做法是把连接池上限控制在 max_connections 的 80% 以内,并把空闲连接超时调短,避免空间查询后残留大内存会话。对并发超过 50 的场景,这个策略尤其重要。
3.3 空间表的 VACUUM 与 GIST 索引膨胀
空间数据表更新频繁后,GIST 索引比普通 B-Tree 更容易膨胀。尤其是大量做 INSERT、UPDATE、DELETE 后,旧索引条目残留会拖慢查询。PG 17 的 autovacuum 默认配置对普通表够用,但空间表最好单独调低触发阈值。在业务表上执行:
ALTER TABLE project_point SET (autovacuum_vacuum_scale_factor = 0.01);autovacuum_vacuum_scale_factor默认是 0.2,意思是表变化量达到 20% 才触发 vacuum;空间表改成 0.01 后,更新 1% 就会触发,虽然会增加一点维护开销,但能显著减少索引膨胀带来的查询退化。这个设置和maintenance_work_mem配合,常见做法是先用普通默认参数跑一段时间,观察索引膨胀明显时再调低。
4. 从数据到结果:建表、SRID 与空间索引
4.1 按 SRID 选择几何列类型:geometry 与 geography 的取舍
建表之前,先决定用 geometry 还是 geography。绝大多数场景里,用 geometry 配 SRID 4326(WGS84 经纬度)就够了;但如果业务要求直接以米为单位算距离,比如“找半径 1 公里内的点”,用 geography 类型最省心,因为它的内置计算把球面距离处理掉了。反过来,geography 类型对函数支持不如 geometry 完整,性能也普遍更慢。所以最常见的做法是:数据是经纬度,就用geometry(Point, 4326);需要真实距离时,查询把列临时转型成 geography。
下面是我常用的建表语句:
CREATE TABLE project_point ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, geom geometry(Point, 4326) NOT NULL ); ALTER TABLE project_point ADD CONSTRAINT enforce_srid_geom CHECK (ST_SRID(geom) = 4326);第一段建表把空间列固定为geometry(Point, 4326),PostgreSQL 处理该列时知道每个值都带 4326 的空间参考,不必每次调用都解析 SRID。第二段加 CHECK 约束,防止某条 SQL 显式写入ST_SetSRID(..., 3857)之类的数据,把列里塞成混合 SRID。这个约束很多人省略,但数据源多起来之后,它就是保底设计。注意 4326 下做米制距离要转型。插入两个点并算距离:
INSERT INTO project_point (name, geom) VALUES ('第一个点', ST_SetSRID(ST_MakePoint(116.391, 39.907), 4326)), ('第二个点', ST_SetSRID(ST_MakePoint(116.401, 39.917), 4326)); SELECT a.name, b.name, ST_Distance(a.geom::geography, b.geom::geography) AS dist_m FROM project_point a, project_point b WHERE a.id = 1 AND b.id = 2;::geography转型后,ST_Distance返回的单位是米;不转型,在 4326 几何坐标上算出的距离是“度”,不是米。很多新手在这里踩坑,所以要记住:几何坐标系里做距离计算,先确认返回单位。如果担心每次转型开销,可以在建表时增加一个 geography 生成列,或者直接在 3857 投影下存。
4.2 用 ST_Transform 与 ST_Intersects 完成一次空间查询
实际项目中,常见场景是边界数据是 4326、业务数据是其他投影,或者数据都在 4326 但需要按米做缓冲区。ST_Transform用于把一个几何对象从一个 SRID 转到另一个。一个典型错误是查询时直接写ST_Transform(geom, 3857),这样每次查询都做一次坐标系转换,索引帮不上忙。我一般优先把转换放在数据入库阶段,或者在目标 SRID 上建表达式索引。
CREATE TABLE project_boundary ( id int PRIMARY KEY, name text, geom geometry(MultiPolygon, 4326) NOT NULL ); SELECT p.name, b.name FROM project_point p JOIN project_boundary b ON ST_Intersects(p.geom, b.geom) WHERE p.id IN (1, 2);ST_Intersects判断两个几何是否有交集,是空间 JOIN 最常用的方式。它并不复杂:先读边界数据,再对每个点做几何相交判断。数据量一旦到百万行,就必须靠 GIST 索引,否则这个 JOIN 会退化成全表扫描。如果边界来自不同投影,可以先用ST_Transform统一到同一坐标系,再 JOIN。
4.3 用 GIST 索引加速空间过滤
空间列上默认没有索引,只有主键索引。没有索引时,PostgreSQL 要顺序扫描表,依次判断每个点在不在范围内;数据量大时,这种顺序扫描会拖垮查询。PostGIS 的标准索引是 GIST,适合 2D/3D 几何的包含、相交、距离查询。创建方式不复杂:
CREATE INDEX idx_project_point_geom ON project_point USING GIST (geom); CREATE INDEX idx_project_boundary_geom ON project_boundary USING GIST (geom);创建完后再次执行第 4.2 节的 JOIN 查询,执行计划会从 Seq Scan 变成 Index Scan,或至少出现 Bitmap Index Scan。注意 GIST 索引并非万能:如果查询里写了ST_Transform(geom, 3857),原始 geom 上的索引不会生效,因为列被函数包了一层。解决办法有两个:要么在原始 SRID 上做过滤,要么创建表达式索引:
CREATE INDEX idx_project_point_geom_3857 ON project_point USING GIST (ST_Transform(geom, 3857));表达式索引的代价是写放大:每次写入都要额外计算一次投影转换,索引文件也更大。这只能当补救方案,不是首选。真正高频查询的投影坐标系,应该在入库时就落定,让索引列保持干净。
5. 避坑与排查:pg17 配 PostGIS 3.5.0 这五个坑最值得记录
5.1 现象:CREATE EXTENSION 报“无法加载 DLL 库”
新机器上执行CREATE EXTENSION postgis;报错,提示找不到某个 DLL 或无法加载库。原因不是 SQL 写错,而是 PostgreSQL 进程运行时找不到 PostGIS 的依赖动态库。bundle 把依赖库放在lib目录下,但加载扩展时系统会按 PATH 找,如果 PATH 没包含D:\pg17_postgis\bin或lib目录,就会报这个错。解决:把解压目录的bin和lib都加入系统 PATH,然后重启数据库,不只是重连 psql,因为扩展加载发生在数据库进程里。曾有一台机器折腾半天,最后发现只是 PATH 顺序不对,bin 目录排在旧版本目录后面,加载到的还是老库,版本对不上导致加载失败。
5.2 现象:pg_ctl 启动服务提示“拒绝访问”
Windows 上数据目录放在 C 盘系统目录或 Program Files 下时,数据库进程可能无法写日志或创建会话句柄,pg_ctl 输出“拒绝访问”。原因通常是两个:目录权限没有给到当前系统用户,或安全软件拦了 postgres.exe 的写操作。解决:把数据目录挪到D:\pg17_postgis\data,在目录属性里确认当前用户有读写权限。如果还不行,就去 Windows 事件查看器里筛 PostgreSQL 日志和应用程序错误,把事件 ID 定位到具体模块。不要一上来就“以管理员身份运行”数据库,那样会把权限问题掩盖掉,等以后换普通用户部署,又会翻车。
5.3 现象:空间查询全表扫描,索引不生效
EXPLAIN 看执行计划,明明建了 GIST 索引,查询仍然 Seq Scan。最常见原因是 WHERE 条件里的列被函数包裹,比如ST_Transform(geom, 3857)或ST_SetSRID(geom, 4326)。优化器无法直接利用 geom 上的索引,因为索引建立在原始值上。解决:避免在索引列套函数,如果必须用,就建表达式索引。另一个容易被忽略的原因是常量 SRID 不匹配,比如列是 4326,过滤条件里用 3857,两者永远不相交,优化器判断扫全表更便宜。建表时统一 SRID,并加上第 4.1 节里的 CHECK 约束,可以从源头避开这个坑。
5.4 现象:中文数据写入后乱码
用 psql 执行 SQL 或在应用层写入中文,查询出来是乱码。这通常不是 PostGIS 的问题,而是 initdb 时没固定编码。Windows 的中文区域默认可能以 GBK 或其他编码创建数据库,客户端连接编码和服务器不一致时,显示自然混乱。解决:initdb 时加-E UTF8 --locale=C;已经初始化的数据目录,只改client_encoding只能治标。正确做法是在应用连接串里加client_encoding=utf8,同时确认数据表默认编码是 UTF8。这个坑表面看和空间数据无关,但空间字段带着的中文属性一旦乱码,整张表基本没法正常使用。
5.5 现象:把 bundle 当成“常规升级包”,直接覆盖旧目录
有人以为可以把新版 bundle 直接覆盖旧版本目录完成升级,结果启动后报系统表不兼容。原因是 PostgreSQL 主版本升级必须走导出导入或专门迁移流程,PostGIS 扩展版本也受二进制动态库影响,只换文件不迁移数据不可行。解决:升级前用pg_dump把空间数据导成自定义格式,换目录、初始化新实例、创建扩展,再pg_restore回去。空间数据里的 SRID、spatial_ref_sys表、拓扑关系常有跨版本差异,恢复后要重点验证 GIST 索引是否重建成功。bundle 的优势是环境干净,但正因为干净,没有任何自动迁移工具,迁移节奏得自己控制。
6. 验证与进阶:让空间查询可被信任的两个小技巧
6.1 用 EXPLAIN ANALYZE 验证 GIST 索引真正生效
配置完成并不代表查询一定快,用 EXPLAIN ANALYZE 验证。比如验证第 4 章的 JOIN 是否走索引:
EXPLAIN (ANALYZE, BUFFERS) SELECT p.name, b.name FROM project_point p JOIN project_boundary b ON ST_Intersects(p.geom, b.geom) WHERE p.id IN (1, 2);重点看输出里有没有Index Scan或Bitmap Index Scan on idx_project_point_geom,以及 Planning Time 和 Execution Time。如果看到Seq Scan on project_point,先检查 WHERE 条件是否在 geom 列上套了函数,或者是不是数据量太少,优化器觉得扫全表更划算。数据量只有几千行时,即使索引存在,优化器也可能选 Seq Scan,这不能说明索引有问题;要评估索引价值,就插入几十万行测试数据,再看执行计划变化。
输出里的 buffers 数值也有意义。如果共享缓冲区数值持续快速增长,说明查询大量读取缓冲页;如果伴随大量rows removed by filter,说明空间过滤效率不高。把这些指标作为调整 GIST 索引和 SRID 策略的参考,比单看执行时间更可靠。
6.2 把常用空间逻辑固定成视图,应用层只消费简单字段
另一个我长期坚持的习惯:不要让应用层直接面对 ST_* 长函数和 SRID 概念。数据库内部建好视图,把经纬度和空间判断封装好,应用层只 select 普通字段,既减少传输量,也避免每个开发都去查空间函数文档。比如:
CREATE VIEW project_point_web AS SELECT id, name, ST_X(geom) AS lon, ST_Y(geom) AS lat FROM project_point;视图只吐经纬度数字,应用端拿到的是普通数字,不是 geometry 对象,省去 Web GIS 组件里常见的地图投影转换。如果要按范围查询,再封装一个 ST_DWithin 的函数,应用层只需要传中心点和半径。注意视图内不要写死 SRID,应该由业务层在请求参数里携带。
最后回到建表那句 CHECK 约束。我曾经在一个模拟项目X里漏掉它,后来从多个数据源导入点数据时,出现了 4326、3857、4490 混在一列的场面,空间索引几乎失效,最后只能重洗数据。从那以后,凡是要进空间库的数据,SRID、几何类型、坐标维度三层约束一遍都不省。也希望帮到你。
本文还有配套的精品资源,点击获取