PostgreSQL中的索引—10(Bloom)

先验知识:

签名、过滤器:为一行数据设置的过滤器位数,对应于一般的Bloom过滤器

误报率:假阳性/(假阳性+真阳性),误报率越低,索引越快

在之前的文章中,我们讨论了PostgreSQL索引引擎和访问方法的接口,以及B树、GiST、SP GiST、GIN、RUM和BRIN。但我们仍然需要看看Bloom索引。

Bloom

一般概念

经典的Bloom过滤器是一种数据结构,使我们能够快速检查集合中是否有指定元素。过滤器是高度紧凑的,它允许出现假阳性:它可能错误地认为元素是集合的成员(但其实不是);但是不会出现假阴性:以为一个元素是集合的成员,但其实不是。

过滤器是一个由m位组成的数组(也称为签名),最初由零填充。选择k个不同的哈希函数,将任一元素映射到签名的k位。要向集合中添加该元素,我们需要将签名中的这k位都设置为1。因此,如果与一个元素对应的所有位都为一,则该元素可能是该集合的一个成员,但如果至少有一位等于零,则该元素肯定不在该集合中。

对于DBMS,我们实际上为每个索引行构建了N个单独的过滤器。通常,索引中包含几个字段,这些字段的值构成了每行的元素集。

通过选择签名m的长度,我们可以在索引大小和误报概率之间找到折衷。Bloom索引的应用领域是大的、相当“宽”的表,可以在每个字段上使用过滤器进行查询。与BRIN一样,这种访问方法可以被视为顺序扫描的加速器:索引找到的所有匹配项都必须与表一起重新检查,但有可能完全避免考虑大多数行。

结构

我们已经在GiST访问方法的上下文中讨论了签名树(在索引树中不存字符串,而是通过哈希函数将字符串转换为哈希值,取哈希值后几位作为该字符串的签名)。与这些树不同,Bloom索引是一个扁平结构。它由一个元页面和一个带有索引行的常规页面组成。每个索引行包含一个签名和对表行的引用(TID),如图所示。

参数值的创建和选择

创建Bloom索引时,需指定签名的总大小(“长度”),以及为索引中包含的每个字段设置的位数(“col1”-“col32”):

create index on ... using bloom(...) with (length=..., col1=..., col2=..., ...);

指定位数的方法看起来很奇怪:这些数字必须是运算符类的参数,而不是索引。问题是,目前无法对运算符类进行参数化,尽管这方面的工作正在进行中。

【该功能(CommitTest条目)最终进入PostgreSQL 13。】

我们如何选择合适的值呢?该理论指出,给定滤波器返回假阳性的概率p,最佳签名位数(签名是一个索引行包含一个)可以估计为m=−nlog2p/ln 2,其中n是索引中的字段数,要设置的位数(这个是整个索引的大小)为k=−log2p。

签名以两字节整数数组的形式存储在索引中,因此m的值可以安全地四舍五入到16。

在选择概率p时,我们需要考虑索引的大小,它大约等于(m/8+6)N,其中N是表中的行数,6是以字节为单位的TID指针的大小。

需要注意的几点:

  • 误报概率p与一个过滤器有关,因此,我们希望在表扫描期间得到Np误报(当然,对于返回多行的查询)。例如,对于一个有一百万行且概率为0.01的表,在查询计划中,平均而言,我们可以预期通过索引复查删除的行为10000。
  • Bloom过滤器是一种概率结构。只有当平均相当多的值时,谈论特定的数字才有意义,而在每个特定的情况下,我们可以得到我们能想到的任何东西。
  • 上述估算基于一个理想化的数学模型和一些假设。实际上,结果可能更糟。所以,不要高估公式:它们只是为未来实验选择初始值的一种手段。
  • 对于每个字段,访问方法使我们能够选择要设置的位数。有一个合理的假设,即实际的最佳数量取决于列中值的分布。要深入研究,你可以阅读这篇文章(欢迎参考其他研究)。但请先重读上一条。

更新

在表中插入新行时,会创建一个签名:对于所有索引字段的值,其所有对应位都设置为1。理论上,我们必须有k个不同的散列函数,而实际上,伪随机数生成器就足够了,它的种子每次都是通过唯一的散列函数来选择的。

常规Bloom筛选器不支持删除元素,但Bloom索引不需要这样做:删除表行时,整个签名以及索引行都会被删除。

