1. 程式人生 > 實用技巧 >PostgreSQL中的索引(四) --Btree

PostgreSQL中的索引(四) --Btree

我們已經討論了PostgreSQL的索引引擎和訪問方法的介面,以及雜湊索引。現在我們將考慮b樹,最傳統和最廣泛使用的索引。本文篇幅很大,請耐心等待。

Btree的結構

B-tree索引型別,以«btree»訪問方法實現的,適合於可排序的資料。換句話說,必須為資料型別定義«greater»、«greater or equal»、«less»、«less or equal»和«equal»操作符。注意,相同的資料有時可能排序不同,這又回到了操作符家族的概念。

b-樹的索引行被打包到頁中。在葉子頁中,這些行包含要索引的資料(鍵)和對錶行的引用(tid)。在內部頁中,每一行引用索引的一個子頁,幷包含該頁中的最小值。

B樹有一些重要的特徵:

·B-樹是平衡的,即每個葉子頁與根頁之間由相同數量的內部頁分隔。因此,搜尋任何值都需要相同的時間。

·B-樹是多分支的,也就是說,每個頁(通常為8KB)包含很多(數百個)tid。因此,b-樹的深度非常小,對於非常大的表,實際上可以達到4-5。

·索引中的資料按非降序排序(頁之間和每個頁內部都是如此),同級別頁通過雙向列表彼此連線。因此,我們可以通過向一個或另一個方向遍歷列表來獲得有序資料集,而不必每次都返回到根。

下面是一個簡化的示例,說明在一個具有整型鍵的欄位上建立索引。

索引的第一頁是元資料頁,它引用索引根。內部節點位於根的下面,葉子頁位於最下面一行。向下的箭頭表示葉子節點對錶行(tid)的引用。

等價檢索

讓我們考慮根據條件“indexed-field = expression”在樹中搜索一個值。比如說,我們對49的鍵感興趣。

搜尋從根節點開始,我們需要確定要向下搜尋哪個子節點。通過了解根節點(4、32、64)中的鍵,我們可以計算出子節點中的值範圍。因為32≤49 < 64,我們需要下降到第二個子節點。接下來,遞迴地重複相同的過程,直到我們到達一個可以從中獲得所需TIDs的葉節點。

實際上,一些特殊情況使這個看似簡單的過程變得複雜。例如,索引可以包含非唯一鍵,並且可能有許多相等的值,以至於不能容納在一個頁。回到我們的示例,似乎應該從內部節點的引用向下延伸到值49。但是,從圖中可以清楚地看出,這樣我們將跳過前面頁中的一個«49»鍵。因此,一旦我們在一個內部頁面中找到了一個完全相等的鍵,我們就必須往左下降一個位置,然後從左到右檢視底層的索引行來搜尋所查詢的鍵。

(另一個複雜的問題是,在搜尋過程中,其他程序可以更改資料:可以重新構建樹,可能將頁一分為二,等等。所有的演算法都是為這些併發操作而設計的,在任何可能的情況下都不會相互干擾,也不會導致額外的鎖。但我們將避免對此進行詳述。)

不等價檢索

當按條件“indexed-field≤expression”(或“indexed-field≥expression”)進行搜尋時,首先按相等條件“indexed-field = expression”在索引中找到一個值(如果有),然後按適當的方向遍歷頁頁,直到結束。

n≤35時的過程如圖所示:

«greater»和«less»操作符以類似的方式被支援,除了最初找到的值必須被剔除。

範圍檢索

當按照“expression1≤indexed-field≤expression2”的範圍進行搜尋時,根據條件“indexed-field = expression1”找到一個值,當滿足條件“indexed-field≤expression2”時,繼續遍歷頁;反之亦然:從第二個表達開始,向相反的方向走,直到我們到達第一個表達。

條件23≤n≤64時的過程如圖:

示例

讓我們看一個查詢計劃的示例。與往常一樣,我們使用演示資料庫,這一次我們將考慮aircraft表。它只包含9行,計劃器將選擇不使用索引,因為整個表只在一個頁中。

demo=# select * from aircrafts;
 aircraft_code |        model        | range 
