PostgreSQL中的索引(四) --Btree

萝らか妹 提交于 2020-08-16 14:49:29

我们已经讨论了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-树中数据的逻辑一致性,并使我们能够提前检测故障。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!