PostGIS空间查询语句怎么写?常用函数有哪些?

GIS基础理论
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

很多刚接触空间数据库的同学都会问:PostGIS空间查询语句怎么写?常用函数有哪些? 如果你已经把点、线、面数据导入 PostgreSQL,但不知道如何按范围筛选、判断相交、计算距离、统计落入面内的点,这篇文章就按真实 GIS 工作流,把常用 PostGIS 空间查询语句和函数用法梳理清楚。

PostGIS空间查询语句和PostGIS常用空间函数示意图
PostGIS空间查询通常围绕 geometry 字段、空间关系函数和空间索引展开。

引言:PostGIS空间查询语句先看清三个对象

PostGIS空间查询语句 前,先不要急着背函数。大多数空间查询都可以拆成三个对象:空间表、空间字段、空间条件。

  • 空间表:例如道路表 roads、地块表 parcels、兴趣点表 poi。
  • 空间字段:通常叫 geom、geometry 或 shape,类型可能是 POINT、LINESTRING、POLYGON、MULTIPOLYGON。
  • 空间条件:例如相交、包含、距离小于 500 米、落入某个行政区。

PostGIS 本质上是在 PostgreSQL 中增加了空间类型和空间函数,所以空间查询语句仍然是 SQL,只是 WHERE 条件里多了 ST_Intersects、ST_Within、ST_DWithin 等空间函数。

背景:为什么普通SQL不能直接完成GIS空间查询

普通 SQL 擅长处理属性字段,例如名称、编号、类型、时间。但 GIS 数据还有几何形状,例如点的位置、线的走向、面的边界。判断两个地块是否重叠、某个点是否落在行政区内,不能只靠文本或数字比较。

PostGIS 通过 geometry 或 geography 类型存储空间对象,并提供空间关系、空间量算、坐标转换、几何构造等函数,让数据库可以直接完成 GIS 分析。

例如,一个传统属性查询可能是:

SELECT *
FROM poi
WHERE type = 'school';

而一个 PostGIS 空间查询可能是:

SELECT p.*
FROM poi p
JOIN district d
ON ST_Within(p.geom, d.geom)
WHERE d.name = '海淀区';

这条语句的意思是:查询落在“海淀区”面范围内的所有 POI 点。

原理:PostGIS空间查询语句的基本结构

常见 PostGIS空间查询语句 一般有四种结构。

1. 直接查询空间字段

用于查看表中几何对象、坐标系、几何类型。

SELECT id, ST_GeometryType(geom), ST_SRID(geom)
FROM roads
LIMIT 10;

这里 ST_GeometryType 用于查看几何类型,ST_SRID 用于查看坐标参考系统编号。写空间查询前,先检查 SRID 很重要。

2. 用空间关系函数做WHERE条件

这是最常见的写法,例如查询与某个区域相交的道路。

SELECT r.*
FROM roads r, district d
WHERE d.name = '朝阳区'
  AND ST_Intersects(r.geom, d.geom);

ST_Intersects 表示两个几何对象是否有任意空间交集,常用于线穿过面、面与面重叠、点落在面边界或内部等场景。

3. 用空间连接做跨表查询

如果需要把一个图层的数据关联到另一个图层,通常使用 JOIN。

SELECT p.id, p.name, d.name AS district_name
FROM poi p
JOIN district d
ON ST_Within(p.geom, d.geom);

这类查询在 GIS 中非常常见,例如给 POI 挂接行政区名称,给地块挂接规划分区,统计每个街道内的设施数量。

4. 用空间函数生成新结果

PostGIS 不只能筛选数据,还能生成缓冲区、交集结果、中心点等新几何。

SELECT id, ST_Buffer(geom, 500) AS buffer_geom
FROM roads;

如果 roads 的坐标系单位是米,这条语句会生成 500 米缓冲区。但如果数据是经纬度坐标,直接 ST_Buffer(geom, 500) 就很可能得到错误结果。

步骤:常用PostGIS空间查询语句怎么写

步骤1:检查空间字段和坐标系

