GIS

PostGIS Spatial Query是什么

了解PostGIS空间查询的基本概念、常见空间函数、空间范围查询、距离查询、相交查询、包含查询、最近邻查询以及空间索引的使用方法。

阅读约 153 分钟

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 数据