15. 空间索引

回想一下,空间索引是空间数据库的三个关键特征之一。索引使得对大型数据集使用空间数据库成为可能。如果没有索引,任何对要素的搜索都需要对数据库中的每个记录进行“顺序扫描”。索引通过将数据组织成搜索树来加快搜索速度,该搜索树可以快速遍历以查找特定记录。

空间索引是 PostGIS 的最大优势之一。在之前的示例中,构建空间连接需要将整个表相互比较。这可能会非常昂贵:连接两个各有 10,000 条记录的表,如果没有索引,则需要 100,000,000 次比较;而使用索引,成本可能低至 20,000 次比较。

我们的数据加载文件已经包含了所有表的空间索引,因此为了演示索引的功效,我们必须首先删除它们。

让我们在没有空间索引的情况下对 nyc_census_blocks 运行查询。

我们的第一步是**删除**索引。

DROP INDEX nyc_census_blocks_geom_idx;

注意

DROP INDEX 语句从数据库系统中删除现有索引。有关更多信息,请参阅 PostgreSQL 文档

现在,观察 pgAdmin 查询窗口右下角的“计时”仪表,并运行以下查询。我们的查询搜索每个普查区块,以识别包含以“B”开头的地铁站的区块。

SELECT count(blocks.blkid)
 FROM nyc_census_blocks blocks
 JOIN nyc_subway_stations subways
 ON ST_Contains(blocks.geom, subways.geom)
 WHERE subways.name LIKE 'B%';
    count
---------------
    46

nyc_census_blocks 表非常小(只有几千条记录),因此即使没有索引,在我的测试计算机上,查询也只花费 **300 毫秒**。

现在重新添加空间索引,并再次运行查询。

CREATE INDEX nyc_census_blocks_geom_idx
  ON nyc_census_blocks
  USING GIST (geom);

注意

USING GIST 子句告诉 PostgreSQL 在构建索引时使用通用索引结构 (GIST)。如果创建索引时收到类似 ERROR: index row requires 11340 bytes, maximum size is 8191 的错误,则很可能忘记添加 USING GIST 子句。

在我的测试计算机上,时间降至 **50 毫秒**。您的表越大,索引查询的相对速度提升就越大。

15.1. 空间索引的工作原理

标准数据库索引基于被索引的列的值创建一个层次树。空间索引略有不同 - 它们无法索引几何特征本身,而是索引特征的边界框。

_images/bbox.png

在上图中,与黄色星形相交的线条数量为 **一个**,即红色线。但是,与黄色框相交的要素的边界框为 **两个**,即红色和蓝色框。

数据库有效回答“哪些线与黄色星形相交”的问题的方法是,首先使用索引(速度非常快)回答“哪些框与黄色框相交”的问题,然后**仅针对第一次测试返回的那些要素**进行“哪些线与黄色星形相交”的精确计算。

对于大型表,这种先评估近似索引,然后进行精确测试的“两遍”系统可以大大减少回答查询所需的计算量。

PostGIS 和 Oracle Spatial 共享相同的“R 树”1 空间索引结构。R 树将数据分解为矩形、子矩形和子子矩形等。它是一种自调整索引结构,可自动处理可变数据密度、不同数量的对象重叠和对象大小。

_images/index-01.png

15.2. 空间索引函数

如果存在空间索引,只有一部分函数会自动使用它。

前四个是在查询中最常用的,并且ST_DWithin 对于执行“在距离内”或“在半径内”的样式查询非常重要,同时仍然可以从索引中获得性能提升。

为了将索引加速添加到此列表中不包含的其他函数(最常见的是ST_Relate),请添加如下所述的仅索引子句。

15.3. 仅索引查询

PostGIS 中大多数常用函数(ST_ContainsST_IntersectsST_DWithin 等)会自动包含索引过滤器。但是,某些函数(例如,ST_Relate)不包含索引过滤器。