---------------+---------------------+-------
 773           | Boeing 777-300      | 11100
 763           | Boeing 767-300      |  7900
 SU9           | Sukhoi SuperJet-100 |  3000
 320           | Airbus A320-200     |  5700
 321           | Airbus A321-200     |  5600
 319           | Airbus A319-100     |  6700
 733           | Boeing 737-300      |  4200
 CN1           | Cessna 208 Caravan  |  1200
 CR2           | Bombardier CRJ-200  |  2700
(9 rows)
demo=# create index on aircrafts(range);

demo=# set enable_seqscan = off;

索引建立預設就是btree索引。

使用等價檢索:

demo=# explain(costs off) select * from aircrafts where range = 3000;
                    QUERY PLAN                     
---------------------------------------------------
 Index Scan using aircrafts_range_idx on aircrafts
   Index Cond: (range = 3000)
(2 rows)

不等價檢索:

demo=# explain(costs off) select * from aircrafts where range < 3000;
                    QUERY PLAN                    
---------------------------------------------------
 Index Scan using aircrafts_range_idx on aircrafts
   Index Cond: (range < 3000) 
(2 rows)

根據範圍查詢:

demo=# explain(costs off) select * from aircrafts
where range between 3000 and 5000;
                     QUERY PLAN                      
-----------------------------------------------------
 Index Scan using aircrafts_range_idx on aircrafts
   Index Cond: ((range >= 3000) AND (range <= 5000))
(2 rows)

排序

讓我們再次強調一點,對於任何型別的掃描(索引、僅索引或點陣圖),«btree»訪問方法都返回有序的資料,我們可以在上面的圖中清楚地看到這一點。

因此,如果一個表在排序條件下有一個索引,那麼優化器將同時考慮兩種選項:表的索引掃描(它可以隨時返回排序後的資料)和表的順序掃描(隨後對結果進行排序)。

排序順序

在建立索引時,我們可以顯式地指定排序順序。例如,我們可以通過以下方式根據飛行範圍建立索引:

demo=# create index on aircrafts(range desc);

在這種情況下,較大的值將出現在左邊的樹中,較小的值將出現在右邊。如果我們可以在任意方向遍歷索引值,為什麼還需要這樣做呢?

其目的是建立多列索引。讓我們建立一個檢視來顯示飛機模型與傳統的劃分為短,中,和遠端飛機:

demo=# create view aircrafts_v as
select model,
       case
           when range < 4000 then 1
           when range < 10000 then 2
           else 3
       end as class
from aircrafts;

demo=# select * from aircrafts_v;
        model        | class
---------------------+-------
 Boeing 777-300      |     3
 Boeing 767-300      |     2
 Sukhoi SuperJet-100 |     1
 Airbus A320-200     |     2
 Airbus A321-200     |     2
 Airbus A319-100     |     2
 Boeing 737-300      |     2
 Cessna 208 Caravan  |     1
 Bombardier CRJ-200  |     1
(9 rows)

讓我們建立一個索引(使用表示式):

demo=# create index on aircrafts(
  (case when range < 4000 then 1 when range < 10000 then 2 else 3 end),
  model);

現在我們可以使用這個索引來獲得資料按兩列升序排序:

demo=# select class, model from aircrafts_v order by class, model;
 class |        model        
-------+---------------------
     1 | Bombardier CRJ-200
     1 | Cessna 208 Caravan
     1 | Sukhoi SuperJet-100
     2 | Airbus A319-100
     2 | Airbus A320-200
     2 | Airbus A321-200
     2 | Boeing 737-300
     2 | Boeing 767-300
     3 | Boeing 777-300
(9 rows)
demo=# explain(costs off)
select class, model from aircrafts_v order by class, model;
                       QUERY PLAN                       
--------------------------------------------------------
 Index Scan using aircrafts_case_model_idx on aircrafts
(1 row)

同樣的,我們可以執行查詢來對資料進行降序排序:

demo=# select class, model from aircrafts_v order by class desc, model desc;
 class |        model        
-------+---------------------
     3 | Boeing 777-300
     2 | Boeing 767-300
     2 | Boeing 737-300
     2 | Airbus A321-200
     2 | Airbus A320-200
     2 | Airbus A319-100
     1 | Sukhoi SuperJet-100
     1 | Cessna 208 Caravan
     1 | Bombardier CRJ-200
(9 rows)
demo=# explain(costs off)
select class, model from aircrafts_v order by class desc, model desc;
                           QUERY PLAN                            
