我们已经讨论了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-树中数据的逻辑一致性,并使我们能够提前检测故障。
来源:oschina
链接:https://my.oschina.net/u/4361193/blog/4497643