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

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

  • 相关阅读:
    团队冲刺第二天站立会议
    团队冲刺第一天站立会议
    Scrum仪式之Sprint计划会议
    软件需求分析
    我们的团队
    No.1_1 java语言基础_学习笔记
    java 基础学习
    LoadRunner 11 安装及破解
    Linux查看程序端口占用情况
    windows 下查看端口占用命令
  • 原文地址:https://www.cnblogs.com/abclife/p/13446937.html
Copyright © 2020-2023  润新知