-----------------------------------------------------------------
 Index Scan BACKWARD using aircrafts_case_model_idx on aircrafts
(1 row)

但是,我們不能使用這個索引來獲得按一列降序排序、按另一列升序排序的資料。這將需要分別排序:

demo=# explain(costs off)
select class, model from aircrafts_v order by class ASC, model DESC;
                   QUERY PLAN                    
-------------------------------------------------
 Sort
   Sort Key: (CASE ... END), aircrafts.model DESC
   ->  Seq Scan on aircrafts
(3 rows)

注意,作為最後一種手段,計劃器選擇了順序掃描,而不考慮之前設定的«enable_seqscan = off»。這是因為實際上該設定並沒有禁止表掃描,而只是設定了其cost設定的很大——請檢視帶有«costs on»的計劃。

為了使這個查詢使用索引,後者必須建立所需的排序方向:

demo=# create index aircrafts_case_asc_model_desc_idx on aircrafts(
 (case
    when range < 4000 then 1
    when range < 10000 then 2
    else 3
  end) ASC,
  model DESC);

demo=# explain(costs off)
select class, model from aircrafts_v order by class ASC, model DESC;
                           QUERY PLAN                            
-----------------------------------------------------------------
 Index Scan using aircrafts_case_asc_model_desc_idx on aircrafts
(1 row)

列的順序

使用多列索引時出現的另一個問題是索引中列出列的順序。對於B-tree,這個順序非常重要:頁內的資料將按第一個欄位排序,然後按第二個欄位排序,依此類推。

我們可以用符號的方式表示我們在範圍區間和模型上建立的索引:

實際上,這樣一個小索引肯定能在一個根頁中。在圖中,為了清晰起見,它被特意分佈在幾頁中。

從這個圖表中可以清楚地看出,通過諸如«class = 3»(僅通過第一個欄位進行搜尋)或«class = 3和model = 'Boeing 777-300'»(通過兩個欄位進行搜尋)這樣的謂詞進行搜尋將會非常有效。

然而,根據謂詞«model = 'Boeing 777-300'»進行搜尋的效率會低得多:從根節點開始,我們無法確定要向下搜尋到哪個子節點,因此,我們將不得不向下搜尋所有子節點。這並不意味著像這樣的索引永遠不能使用——它的效率是一個問題。例如,如果我們有三個級別的飛機,每個級別有很多模型,我們將不得不瀏覽索引的大約三分之一,這可能比全表掃描更有效率……或者低效。

但是,如果我們建立一個這樣的索引:

demo=# create index on aircrafts(
  model,
  (case when range < 4000 then 1 when range < 10000 then 2 else 3 end));

欄位的順序將改變:

有了這個索引,根據謂詞«model = 'Boeing 777-300'»進行搜尋將會有效,但是根據謂詞«class = 3»進行搜尋則不會有效。

NULL值

btree訪問方法會索引空值,並支援按條件is null和is not null進行搜尋。

讓我們考慮航班表,其中null發生的情況:

demo=# create index on flights(actual_arrival);

demo=# explain(costs off) select * from flights where actual_arrival is null;
                      QUERY PLAN                       
-------------------------------------------------------
 Bitmap Heap Scan on flights
   Recheck Cond: (actual_arrival IS NULL)
   ->  Bitmap Index Scan on flights_actual_arrival_idx
         Index Cond: (actual_arrival IS NULL)
(4 rows)

null值位於葉節點的一端或另一端,具體取決於建立索引的方式(null first,或null last)。如果查詢包含排序,這一點很重要:如果SELECT命令在其order BY子句中指定的null值順序與為構建索引指定的順序相同(先為空或後為空),則可以使用索引。

在下面的例子中,這些順序是相同的,因此,我們可以使用索引:

demo=# explain(costs off)
select * from flights order by actual_arrival NULLS LAST;
                       QUERY PLAN                      
--------------------------------------------------------
 Index Scan using flights_actual_arrival_idx on flights
(1 row)

而這裡這些順序是不同的,優化器選擇順序掃描與後續排序:

demo=# explain(costs off)
select * from flights order by actual_arrival NULLS FIRST;
               QUERY PLAN              
----------------------------------------
 Sort
   Sort Key: actual_arrival NULLS FIRST
   ->  Seq Scan on flights
