GIS

PostGIS Spatial Index是什么

了解PostGIS空间索引的基本概念、GiST索引、空间查询原理、创建方法、常见查询以及空间索引优化技巧。

阅读约 116 分钟

PostGIS Spatial Index是什么

Spatial Index(空间索引)是GIS数据库中用于提高空间查询效率的数据结构。

在PostGIS中,空间索引最常见的实现方式是:

GiST

例如一个城市数据库中有:

100万

个地点。

如果没有空间索引,查询附近地点时可能需要:

100万个Geometry
        ↓
逐个进行空间计算
        ↓
得到结果

数据量越大,查询速度越慢。

使用空间索引以后:

100万个Geometry
        ↓
Spatial Index
        ↓
快速筛选候选数据
        ↓
精确空间计算
        ↓
得到结果

因此:

PostGIS空间索引的核心作用,是减少空间查询需要检查的数据量。

为什么需要空间索引

普通数据库中的索引可以快速查找:

id = 100
name = 北京
age > 30

空间查询则更加复杂。

例如:

查询距离某个位置500米以内的所有商店。

查询条件可能是:

距离
范围
相交
包含
重叠
相邻

如果直接对所有Geometry进行计算:

Store 1
Store 2
Store 3
...
Store 1,000,000

数据库需要处理大量空间对象。

空间索引可以先快速缩小搜索范围。

Spatial Index的基本原理

空间索引通常不会直接判断最终的精确空间关系。

它更像是:

第一阶段
快速筛选
    ↓
候选Geometry

第二阶段
精确计算
    ↓
最终结果

例如查询:

某点500米范围内的商店

可以理解为:

Point
 ↓
搜索空间索引
 ↓
找到可能相关的Geometry
 ↓
ST_DWithin精确判断
 ↓
返回结果

因此:

空间索引主要负责“快速缩小范围”,空间函数负责“精确判断”。

Bounding Box

PostGIS空间索引的重要基础之一是:

Bounding Box

即最小外接矩形。

例如一个Polygon:

       Polygon
      /------\
     /        \
    /          \
   /------------\

可以使用一个矩形包围:

+----------------+
|                |
|    Polygon     |
|                |
+----------------+

这个矩形就是Geometry的Bounding Box。

可以理解为:

复杂Geometry
      ↓
Bounding Box
      ↓
简单矩形
      ↓
快速空间判断

矩形判断通常比复杂Geometry之间的精确计算更加高效。

GiST是什么

GiST是PostgreSQL提供的一种通用索引框架。

全称是:

Generalized Search Tree

PostGIS使用GiST来构建非常重要的空间索引。

例如:

CREATE INDEX idx_places_geom
ON places
USING GIST (geom);

其中:

CREATE INDEX
    ↓
创建索引

USING GIST
    ↓
使用GiST

geom
    ↓
空间字段

因此可以简单记住:

PostGIS中最常见的空间索引就是基于GiST创建的。

创建空间索引

假设有一个表:

CREATE TABLE places (
    id SERIAL PRIMARY KEY,
    name TEXT,
    geom geometry(Point, 4326)
);

创建空间索引:

CREATE INDEX idx_places_geom
ON places
USING GIST (geom);

创建完成以后:

places
├── id
├── name
└── geom
      ↓
    GiST Index

数据库就可以在空间查询中利用这个索引。

建议给Geometry创建索引

如果表中存在大量Geometry:

geom geometry(Point, 4326)

通常应该考虑:

CREATE INDEX idx_places_geom
ON places
USING GIST (geom);

尤其是存在以下查询时:

附近查询
范围查询
空间相交
空间包含
空间距离
空间连接

空间索引通常非常重要。

ST_Intersects与空间索引

例如:

SELECT *
FROM roads
WHERE ST_Intersects(
    geom,
    ST_GeomFromText(
        'POLYGON ((...))',
        4326
    )
);

如果Geometry字段建立了合适的空间索引:

roads.geom
    ↓
GiST
    ↓
快速筛选
    ↓
ST_Intersects
    ↓
精确判断

这样可以避免大量无意义的Geometry计算。

ST_DWithin与空间索引

ST_DWithin常用于附近查询。

例如:

SELECT *
FROM places
WHERE ST_DWithin(
    geom,
    ST_SetSRID(
        ST_Point(116.397, 39.908),
        4326
    ),
    0.01
);

它可以用于查询指定距离范围内的数据。

典型应用:

附近商店
附近医院
附近停车场
附近充电站
附近设备

空间索引可以帮助数据库快速缩小搜索范围。

ST_Contains与空间索引

