代码改变世界

PostgreSQL中的索引(九)--BRIN

2020-08-14 16:42  abce  阅读(1178)  评论(0编辑  收藏  举报

在之前的文章中,我们讨论了PostgreSQL索引引擎、访问方法的接口以及以下方法:hash索引、b-tree、GiST、SP-GiST、GIN和RUM。本文的主题是BRIN(Block Range Index)。

与我们已经熟悉的索引不同,BRIN的想法是避免查找绝对不合适的行,而不是快速找到匹配的行。BRIN始终是一个不准确的索引:它根本不包含表行的tid。

简单地说,BRIN可以很好地处理值与其在表中的物理位置相关的列。 换句话说,如果没有ORDER BY子句的查询实际上以递增或递减的顺序返回列值(并且该列上没有索引)。

这种访问方法是在欧洲大型分析数据库项目轴心范围内创建的,关注的是几个tb或几十tb大的表。BRIN的一个重要特性使我们能够在这样的表上创建索引,索引比较小而且维护成本很低。

其工作原理如下。表被分割成ranges(好多个pages的大小):因此被称作block range index(BRIN)。在每个range中存储数据的摘要信息。作为规则,这里是最小值和最大值,但有时也并非如此。假设执行了一个查询,该查询包含某列的条件;如果所查找的值没有进入区间,则可以跳过整个range;但如果它们确实在,所有块中的所有行都必须被查看以从中选择匹配的行。

在BRIN索引中,PostgreSQL会为每个8k大小的存储数据页面读取所选列的最大值和最小值,然后将该信息(页码以及列的最小值和最大值)存储到BRIN索引中。

不把BRIN看作索引,而是看作顺序扫描的加速器,这并不是一个错误。 如果我们把每个range都看作是一个«虚拟»分区,那么我们可以把BRIN看作分区的替代方案。

现在让我们更详细地讨论索引的结构。

结构

第一个(更确切地说,是零)是包含元数据的页。

包含摘要信息的页位于与元数据页的某个偏移位置。这些页面上的每个索引行都包含一个range内的摘要信息。

在元数据页和摘要数据之间,是reverse range map页(缩写为«revmap»)。实际上,这是一个指向相应索引行的指针(TIDs)数组。

 

对于某些ranges,«revmap»中的指针可能指向索引行(图中有一个用灰色标记)。在这种情况下,ranges被认为还没有摘要信息。

扫描索引

如果索引不包含对表行的引用,该如何使用? 此访问方法当然不能按TID返回逐个行,但是可以构建位图。 位图页面可以有两种:精确到行的页和不精确的只到页的页。 这里使用的是不精确的位图。

该算法很简单。 ranges的map被顺序扫描(即,ranges按照其在表中的位置顺序扫描)。 指针用于确定具有每个范围的摘要信息的索引行。 如果范围不包含所寻求的值,则将其跳过,并且如果范围可以包含该值(或摘要信息不可用),则该范围的所有页面都将添加到位图中。 然后,再使用生成的位图。

更新索引

更改表时如何更新索引更有趣。

当将新版本的行添加到表的页时,我们确定该行包含在哪个范围中,并使用ranges的map去查找包含摘要信息的索引行。 所有这些都是简单的算术运算。 例如,假设range的大小为4,然后在第13页上,出现值为42的行版本。 range的数字(从零开始)是13/4 = 3,因此,在《 revmap》中,我们采用偏移量为3的指针(其顺序号为4)。

此范围的最小值为31,最大值为40。由于新值42不在间隔内,因此我们更新最大值(请参见图)。 但是,如果新值仍在存储的范围内,则无需更新索引。

 

所有这些都与页的新版本出现在可使用摘要信息的范围内的情况有关。创建索引时,将为所有可用范围计算摘要信息,但是在进一步扩展表时,可能会出现超出范围的新页面。这里有两个选项:

1.通常,索引不会立即更新。如前所述,在扫描索引时,将扫描整个范围。实际更新是在«vacuum»期间完成的,也可以通过手动调用«brin_summarize_new_values»函数完成。
2.如果我们使用«autosummarize»参数创建索引,则更新将立即完成。但是,当使用新值填充范围页时,更新可能会经常发生,因此,默认情况下此参数处于关闭状态。

当出现新的ranges时,«revmap»的大小可能会增加。每当位于元数据页和摘要数据之间的map需要由另一个页扩展时,现有行版本就会移至其他页。因此,ranges的map始终位于元数据页和摘要数据之间。

当删除一行时,…什么也没有发生。我们可以注意到,有时最小值或最大值将被删除,在这种情况下可以减小间隔。但是要检测到这一点,我们将必须读取该范围内的所有值,这是代价昂贵的操作。

索引的正确性不受影响,但是搜索可能需要查看比实际需要更多的ranges。通常,可以手动重新计算此类区域的摘要信息(通过调用«brin_desummarize_range»和«brin_summarize_new_values»函数),但是我们如何检测到这种需求?无论如何,没有常规的程序可用于此目的。

最后,更新一行只是删除过时的版本,并增加新的版本。

示例

让我们尝试为演示数据库表中的数据构建自己的小型数据仓库。 假设出于BI报告的目的,需要使用非规范化表格来反映从机场起飞或降落在机场的航班到机舱座位的准确性。 每个机场的数据每天都会在适当时区的午夜12点添加到表中。 数据将不会被更新或删除。

该表如下所示:

demo=# create table flights_bi(
  airport_code char(3),
  airport_coord point,         -- geo coordinates of airport
  airport_utc_offset interval, -- time zone
  flight_no char(6),           -- flight number
  flight_type text,           -- flight type: departure / arrival 
  scheduled_time timestamptz,  -- scheduled departure/arrival time of flight
  actual_time timestamptz,     -- actual time of flight
  aircraft_code char(3),
  seat_no varchar(4),          -- seat number
  fare_conditions varchar(10), -- travel class
  passenger_id varchar(20),
  passenger_name text
);