通常,更新包括删除过时的行版本和插入新版本(重新计算签名)。

扫描

由于Bloom过滤器唯一能做的就是检查集合中元素的成员身份,Bloom 索引支持的唯一操作就是相等性检查(就像在hash index中一样)。

正如我们已经提到的,Bloom索引是扁平的,所以在索引访问过程中,它总是连续地、完整地读取。在读取过程中,生成一个位图,然后使用该位图访问表。

在常规索引访问中,假设需要读取的索引行很少,而且很快就会再次需要它们,因此,它们存储在缓冲区缓存中。然而,读取Bloom索引实际上是一次顺序扫描。为了防止从缓存中逐出有用的信息,读取是通过一个小缓冲环完成的,与表的顺序扫描完全相同。

我们应该考虑到,Bloom索引的规模越大,它对规划器的吸引力就越小。这种依赖关系是线性的,与树型索引不同。

示例

让我们以前一篇文章中的一个大“flights_bi”表为例来看看Bloom索引。提醒一下,这个表的大小是4GB,大约有3000万行。表的定义:

demo=# \d flights_bi

                          Table "bookings.flights_bi"
       Column       |           Type           | Collation | Nullable | Default 
--------------------+--------------------------+-----------+----------+---------
 airport_code       | character(3)             |           |          | 
 airport_coord      | point                    |           |          | 
 airport_utc_offset | interval                 |           |          | 
 flight_no          | character(6)             |           |          | 
 flight_type        | text                     |           |          | 
 scheduled_time     | timestamp with time zone |           |          | 
 actual_time        | timestamp with time zone |           |          | 
 aircraft_code      | character(3)             |           |          | 
 seat_no            | character varying(4)     |           |          | 
 fare_conditions    | character varying(10)    |           |          | 
 passenger_id       | character varying(20)    |           |          | 
 passenger_name     | text                     |           |          | 

让我们首先创建扩展:尽管从9.6版开始的标准交付中包含Bloom索引,但它在默认情况下不可用。

demo=# create extension bloom;

上次我们可以使用BRIN对三个字段进行索引(“计划时间”、“实际时间”、“机场utc偏移”)。由于Bloom索引不依赖于数据的物理顺序,所以让我们尝试在索引中包含表的几乎所有字段。但是,让我们排除时间字段(“scheduled_time”和“actual_time”):该方法只支持相等的比较,但对精确时间的查询几乎没有人感兴趣(然而,我们可以在表达式上建立索引,将时间四舍五入到一天,但我们不会这样做)。我们还必须排除机场的地理坐标(“机场坐标”):展望未来,“点”类型不受支持。

要选择参数值,让我们将假阳性概率设置为0.01(记住,实际上我们会得到更高的假阳性概率)。将n=9,N=30000000带入上述公式得出签名大小为96位,并建议每个元素设置7位(就是每行都会创建一个96位的过滤器,而该行的每个字段将改变其中7位)。该索引的估计大小为515MB(约为表的八分之一)。

(由于最小签名大小为16位,这些公式得出的索引大小(比实际的)要小两倍,但可信度只有0.5,这非常糟糕。)

操作符类

所以,让我们尝试着创建索引。

demo=# create index flights_bi_bloom on flights_bi
using bloom(airport_code, airport_utc_offset, flight_no, flight_type, aircraft_code, seat_no, fare_conditions, passenger_id, passenger_name)
with (length=96, col1=7, col2=7, col3=7, col4=7, col5=7, col6=7, col7=7, col8=7, col9=7);

ERROR:  data type character has no default operator class for access method "bloom"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

不幸的是,该扩展只为我们提供了两个操作符类:

demo=# select opcname, opcintype::regtype
from pg_opclass
where opcmethod = (select oid from pg_am where amname = 'bloom')
order by opcintype::regtype::text;

 opcname  | opcintype
----------+-----------
 int4_ops | integer
 text_ops | text
(2 rows)

但幸运的是,为其他数据类型创建类似的类也很容易。Bloom访问方法的运算符类必须只包含一个运算符(相等)和一个辅助函数(哈希)。查找任意类型所需运算符和函数的最简单方法是查看“hash”方法的运算符类的系统目录:

