PostgreSQL中的索引(九)--BRIN


在之前的文章中,我們討論了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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM