在做 LBS(基于位置的服务)、配送范围圈选、门店半径营销、轨迹分析时,最直接的需求往往是”找出距离某点 5 公里内的所有门店””判断一条配送路线是否穿过某个禁行区域”。如果没有合适的工具,你只能把经纬度拉到内存里用勾股定理硬算,数据量一大就崩,还无法和数据表做关联。PostGIS 是 PostgreSQL 最成熟的空间扩展,它把普通数据库升级成真正的空间数据库,让你用一条 SQL 完成地理空间查询。本文用可复现的实战示例,带你从零把 PostGIS 跑起来,并讲清空间索引、坐标系与性能优化的关键坑。
一、PostGIS 到底能解决什么问题
普通的关系型数据库只会比较数字和字符串,对”点、线、面”这类几何对象毫无概念。PostGIS 在 PostgreSQL 之上增加了三类能力:空间数据类型(geometry / geography)、几百个空间函数(ST_Distance、ST_Contains、ST_Intersects 等),以及 GiST 空间索引。它让地理计算下沉到数据库引擎内部执行,相比在应用层用代码算,优势非常明显:数据不用出库、能利用索引加速、可直接与业务表 JOIN、结果天然一致。如果你已经在用 PostgreSQL 基础,接入 PostGIS 几乎是零成本的升级。
二、环境准备与扩展启用
绝大多数 Linux 发行版都提供了 PostGIS 的安装包,例如 Debian/Ubuntu 上执行 apt install postgresql-postgis 即可。启用扩展只需要一条 SQL,注意扩展必须在具体数据库内创建,且通常需要超级用户权限:
-- 安装后启用扩展(需要超级用户)
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
-- 查看版本与编译能力
SELECT postgis_version();
SELECT postgis_full_version();
如果返回版本号,说明扩展已就绪。postgis_full_version() 还会告诉你是否启用了 GEOS、PROJ、GDAL 等底层依赖,这些库决定了能否做复杂的空间运算。
三、核心数据类型:geometry 与 geography
PostGIS 提供两种主流坐标表示。geometry 工作在”平面坐标系”上,单位取决于你设定的投影,适合局部范围、需要精确面积计算的场景;geography 直接工作在 WGS84 椭球面上,单位天然是米,适合跨大范围的距离计算,但算面积会慢一些。对大多数 LBS 场景,用 geography 最省心。
| 类型 | 单位 | 基准 | 适用场景 |
|---|---|---|---|
| geometry | 投影后单位(度/米) | 平面坐标系 | 局部精确面积、工程测量 |
| geography | 米 | WGS84 椭球面 | 跨城距离、全球范围检索 |
下面创建一张门店表,用 geometry(Point, 4326) 存储经纬度(4326 即 WGS84 的 SRID):
CREATE TABLE stores (
id serial PRIMARY KEY,
name text NOT NULL,
geom geometry(Point, 4326) -- WGS84 经纬度
);
INSERT INTO stores (name, geom) VALUES
('中关村店', ST_SetSRID(ST_MakePoint(116.316, 39.984), 4326)),
('望京店', ST_SetSRID(ST_MakePoint(116.470, 39.997), 4326)),
('国贸店', ST_SetSRID(ST_MakePoint(116.459, 39.909), 4326));
注意 ST_MakePoint 的参数是(经度, 纬度),顺序写反是新手最常见的坑,会导致所有坐标跑到非洲或海里。
四、空间索引:GiST 让查询飞起来
如果不建索引,任何空间查询都会对全表做逐行计算,几千条数据尚可,几十万条就不可接受了。PostGIS 的标准做法是建 GiST 索引:
CREATE INDEX idx_stores_geom
ON stores USING GiST (geom);
GiST 会把几何对象用”包围盒”组织成一棵平衡树,查询时先用包围盒快速圈定候选集,再做精确计算。关键点在于:不是所有空间函数都能用到索引——ST_DWithin、ST_Intersects、ST_Contains 能走索引,而裸的 ST_Distance(a, b) < 5000 往往走不了索引,应当改写成 ST_DWithin。
五、常用空间函数实战
5.1 半径检索:ST_DWithin
以”北京站”为中心,找出 5 公里内的门店。这里把 geometry 转成 geography,这样半径单位就是米:
SELECT name,
ST_Distance(geom::geography,
ST_SetSRID(ST_MakePoint(116.427, 39.903), 4326)::geography) AS dist_m
FROM stores
WHERE ST_DWithin(
geom::geography,
ST_SetSRID(ST_MakePoint(116.427, 39.903), 4326)::geography,
5000)
ORDER BY dist_m;
5.2 最近邻查询:KNN 运算符 <->
想找”离我最近的 3 家门店”,用 KNN 运算符 <-> 做排序,GiST 索引能直接支持这种最近邻搜索,无需全表扫描:
SELECT name,
ST_Distance(geom::geography, ref::geography) AS dist_m
FROM stores,
(SELECT ST_SetSRID(ST_MakePoint(116.427, 39.903), 4326) AS ref) r
ORDER BY geom <-> ref
LIMIT 3;
5.3 区域判定:ST_Contains
判断哪些门店落在某个多边形(例如朝阳区范围)内:
WITH chaoyang AS (
SELECT ST_SetSRID(
ST_MakePolygon(ST_MakeLine(ARRAY[
ST_MakePoint(116.40, 39.95),
ST_MakePoint(116.52, 39.95),
ST_MakePoint(116.52, 39.99),
ST_MakePoint(116.40, 39.99),
ST_MakePoint(116.40, 39.95)])), 4326) AS poly
)
SELECT s.name
FROM stores s, chaoyang c
WHERE ST_Contains(c.poly, s.geom);
| 函数 | 作用 | 能否走索引 |
|---|---|---|
| ST_DWithin | 是否在半径内 | 能 |
| ST_Intersects | 是否相交 | 能 |
| ST_Contains | 是否包含 | 能 |
| ST_Distance | 两点精确距离 | 裸用不能,配合 <-> 能 |
六、坐标系与 SRID 的注意事项
SRID 是空间数据的”单位制”,混用会导致结果差出十万八千里。国内互联网业务几乎都用 4326(WGS84 经纬度),而高德、腾讯地图的 GCJ-02 火星坐标系必须先纠偏再入库,否则会和真实位置偏移几百米。如果你的业务只在国内,也可以把数据投影到合适的平面坐标系(如 4547 国家 2000 高斯投影)来换取更高的面积精度。无论选哪种,整张表必须统一 SRID,否则索引与函数都会报错或给出错误结果。
七、性能优化清单
结合我们在 PostgreSQL 慢查询排查 中的经验,给 PostGIS 查询做优化可以遵循下面几条:
- 所有空间列都建 GiST 索引,且避免对列套函数(如 ST_Transform(geom))导致索引失效。
- 半径检索统一用 ST_DWithin 而非 ST_Distance < N。
- 跨大范围距离计算优先用 geography,避免投影变形累积误差。
- 只 SELECT 需要的列,空间对象体积大,减少回表能明显提速。
- 数据量大时考虑按城市/网格分区,参考 PostgreSQL 分区表 的思路做空间分区。
八、PostGIS 不是万能的
当数据量到千万级、又要毫秒级响应时,PostGIS 单实例会吃力。此时可以:把热点只读查询下沉到 Redis 的 GEO 命令(适合简单半径圈选);或者把 PostGIS 作为”权威存储 + 复杂分析”,把轻量检索交给缓存层。如果你还需要在向量维度和地理维度同时检索,可以看看 pgvector 向量检索 与 PostGIS 的协同。生产环境要保证高可用,建议参考 PostgreSQL 流复制 搭建主从集群。
| 方案 | 优势 | 局限 |
|---|---|---|
| PostGIS | 功能最全、可 JOIN、复杂分析 | 超大数据量需分库分表 |
| Redis GEO | 毫秒级、极简 | 只能半径检索,无多边形 |
| 专业 GIS(如 GeoServer) | 地图渲染、瓦片服务 | 运维重、学习曲线陡 |
九、总结
PostGIS 用极低的接入成本,把 PostgreSQL 变成一台能理解”点线面”的空间数据库。记住三件事就能避开 90% 的坑:用 GiST 索引加速、用 ST_DWithin 代替裸距离比较、全表统一 SRID。从门店半径检索、配送范围圈选到轨迹围栏,这些原本要在代码里绕来绕去的需求,现在一条 SQL 就能优雅解决。当数据再上一层量级时,再用缓存层或主从集群兜底,整套地理空间能力就稳了。