例如:

查询某个行政区内的所有POI。

可以:

SELECT *
FROM places
WHERE ST_Contains(
    district.geom,
    places.geom
);

空间索引可以帮助减少需要进行精确空间判断的对象。

典型结构:

行政区Polygon
      ↓
Spatial Index
      ↓
候选POI
      ↓
ST_Contains
      ↓
最终结果

ST_Within与空间索引

例如:

SELECT *
FROM places
WHERE ST_Within(
    places.geom,
    district.geom
);

表示:

places
    ↓
是否位于
    ↓
district

对于大量空间数据:

Spatial Index
      ↓
快速筛选
      ↓
ST_Within

可以明显减少计算范围。

空间索引与普通索引的区别

普通B-Tree索引:

id
name
price
date

更适合:

=
>
<
BETWEEN
ORDER BY

空间索引:

Geometry
Geography

更适合:

相交
包含
范围
距离
空间关系

可以简单理解:

索引类型主要处理
B-Tree数值、字符串、排序
GiST空间数据等复杂类型
空间索引空间范围与空间关系

GiST索引示例

完整示例:

CREATE TABLE buildings (
    id SERIAL PRIMARY KEY,
    name TEXT,
    geom geometry(Polygon, 4326)
);

创建空间索引:

CREATE INDEX idx_buildings_geom
ON buildings
USING GIST (geom);

查询:

SELECT *
FROM buildings
WHERE ST_Intersects(
    geom,
    ST_GeomFromText(
        'POLYGON ((...))',
        4326
    )
);

数据库可以利用:

idx_buildings_geom

来加速查询。

查看索引

可以使用:

SELECT
    indexname,
    indexdef
FROM pg_indexes
WHERE tablename = 'buildings';

可以看到类似:

idx_buildings_geom
CREATE INDEX ...
USING gist (geom)

EXPLAIN查看查询计划

可以使用:

EXPLAIN
SELECT *
FROM buildings
WHERE ST_Intersects(
    geom,
    ST_GeomFromText(
        'POLYGON ((...))',
        4326
    )
);

如果需要看到实际执行情况:

EXPLAIN ANALYZE
SELECT *
FROM buildings
WHERE ST_Intersects(
    geom,
    ST_GeomFromText(
        'POLYGON ((...))',
        4326
    )
);

可以观察数据库是否使用了:

Index Scan
Bitmap Index Scan
Seq Scan

如果数据量较大,而查询始终使用:

Seq Scan

就需要进一步检查:

空间索引
查询条件
数据量
统计信息
函数写法
坐标系

Index Scan

理想情况下,数据库可能使用:

Index Scan

或者:

Bitmap Index Scan

例如:

Query
 ↓
GiST Index
 ↓
Candidate Rows
 ↓
Geometry Check
 ↓
Result

这通常比:

Query
 ↓
Sequential Scan
 ↓
逐条检查全部数据

更加高效。

Sequential Scan

Sequential Scan也叫:

Seq Scan

表示数据库逐行扫描表。

例如:

1
2
3
4
...
1,000,000

如果空间数据非常多:

100万Geometry

就可能产生大量计算。

但需要注意:

Seq Scan并不一定代表查询有问题。

如果表非常小:

100条

数据库可能认为直接扫描比使用索引更加高效。

所以:

没有使用索引
≠
一定有问题

应该结合:

数据量
查询成本
执行计划

进行判断。

空间索引不是越多越好

索引可以提高查询速度,但也会增加:

磁盘空间
INSERT成本
UPDATE成本
DELETE成本
维护成本

例如:

INSERT
 ↓
写入Geometry
 ↓
更新空间索引

因此不应该为每个字段都建立索引。

通常应该根据实际查询场景建立。

Geometry字段修改后的影响

如果Geometry发生大量更新:

UPDATE buildings
SET geom = ...;

空间索引也需要维护。

大量数据更新后,可以考虑:

VACUUM ANALYZE buildings;

或者:

ANALYZE buildings;

帮助数据库获得更加准确的统计信息。

空间索引与坐标系

空间索引本身并不能解决坐标系错误。

例如:

数据
EPSG:4326

查询Geometry
EPSG:3857

如果没有正确进行坐标转换:

Spatial Query
      ↓
错误坐标
      ↓
错误结果

因此空间查询之前需要确认:

Geometry类型
SRID
坐标单位
查询Geometry

ST_SetSRID与空间索引

需要特别注意:

ST_SetSRID

和:

ST_Transform

并不是一回事。

例如:

ST_SetSRID(
    ST_Point(116.397, 39.908),
    4326
)

