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的键感兴趣。
实际上,一些特殊情况使这个看似简单的过程变得复杂。例如,索引可以包含非唯一键,并且可能有许多相等的值,以至于不能容纳在一个页。回到我们的示例,似乎应该从内部节点的引用向下延伸到值49。但是,从图中可以清楚地看出,这样我们将跳过前面页中的一个«49»键。因此,一旦我们在一个内部页面中找到了一个完全相等的键,我们就必须往左下降一个位置,然后从左到右查看底层的索引行来搜索所查找的键。
(另一个复杂的问题是,在搜索过程中,其他进程可以更改数据:可以重新构建树,可能将页一分为二,等等。所有的算法都是为这些并发操作而设计的,在任何可能的情况下都不会相互干扰,也不会导致额外的锁。但我们将避免对此进行详述。)
不等价检索
当按条件“indexed-field≤expression”(或“indexed-field≥expression”)进行搜索时,首先按相等条件“indexed-field = expression”在索引中找到一个值(如果有),然后按适当的方向遍历页页,直到结束。
«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));
字段的顺序将改变:
NULL值
btree访问方法会索引空值,并支持按条件is null和is not 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-树中数据的逻辑一致性,并使我们能够提前检测故障。