(3 rows)

要使用索引,它必須在開始處設定null值:

demo=# create index flights_nulls_first_idx on flights(actual_arrival NULLS FIRST);

demo=# explain(costs off)
select * from flights order by actual_arrival NULLS FIRST;
                     QUERY PLAN                      
-----------------------------------------------------
 Index Scan using flights_nulls_first_idx on flights
(1 row)

這樣的問題肯定是由於nulls無法排序造成的,也就是說,NULL和其他值的比較結果是未定義的:

demo=# \pset null NULL

demo=# select null < 42;
 ?column?
----------
 NULL
(1 row)

這與b-樹的概念背道而馳,也不適合一般的模式。然而,null在資料庫中扮演著如此重要的角色,以至於我們總是不得不為它們設定例外。

因為可以對null進行索引,所以即使在表上不施加任何條件,也可以使用索引(因為索引肯定包含表中所有行上的資訊)。如果查詢需要資料排序,並且索引確保所需的順序,那麼這樣做是有意義的。在這種情況下,計劃器可以選擇索引訪問來節省單獨排序。

屬性

讓我們看看«btree»訪問方法的屬性(已經提供了查詢)。

postgres=# select a.amname, p.name, pg_indexam_has_property(a.oid,p.name)
from pg_am a,
     unnest(array['can_order','can_unique','can_multi_col','can_exclude']) p(name)
where a.amname = 'btree'
order by a.amname;
 amname |     name      | pg_indexam_has_property
--------+---------------+-------------------------
 btree  | can_order     | t
 btree  | can_unique    | t
 btree  | can_multi_col | t
 btree  | can_exclude   | t
(4 rows)

正如我們所見,B-tree可以對資料進行排序並支援唯一性——這是為我們提供這些屬性的唯一訪問方法。還允許使用多列索引,但是其他訪問方法(儘管不是所有方法)可能也支援這樣的索引。我們將在下次討論對排除約束的支援。

postgres=# select p.name, pg_index_has_property('t_a_idx'::regclass,p.name)
from unnest(array[
       'clusterable','index_scan','bitmap_scan','backward_scan'
     ]) p(name);
     name      | pg_index_has_property
---------------+-----------------------
 clusterable   | t
 index_scan    | t
 bitmap_scan   | t
 backward_scan | t
(4 rows)

«btree»訪問方法支援兩種獲取值的技術:索引掃描和點陣圖掃描。正如我們所看到的,訪問方法可以在樹遍歷過程中«forward»和«backward»

postgres=# select p.name,
     pg_index_column_has_property('t_a_idx'::regclass,1,p.name)
from unnest(array[
       'asc','desc','nulls_first','nulls_last','orderable','distance_orderable',
       'returnable','search_array','search_nulls'
     ]) p(name);
        name        | pg_index_column_has_property
--------------------+------------------------------
 asc                | t
 desc               | f
 nulls_first        | f
 nulls_last         | t
 orderable          | t
 distance_orderable | f
 returnable         | t
 search_array       | t
 search_nulls       | t
(9 rows)

該層的前四個屬性解釋了特定列的值是如何精確排序的。在這個例子中,值按升序排序(«asc»),最後提供null值(«nulls_last»)。但是正如我們已經看到的,其他的組合是可能的。

«search_array»屬性表示通過索引支援這樣的表示式:

demo=# explain(costs off)
select * from aircrafts where aircraft_code in ('733','763','773');
                           QUERY PLAN                            
-----------------------------------------------------------------
 Index Scan using aircrafts_pkey on aircrafts
   Index Cond: (aircraft_code = ANY ('{733,763,773}'::bpchar[]))
(2 rows)

«returnable»屬性表示支援index-only掃描,這是合理的,因為索引行本身儲存索引值(例如,與雜湊索引不同)。這裡有必要談一談基於b樹的索引覆蓋。

具有附加行的惟一索引(Unique indexes with additional rows)

正如我們前面所討論的,覆蓋索引是儲存查詢所需的所有值的索引,它不需要(幾乎)訪問表本身。

但是,讓我們假設我們想要為唯一索引新增查詢所需的額外列。但是,這種組合值的唯一性並不能保證鍵的唯一性,因此將需要同一列上的兩個索引:一個惟一用於支援完整性約束,另一個惟一用於覆蓋。這肯定是低效的。

