带你认识PostgreSQL检索神器——Brin Index

905 阅读4分钟

Greenplum是一款强大而稳定的企业级分布式数据库。虽然基于 PostgreSQL,但Greenplum针对大数据的场景和用户对性能的极致追求开发了大量的特性和做了极致甚至苛刻的优化。此外,Greenplum紧密拥抱Postgres社区,以敏捷的方式快速升级Postgres内核。在Postgres 9.5的内核中,Postgres引入了一种全新的索引类型,名为Brin Index,本文将详细介绍Brin Index的内部实现以及性能表现。

01 什么是Brin Index

Brin全称Block Range Indexes,顾名思义即数据块范围的索引,它的设计初衷是为了解决当数据表极其庞大时的迅速扫描问题。

众所周知,Heap表以页面为单位进行组织,所以表的扫描也是以页面为单位。Brin索引的基本思想就是在索引中记录一组连续页面中字段值的大致统计信息,例如连续页面里某字段的最大值和最小值,页面扫描的时候根据Brin统计信息和查询条件直接跳过明显不符合查询条件的页面,从而达到快速扫描的效果。

在实际的查询计划中,位图扫描基于Brin索引完成整个扫描过程,如下所示:

gpadmin=# explain select * from t1 where c1 > 10 and c1 < 100;
-----------QUERY PLAN--------------
Gather Motion 3:1  (slice1; segments: 3)  (cost=400.00..404.04 rows=1 width=64)
  ->  Bitmap Heap Scan on t1  (cost=400.00..404.02 rows=1 width=64)
     Recheck Cond: ((c1 > 10) AND (c1 < 100))
        ->  Bitmap Index Scan on t1_idx_1  (cost=0.00..400.00 rows=1 width=0)
            Index Cond: ((c1 > 10) AND (c1 < 100))

第一部分,Bitmap Index Scan使用Brin索引和查询条件构造位图,记录有效页面并过滤掉绝大多数无用的页面,第二部分,Bitmap Heap Scan基于位图精准扫描有效的页面并使用查询条件对页面内元组进行二次过滤。实际上,在Brin索引出现之前,位图扫描通常使用Btree索引来构造位图,关于位图扫描的详细介绍,读者可以参考我们的另外一篇文章Greenplum执行器位图——让查询更有效

02 Heap表的Brin索引实现

了解Brin索引实现的关键在于认识Brin索引文件的布局,Brin索引文件同样以页为单位进行存储,其布局如下图所示:

从图中可以看到,Brin索引文件中的页面有多种类型,其中比较重要的是Revmap页和Regular页,Revmap页中存放这一个叫做页面范围映射的结构(简称revmap),其中的每一条记录都保存了 <页面范围ID, Regular Page Tuple ID>的映射关系。

一个页面范围单位由pages_per_range参数来决定,默认是128个连续页面为一个页面范围单位,用户在创建Brin索引的时候可以指定一个页面范围的大小,例如:

create index idx_t1_id on t1 using brin (id) with (pages_per_range=64);

Regular Page中存放Brin索引元组,不同于Btree索引,Brin索引元组中保存着一个页面范围单位对应的统计信息,例如Min/Max,显然通过Revmap结构就能得到一个页面范围及其对应的统计信息。

当一个页面范围内的Heap页面添加了新的tuple并且新的tuple的值打破了该页面范围原来的统计信息边界,我们需要更新相应的Brin索引元组。当一个tuple从一个Heap页面删除时,我们可以不更新其对应的Brin索引,这样会造成统计信息没有及时更新,但是正确性是没有问题的,在Vacuum的时候,Vaccum会重新统计各个页面范围的边界,让索引信息更加的准确。

当Bitmap Index Scan构建位图的时候,我们对revmap进行顺序扫描,如果查询条件满足一个页面范围的统计边界条件,那么该页面范围内的所有页面都会用于构建位图,否则的话该页面范围内的所有页面将会被跳过。

03 Brin索引的性能表现

该部分我们对比一下Brin索引和Btree索引。

首先创建一张表并插入数据

create table t1 (c1 int, c2 text, c3 text);
insert into t1 select i, 'ssfesregeugruehpeoghregygeye', 'frheuogygryeosihfuiewhurheuhre' from generate_series(1, 10000000) i;
gpadmin=# \d+
              List of relations
Schema | Name | Type  |  Owner  | Storage |  Size  | Description
--------+------+-------+---------+---------+--------+-------------
 public | t1   | table | gpadmin | heap    | 881 MB |
(1 row)

可以看到t1表的大小大约在900MB左右。

同时在t1上创建Brin索引和Btree索引。

create index t1_idx_1 on t1 using brin(c1);
create index t1_idx_2 on t1 using btree(c1);
gpadmin=# \di+
List of relations
Schema | Name | Type | Owner | Table | Size | Description
--------+----------+-------+---------+-------+--------+-------------
public | t1_idx_1 | index | gpadmin | t1  | 768 kB |
public | t1_idx_2 | index | gpadmin | t1  | 213 MB |

可以看到,Brin索引的大小仅为768KB,而Btree索引的大小为213MB,显然在索引大小上,Brin索引有较大的优势,这也是Brin索引最大的一个特点,尤其当数据表及其大的时候,Brin索引的大小优势会显得极其吸引人。

同时,Brin索引在查询性能方面也有不错的提升。

当不使用索引的情况下,下面查询耗时4080ms。

gpadmin=# select count(*) from t1 where c1 > 10 and c1 < 1000;
count
-------
 989
 (1 row)
 
 Time: 4080.735 ms

当使用Brin索引时,同样查询耗时58ms。

gpadmin=# select count(*) from t1 where c1 > 10 and c1 < 1000;
count
-------
989
(1 row)
​
Time: 58.739 ms

当使用Btree索引时,同样的查询耗时4ms。

gpadmin=# select count(*) from t1 where c1 > 10 and c1 < 1000;
count
-------
989
(1 row)
Time: 3.738 ms

考虑到Brin索引是一个范围索引,其具有一定的稀疏性,索引的精度在一些情况下不如Btree索引,上面的性能差距可以理解。虽然如此,我们也看到Brin索引相对于顺序扫描还是取得了巨大的性能提升,这也体现了Brin索引的价值。

04 结语

Brin索引作为一个新的索引类型,具有极具优势的磁盘空间表现以及优秀的查询性能表现,同时还具有比较好的索引更新性能,这些特性十分符合超大型数据表的要求,在现实应用中被大家广泛的使用,本文简单介绍了Brin索引及其实现,希望对大家理解索引有所帮助。