我们可以使用嵌套循环模拟加载数据的过程:一个按天的外部循环(我们将考虑一个大型数据库,因此是365天)和一个按时区的内部循环(从UTC+02到UTC+12)。查询很长,而且不是特别有意思。

DO $$
<<local>>
DECLARE
  curdate date := (SELECT min(scheduled_departure) FROM flights);
  utc_offset interval;
BEGIN
  WHILE (curdate <= bookings.now()::date) LOOP
    utc_offset := interval '12 hours';
    WHILE (utc_offset >= interval '2 hours') LOOP
      INSERT INTO flights_bi
        WITH flight (
          airport_code,
          airport_coord,
          flight_id,
          flight_no,
          scheduled_time,
          actual_time,
          aircraft_code,
          flight_type
        ) AS (
        -- прибытия
        SELECT a.airport_code,
               a.coordinates,
               f.flight_id,
               f.flight_no,
               f.scheduled_departure,
               f.actual_departure,
               f.aircraft_code,
               'departure'
        FROM   airports a,
               flights f,
               pg_timezone_names tzn
        WHERE  a.airport_code = f.departure_airport
        AND    f.actual_departure IS NOT NULL
        AND    tzn.name = a.timezone
        AND    tzn.utc_offset = local.utc_offset
        AND    timezone(a.timezone, f.actual_departure)::date = curdate
        UNION ALL
        -- вылеты
        SELECT a.airport_code,
               a.coordinates,
               f.flight_id,
               f.flight_no,
               f.scheduled_arrival,
               f.actual_arrival,
               f.aircraft_code,
               'arrival'
        FROM   airports a,
               flights f,
               pg_timezone_names tzn
        WHERE  a.airport_code = f.arrival_airport
        AND    f.actual_arrival IS NOT NULL
        AND    tzn.name = a.timezone
        AND    tzn.utc_offset = local.utc_offset
        AND    timezone(a.timezone, f.actual_arrival)::date = curdate
      )
      SELECT f.airport_code,
             f.airport_coord,
             local.utc_offset,
             f.flight_no,
             f.flight_type,
             f.scheduled_time,
             f.actual_time,
             f.aircraft_code,
             s.seat_no,
             s.fare_conditions,
             t.passenger_id,
             t.passenger_name
      FROM   flight f
             JOIN seats s
               ON s.aircraft_code = f.aircraft_code
             LEFT JOIN boarding_passes bp
               ON bp.flight_id = f.flight_id
              AND bp.seat_no = s.seat_no
             LEFT JOIN ticket_flights tf
               ON tf.ticket_no = bp.ticket_no
              AND tf.flight_id = bp.flight_id
             LEFT JOIN tickets t
               ON t.ticket_no = tf.ticket_no;

      RAISE NOTICE '%, %', curdate, utc_offset;
      utc_offset := utc_offset - interval '1 hour';
    END LOOP;
    curdate := curdate + 1;
  END LOOP;
