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

在上图中,与黄色星形相交的线数为1,即红线。但与黄色框相交的要素的边界框为2,即红色和蓝色。

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

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

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

_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: 如果 A 的边界框与 B 的边界框相同,则返回 TRUE。

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

脚注

1

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