PostGIS Spatial Query是什么
Spatial Query(空间查询)是指根据空间位置、空间范围或空间关系,从GIS数据库中查询空间数据。
普通SQL主要查询:
id
name
type
status
date
例如:
SELECT *
FROM cities
WHERE name = '北京';
PostGIS则可以根据空间条件查询:
附近有哪些商店?
哪些道路经过这个区域?
哪些建筑位于这个行政区?
哪些设备距离用户500米以内?
哪个POI离当前位置最近?
例如:
SELECT *
FROM places
WHERE ST_DWithin(
geom,
:point,
500
);
因此:
PostGIS Spatial Query就是使用SQL和空间函数,根据空间位置、距离、范围以及空间关系查询GIS数据。
Spatial Query可以查询什么
PostGIS常见空间查询包括:
范围查询
距离查询
附近查询
相交查询
包含查询
位于查询
重叠查询
最近邻查询
空间连接
可以理解为:
Spatial Query
├── Where
│ └── 空间范围
│
├── Distance
│ └── 距离
│
├── Relation
│ ├── Intersects
│ ├── Contains
│ ├── Within
│ └── Touches
│
└── Nearest
└── 最近邻
PostGIS空间查询的基本结构
一个典型空间查询:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
:geometry
);
结构可以理解为:
SELECT
↓
需要返回的数据
FROM
↓
数据表
WHERE
↓
查询条件
ST_Intersects
↓
空间关系判断
geom
↓
数据库中的Geometry
:geometry
↓
查询Geometry
创建示例数据
首先创建一个简单的地点表:
CREATE TABLE places (
id SERIAL PRIMARY KEY,
name TEXT,
type TEXT,
geom geometry(Point, 4326)
);
插入数据:
INSERT INTO places (name, type, geom)
VALUES
(
'商店A',
'shop',
ST_SetSRID(
ST_Point(116.397, 39.908),
4326
)
),
(
'商店B',
'shop',
ST_SetSRID(
ST_Point(116.405, 39.915),
4326
)
),
(
'医院A',
'hospital',
ST_SetSRID(
ST_Point(116.410, 39.920),
4326
)
);
创建空间索引:
CREATE INDEX idx_places_geom
ON places
USING GIST (geom);
之后就可以进行空间查询。
空间范围查询
范围查询是WebGIS中最常见的空间查询之一。
例如:
查询某个矩形范围内的所有POI。
可以使用:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
ST_MakeEnvelope(
116.38,
39.89,
116.42,
39.93,
4326
)
);
其中:
116.38
↓
minX
39.89
↓
minY
116.42
↓
maxX
39.93
↓
maxY
形成:
+----------------------+
| |
| 查询区域 |
| |
+----------------------+
数据库只需要查询这个区域相关的数据。
ST_MakeEnvelope
ST_MakeEnvelope用于创建矩形范围。
语法:
ST_MakeEnvelope(
xmin,
ymin,
xmax,
ymax,
srid
)
例如:
ST_MakeEnvelope(
116.38,
39.89,
116.42,
39.93,
4326
)
可以理解为:
左下角
(116.38, 39.89)
右上角
(116.42, 39.93)
这是WebGIS地图范围查询非常常用的方法。
ST_Intersects
ST_Intersects用于判断两个Geometry是否相交。
例如:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
:polygon
);
返回:
true
或者:
false
典型问题:
哪些道路经过这个区域?
哪些POI位于当前地图范围?
哪些建筑与规划区域相交?
ST_Contains
ST_Contains用于判断一个Geometry是否包含另一个Geometry。
例如:
SELECT *
FROM places
WHERE ST_Contains(
:district,
geom
);
表示:
district
↓
是否包含
↓
place
典型应用:
查询某个行政区内的所有POI。
例如:
行政区Polygon
↓
ST_Contains
↓
POI Point
ST_Within
ST_Within用于判断一个Geometry是否位于另一个Geometry内部。
例如:
SELECT *
FROM places
WHERE ST_Within(
geom,
:district
);
表示:
place
↓
是否位于
↓
district
与ST_Contains可以从两个方向理解:
ST_Contains
A是否包含B
ST_Within
A是否位于B内部
ST_DWithin
ST_DWithin用于判断两个Geometry是否处于指定距离范围内。
例如:
SELECT *
FROM places
WHERE ST_DWithin(
geom,
:point,
500
);
表示:
查询距离point不超过500个单位的地点。
典型应用:
附近商店
附近医院
附近停车场
附近充电站
附近设备
附近人员
ST_Distance
ST_Distance用于计算两个Geometry之间的距离。
例如:
SELECT
name,
ST_Distance(
geom,
:point
) AS distance
FROM places;
返回:
name distance
商店A ...
商店B ...
医院A ...
可以进一步排序:
SELECT
name,
ST_Distance(
geom,
:point
) AS distance
FROM places
ORDER BY distance;
这样就可以按照距离从近到远排列。
附近查询
附近查询是LBS系统最常见的空间查询之一。
例如:
查询用户500米范围内的商店。
可以:
SELECT *
FROM places
WHERE type = 'shop'
AND ST_DWithin(
geom,
:point,
500
);
查询流程:
用户位置
↓
Point
↓
500米范围
↓
ST_DWithin
↓
空间索引
↓
附近商店
最近邻查询
如果需要查询最近的对象,可以使用:
<->
例如:
SELECT
id,
name
FROM places
ORDER BY geom <-> :point
LIMIT 5;
表示:
查询距离point最近的5个地点。
例如:
用户
↓
最近
↓
商店A
↓
商店B
↓
商店C
这就是KNN(K-Nearest Neighbor)查询。
KNN查询
KNN即:
K-Nearest Neighbor
意思是:
查询距离目标对象最近的K个对象。
例如:
SELECT *
FROM places
ORDER BY geom <-> :point
LIMIT 10;
表示:
最近10个地点
常见应用:
最近医院
最近餐厅
最近停车场
最近充电站
最近设备
最近仓库
距离排序
除了直接使用:
ORDER BY geom <-> :point
也可以返回实际距离:
SELECT
id,
name,
ST_Distance(
geom,
:point
) AS distance
FROM places
ORDER BY geom <-> :point
LIMIT 10;
这样可以同时获得:
名称
距离
例如:
商店A 120
商店B 230
商店C 450
按类型进行空间查询
空间查询通常还会结合普通SQL条件。
例如:
查询500米内的医院。
SELECT *
FROM places
WHERE type = 'hospital'
AND ST_DWithin(
geom,
:point,
500
);
可以理解为:
普通条件
+
空间条件
↓
最终结果
这是实际业务中非常常见的查询方式。
按状态进行空间查询
例如设备表:
CREATE TABLE devices (
id SERIAL PRIMARY KEY,
name TEXT,
status TEXT,
geom geometry(Point, 4326)
);
查询:
某个区域内所有在线设备。
SELECT *
FROM devices
WHERE status = 'online'
AND ST_Intersects(
geom,
:polygon
);
这种查询可以同时使用:
status
+
geom
两个维度进行筛选。
地图视野查询
WebGIS地图通常有一个当前视野范围:
minX
minY
maxX
maxY
例如:
地图
+----------------------+
| |
| 当前视野 |
| |
+----------------------+
后端可以:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
ST_MakeEnvelope(
:minX,
:minY,
:maxX,
:maxY,
4326
)
);
这是典型的:
地图
↓
Extent
↓
API
↓
PostGIS
↓
Spatial Query
↓
GeoJSON
↓
地图
WebGIS中的空间查询
一个完整的WebGIS查询流程通常是:
用户拖动地图
↓
获得当前Extent
↓
发送API请求
↓
后端接收BBox
↓
构造Geometry
↓
PostGIS空间查询
↓
返回GeoJSON
↓
前端绘制Feature
例如:
GET /api/places
?
minX=116.38
&minY=39.89
&maxX=116.42
&maxY=39.93
后端:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
ST_MakeEnvelope(
:minX,
:minY,
:maxX,
:maxY,
4326
)
);
最终:
PostGIS
↓
GeoJSON
↓
OpenLayers / Leaflet
↓
地图
空间查询与空间索引
空间查询和空间索引通常是一起使用的。
例如:
CREATE INDEX idx_places_geom
ON places
USING GIST (geom);
然后:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
:polygon
);
查询过程可以理解为:
空间查询
↓
GiST空间索引
↓
快速筛选候选对象
↓
精确空间判断
↓
返回结果
因此:
Spatial Query负责提出空间问题,Spatial Index负责提高查询效率。
Bounding Box过滤
很多空间查询都可以利用Bounding Box进行快速筛选。
例如:
复杂Polygon
↓
Bounding Box
↓
快速筛选
↓
候选对象
↓
精确计算
这样数据库就不需要对所有Geometry执行完整的空间计算。
空间查询中的SRID
空间查询必须特别注意SRID。
例如数据库:
geom
SRID = 4326
查询Geometry也应该正确使用:
SRID = 4326
例如:
ST_SetSRID(
ST_Point(116.397, 39.908),
4326
)
如果两个Geometry的空间参考不一致:
4326
+
3857
不能简单认为它们可以直接进行正确的空间计算。
因此空间查询之前需要确认:
Geometry
↓
SRID
↓
坐标单位
↓
空间查询
ST_SetSRID和ST_Transform
这两个函数非常容易混淆。
ST_SetSRID:
ST_SetSRID(
geom,
4326
)
表示:
给Geometry设置SRID
它不会改变坐标数值。
ST_Transform:
ST_Transform(
geom,
3857
)
表示:
真正转换坐标
例如:
EPSG:4326
↓
ST_Transform
↓
EPSG:3857
因此:
ST_SetSRID
↓
设置参考系
ST_Transform
↓
转换坐标
Geometry距离查询的单位
使用Geometry进行距离查询时,需要特别注意坐标系。
例如:
ST_Distance(
geomA,
geomB
)
结果单位取决于Geometry的坐标单位。
如果使用:
EPSG:4326
坐标通常是:
经度
纬度
并不是直接以米为单位。
如果业务需要进行实际地面距离计算,需要根据数据和查询场景选择合适的投影坐标系或使用Geography。
Geography距离查询
Geography适合进行基于地球曲面的地理计算。
例如:
SELECT ST_Distance(
geogA,
geogB
);
在常见的WGS84 Geography场景中,距离结果通常以米表示。
因此:
Geometry
↓
平面坐标计算
Geography
↓
地球曲面计算
对于全球范围或者经纬度距离计算,需要特别考虑这一点。
空间查询与GeoJSON
WebGIS通常需要返回GeoJSON。
PostGIS可以使用:
ST_AsGeoJSON()
例如:
SELECT
id,
name,
ST_AsGeoJSON(geom) AS geometry
FROM places;
可以将数据库Geometry转换成GeoJSON Geometry。
典型流程:
PostGIS Geometry
↓
ST_AsGeoJSON
↓
GeoJSON
↓
API
↓
OpenLayers / Leaflet
返回GeoJSON Feature
实际WebGIS中通常不仅需要Geometry,还需要属性。
可以构造:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [116.397, 39.908]
},
"properties": {
"name": "商店A",
"type": "shop"
}
}
因此:
PostGIS
├── Geometry
└── Attributes
↓
GeoJSON
↓
Feature
空间连接
Spatial Join(空间连接)也是非常重要的空间查询。
例如:
查询每个POI属于哪个行政区。
可以:
SELECT
p.id,
p.name,
d.name AS district
FROM places p
JOIN districts d
ON ST_Within(
p.geom,
d.geom
);
结构:
places
+
districts
↓
空间关系
↓
ST_Within
↓
Spatial Join
这类查询在GIS分析中非常常见。
查询道路经过哪些区域
例如:
SELECT
r.name,
d.name AS district
FROM roads r
JOIN districts d
ON ST_Intersects(
r.geom,
d.geom
);
表示:
道路
↓
与行政区相交
↓
得到经过的行政区
查询区域之间的交集
例如:
SELECT ST_Intersection(
a.geom,
b.geom
)
FROM areas a
JOIN areas b
ON ST_Intersects(
a.geom,
b.geom
);
这里:
ST_Intersects
负责判断是否相交。
而:
ST_Intersection
负责计算实际的交集Geometry。
两者不要混淆:
ST_Intersects
↓
判断
↓
true / false
ST_Intersection
↓
计算
↓
Geometry
查询包含关系
例如行政区与POI:
SELECT
d.name,
p.name
FROM districts d
JOIN places p
ON ST_Contains(
d.geom,
p.geom
);
结果可以表示:
行政区A
├── 商店A
├── 商店B
└── 医院A
查询相邻对象
GIS中还经常需要判断:
相交
包含
相邻
重叠
相离
可以使用不同的空间关系函数。
例如:
ST_Intersects
ST_Contains
ST_Within
ST_Touches
ST_Overlaps
ST_Disjoint
这些函数共同构成PostGIS空间关系查询能力。
ST_Touches
ST_Touches用于判断两个Geometry是否接触边界。
例如:
SELECT *
FROM areas a
JOIN areas b
ON ST_Touches(
a.geom,
b.geom
);
可以用于:
相邻地块
相邻行政区
共享边界区域
ST_Overlaps
ST_Overlaps用于判断两个Geometry是否存在重叠关系。
例如:
SELECT *
FROM areas a
JOIN areas b
ON ST_Overlaps(
a.geom,
b.geom
);
可以用于:
土地重叠
规划区域重叠
服务区域重叠
ST_Disjoint
ST_Disjoint用于判断两个Geometry是否完全不相交。
例如:
SELECT *
FROM places p
WHERE ST_Disjoint(
p.geom,
:polygon
);
返回:
true
false
表示:
true
↓
两个Geometry不相交
空间查询与普通SQL
PostGIS空间查询通常是:
普通条件
+
空间条件
+
排序
+
分页
例如:
SELECT
id,
name,
type
FROM places
WHERE type = 'shop'
AND ST_DWithin(
geom,
:point,
500
)
ORDER BY name
LIMIT 20
OFFSET 0;
这就是非常典型的业务查询。
空间查询分页
例如:
SELECT *
FROM places
WHERE ST_Intersects(
geom,
:bbox
)
ORDER BY id
LIMIT 100
OFFSET 0;
可以用于:
地图数据分页
大数据量POI加载
后台空间数据管理
不过对于非常大的空间数据集,应该结合:
空间索引
地图缩放级别
瓦片
Vector Tile
分页策略
进行整体设计。
使用EXPLAIN分析空间查询
可以使用:
EXPLAIN ANALYZE
SELECT *
FROM places
WHERE ST_Intersects(
geom,
:polygon
);
查看数据库的执行计划。
重点关注:
Index Scan
Bitmap Index Scan
Seq Scan
Execution Time
如果数据量很大而始终进行:
Seq Scan
需要进一步检查:
空间索引
查询条件
统计信息
数据量
SQL写法
空间查询性能优化
常见优化方法:
1. 创建GiST空间索引
2. 合理使用空间函数
3. 限制查询范围
4. 使用ST_DWithin进行附近查询
5. 使用KNN进行最近邻查询
6. 避免一次返回大量Geometry
7. 控制地图查询范围
8. 检查EXPLAIN ANALYZE
9. 定期更新统计信息
10. 根据业务选择Geometry或Geography
一个完整的附近查询示例
创建表:
CREATE TABLE shops (
id SERIAL PRIMARY KEY,
name TEXT,
geom geometry(Point, 4326)
);
创建索引:
CREATE INDEX idx_shops_geom
ON shops
USING GIST (geom);
查询位置:
SELECT
id,
name
FROM shops
WHERE ST_DWithin(
geom,
ST_SetSRID(
ST_Point(116.397, 39.908),
4326
),
0.01
);
如果需要实际米制距离,应根据坐标类型和坐标系选择合适的距离计算方式。
一个完整的地图范围查询
例如前端传入:
minX
minY
maxX
maxY
SQL:
SELECT
id,
name,
type,
ST_AsGeoJSON(geom) AS geometry
FROM places
WHERE ST_Intersects(
geom,
ST_MakeEnvelope(
:minX,
:minY,
:maxX,
:maxY,
4326
)
);
完整流程:
地图
↓
当前Extent
↓
minX/minY/maxX/maxY
↓
API
↓
PostGIS
↓
ST_MakeEnvelope
↓
ST_Intersects
↓
GiST
↓
查询结果
↓
ST_AsGeoJSON
↓
GeoJSON
↓
地图
一个完整的最近邻查询
SELECT
id,
name,
ST_Distance(
geom,
:point
) AS distance
FROM places
ORDER BY geom <-> :point
LIMIT 10;
表示:
指定Point
↓
空间索引
↓
距离排序
↓
最近10个对象
一个完整的空间连接查询
SELECT
p.id,
p.name,
d.name AS district
FROM places p
JOIN districts d
ON ST_Within(
p.geom,
d.geom
);
结果:
POI
↓
判断所在Polygon
↓
行政区
Spatial Query与Spatial Index的关系
两者需要明确区分:
Spatial Query
↓
我要查询什么?
Spatial Index
↓
如何更快找到相关数据?
例如:
问题:
查询某区域内的POI
↓
Spatial Query:
ST_Intersects()
↓
Spatial Index:
GiST
↓
候选对象
↓
精确空间判断
↓
结果
因此:
Spatial Query解决“查什么”,Spatial Index解决“怎么查得快”。
PostGIS空间查询知识结构
可以这样理解:
PostGIS Spatial Query
│
├── Range Query
│ └── ST_Intersects
│
├── Distance Query
│ ├── ST_DWithin
│ └── ST_Distance
│
├── Relation Query
│ ├── ST_Contains
│ ├── ST_Within
│ ├── ST_Touches
│ ├── ST_Overlaps
│ └── ST_Disjoint
│
├── Nearest Query
│ ├── <->
│ └── KNN
│
├── Spatial Join
│ └── JOIN + Spatial Relation
│
├── Map Query
│ └── BBOX / Extent
│
├── Output
│ └── ST_AsGeoJSON
│
└── Performance
├── GiST
├── Bounding Box
└── EXPLAIN ANALYZE
PostGIS空间查询完整流程
一个典型的WebGIS空间查询可以概括为:
用户
↓
地图操作
↓
Point / BBox / Polygon
↓
API
↓
SQL
↓
PostGIS
↓
Spatial Index
↓
候选Geometry
↓
Spatial Function
↓
精确空间判断
↓
查询结果
↓
GeoJSON
↓
前端地图
Geometry、Spatial Query与Spatial Index
可以用三个概念理解PostGIS:
Geometry
↓
存储空间对象
Spatial Query
↓
查询空间对象
Spatial Index
↓
加速空间查询
例如:
Geometry
↓
Point / Line / Polygon
↓
Spatial Query
↓
ST_Intersects / ST_DWithin
↓
Spatial Index
↓
GiST
↓
查询结果
总结
PostGIS Spatial Query是PostGIS进行GIS数据查询的核心能力之一。
它可以根据:
空间范围
距离
空间关系
位置
最近邻
查询GIS数据。
最常用的空间查询函数包括:
ST_Intersects
ST_Contains
ST_Within
ST_DWithin
ST_Distance
ST_Touches
ST_Overlaps
ST_Disjoint
最近邻查询常使用:
<->
地图范围查询通常使用:
ST_MakeEnvelope
+
ST_Intersects
WebGIS通常使用:
PostGIS
↓
Spatial Query
↓
ST_AsGeoJSON
↓
GeoJSON
↓
OpenLayers / Leaflet
而为了提高查询性能,需要配合:
GiST
Spatial Index
Bounding Box
EXPLAIN ANALYZE
最终可以记住:
PostGIS Spatial Query就是利用SQL和空间函数,从空间数据库中按照位置、范围、距离和空间关系查找GIS数据。
相关工具
使用 IYATools 在线工具快速处理 GIS 数据