END;
$$;
demo=# select count(*) from flights_bi;
  count
----------
 30517076
(1 row)

demo=# select pg_size_pretty(pg_total_relation_size('flights_bi'));
 pg_size_pretty
----------------
 4127 MB
(1 row)

我们得到3000万行和4 GB。尺寸不是很大,但对于一台笔记本电脑来说已经足够了:连续扫描花了我大约10秒。

On what columns should we create the index?

由于BRIN索引的体积较小且管理成本适中,并且很少发生更新(如果有的话)。

我们已经提到,数据必须与其物理位置有所关联。这里要记住,PostgreSQL收集表列统计信息,其中包括相关性。计划器使用此值在常规索引扫描和位图扫描之间进行选择,我们可以使用它来估计BRIN索引的适用性。

在上面的示例中,数据显然按天排序(按 «scheduled_time»和«actual_time»排序-差别不大)。这是因为将行添加到表中(没有删除和更新)时,它们在文件中一个接一个地排放。在数据加载的模拟中,我们甚至没有使用ORDER BY子句,因此,通常一天内的日期可以以任意方式混合,但是必须有序。让我们检查一下:

demo=# analyze flights_bi;

demo=# select attname, correlation from pg_stats where tablename='flights_bi' 
order by correlation desc nulls last;
      attname       | correlation
--------------------+-------------
 scheduled_time     |    0.999994
 actual_time        |    0.999994
 fare_conditions    |    0.796719
 flight_type        |    0.495937
 airport_utc_offset |    0.438443
 aircraft_code      |    0.172262
 airport_code       |   0.0543143
 flight_no          |   0.0121366
 seat_no            |  0.00568042
 passenger_name     |   0.0046387
 passenger_id       | -0.00281272
 airport_coord      |            
(12 rows)

该值不太接近零(在这种情况下,理想情况下,接近正负1),表明BRIN索引是合适的。

出差航班类别«fare_condition»(该列包含三个唯一值)和航班类型«flight_type»(两个唯一值)出乎意料地位于第二和第三位。 这是一种错觉:形式上的相关性很高,而实际上在几个连续的页面上肯定会遇到所有可能的值,这意味着BRIN不会发挥任何作用。

接下来是时区«airport_utc_offset»:在所考虑的示例中,在一天周期内,按时区«by construction»对机场进行了排序。

我们将进一步试验这两个字段(时间和时区)。

Possible weakening of the correlation

更改数据时,很容易削弱«by construction» 位置的相关性。此处的问题不是更改特定值,而是多版本并发控件的结构:过时的行版本在一页上被删除,但是只要有可用空间,就可以插入新版本。因此,整个行在更新期间会混合在一起。

我们可以通过减小«fillfactor»存储参数的值来部分控制此效果,并通过这种方式在页面上留下可用空间以供将来更新。但是,我们是否要增加已经很大的表的大小?此外,这不能解决删除问题:它们还通过释放现有页面内某处的空间来为新行«set traps»。因此,应该到达文件末尾的行将插入到任意位置。