只是告诉PostGIS:

这些坐标属于4326

而:

ST_Transform(
    geom,
    3857
)

才是真正进行坐标转换。

如果在查询中大量对索引字段执行:

ST_Transform(geom, ...)

需要特别注意查询计划和索引使用情况。

不要随意对索引字段进行函数包装

例如索引建立在:

geom

但查询写成:

WHERE ST_Transform(geom, 3857) ...

此时数据库是否能够有效利用原有索引,需要结合具体查询和索引情况判断。

更合理的方式通常是:

统一坐标系
      ↓
准备查询Geometry
      ↓
使用索引字段进行空间查询

对于确实需要经常使用转换后坐标进行查询的场景,也可以考虑针对表达式建立合适的索引。

Geography空间索引

PostGIS的Geography类型也可以建立空间索引。

例如:

CREATE TABLE places (
    id SERIAL PRIMARY KEY,
    name TEXT,
    geog geography(Point, 4326)
);

创建索引:

CREATE INDEX idx_places_geog
ON places
USING GIST (geog);

之后可以用于:

距离查询
附近查询
空间关系查询

多列空间查询

实际项目中经常同时存在:

tenant_id
type
status
geom

例如:

查询某个租户
+
某种设备
+
某个区域内

查询:

SELECT *
FROM devices
WHERE tenant_id = 10
  AND type = 'camera'
  AND ST_Intersects(
      geom,
      :polygon
  );

这时候可以根据实际数据规模和查询模式考虑:

普通索引
+
空间索引

例如:

CREATE INDEX idx_devices_tenant
ON devices (tenant_id);

CREATE INDEX idx_devices_geom
ON devices
USING GIST (geom);

然后让数据库根据查询计划选择合适的执行方式。

空间索引与空间连接

空间连接是GIS中非常常见的操作。

例如:

查询每个POI属于哪个行政区。

可以:

SELECT
    p.id,
    p.name,
    d.name
FROM places p
JOIN districts d
ON ST_Within(
    p.geom,
    d.geom
);

数据量较大时:

places
   +
districts
   ↓
Spatial Index
   ↓
候选空间对象
   ↓
ST_Within
   ↓
空间连接结果

空间索引对于这类操作非常重要。

空间索引与最近邻查询

PostGIS还支持利用GiST进行最近邻查询。

例如:

SELECT
    id,
    name
FROM places
ORDER BY geom <-> ST_SetSRID(
    ST_Point(116.397, 39.908),
    4326
)
LIMIT 10;

其中:

<->

是PostGIS中常用于距离排序的空间操作符。

可以用于:

最近商店
最近医院
最近设备
最近道路
最近POI

典型流程:

用户位置
   ↓
空间索引
   ↓
最近对象
   ↓
LIMIT
   ↓
返回结果

Spatial Index与KNN

KNN表示:

K-Nearest Neighbor

即:

K个最近邻对象。

例如:

SELECT *
FROM places
ORDER BY geom <-> :point
LIMIT 5;

表示查找:

距离point最近的5个对象

这在LBS系统中非常常见。

空间索引的典型应用

附近搜索

用户
 ↓
500米
 ↓
附近商店

行政区查询

Point
 ↓
属于哪个Polygon

道路查询

道路
 ↓
经过哪些区域

服务范围

医院
 ↓
Buffer
 ↓
服务区域

地图查询

当前地图视野
 ↓
Bounding Box
 ↓
查询范围内Feature

WebGIS

地图缩放
 ↓
当前Extent
 ↓
API
 ↓
PostGIS
 ↓
Spatial Index
 ↓
GeoJSON
 ↓
地图显示

WebGIS中的空间索引

WebGIS经常按照地图当前范围查询数据。

例如:

地图当前范围:

minX
minY
maxX
maxY

可以构造一个Polygon:

Bounding Box
       ↓
PostGIS
       ↓
ST_Intersects
       ↓
Spatial Index
       ↓
返回当前地图范围内的数据

这对于:

OpenLayers
Leaflet
MapLibre
Cesium

等地图应用都非常常见。

空间索引与海量数据

假设数据库有:

1000万

条POI。

用户查询:

某个城市范围内的POI

没有空间索引:

1000万
 ↓
逐条判断
 ↓
大量计算

有空间索引:

1000万
 ↓
GiST
 ↓
空间范围筛选
 ↓
少量候选数据
 ↓
精确判断

空间索引的价值会随着数据规模增长越来越明显。

空间索引的维护

创建索引:

CREATE INDEX idx_places_geom
ON places
USING GIST (geom);