demo=# select distinct
       opc.opcintype::regtype::text,
       amop.amopopr::regoperator,
       ampr.amproc
  from pg_am am, pg_opclass opc, pg_amop amop, pg_amproc ampr
 where am.amname = 'hash'
   and opc.opcmethod = am.oid
   and amop.amopfamily = opc.opcfamily
   and amop.amoplefttype = opc.opcintype
   and amop.amoprighttype = opc.opcintype
   and ampr.amprocfamily = opc.opcfamily
   and ampr.amproclefttype = opc.opcintype
order by opc.opcintype::regtype::text;

 opcintype |       amopopr        |    amproc    
-----------+----------------------+--------------
 abstime   | =(abstime,abstime)   | hashint4
 aclitem   | =(aclitem,aclitem)   | hash_aclitem
 anyarray  | =(anyarray,anyarray) | hash_array
 anyenum   | =(anyenum,anyenum)   | hashenum
 anyrange  | =(anyrange,anyrange) | hash_range
 ……

我们将使用以下信息创建两个缺少的类(为character与interval创建需要的操作符类):

demo=# CREATE OPERATOR CLASS character_ops
DEFAULT FOR TYPE character USING bloom AS
  OPERATOR  1  =(character,character),
  FUNCTION  1  hashbpchar;
demo=# CREATE OPERATOR CLASS interval_ops
DEFAULT FOR TYPE interval USING bloom AS
  OPERATOR  1  =(interval,interval),
  FUNCTION  1  interval_hash;

没有为点(“点”类型)定义的哈希函数,正是因为这个原因,我们不能在这样的字段上构建Bloom索引(就像我们不能在这种类型的字段上执行散列连接一样)。

再试一次:

demo=# create index flights_bi_bloom on flights_bi
using bloom(airport_code, airport_utc_offset, flight_no, flight_type, aircraft_code, seat_no, fare_conditions, passenger_id, passenger_name)
with (length=96, col1=7, col2=7, col3=7, col4=7, col5=7, col6=7, col7=7, col8=7, col9=7);

CREATE INDEX

索引的大小为526MB,略大于预期。这是因为公式没有考虑页面开销。

demo=# select pg_size_pretty(pg_total_relation_size('flights_bi_bloom'));
 pg_size_pretty
----------------
 526 MB
(1 row)

查询

我们现在可以使用各种条件执行搜索,索引将支持它。

正如我们已经提到的,Bloom过滤器是一种概率结构,因此,效率在很大程度上取决于每个特定情况(如果有,则需要查很久才知道在哪,如果没有,则很快知道没有,而且与散列出来的位的情况有关,取决于冲突多不多,而冲突多不多又与索引的签名(即过滤器)位数以及每个元素设置的位数有关)。例如,让我们看看与两名乘客有关的行,Miroslav Sidorov:

demo=# explain(costs off,analyze)
select * from flights_bi where passenger_name='MIROSLAV SIDOROV';

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=2639.010..3010.692 rows=2 loops=1)
   Recheck Cond: (passenger_name = 'MIROSLAV SIDOROV'::text)
   Rows Removed by Index Recheck: 38562
   Heap Blocks: exact=21726
   ->  Bitmap Index Scan on flights_bi_bloom (actual time=1065.191..1065.191 rows=38564 loops=1)
         Index Cond: (passenger_name = 'MIROSLAV SIDOROV'::text)
 Planning time: 0.109 ms
 Execution time: 3010.736 ms

 Marfa Soloveva:

demo=# explain(costs off,analyze)
select * from flights_bi where passenger_name='MARFA SOLOVEVA';

                                            QUERY PLAN
---------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=9980.884..10142.684 rows=2 loops=1)
   Recheck Cond: (passenger_name = 'MARFA SOLOVEVA'::text)
   Rows Removed by Index Recheck: 3950168
   Heap Blocks: exact=45757 lossy=67332
   ->  Bitmap Index Scan on flights_bi_bloom (actual time=1037.588..1037.588 rows=212972 loops=1)
         Index Cond: (passenger_name = 'MARFA SOLOVEVA'::text)
 Planning time: 0.157 ms
 Execution time: 10142.730 ms

在一种情况下,该过滤器只允许4万个误报,而在另一种情况下(通过索引重新检查删除的行)最多允许400万个误报。查询的执行时间相应不同。

下面是按乘客ID而不是姓名搜索同一行的结果。Miroslav:

demo=# explain(costs off,analyze)
demo-# select * from flights_bi where passenger_id='5864 006033';
                                           QUERY PLAN