在我們公司,Anastasiya Lubennikova lubennikovaav改進了«btree»方法,使得附加的、非惟一的列可以包含在惟一索引中。我們希望這個補丁能被社群採納,成為PostgreSQL的一部分,但這不會在第10版出現。在這一點上,補丁是可用的專業標準9.5+,它看起來是這樣的。

事實上,這個補丁是提交給PostgreSQL 11的。

讓我們考慮一下預訂表:

demo=# \d bookings
              Table "bookings.bookings"
    Column    |           Type           | Modifiers
--------------+--------------------------+-----------
 book_ref     | character(6)             | not null
 book_date    | timestamp with time zone | not null
 total_amount | numeric(10,2)            | not null
Indexes:
    "bookings_pkey" PRIMARY KEY, btree (book_ref)
Referenced by:
    TABLE "tickets" CONSTRAINT "tickets_book_ref_fkey" FOREIGN KEY (book_ref) REFERENCES bookings(book_ref)

在這個表中,主鍵(book_ref,booking code)是由一個常規的«btree»索引提供的。讓我們用一個額外的列建立一個新的唯一索引:

demo=# create unique index bookings_pkey2 on bookings(book_ref) INCLUDE (book_date);

現在我們用一個新的索引替換現有的索引(在事務中,同時應用所有的變化):

demo=# begin;

demo=# alter table bookings drop constraint bookings_pkey cascade;

demo=# alter table bookings add primary key using index bookings_pkey2;

demo=# alter table tickets add foreign key (book_ref) references bookings (book_ref);

demo=# commit;

這是我們得到的:

demo=# \d bookings
              Table "bookings.bookings"
    Column    |           Type           | Modifiers
--------------+--------------------------+-----------
 book_ref     | character(6)             | not null
 book_date    | timestamp with time zone | not null
 total_amount | numeric(10,2)            | not null
Indexes:
    "bookings_pkey2" PRIMARY KEY, btree (book_ref) INCLUDE (book_date)
Referenced by:
    TABLE "tickets" CONSTRAINT "tickets_book_ref_fkey" FOREIGN KEY (book_ref) REFERENCES bookings(book_ref)

現在一個索引作為唯一性約束,並作為這個查詢的覆蓋索引,例如:

demo=# explain(costs off)
select book_ref, book_date from bookings where book_ref = '059FC4';
                    QUERY PLAN                    
--------------------------------------------------
 Index Only Scan using bookings_pkey2 on bookings
   Index Cond: (book_ref = '059FC4'::bpchar)
(2 rows)

索引的建立

眾所周知,但同樣重要的是,對於一個大型表,最好在沒有索引的情況下載入資料,然後再建立所需的索引。這樣不僅速度更快,而且索引的空間大小很可能更小。

問題在於,建立«btree»索引使用了一種比按行向樹中插入值更有效的過程。粗略地說,表中所有可用的資料都被排序,並建立這些資料的葉。然後內部頁被“建立在”這個基礎上,直到整個金字塔都收斂到根。

這個過程的速度取決於可用RAM的大小,而可用RAM的大小受到«maintenance_work_mem»引數的限制。因此,增大引數值可以加快處理速度。對於唯一索引,除了«maintenance_work_mem»外,還要分配大小«work_mem»的記憶體。

比較語義

上次我們提到過,PostgreSQL需要知道對不同型別的值呼叫哪個雜湊函式,以及這種關聯儲存在«雜湊»訪問方法中。同樣,系統必須弄清楚如何對值進行排序。這在排序、分組(有時)、合併和連線等操作中是必需的。PostgreSQL不會將自己繫結到操作符名稱(比如>、<、=),因為使用者可以定義自己的資料型別,併為相應的操作符提供不同的名稱。由«btree»訪問方法使用的操作符家族定義了操作符名稱。

例如,這些比較運算子用於«bool_ops»運算子族:

postgres=# select   amop.amopopr::regoperator as opfamily_operator,
         amop.amopstrategy
from     pg_am am,
         pg_opfamily opf,
         pg_amop amop
where    opf.opfmethod = am.oid
and      amop.amopfamily = opf.oid
and      am.amname = 'btree'
and      opf.opfname = 'bool_ops'
order by amopstrategy;
  opfamily_operator  | amopstrategy