写任何 PostGIS空间查询语句 前,先确认表里是否有空间字段,以及 SRID 是否一致。

SELECT f_table_name, f_geometry_column, srid, type
FROM geometry_columns
WHERE f_table_name IN ('poi', 'district');

也可以直接抽样查看:

SELECT ST_SRID(geom), ST_GeometryType(geom)
FROM poi
LIMIT 5;

如果两个图层 SRID 不一致,空间关系判断可能失败或结果不可信。常见做法是用 ST_Transform 临时转换。

SELECT p.*
FROM poi p
JOIN district d
ON ST_Within(
  ST_Transform(p.geom, 3857),
  ST_Transform(d.geom, 3857)
);

注意:ST_Transform 是坐标转换,前提是原始 SRID 正确;ST_SetSRID 只是给几何对象贴 SRID 标签,并不会改变坐标值。

步骤2:查询点是否在面内:ST_Within

点落入行政区、监测站落入流域、门店落入商圈,这类问题通常使用 ST_Within。

SELECT p.*
FROM poi p
JOIN district d
ON ST_Within(p.geom, d.geom)
WHERE d.name = '西湖区';

ST_Within(A, B) 表示 A 是否完全位于 B 内部。对于点在面内的判断,它非常直观。

步骤3:查询两个图层是否相交:ST_Intersects

道路穿过某个区、河流经过某个保护区、地块与规划范围有重叠,通常使用 ST_Intersects。

SELECT r.id, r.name
FROM roads r
JOIN district d
ON ST_Intersects(r.geom, d.geom)
WHERE d.name = '越秀区';

ST_Intersects 的适用范围比 ST_Within 更广,只要两个几何对象有交集就返回 true。实际项目中,它是最常用的 PostGIS 常用空间函数之一。

步骤4:查询一定距离范围内的数据:ST_DWithin

如果要查询道路 500 米范围内的学校,不建议先生成缓冲区再相交。更推荐 ST_DWithin,因为它语义清楚,也更容易利用空间索引。

SELECT s.*
FROM schools s
JOIN roads r
ON ST_DWithin(s.geom, r.geom, 500)
WHERE r.name = '人民路';

这里的 500 表示距离阈值,单位取决于数据坐标系。如果 geom 使用投影坐标系,单位通常是米;如果是 EPSG:4326 经纬度,单位是度,不是米。

如果要按米进行距离查询,建议把数据转换到合适的投影坐标系后查询:

SELECT s.*
FROM schools s
JOIN roads r
ON ST_DWithin(
  ST_Transform(s.geom, 3857),
  ST_Transform(r.geom, 3857),
  500
)
WHERE r.name = '人民路';

对于严肃量算,不要机械使用 3857。应优先选择本地区合适的投影坐标系,例如地方高斯投影、UTM 分带或项目指定坐标系。

步骤5:计算距离:ST_Distance

ST_Distance 用于计算两个几何对象之间的距离。它经常和 ORDER BY 配合,用来找最近对象。

SELECT s.id, s.name,
       ST_Distance(s.geom, r.geom) AS distance
FROM schools s
JOIN roads r
ON r.name = '人民路'
ORDER BY distance
LIMIT 10;

如果要查询最近的 10 个学校,最好确保使用米制投影坐标系,否则距离结果可能不是你以为的“米”。

步骤6:统计每个面内有多少个点

这是 GIS 空间统计中非常常见的需求,例如统计每个街道的 POI 数量。

SELECT d.id, d.name, COUNT(p.id) AS poi_count
FROM district d
LEFT JOIN poi p
ON ST_Within(p.geom, d.geom)
GROUP BY d.id, d.name
ORDER BY poi_count DESC;

这里使用 LEFT JOIN 可以保留没有 POI 的行政区。如果使用 INNER JOIN,没有点落入的区域会被排除。

步骤7:裁剪相交部分:ST_Intersection

如果不仅想知道是否相交,还想得到相交后的几何结果,可以使用 ST_Intersection。

SELECT r.id,
       ST_Intersection(r.geom, d.geom) AS clipped_geom