顺便说一句,这是一个奇怪的事实。由于BRIN索引不包含对表行的引用,因此它的可用性不应阻止HOT更新,但它确实可以。

因此,BRIN主要设计用于甚至根本不更新或更新很小的大型甚至大型表。但是,它完美地应对了新行的增加(到表的末尾)。这并不奇怪,因为创建此访问方法是为了查看数据仓库和分析报告。

What size of a range do we need to select?

如果处理一个TB的表,那么在选择范围大小时,我们主要关心的可能是不要使BRIN索引太大。但是,在我们的情况下,我们可以提供更准确的数据分析能力。

为此,我们可以选择列的唯一值,并查看它们出现在多少页上。值的本地化增加了成功应用BRIN索引的机会。此外,找到的页数量将提示一个range的大小。但是,如果该值在所有页面上都有,则BRIN是无用的。

当然,我们应该使用这种技术来密切注意数据的内部结构。例如,将每个日期(更确切地说是时间戳,还包括时间)视为唯一值是没有意义的-我们需要将其舍入为天。

从技术上讲,可以通过查看隐藏的“ ctid”列的值来完成此分析,该值提供了指向行版本(TID)的指针:页的编号和页内行的编号。不幸的是,没有传统的技术可以将TID分解为两个部分,因此,我们必须通过文本表示来转换类型:

demo=# select min(numblk), round(avg(numblk)) avg, max(numblk)
from ( 
  select count(distinct (ctid::text::point)[0]) numblk
  from flights_bi
  group by scheduled_time::date
) t;
 min  | avg  | max  
------+------+------
 1192 | 1500 | 1796
(1 row)
demo=# select relpages from pg_class where relname = 'flights_bi';
 relpages
----------
   528172
(1 row)

我们可以看到,每天几乎均匀地分布在页面上,并且天彼此之间略有混合(1500&times 365 = 547500,这仅比表528172中的页面数大一点)。 无论如何,这实际上是«by construction»明确的。

此处的重要信息是特定数量的页面。 传统的range大小为128页,每天将填充9-14个range。 这似乎很现实:查询特定的一天,我们可以预期出现10%左右的错误。

我们试试吧:

demo=# create index on flights_bi using brin(scheduled_time);

索引的大小只有184 KB:

demo=# select pg_size_pretty(pg_total_relation_size('flights_bi_scheduled_time_idx'));
 pg_size_pretty
----------------
 184 kB
(1 row)

在这种情况下,以损失精度为代价增加范围的大小几乎没有意义。 但是如果需要,我们可以减小大小,相反,准确性会提高(以及索引的大小)。

现在让我们看一下时区。 在这里,我们也不能使用暴力手段。 所有值都应除以天周期数,因为每天都会重复分配。 此外,由于只有几个时区,我们可以查看整个分布:

demo=# select airport_utc_offset, count(distinct (ctid::text::point)[0])/365 numblk
from flights_bi                                    
group by airport_utc_offset
order by 2;
 airport_utc_offset | numblk
--------------------+--------
 12:00:00           |      6
 06:00:00           |      8
 02:00:00           |     10
 11:00:00           |     13
 08:00:00           |     28
 09:00:00           |     29
 10:00:00           |     40
 04:00:00           |     47
 07:00:00           |    110
 05:00:00           |    231
 03:00:00           |    932
(11 rows)

平均而言,每个时区的数据每天有133页,但分布极不均匀:Petropavlovsk-Kamchatskiy和Anadyr只有6页,而Moscow及其邻近地区需要数百页。默认的range大小在这里是不行的;例如,让我们将其设置为4个页面。

demo=# create index on flights_bi using brin(airport_utc_offset) with (pages_per_range=4);

demo=# select pg_size_pretty(pg_total_relation_size('flights_bi_airport_utc_offset_idx'));
 pg_size_pretty
----------------
 6528 kB
(1 row)

执行计划

让我们看看索引是如何工作的。让我们选择某一天,比如一周前(在演示数据库中,«today»是由«booking.now»决定的):