---------------------+-------------- 
 <(boolean,boolean)  |            1
 <=(boolean,boolean) |            2
 =(boolean,boolean)  |            3
 >=(boolean,boolean) |            4
 >(boolean,boolean)  |            5
(5 rows) 

在這裡我們可以看到五個比較運算子,但是正如前面提到的,我們不應該依賴它們的名字。為了弄清每個操作符做哪些比較,引入了策略概念。定義了五種策略來描述操作符語義:

·1--less

·2--less or equal

·3--equal

·4--greater or equal

·5--greater

一些操作符族可以包含實現一個策略的多個操作符。例如,«integer_ops»運算子族包含策略1的以下運算子:

postgres=# select   amop.amopopr::regoperator as opfamily_operator
from     pg_am am,
         pg_opfamily opf,
         pg_amop amop
where    opf.opfmethod = am.oid
and      amop.amopfamily = opf.oid
and      am.amname = 'btree'
and      opf.opfname = 'integer_ops'
and      amop.amopstrategy = 1
order by opfamily_operator;
  opfamily_operator  
---------------------- 
 <(integer,bigint)
 <(smallint,smallint)
 <(integer,integer)
 <(bigint,bigint)
 <(bigint,integer)
 <(smallint,integer)
 <(integer,smallint)
 <(smallint,bigint)
 <(bigint,smallint)
(9 rows) 

由於這一點,在比較一個操作符族中包含的不同型別的值時,優化器可以避免型別強制轉換。

支援新資料型別的索引

