由于数据量巨大和几何图形复杂程度高,在构建GIS应用时很容易碰到性能问题。查询中常常设计空间相交操作,这和一般数据库的join不一样。如果有一个复杂几何体的大数据集,查询给定的AOI和数据及中哪些几何体相交,计算量会非常大。

下面用PostGIS的geometry(Polygon,4326)类型的河流数据单独存储在一张表作为目标表。

1
2
3
4
5
CREATE TABLE river (
rid uuid PRIMARY KEY,
name text,
shape geometry(polygon,4326)
);

ST_MakeEnvelope构建给定AOI矩形查询相交的河流,会进行全表扫描,测试耗时25.78ms。

1
2
3
SELECT *
FROM river r
WHERE ST_Intersects(r.shape, ST_MakeEnvelope(24, 47, 25, 48, 4326));

策略一:空间索引

添加空间索引后,从序列扫描变为索引扫描,耗时减少97%,为0.752ms。

1
CREATE INDEX river_shape_idx ON river USING gist(shape);

添加空间索引后空间索引PostGIS只需要对表中的一小部分多边形进行相交计算。

在处理相对简单和比较小的多边形(比如建筑物polygon)的时候,通常添加空间索引就已经足够了。大多数相交的外边框都会产生实际的多边形相交,很少误报,所以性能足够高。

策略二:边界分割

考虑另外一种情况,比如横跨整个大陆的河流,这个时候AOI大概率会和河流的外边框相交,空间索引就不起作用了。

大型河流与外边框

解决方案是对河流数据进行分割,把复杂的大型多边形切分成简单的多边形另外存储。这里将river中的数据每10个点一组拆分到river_segment表中。

这里可以设置数据库触发器,在river更新时自动同步。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE river_segment (
sid uuid PRIMARY KEY,
rid uuid REFERENCES river(rid),
name text,
shape geometry(polygon,4326)
);

CREATE INDEX river_segment_shape_idx ON river_segment USING gist(shape);

INSERT INTO river_segment
SELECT uuid_generate_v4(), rid, ST_Subdivide(shape, 10)
FROM river;

在分割后的表中进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT *
FROM river_segment r
WHERE ST_Intersects(r.shape, ST_MakeEnvelope(24, 47, 24, 48, 4326));

-- 可以对相交的河流分段进行合并,并获取原始河流
WITH intersected_rivers AS (
SELECT DISTINCT r.rid
FROM river_segment r
WHERE ST_Intersects(r.shape, ST_MakeEnvelope(24, 47, 25, 48, 4326))
)
SELECT r.rid,
r.shape as original_shape,
ST_Intersection(r.shape, ST_MakeEnvelope(24, 47, 25, 48, 4326)) as clipped_shape
FROM river r
INNER JOIN intersected_rivers i ON i.id = r.id;

这种优化适合大型的复杂多边形,查询性能提升超过插入的分割操作,能有效优化读取性能。

策略三:只用索引查询

&&操作符进行索引查询可能是最快的方式,会返回外边框与AOI外边框相交的所有geometry。只检查R-Tree索引,而不验证geometry是否真的相交。

1
2
3
SELECT *
FROM river r
WHERE r.shape && ST_MakeEnvelope(24, 47, 24, 48, 4326);

这种方法的弊端在于,查询结果可能并不真正相交,尤其在道路、河流这类实际geometry只在外边框占据很小部分的。而对于建筑物这类会比较适用,不在意少量误报的情况下,查询性能会明显提升。

策略四:多边形简化

对于大型多边形,比如岛屿、主要道路这类,可以在查询时简化多边形。比如按照缩放级别对多边形进行简化,缩放级别通常在0-23之间,3级大约是一个大陆的大小,15级展示单个建筑物,超过20级展示地图细节。下面计算每个缩放级别中大概的分辨率,每向下缩放一个级别,分别率减半。

1
pixelSize = basePixelSize / (2 ^ zoomLevel)

basePixelSize是缩放级别0时的像素大小,大约是78公里。根据使用的SRID,我们需要将其转换为SRID的单位。在上面的例子中,我们使用了4326,它使用度作为单位。可以通过除以111公里/像素将这78公里转换为度数,结果是在缩放级别0时每像素0.7度。

1
pixelSize = 0.7 / (2 ^ zoomLevel)

使用ST_Simplify查询简化后的多边形:

1
2
3
4
5
6
7
SELECT *
FROM river r
WHERE ST_Simplify(
ST_Intersects(r.shape, ST_MakeEnvelope(24, 47, 25, 48, 4326)),
0.7 / (2 ^ zoomLevel),
false
)

两种应用方法:

  • 实时–每次用户发出请求时,确定所请求的缩放级别所需的详细程度,并在将多边形返回给客户端之前对其进行简化。
  • 预先计算–已知缩放级别的范围,可以预先计算并存储原始多边形的所有简化版本,然后根据请求的缩放级别检索它们。

如果以上策略应用得当,可以极大地提高地理空间应用的性能,处理大量或复杂计算的几何图形。

参考链接:https://medium.com/symphonyis/boosting-postgis-performance-c68a478daa0a