FROM roads r
JOIN district d
ON ST_Intersects(r.geom, d.geom)
WHERE d.name = '天河区';

这个语句会得到道路落在天河区范围内的部分。实际使用时,通常先用 ST_Intersects 做筛选,再用 ST_Intersection 生成结果,避免对所有记录做昂贵的几何计算。

常见坑:PostGIS空间查询结果不对的原因

1. SRID不一致或未知

如果一个表是 EPSG:4326,另一个表是 EPSG:3857,直接做 ST_Intersects 很容易得到空结果或错误结果。先用 ST_SRID 检查,再用 ST_Transform 统一坐标系。

SELECT ST_SRID(geom), COUNT(*)
FROM poi
GROUP BY ST_SRID(geom);

2. 把经纬度当成米来算距离

EPSG:4326 的单位是度,不是米。下面这条语句中的 500 表示 500 度,明显不符合实际。

SELECT *
FROM poi
WHERE ST_DWithin(geom, ST_SetSRID(ST_Point(120.15, 30.28), 4326), 500);

更稳妥的方式是转换到合适投影坐标系,或使用 geography 类型进行米制距离计算。

SELECT *
FROM poi
WHERE ST_DWithin(
  geom::geography,
  ST_SetSRID(ST_Point(120.15, 30.28), 4326)::geography,
  500
);

3. 没有建立空间索引导致查询很慢

空间表数据量一大,如果没有 GiST 空间索引,ST_Intersects、ST_DWithin 等查询会明显变慢。

CREATE INDEX idx_poi_geom
ON poi
USING GIST (geom);

建完索引后,建议执行 ANALYZE 更新统计信息。

ANALYZE poi;

4. 几何对象无效

面自相交、环方向异常、重复节点等问题,可能导致 ST_Intersection、ST_Within 的结果异常。可以先检查几何有效性。

SELECT id, ST_IsValid(geom), ST_IsValidReason(geom)
FROM parcels
WHERE NOT ST_IsValid(geom);

简单修复可以尝试 ST_MakeValid,但修复后仍要抽样检查结果。

UPDATE parcels
SET geom = ST_MakeValid(geom)
WHERE NOT ST_IsValid(geom);

5. 忽略边界条件

ST_Within 对边界很严格。如果点正好落在面的边界上,结果可能不符合你的业务预期。此时可根据需求考虑 ST_Covers、ST_Intersects 或小距离容差。

SELECT p.*
FROM poi p
JOIN district d
ON ST_Covers(d.geom, p.geom);

ST_Covers(A, B) 表示 A 覆盖 B,包括边界情况。在行政区归属判断中,有时比 ST_Within 更符合业务理解。

方法比较:PostGIS常用空间函数怎么选

函数 典型用途 适合场景 注意事项
ST_Intersects 判断是否相交 道路穿过区域、面与面重叠、点落在面上 范围广,适合初筛
ST_Within 判断A是否在B内部 点在面内、设施归属行政区 边界点可能不算 within
ST_Contains 判断A是否包含B 行政区包含POI、地块包含建筑 与 ST_Within 方向相反
ST_Covers 判断覆盖关系 需要包含边界的归属判断 常用于边界敏感场景
ST_DWithin 判断距离是否在阈值内 查询500米范围内对象 注意单位和坐标系
ST_Distance 计算距离 最近点、距离排序 大数据量下不要无条件全表计算
ST_Buffer 生成缓冲区 影响范围、服务半径可视化 经纬度坐标下谨慎使用
ST_Intersection 生成相交几何 裁剪、叠加分析 计算成本较高,先过滤再计算
ST_Transform 坐标转换 统一坐标系、米制量算 原始 SRID 必须正确

实际写 PostGIS空间查询语句 时,可以按这个思路选择函数:

  • 只问“有没有关系”:优先 ST_Intersects、ST_Within、ST_Contains、ST_Covers。
  • 只问“距离内有没有”:优先 ST_DWithin。
  • 要计算具体距离:使用 ST_Distance。
  • 要生成新几何:使用 ST_Buffer、ST_Intersection、ST_Union。
  • 结果不对先查坐标系:使用 ST_SRID、ST_Transform。

