GiST的封面索引

覆盖指数不仅是可能派上用场的另一个功能。 这东西纯粹是实用的。 没有它们,“仅索引扫描”可能无法获胜。 尽管覆盖指数在不同情况下以不同方式有效。

这实际上不是涉及索引的:严格来说,所谓的包含索引已出现在Postgres中。 但是,按照顺序:覆盖索引是包含查询所需的所有列值的索引; 但是,不再需要访问表本身。 差不多了 您可以在Yegor Rogov的10系列索引文章中阅读有关“几乎”和其他细微差别的文章。 并且包含索引是专门为典型查询的搜索而创建的:无法搜索的字段的值被添加到搜索索引中,仅需要它们,以免再次引用该表。 此类索引由关键字INCLUDE组成。

Anastasia Lubennikova(Postgres Professional)最终确定了btree方法,以便可以在索引中包含其他列。 该修补程序包含在PostgreSQL 11中。但是GiST / SP-GiST访问方法的修补程序在发布此版本之前没有时间成熟。 GiST到12日成熟。

很久以前就出现了对GiST具有包容性索引的建设性愿望:Andrey Borodin于2018年4月中旬社区提供了一个测试补丁。 他完成了所有基本的非常困难的工作。

在2019年8月上旬,Alexander Korotkov添加了外观改进并提交了补丁。

为了进行演示和研究,我们将生成一组300万个矩形。 同时,请注意几句话,因为并非所有的操作都是直观的。

框的类型-即矩形-早已存在于Postgres中,它由2个点(几何类型点)定义-矩形的相对顶点(即矩形不能倾斜,乱扔到侧面)。 我们阅读了文档 :“类型框的值以下列形式之一书写:

( ( x1 , y1 ) , ( x2 , y2 ) ) ( x1 , y1 ) , ( x2 , y2 ) x1 , y1 , x2 , y2 

在实践中,您必须像这样写:

 SELECT box('1,2', '3,4'); box ------------- (3,4),(1,2) (1 row) 

首先,Postgres向我们显示右上角,然后向左下角。 如果我们这样写

 SELECT box('5,2', '3,4'); box ------------- (5,4),(3,2) (1 row) 

那么我们将确保Postgres不会给出他们给他的峰值。 他从我们的左上和右下计算出右上和左下。 当顶点的位置事先未知时(例如在随机生成的情况下),这是一个方便的属性。 符号“ 1,2”,“ 3,4”等效于点(1,2),点(3,4)。 这种形式有时更方便。


商业:搜索300万个矩形


 CREATE TABLE boxes(id serial, thebox box, name text); 

我们将生成300万个随机矩形。 我们想要一个正态分布,但是为了不使用tablefunc扩展名,我们使用“差”的方法:我们使用random()-random(),这也给出了一张漂亮的图片(参见图)。对于矩形,矩形越大,越靠近中心。 它们的重心也是随机的。 这种分布是某些类型的真实城市数据的特征。 那些想研究统计定律或刷新记忆的人可以在这里阅读有关随机变量的区别。




 INSERT INTO boxes(thebox, name) SELECT box( point( random()-random(), random()-random() ), point( random()-random(), random()-random() ) ), 'box no.' || x FROM generate_series(1,3000000) AS g(x); 


显示\dt+的表的大小为242MB。 现在您可以开始搜索。

我们正在寻找没有索引:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------------- Gather (cost=1000.00..47853.00 rows=3000 width=46) (actual time=0.140..246.998 rows=139189 loops=1) Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on boxes (cost=0.00..46553.00 rows=1250 width=46) (actual time=0.011..106.708 rows=46396 loops=3) Filter: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Rows Removed by Filter: 953604 Planning Time: 0.040 ms Execution Time: 259.262 ms (8 rows) 

我们看到有一个并行Seq扫描-顺序扫描(尽管是并行的)。

创建一个常规的非包含索引:

 CREATE INDEX ON boxes USING gist(thebox); 