-------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=13747.305..16907.387 rows=2 loops=1)
   Recheck Cond: ((passenger_id)::text = '5864 006033'::text)
   Rows Removed by Index Recheck: 9620258
   Heap Blocks: exact=50510 lossy=165722
   ->  Bitmap Index Scan on flights_bi_bloom (actual time=937.202..937.202 rows=426474 loops=1)
         Index Cond: ((passenger_id)::text = '5864 006033'::text)
 Planning time: 0.110 ms
 Execution time: 16907.423 ms

Marfa:

demo=# explain(costs off,analyze)
select * from flights_bi where passenger_id='2461 559238';


                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=3881.615..3934.481 rows=2 loops=1)
   Recheck Cond: ((passenger_id)::text = '2461 559238'::text)
   Rows Removed by Index Recheck: 30669
   Heap Blocks: exact=27513
   ->  Bitmap Index Scan on flights_bi_bloom (actual time=1084.391..1084.391 rows=30671 loops=1)
         Index Cond: ((passenger_id)::text = '2461 559238'::text)
 Planning time: 0.120 ms
 Execution time: 3934.517 ms

效率又大不相同了,这一次玛法更幸运了。

请注意,由于假阳性概率p变为p的二次方,同时通过两个字段进行搜索将更加有效:

demo=# explain(costs off,analyze)
select * from flights_bi
where passenger_name='MIROSLAV SIDOROV'
  and passenger_id='5864 006033';
                                                       QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=872.593..877.915 rows=2 loops=1)
   Recheck Cond: (((passenger_id)::text = '5864 006033'::text)
               AND (passenger_name = 'MIROSLAV SIDOROV'::text))
   Rows Removed by Index Recheck: 357
   Heap Blocks: exact=356
   ->  Bitmap Index Scan on flights_bi_bloom (actual time=832.041..832.041 rows=359 loops=1)
         Index Cond: (((passenger_id)::text = '5864 006033'::text)
                   AND (passenger_name = 'MIROSLAV SIDOROV'::text))
 Planning time: 0.524 ms
 Execution time: 877.967 ms

但是,根本不支持使用布尔“或”进行搜索;这是规划器的限制,而不是访问方法的限制。当然,仍然可以选择读取两次索引,构建两个位图,然后将它们连接起来,但这很可能太昂贵,(导致)无法选择此计划。

与BRIN和Hash的比较

Bloom索引和BRIN索引的应用领域明显交叉。对于这些大型表格,需要确保按不同字段进行搜索,搜索精度被牺牲在紧凑性上。

BRIN索引更紧凑(例如,在我们的示例中最多可达几十兆字节),可以支持按范围搜索,但在文件中数据的物理排序方面有很大的限制。Bloom索引更大(数百兆字节),但除了需要合适的哈希函数之外没有任何限制。

与Bloom索引一样,Hash索引支持唯一的相等性检查操作。Hash索引确保了Bloom无法实现的搜索精度,但索引大小要大得多(在我们的示例中,在一个字段上创建的索引有1GB,不能在多个字段上创建哈希索引)。

属性

像往常一样,让我们看看Bloom的属性。

以下是访问方法的属性:

 amname |     name      | pg_indexam_has_property
--------+---------------+-------------------------
 bloom  | can_order     | f
 bloom  | can_unique    | f
 bloom  | can_multi_col | t
 bloom  | can_exclude   | f

显然,访问方法使我们能够在多个列上建立索引。在一列上创建Bloom索引几乎没有意义。

以下是索引层的特性:

     name      | pg_index_has_property
---------------+-----------------------
 clusterable   | f
 index_scan    | f
 bitmap_scan   | t
 backward_scan | f

唯一可用的扫描技术是位图扫描。由于索引总是被完全扫描,所以实现逐TID返回行的常规索引访问是没有意义的。

        name        | pg_index_column_has_property 
--------------------+------------------------------
 asc                | f
 desc               | f
 nulls_first        | f
 nulls_last         | f
 orderable          | f
 distance_orderable | f
 returnable         | f
 search_array       | f
 search_nulls       | f

这里只有false;该方法甚至不能处理空值。

最后

未来,当新的指数类型出现时,这一系列文章可能将继续下去,但现在是时候停止了。

我要感谢我的博士后专业团队的同事们(其中一些人是所讨论的许多访问方法的作者),他们阅读了草稿并提供了他们的意见。当然,我也非常感谢你们的耐心和宝贵的评论。你的注意力鼓励我达到这一点。非常感谢。

 (这系列的文章翻译完了!)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值