检查清单:写PostGIS空间查询语句前后要确认什么

  • 确认空间字段:表中是否有 geom 字段,字段类型是否正确。
  • 确认坐标系:参与查询的图层 SRID 是否一致。
  • 确认单位:距离参数是米、度,还是投影坐标单位。
  • 确认几何有效性:面数据是否存在自相交、空几何、无效几何。
  • 确认空间索引:大表是否创建 GiST 索引。
  • 确认边界规则:点在边界上时,应使用 ST_Within、ST_Covers 还是 ST_Intersects。
  • 确认查询范围:先用 WHERE 缩小属性范围,再做复杂空间计算。
  • 确认输出结果:抽样导出到 QGIS 或 ArcGIS Pro 中叠加检查。

如果查询性能不理想,可以使用 EXPLAIN 查看执行计划,确认是否走了空间索引。

EXPLAIN
SELECT p.*
FROM poi p
JOIN district d
ON ST_Intersects(p.geom, d.geom)
WHERE d.name = '上城区';

如果执行计划没有使用索引,检查索引是否存在、表是否 ANALYZE、函数写法是否阻碍索引使用。

FAQ:PostGIS空间查询语句常见问题

PostGIS空间查询语句和普通SQL有什么区别?

语法基础仍然是 SQL,但空间查询会在 SELECT、WHERE、JOIN 或 ORDER BY 中使用 PostGIS 空间函数,例如 ST_Intersects、ST_Within、ST_DWithin、ST_Distance。它处理的是几何关系,而不仅是属性字段比较。

PostGIS常用空间函数应该先学哪些?

建议先掌握 ST_SRID、ST_Transform、ST_Intersects、ST_Within、ST_DWithin、ST_Distance、ST_Buffer、ST_Intersection。这些函数可以覆盖大部分入门到实际项目中的空间查询需求。

ST_Within 和 ST_Contains 有什么区别?

ST_Within(A, B) 表示 A 在 B 内部;ST_Contains(A, B) 表示 A 包含 B。两者方向相反。例如判断 POI 是否在行政区内,可以写 ST_Within(poi.geom, district.geom),也可以写 ST_Contains(district.geom, poi.geom)。

为什么 ST_DWithin 查询距离不准?

最常见原因是坐标系单位不对。如果 geom 是 EPSG:4326 经纬度,距离参数单位是度,不是米。要按米查询,可以转换到合适的投影坐标系,或在经纬度数据上使用 geography 类型。

为什么 PostGIS 空间查询很慢?

常见原因包括没有创建 GiST 空间索引、没有执行 ANALYZE、数据量过大却直接做 ST_Intersection、在查询中对索引字段包裹了复杂转换函数。优化思路是先用空间索引过滤,再做复杂几何计算。

ST_Intersects 能不能代替 ST_Within?

不能简单代替。ST_Intersects 只要求两个几何有交集,ST_Within 要求一个几何完全位于另一个几何内部。如果业务是“是否有交集”,用 ST_Intersects;如果业务是“是否归属于某个面”,通常用 ST_Within 或 ST_Covers。

PostGIS查询结果如何在QGIS里检查?

可以在 QGIS 中连接 PostgreSQL/PostGIS 数据库,把查询结果保存为视图或新表后加载。也可以在 QGIS 的数据库管理器中直接执行 SQL,查看结果是否与原始图层空间位置一致。

结论:先理解空间关系,再选择PostGIS函数

PostGIS空间查询语句 并不难,关键是先把 GIS 问题翻译成空间关系:在内部、相交、距离内、最近、裁剪还是统计。然后再选择合适的 PostGIS 常用空间函数。

入门阶段建议重点练习三类语句:用 ST_Within 做点面归属,用 ST_Intersects 做相交筛选,用 ST_DWithin 做距离范围查询。再配合 ST_SRID、ST_Transform、空间索引和几何有效性检查,就能解决大多数实际 GIS 数据库查询问题。