demo=# \set d 'bookings.now()::date - interval \'7 days\''

demo=# explain (costs off,analyze)
  select *
  from flights_bi
  where scheduled_time >= :d and scheduled_time < :d + interval '1 day';
                                  QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=10.282..94.328 rows=83954 loops=1)
   Recheck Cond: ...
   Rows Removed by Index Recheck: 12045
   Heap Blocks: lossy=1664
   ->  Bitmap Index Scan on flights_bi_scheduled_time_idx
       (actual time=3.013..3.013 rows=16640 loops=1)
         Index Cond: ...
 Planning time: 0.375 ms
 Execution time: 97.805 ms

计划器使用了创建的索引。它有多精确?满足查询条件的行数(“位图堆扫描”节点的«rows»)与使用索引返回的总行数(相同的值加上通过索引重新检查删除的行)之比告诉我们这一点。在这种情况下,为83954 /(83954 + 12045),大约为预期值的90%。

位图索引扫描节点的«actual rows»中的16640数字从何而来?问题是计划的此节点构建了不准确的(逐页)位图,并且完全不知道该位图将接触多少行,而需要显示某些内容。因此,绝望地假设一页包含10行。位图总共包含1664页(此值在«Heap Blocks: lossy=1664»中显示);因此,我们只得到16640。这是一个毫无意义的数字,我们不应该给予态度的关注。

机场呢?例如,让我们以符拉迪沃斯托克(Vladivostok)的时区为例,该时区每天填充28页:

demo=# explain (costs off,analyze)
  select *
  from flights_bi
  where airport_utc_offset = interval '8 hours';
                                   QUERY PLAN
----------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=75.151..192.210 rows=587353 loops=1)
   Recheck Cond: (airport_utc_offset = '08:00:00'::interval)
   Rows Removed by Index Recheck: 191318
   Heap Blocks: lossy=13380
   ->  Bitmap Index Scan on flights_bi_airport_utc_offset_idx
       (actual time=74.999..74.999 rows=133800 loops=1)
         Index Cond: (airport_utc_offset = '08:00:00'::interval)
 Planning time: 0.168 ms
 Execution time: 212.278 ms

计划器再次使用创建的索引。准确性较差(在本例中约为75%),但这是预期的,因为相关性较低。

几个BRIN索引(就像其他任何索引一样)当然可以在位图级别上连接。例如,以下是选定时区一个月的数据(注意«BitmapAnd»节点):

demo=# \set d 'bookings.now()::date - interval \'60 days\''

demo=# explain (costs off,analyze)
  select *
  from flights_bi
  where scheduled_time >= :d and scheduled_time < :d + interval '30 days'
    and airport_utc_offset = interval '8 hours';
                                   QUERY PLAN
---------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=62.046..113.849 rows=48154 loops=1)
   Recheck Cond: ...
   Rows Removed by Index Recheck: 18856
   Heap Blocks: lossy=1152
   ->  BitmapAnd (actual time=61.777..61.777 rows=0 loops=1)
         ->  Bitmap Index Scan on flights_bi_scheduled_time_idx
             (actual time=5.490..5.490 rows=435200 loops=1)
               Index Cond: ...
         ->  Bitmap Index Scan on flights_bi_airport_utc_offset_idx
             (actual time=55.068..55.068 rows=133800 loops=1)
               Index Cond: ...
 Planning time: 0.408 ms
 Execution time: 115.475 ms

BRIN和B-tree的比较

如果我们在与BRIN相同的字段上创建常规的B-tree索引呢?

demo=# create index flights_bi_scheduled_time_btree on flights_bi(scheduled_time);

demo=# select pg_size_pretty(pg_total_relation_size('flights_bi_scheduled_time_btree'));
 pg_size_pretty
----------------
 654 MB
(1 row)

它似乎比我们的BRIN还要大几千倍!然而,查询的执行速度要快一些:计划器使用统计数据来计算出数据是物理有序的,不需要建立位图,主要是索引条件不需要重新检查:

demo=# explain (costs off,analyze)
  select *
  from flights_bi
  where scheduled_time >= :d and scheduled_time < :d + interval '1 day';
                          QUERY PLAN
----------------------------------------------------------------
 Index Scan using flights_bi_scheduled_time_btree on flights_bi
 (actual time=0.099..79.416 rows=83954 loops=1)
   Index Cond: ...
 Planning time: 0.500 ms
 Execution time: 85.044 ms

这就是BRIN的优点:我们牺牲了效率,但却获得了很大的空间。

操作符类

## minmax

对于其值可以相互比较的数据类型,摘要信息由最小值和最大值组成。 相应的运算符类别的名称包含«minmax»,例如«date_minmax_ops»。 实际上,这些是我们到目前为止正在考虑的数据类型,并且大多数类型都是这种类型。

inclusive

比较运算符并不是为所有数据类型定义的。例如,它们不是为表示机场地理坐标的点(«point»类型)定义的。顺便说一下,正是由于这个原因,统计数据没有显示这一列的相关性。

demo=# select attname, correlation
from pg_stats
where tablename='flights_bi' and attname = 'airport_coord';
    attname    | correlation
---------------+-------------
 airport_coord |            
(1 row)

但是许多这样的类型使我们能够引入«bounding area»的概念,例如,几何形状的边界矩形。 我们详细讨论了GiST索引如何使用此功能。 同样,BRIN还可以收集具有以下数据类型的列的摘要信息:范围内所有值的边界区域仅是摘要值。

与GiST不同,BRIN的摘要数据必须与所索引的值具有相同的类型。 因此,尽管很明显坐标可以在BRIN中工作,但我们无法建立点的索引:经度与时区紧密相关。 幸运的是,在将点转换为退化的矩形后,没有任何事情会妨碍在表达式上创建索引。 同时,我们将范围的大小设置为一页,以显示极限情况:

demo=# create index on flights_bi using brin (box(airport_coord)) with (pages_per_range=1);

即使在这种极端情况下,索引的大小也只有30 MB:

demo=# select pg_size_pretty(pg_total_relation_size('flights_bi_box_idx'));
 pg_size_pretty
----------------
 30 MB
(1 row)

现在,我们可以创建通过坐标限制机场的查询。例如:

demo=# select airport_code, airport_name
from airports 
where box(coordinates) <@ box '120,40,140,50';
 airport_code |  airport_name  
--------------+-----------------
 KHV          | Khabarovsk-Novyi
 VVO          | Vladivostok
(2 rows)

然而,计划器拒绝使用索引。

demo=# analyze flights_bi;

demo=# explain select * from flights_bi
where box(airport_coord) <@ box '120,40,140,50';
                             QUERY PLAN                              
---------------------------------------------------------------------
 Seq Scan on flights_bi  (cost=0.00..985928.14 rows=30517 width=111)
   Filter: (box(airport_coord) <@ '(140,50),(120,40)'::box)

为什么?让我们禁用顺序扫描,看看会发生什么:

demo=# set enable_seqscan = off;

demo=# explain select * from flights_bi 
where box(airport_coord) <@ box '120,40,140,50';
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi  (cost=14079.67..1000007.81 rows=30517 width=111)
   Recheck Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)
   ->  Bitmap Index Scan on flights_bi_box_idx
       (cost=0.00..14072.04 rows=30517076 width=0)
         Index Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)

看起来索引是可以使用的,但是计划器假设位图必须建立在整个表上(看看位图索引扫描节点的«rows»),所以在这种情况下计划器选择顺序扫描也就不足为奇了。这里的问题是,对于几何类型,PostgreSQL不会收集任何统计数据,计划器就盲目了:

demo=# select * from pg_stats where tablename = 'flights_bi_box_idx' \gx
-[ RECORD 1 ]----------+-------------------
schemaname             | bookings
tablename              | flights_bi_box_idx
attname                | box
inherited              | f
null_frac              | 0
avg_width              | 32
n_distinct             | 0
most_common_vals       |
most_common_freqs      |
histogram_bounds       |
correlation            |
most_common_elems      |
most_common_elem_freqs | 
elem_count_histogram   |