要使用索引进行边界框搜索(且不进行过滤),请使用 && 运算符。对于几何图形,&& 运算符表示“边界框重叠或接触”,就像对于数字,= 运算符表示“值相同”一样。

让我们比较一下“西村”人口的仅索引查询和更精确的查询。使用 &&,我们的仅索引查询如下所示

SELECT Sum(popn_total)
FROM nyc_neighborhoods neighborhoods
JOIN nyc_census_blocks blocks
ON neighborhoods.geom && blocks.geom
WHERE neighborhoods.name = 'West Village';
49821

现在,让我们使用更精确的 ST_Intersects 函数执行相同的查询。

SELECT Sum(popn_total)
FROM nyc_neighborhoods neighborhoods
JOIN nyc_census_blocks blocks
ON ST_Intersects(neighborhoods.geom, blocks.geom)
WHERE neighborhoods.name = 'West Village';
26718

答案低得多!第一个查询对边界框与社区边界框相交的每个区块进行了求和;第二个查询仅对与社区本身相交的那些区块进行了求和。

15.4. 分析

PostgreSQL 查询计划器会智能地选择何时使用或不使用索引来评估查询。与直觉相反,执行索引搜索并不总是更快:如果搜索将返回表中的每个记录,则遍历索引树以获取每个记录实际上比从头开始顺序读取整个表要慢。

知道查询矩形的大小不足以确定查询将返回大量还是少量记录。在下面,红色正方形很小,但将比蓝色正方形返回更多的记录。

_images/index-02.png

为了弄清楚它正在处理的情况(读取表的一小部分与读取表的大部分),PostgreSQL 会保留有关每个索引表列中数据分布的统计信息。默认情况下,PostgreSQL 会定期收集统计信息。但是,如果您在短时间内大幅更改表的内容,则统计信息将不是最新的。

为了确保统计信息与您的表内容匹配,明智的做法是在表中进行批量数据加载和删除后运行 ANALYZE 命令。这将强制统计系统收集所有索引列的数据。

ANALYZE 命令要求 PostgreSQL 遍历表并更新其用于查询计划估计的内部统计信息(稍后将讨论查询计划分析)。

ANALYZE nyc_census_blocks;

15.5. 清理

值得强调的是,仅仅创建索引不足以使 PostgreSQL 有效地使用它。每当对表发出大量 UPDATE、INSERT 或 DELETE 时,都必须执行 VACUUM。 VACUUM 命令要求 PostgreSQL 回收因记录更新或删除而留在表页中的任何未使用空间。

清理对于数据库的有效运行至关重要,因此 PostgreSQL 默认提供“自动清理”功能。

自动清理会在由活动级别确定的合理时间间隔内对表进行清理(回收空间)和分析(更新统计信息)。虽然这对于高度事务性的数据库至关重要,但在添加索引或批量加载数据后,不建议等待自动清理运行。每当执行批量更新时,都应手动运行 VACUUM

可以根据需要单独执行清理和分析数据库。发出 VACUUM 命令不会更新数据库统计信息;同样,发出 ANALYZE 命令不会恢复未使用的表行。这两个命令都可以针对整个数据库、单个表或单个列运行。

VACUUM ANALYZE nyc_census_blocks;

15.6. 函数列表

geometry_a && geometry_b:如果 A 的边界框与 B 的边界框重叠,则返回 TRUE。

geometry_a = geometry_b:在 PostGIS 2.4 之前,如果 A 的边界框与 B 的边界框相同,则返回 true。从 2.4 开始,仅当 A 的几何图形与 B 的几何图形相同时才返回 TRUE。

geometry_a ~= geometry_b: 如果 A 的边界框等于 B 的边界框,则返回 TRUE。

ST_Intersects(geometry_a, geometry_b): 如果几何体/地理体“空间相交”(共享空间的任何部分),则返回 TRUE,如果它们不相交(它们是分离的),则返回 FALSE。

脚注

1

https://postgis.net.cn/docs/support/rtree.pdf