boxes_thebox_idx索引的大小,显示为\di+ 262MB。 响应相同的请求,我们得到:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on boxes (cost=159.66..9033.30 rows=3000 width=46) (actual time=29.101..80.283 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_idx (cost=0.00..158.91 rows=3000 width=0) (actual time=25.029..25.029 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 0.053 ms Execution Time: 86.206 ms (7 rows) 

搜索时间减少了三倍,他们获得了位图索引扫描,而不是并行序列扫描。 它不并行化,但是工作更快。

现在杀死旧索引并创建一个包含性的索引:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); 

boxes_thebox_name_idx boxes_thebox_name_idx索引:356MB。 出发:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on boxes (cost=207.66..9081.30 rows=3000 width=46) (actual time=86.822..152.014 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_name_idx (cost=0.00..206.91 rows=3000 width=0) (actual time=83.044..83.044 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 3.807 ms Execution Time: 157.997 ms (7 rows) 


使用了“仅索引扫描”,但图片令人遗憾:时间几乎是没有索引时的2倍。 我们在第一部分中阅读了索引创建者的手册:

‹Rang PostgreSQL索引不包含允许您判断行的可见性的信息。 因此,访问方法将返回属于搜索条件的所有行版本,无论它们是否对当前事务可见。 但是,如果索引机制每次必须查看表以确定可见性,则此扫描方法与普通索引扫描没有什么不同。 PostgreSQL支持所谓的表可见性图,该事实解决了该问题,在这种情况下,真空处理会标记页面,在这些页面中,数据的更改时间不足以使所有事务都能看到它,而与启动时间和隔离级别无关。 如果索引返回的行的标识符引用了该页面,则无法检查可见性。

我们做真空。 重复:

 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Index Only Scan using boxes_thebox_name_idx on boxes (cost=0.41..236.91 rows=3000 width=46) (actual time=0.104..38.651 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Fetches: 0 Planning Time: 0.052 ms Execution Time: 44.337 ms (5 rows) 

完全不同的事情! 与非包容性指数相比,收益是前者的两倍。


选择性和增益


包含索引的性能高度依赖于查询条件的选择性。 为了稍微研究这种依赖关系,我们将解决反问题:我们将生成一个带有类型为point的索引的标签,并查找在给定框中将要落入的点数。 将点均匀地摊开。

 CREATE TABLE test_covergist(id serial, tochka point, name text); 

 INSERT INTO test_covergist(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,3000000) AS g(x); 

该表的大小为211 MB。

 CREATE INDEX on test_covergist USING gist(tochka); 

大小213 MB。

显然,我们会将所有可用点都变成一个正方形:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1087.964..1864.059 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared read=54287 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1084.949..1084.949 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared read=27262 Planning Time: 0.102 ms Execution Time: 2029.501 ms (9 rows) 

我们要求EXPLAIN显示缓冲区。 它将派上用场。 现在请求执行时间超过2秒,可以看到Buffers:共享读取= 54287。 在另一种情况下,我们可能会看到共享读取和共享命中的混合结果,即从磁盘(或从OS缓存)中读取某些缓冲区,并从缓冲区缓存中读取某些缓冲区。 我们知道表和索引的大概大小,因此我们将通过设置共享缓冲区以使所有内容适合自己来保护自己-使用以下选项重新启动Postgres

 -o "-c shared_buffers=1GB" 

现在:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=231.032..613.326 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared hit=54248 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=228.068..228.068 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=27223 Planning Time: 0.070 ms Execution Time: 755.915 ms (9 rows) 

即,共享阅读成为共享命中,并且时间减少了三倍。

EXPLAIN中的另一个重要细节:返回了300万个点,并且预测返回的记录数为3000。Spoiler:此数字不会有任何选择性地变化。 当使用盒子或点类型时,优化器不知道如何评估基数。 而且计划不会改变:对于任何大小的矩形,在test_covergist_tochka_idx上都会有一个位图索引扫描。

这是另外两个带有已发布记录数量的度量,其数量级有所不同:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=27.889..134.054 rows=269882 loops=1) Recheck Cond: ('(300000,300000),(0,0)'::box @> tochka) Heap Blocks: exact=27024 Buffers: shared hit=29534 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=24.847..24.847 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Buffers: shared hit=2510 Planning Time: 0.074 ms Execution Time: 151.269 ms (9 rows) 

它返回的记录减少了10倍(实际...行= 269882),时间减少了大约5倍。

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','30000,30000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1.882..16.095 rows=2780 loops=1) Recheck Cond: ('(30000,30000),(0,0)'::box @> tochka) Heap Blocks: exact=2624 Buffers: shared hit=2655 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1.035..1.035 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Buffers: shared hit=31 Planning Time: 0.154 ms Execution Time: 16.702 ms (9 rows) 

30K×30K平方(2780)的内容仅在16 ms内即可计数。 而且,当有数十条记录时,它们已经在毫秒级的时间内被提取出来了,这样的测量结果并不是很可靠。

最后,使用包含索引对它们进行度量:

 CREATE INDEX on test_covergist USING gist(tochka) INCLUDE(name); 

大小316 MB。

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.160..568.707 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=40492 Planning Time: 0.090 ms Execution Time: 709.837 ms (6 rows) 

尽管仅索引扫描,但时间几乎与常规索引相同。

但是:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.083..53.277 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=3735 Planning Time: 0.077 ms Execution Time: 66.162 ms (6 rows) 

这是151毫秒。 并且,因此:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.043..0.639 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=52 Planning Time: 0.053 ms Execution Time: 0.791 ms (6 rows) 

对于相同的2780点记录,这已经是毫秒的几分之一。

像枪一样的缓冲器


您可以在尚未开枪但挂在墙上的can弹枪中寻求解释并找到原因:读取的方块数。 在包含索引的情况下,仅读取索引本身的块(堆访存:0)。 在三种情况下,分别是数字40492、3735和52。但是,使用常规索引时,读取的块包括位图堆扫描索引中读取的位的总和(54248个记录,具有300万条记录)和必须从堆中读取的位的总和(27223) ,因为无法从常规索引中提取名称字段。 54248 + 27223 = 81471。 独占是40492。对于其他两种情况:29534 + 2510 = 31044和2655 + 31 = 2686。 在使用常规索引的情况下,无论如何都会读取更多的块,但是随着选择性的提高,读取的块数量开始出现数量级差异,而​​不是2倍,这是由于来自堆的必要块数量的减少比读取索引块的下降更慢。

返回的记录总数 (千)30002702.7
读取块 (普通/含)81471/4049231044/37352686/52
时间755/710151/6616 / 0.7


但是也许关键不是选择性,而是表格的大小? 以防万一,我们重复相同的步骤,生成一个包含30万条而不是300万条记录的表:

 CREATE TABLE test_covergist_small(id serial, tochka point, name text); INSERT INTO test_covergist_small(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,300000) AS g(x); CREATE INDEX ON test_covergist_small USING gist(tochka); EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist_small WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist_small (cost=14.61..867.19 rows=300 width=31) (actual time=36.115..130.435 rows=300000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=2500 Buffers: shared hit=5225 -> Bitmap Index Scan on test_covergist_small_tochka_idx (cost=0.00..14.53 rows=300 width=0) (actual time=35.894..35.895 rows=300000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=2725 Planning Time: 0.060 ms Execution Time: 158.580 (9 rows) 

接下来,对包含索引重复相同的操作。 结果如下:

返回的记录总数 (千)300270.25
读取块 (普通/含)5225/37263026/352270/8
时间158/17820/130.4 / 0.2


在点覆盖率100%的情况下,查询甚至比使用常规索引慢一些。 此外,与300万情况一样,一切都准备就绪。 即,选择性很重要。

我们公司在真实数据上测试了包含在内的GiST索引-在莫斯科地图上包含数百万个矩形的集合。 结论是相同的:在许多情况下,此类索引明显加快了查询速度。 但是,无法用图片和测试次数来说明本文:此数据不在公共领域。

而不是结论


让我们回头再看一下随机矩形。 让我们尝试对spgist做同样的事情。 通过阅读PostgreSQL-6中的索引文章您可以回顾或了解理解SP-GiST和GiST之间的区别的含义。 创建一个包含索引:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); ERROR: access method "spgist" does not support included columns 

SP,对于SP-GiST,尚未实现包含索引。
因此,还有改进的空间!



Source: https://habr.com/ru/post/zh-CN474166/


All Articles