但是对于这个索引没有什么可抱怨的——它确实有效而且运行良好:

demo=# explain (costs off,analyze)
select * from flights_bi where box(airport_coord) <@ box '120,40,140,50';
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Bitmap Heap Scan on flights_bi (actual time=158.142..315.445 rows=781790 loops=1)
   Recheck Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)
   Rows Removed by Index Recheck: 70726
   Heap Blocks: lossy=14772
   ->  Bitmap Index Scan on flights_bi_box_idx
       (actual time=158.083..158.083 rows=147720 loops=1)
         Index Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)
 Planning time: 0.137 ms
 Execution time: 340.593 ms

原理

扩展“ pageinspect”使我们能够查看BRIN索引的内部。

首先,元信息将提示我们range的大小以及«revmap»分配了多少个页:

demo=# select *
from brin_metapage_info(get_raw_page('flights_bi_scheduled_time_idx',0));
   magic    | version | pagesperrange | lastrevmappage
------------+---------+---------------+----------------
 0xA8109CFA |       1 |           128 |              3
(1 row)

此处的第1至3页分配给«revmap»,其余的则包含摘要数据。 从《 revmap》中,我们可以获得每个range的摘要数据的引用。第一个range的信息(包含前128页)位于以下位置:

demo=# select *
from brin_revmap_data(get_raw_page('flights_bi_scheduled_time_idx',1))
limit 1;
  pages  
---------
 (6,197)
(1 row)

以下是摘要数据(summary data):

demo=# select allnulls, hasnulls, value
from brin_page_items(
  get_raw_page('flights_bi_scheduled_time_idx',6),
  'flights_bi_scheduled_time_idx'
)
where itemoffset = 197;
 allnulls | hasnulls |                       value                        
----------+----------+----------------------------------------------------
 f        | f        | {2016-08-15 02:45:00+03 .. 2016-08-15 17:15:00+03}
(1 row)

下一个range:

emo=# select *
from brin_revmap_data(get_raw_page('flights_bi_scheduled_time_idx',1))
offset 1 limit 1;
  pages  
---------
 (6,198)
(1 row)
demo=# select allnulls, hasnulls, value
from brin_page_items(
  get_raw_page('flights_bi_scheduled_time_idx',6),
  'flights_bi_scheduled_time_idx'
)
where itemoffset = 198;
 allnulls | hasnulls |                       value                        
----------+----------+----------------------------------------------------
 f        | f        | {2016-08-15 06:00:00+03 .. 2016-08-15 18:55:00+03}
(1 row)

  

对于«inclusion»类,«value»字段将显示如下内容

{(94.4005966186523,69.3110961914062),(77.6600036621,51.6693992614746) .. f .. f}

第一个值是嵌入矩形,末尾的«f»字母表示缺少空元素(第一个)和缺少不可合并的值(第二个)。实际上,唯一的不可合并值是«IPv4»和«IPv6»地址(«inet»数据类型)。

属性

访问方法的属性如下:

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

可以在多个列上创建索引。在这种情况下,为每个列收集它自己的摘要统计信息,但是对每个range将它们存储在一起。当然,如果一个相同大小的range适用于所有列,则该索引是有意义的。

索引层属性如下:

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

显然,只支持位图扫描。

然而,缺乏clustering可能看起来令人困惑。从表面上看,由于BRIN索引对行的物理顺序很敏感,因此根据该索引对数据进行聚类是合乎逻辑的。但事实并非如此。我们只能创建«regular»索引(B-tree或GiST,取决于数据类型)并根据它进行集群。顺便问一下,您是否在重新构建时将排他锁、执行时间和磁盘空间消耗考虑在内,对一个可能很大的表进行集群?

列层属性如下:

        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       | t

只能对nulls进行操作。

 

原文地址:https://habr.com/en/company/postgrespro/blog/452900/