文件(https://postgrespro.com/docs/postgrespro/9.6/xindex)提供了為複數建立新資料型別的示例,以及為此類值排序的操作符類的示例。這個例子使用C語言,當速度非常關鍵時,這是絕對合理的。但是,為了更好地理解比較語義,我們可以在同樣的實驗中使用純SQL。

讓我們建立一個包含兩個欄位的新組合型別:實部和虛部:

postgres=# create type complex as (re float, im float);

我們可以建立一個具有新型別欄位的表,並向表中新增一些值:

postgres=# create table numbers(x complex);

postgres=# insert into numbers values ((0.0, 10.0)), ((1.0, 3.0)), ((1.0, 1.0));

現在一個問題出現了:如果複數在數學意義上沒有定義階關係,如何對它們進行序?

結果是,比較運算子已經為我們定義了:

postgres=# select * from numbers order by x;
   x    
--------
 (0,10)
 (1,1)
 (1,3)
(3 rows)

預設情況下,組合型別的排序是按元件方式進行的:比較第一個欄位,然後比較第二個欄位,依此類推,其方式與逐個字元比較文字字串大致相同。但是我們可以定義不同的順序。例如,複數可以被當作向量,用模(長度)來排序,模(長度)是用座標平方和的平方根來計算的(勾股定理)。為了定義這樣的順序,讓我們建立一個輔助函式,計算模數:

postgres=# create function modulus(a complex) returns float as $$
    select sqrt(a.re*a.re + a.im*a.im);
$$ immutable language sql;

現在我們用這個輔助函式系統地為這五個比較運算子定義函式:

postgres=# create function complex_lt(a complex, b complex) returns boolean as $$
    select modulus(a) < modulus(b);
$$ immutable language sql;

postgres=# create function complex_le(a complex, b complex) returns boolean as $$
    select modulus(a) <= modulus(b);
$$ immutable language sql;

postgres=# create function complex_eq(a complex, b complex) returns boolean as $$
    select modulus(a) = modulus(b);
$$ immutable language sql;

postgres=# create function complex_ge(a complex, b complex) returns boolean as $$
    select modulus(a) >= modulus(b);
$$ immutable language sql;

postgres=# create function complex_gt(a complex, b complex) returns boolean as $$
    select modulus(a) > modulus(b);
$$ immutable language sql;

我們會建立相應的運算子。為了說明它們不需要被稱為“>”、“<”等等,讓我們給它們命名比較«weird»。

postgres=# create operator #<#(leftarg=complex, rightarg=complex, procedure=complex_lt);

postgres=# create operator #<=#(leftarg=complex, rightarg=complex, procedure=complex_le);

postgres=# create operator #=#(leftarg=complex, rightarg=complex, procedure=complex_eq);

postgres=# create operator #>=#(leftarg=complex, rightarg=complex, procedure=complex_ge);

postgres=# create operator #>#(leftarg=complex, rightarg=complex, procedure=complex_gt);

這樣,我們可以比較數字:

postgres=# select (1.0,1.0)::complex #<# (1.0,3.0)::complex;
 ?column?
----------
 t
(1 row)

除了五個操作符之外,«btree»訪問方法還需要定義一個函式(過多但方便):如果第一個值小於、等於或大於第二個值,它必須返回-1、0或1。這個輔助函式稱為support。其他訪問方法可能需要定義其他support函式。

postgres=# create function complex_cmp(a complex, b complex) returns integer as $$
    select case when modulus(a) < modulus(b) then -1
                when modulus(a) > modulus(b) then 1 
                else 0
           end;
$$ language sql;

現在我們準備建立一個操作符類(將自動建立相同名稱的操作符族):

postgres=# create operator class complex_ops
default for type complex
using btree as
    operator 1 #<#,
    operator 2 #<=#,
    operator 3 #=#,
    operator 4 #>=#,
    operator 5 #>#,
    function 1 complex_cmp(complex,complex);

以下是排序:

postgres=# select * from numbers order by x;
   x    
--------
 (1,1)
 (1,3)
 (0,10)
(3 rows)

而且它肯定會被«btree»索引所支援。

您可以通過此查詢獲得支援功能:

postgres=# select amp.amprocnum,
       amp.amproc,
       amp.amproclefttype::regtype,
       amp.amprocrighttype::regtype
from   pg_opfamily opf,
       pg_am am,
       pg_amproc amp
where  opf.opfname = 'complex_ops'
and    opf.opfmethod = am.oid
and    am.amname = 'btree'
and    amp.amprocfamily = opf.oid;
 amprocnum |   amproc    | amproclefttype | amprocrighttype
-----------+-------------+----------------+-----------------
         1 | complex_cmp | complex        | complex
(1 row)

內部原理

我們可以使用«pageinspect»擴充套件來探索b-樹的內部結構

demo=# create extension pageinspect;

索引元資料頁:

demo=# select * from bt_metap('ticket_flights_pkey');
 magic  | version | root | level | fastroot | fastlevel
--------+---------+------+-------+----------+-----------
 340322 |       2 |  164 |     2 |      164 |         2
(1 row)

這裡最有趣的是索引層級:對於一個有一百萬行的表,兩個列上的索引只需要2層(不包括root)。

第164塊(根)的統計資訊:

demo=# select type, live_items, dead_items, avg_item_size, page_size, free_size
from bt_page_stats('ticket_flights_pkey',164);
 type | live_items | dead_items | avg_item_size | page_size | free_size
------+------------+------------+---------------+-----------+-----------
 r    |         33 |          0 |            31 |      8192 |      6984
(1 row)

塊中的資料(«data»欄位在這裡犧牲了螢幕寬度,包含了索引鍵的二進位制表示值):

demo=# select itemoffset, ctid, itemlen, left(data,56) as data
from bt_page_items('ticket_flights_pkey',164) limit 5;
 itemoffset |  ctid   | itemlen |                           data                           
------------+---------+---------+----------------------------------------------------------
          1 | (3,1)   |       8 |
          2 | (163,1) |      32 | 1d 30 30 30 35 34 33 32 33 30 35 37 37 31 00 00 ff 5f 00
          3 | (323,1) |      32 | 1d 30 30 30 35 34 33 32 34 32 33 36 36 32 00 00 4f 78 00
          4 | (482,1) |      32 | 1d 30 30 30 35 34 33 32 35 33 30 38 39 33 00 00 4d 1e 00
          5 | (641,1) |      32 | 1d 30 30 30 35 34 33 32 36 35 35 37 38 35 00 00 2b 09 00
(5 rows)

第一個元素與技術有關,並指定塊中所有元素的上限(我們沒有討論實現細節),而資料本身從第二個元素開始。很明顯,最左邊的子節點是塊163,然後是代323,依此類推。反過來,也可以使用相同的函式來研究它們。

還有一個可能有用的擴充套件是“amcheck”,它將被合併到PostgreSQL 10中,更低的版本可以從github獲得。這個擴充套件檢查b-樹中資料的邏輯一致性,並使我們能夠提前檢測故障。