删除索引:

DROP INDEX idx_places_geom;

重新创建:

REINDEX INDEX idx_places_geom;

查看表索引:

SELECT
    indexname,
    indexdef
FROM pg_indexes
WHERE tablename = 'places';

一个完整示例

创建数据表:

CREATE TABLE shops (
    id SERIAL PRIMARY KEY,
    name TEXT,
    geom geometry(Point, 4326)
);

创建空间索引:

CREATE INDEX idx_shops_geom
ON shops
USING GIST (geom);

插入数据:

INSERT INTO shops (name, geom)
VALUES
(
    '商店A',
    ST_SetSRID(
        ST_Point(116.397, 39.908),
        4326
    )
),
(
    '商店B',
    ST_SetSRID(
        ST_Point(116.405, 39.915),
        4326
    )
);

查询空间范围:

SELECT *
FROM shops
WHERE ST_Intersects(
    geom,
    ST_MakeEnvelope(
        116.38,
        39.89,
        116.42,
        39.93,
        4326
    )
);

查询最近对象:

SELECT *
FROM shops
ORDER BY geom <-> ST_SetSRID(
    ST_Point(116.397, 39.908),
    4326
)
LIMIT 5;

整个过程:

创建Geometry字段
       ↓
创建GiST空间索引
       ↓
插入空间数据
       ↓
执行空间查询
       ↓
数据库使用空间索引
       ↓
快速获得候选数据
       ↓
返回最终结果

空间索引常见问题

为什么创建了索引却没有使用

可能原因包括:

数据量太小
查询成本不高
统计信息过期
查询条件不适合索引
函数写法影响索引
空间索引选择性不足

可以使用:

EXPLAIN ANALYZE

查看执行计划。

索引很大怎么办

空间索引本身需要占用磁盘。

可以检查:

SELECT
    pg_size_pretty(
        pg_relation_size('idx_shops_geom')
    );

查看索引大小。

数据更新后查询变慢

可以检查:

ANALYZE shops;

或者:

VACUUM ANALYZE shops;

让PostgreSQL重新获取统计信息。

Spatial Index知识结构

可以这样理解:

Spatial Index
│
├── Bounding Box
│
├── GiST
│
├── Geometry Index
│
├── Geography Index
│
├── Spatial Query
│   ├── ST_Intersects
│   ├── ST_Contains
│   ├── ST_Within
│   ├── ST_DWithin
│   └── KNN
│
├── Query Plan
│   ├── Index Scan
│   ├── Bitmap Index Scan
│   └── Seq Scan
│
└── Optimization
    ├── EXPLAIN ANALYZE
    ├── ANALYZE
    ├── VACUUM
    └── 合理建立索引

PostGIS空间索引完整流程

一个典型PostGIS空间查询可以理解为:

用户请求
    ↓
空间查询条件
    ↓
PostGIS
    ↓
GiST Spatial Index
    ↓
Bounding Box快速筛选
    ↓
候选Geometry
    ↓
空间函数精确计算
    ↓
最终结果
    ↓
GeoJSON
    ↓
WebGIS地图

Geometry、Spatial Index与Spatial Function

这三个概念需要区分:

Geometry
    ↓
保存空间数据

Spatial Index
    ↓
快速找到可能相关的数据

Spatial Function
    ↓
精确计算空间关系

例如:

Geometry
   ↓
GiST
   ↓
ST_Intersects
   ↓
结果

可以记住:

Geometry负责描述空间对象,Spatial Index负责快速查找,Spatial Function负责空间计算。

总结

PostGIS Spatial Index是PostGIS高性能空间查询的重要基础。

最常见的空间索引创建方式是:

CREATE INDEX idx_geom
ON table_name
USING GIST (geom);

其中:

GIST
 ↓
PostgreSQL通用索引框架

geom
 ↓
空间字段

空间索引可以用于加速:

ST_Intersects
ST_Contains
ST_Within
ST_DWithin
最近邻查询
空间连接
地图范围查询

核心原理可以概括为:

大量Geometry
      ↓
Spatial Index
      ↓
快速筛选候选对象
      ↓
空间函数精确计算
      ↓
最终结果

最重要的几个概念是:

Spatial Index
GiST
Bounding Box
ST_Intersects
ST_Contains
ST_Within
ST_DWithin
KNN
EXPLAIN ANALYZE

最终可以记住:

PostGIS空间索引不是用来替代空间计算,而是用来快速减少需要进行空间计算的数据量。

🧰

相关工具

使用 IYATools 在线工具快速处理 GIS 数据