非叶 b树节点如何在innodb中实际表示?
回想一下,b树(更具体地说是b +树)同时具有叶节点和非叶节点。在b +树中,所有叶节点都位于非叶子或“内部”节点的树下面,并指向实际包含行数据的页面。
我知道非叶子节点存储在非叶子节点段中,并使用类似于数据页面的页面。我已经找到了关于如何物理存储数据页面的大量文档,但是我无法找到关于非叶索引页面的内容。
答案 0 :(得分:1)
在学习InnoDB:走向核心之旅,我介绍了innodb_diagrams项目来记录InnoDB内部,它提供了本文中使用的图表。稍后在对innodb_ruby的快速介绍中,我介绍了innodb_space命令行工具的安装和一些快速演示。
InnoDB的INDEX页面的物理结构在InnoDB索引页面的物理结构中有所描述。我们现在将使用一些实际的例子来研究InnoDB如何在逻辑上构建其索引。
除术语外:B +树,根,叶和水平 InnoDB为其索引使用B + Tree结构。当数据不适合内存并且必须从磁盘读取时,B +树特别有效,因为它确保仅根据树的深度访问所请求的任何数据需要固定的最大读取次数,它很好地扩展。
索引树从“根”页开始,其位置是固定的(并永久存储在InnoDB的数据字典中),作为访问树的起点。树可以与单个根页一样小,也可以在多级树中大到数百万个页面。
页面被称为“叶子”页面或“非叶子”页面(在某些上下文中也称为“内部”或“节点”页面)。叶页面包含实际的行数据。非叶子页面仅包含指向其他非叶子页面或叶子页面的指针。树是平衡的,因此树的所有分支都具有相同的深度。
InnoDB在树中为每个页面分配一个“级别”:叶子页面被指定为级别0,并且级别递增到树上。根页面级别基于树的深度。如果区别很重要,那么既不是叶页也不是根页的所有页面也可称为“内部”页面。
叶子和非叶子页面 对于叶子页面和非叶子页面,每个记录(包括infimum和supremum系统记录)都包含一个“下一个记录”指针,该指针存储一个偏移量(在页面内)到下一个记录。链表从infimum开始,按键升序链接所有记录,终止于supremum。记录在页面内没有实际排序(它们占用插入时可用的任何空间);他们唯一的订单来自他们在链表中的位置。
Leaf页面包含非键值作为每条记录中包含的“data”的一部分:
非叶子页面具有相同的结构,但是它们的“数据”不是非关键字段,而是子页面的页码,而不是精确的键,它们代表子页面上的最小键指向:
同级别的网页 大多数索引包含多个页面,因此多个页面按升序和降序链接在一起:
每个页面包含“上一页”和“下一页”的指针(在FIL标题中),INDEX页面用于形成同一级别的双页链接列表(例如叶页,级别) 0形成一个列表,1级页面形成单独的列表等。)。
详细了解单页表格 让我们看一下单个索引页面中与B + Tree相关的大部分内容:
创建并填充表格 可以创建并填充上图中使用的测试表(确保使用innodb_file_per_table并使用Barracuda文件格式):
CREATE TABLE t_btree (
i INT NOT NULL,
s CHAR(10) NOT NULL,
PRIMARY KEY(i)
) ENGINE=InnoDB;
INSERT INTO t_btree (i, s)
VALUES (0, "A"), (1, "B"), (2, "C");
虽然这个表非常小而且不太现实,但它确实很好地展示了记录和记录遍历的工作原理。
验证空间文件的基本结构 该表应该与我们之前检查过的内容相匹配,包括三个标准开销页面(FSP_HDR,IBUF_BITMAP和INODE),后面是索引根的单个INDEX页面,在这种情况下是两个未使用的ALLOCATED页面。
$ innodb_space -f t_btree.ibd space-page-type-regions
start end count type
0 0 1 FSP_HDR
1 1 1 IBUF_BITMAP
2 2 1 INODE
3 3 1 INDEX
4 5 2 FREE (ALLOCATED)
space-index-pages-summary模式将为我们提供每页中的记录计数,并显示预期的3条记录:
$ innodb_space -f t_btree.ibd space-index-pages-summary
page index level data free records
3 18 0 96 16156 3
4 0 0 0 16384 0
5 0 0 0 16384 0
(请注意,space-index-pages-summary也将空的ALLOCATED页面显示为空白页面,记录为零,因为这通常是您对绘图目的感兴趣的。)
空间索引模式将显示有关我们的PRIMARY KEY索引的统计信息,该索引在其内部文件段上占用了一个页面:
$ innodb_space -f t_btree.ibd space-indexes
id root fseg used allocated fill_factor
18 3 internal 1 1 100.00%
18 3 leaf 0 0 0.00%
设置记录描述符 为了让innodb_ruby解析记录的内容,我们需要提供一个记录描述符,它只是一个Ruby类,提供一个返回索引描述的方法:
class SimpleTBTreeDescriber< Innodb的:: RecordDescriber 类型:集群 key" i",:INT,:NOT_NULL row" s"," CHAR(10)",:NOT_NULL 端
我们需要注意,这是聚簇键,提供键的列描述,以及非键(“行”)字段的列描述。有必要让innodb_space使用以下附加参数加载此类:
-r -r ./simple_t_btree_describer.rb -d SimpleTBTreeDescriber
查看记录内容 可以使用页面转储模式转储此示例中的根页面(这是一个叶页面),并提供根页面的页码:
$ innodb_space -f t_btree.ibd -r ./simple_t_btree_describer.rb -d
SimpleTBTreeDescriber -p 3 page-dump
除了我们之前看过的输出的某些部分,它现在将打印一个“记录:”部分,每个记录具有以下结构:
{:format=>:compact,
:offset=>125,
:header=>
{:next=>157,
:type=>:conventional,
:heap_number=>2,
:n_owned=>0,
:min_rec=>false,
:deleted=>false,
:field_nulls=>nil,
:field_lengths=>[0, 0, 0, 0],
:field_externs=>[false, false, false, false]},
:next=>157,
:type=>:clustered,
:key=>[{:name=>"i", :type=>"INT", :value=>0, :extern=>nil}],
:transaction_id=>"0000000f4745",
:roll_pointer=>
{:is_insert=>true, :rseg_id=>8, :undo_log=>{:page=>312, :offset=>272}},
:row=>[{:name=>"s", :type=>"CHAR(10)", :value=>"A", :extern=>nil}]}
这应完美地与上面的详细说明保持一致,因为我已经复制了此示例中的大部分信息以确保准确性。请注意以下几个方面:
:格式为:compact表示记录是Barracuda格式表中的新“紧凑”格式(与Antelope表中的“冗余”相对)。 输出中列出的:键是索引的键字段数组,并且:row是非键字段的数组。 :transaction_id和:roll_pointer字段是每个记录中包含的MVCC的内部字段,因为这是一个集群密钥(PRIMARY KEY)。 :header散列中的下一个字段是相对偏移量(32),必须将其添加到当前记录偏移量(125)以产生下一个记录的实际偏移量(157)。为方便起见,此计算的偏移量包含在:记录哈希中的下一个。 递归索引 使用index-recurse模式可以实现递归整个索引的简单输出,但由于这仍然是单页索引,因此输出将非常短:
$ innodb_space -f t_btree.ibd -r ./simple_t_btree_describer.rb -d
SimpleTBTreeDescriber -p 3 index-recurse
ROOT NODE #3: 3 records, 96 bytes
RECORD: (i=0) -> (s=A)
RECORD: (i=1) -> (s=B)
RECORD: (i=2) -> (s=C)
构建一个非平凡的索引树 InnoDB中的多级索引树(过度简化)如下所示:
如前所述,每个级别的所有页面都彼此双重链接,并且在每个页面内,记录按升序单独链接。非叶子页面包含“指针”(包含子页面编号)而不是非键行数据。
如果我们使用在innodb_ruby快速入门中创建的100万行的简单表模式,那么树结构看起来会更有趣:
$ innodb_space -f t.ibd -r ./simple_t_describer.rb -d SimpleTDescriber -p 3 index-recurse
ROOT NODE #3: 2 records, 26 bytes
NODE POINTER RECORD >= (i=252) -> #36
INTERNAL NODE #36: 1117 records, 14521 bytes
NODE POINTER RECORD >= (i=252) -> #4
LEAF NODE #4: 446 records, 9812 bytes
RECORD: (i=1) -> ()
RECORD: (i=2) -> ()
RECORD: (i=3) -> ()
RECORD: (i=4) -> ()
NODE POINTER RECORD >= (i=447) -> #1676
LEAF NODE #1676: 444 records, 9768 bytes
RECORD: (i=447) -> ()
RECORD: (i=448) -> ()
RECORD: (i=449) -> ()
RECORD: (i=450) -> ()
NODE POINTER RECORD >= (i=891) -> #771
LEAF NODE #771: 512 records, 11264 bytes
RECORD: (i=891) -> ()
RECORD: (i=892) -> ()
RECORD: (i=893) -> ()
RECORD: (i=894) -> ()
这是一个三级索引树,可以通过上面的ROOT,INTERNAL,LEAF行看到。我们可以看到一些页面已经完全填满,468条记录消耗了16 KiB页面中的近15 KiB。
使用页面转储模式查看非叶子页面(第36页,在上面的输出中),记录看起来与前面显示的叶子页面略有不同:
$ innodb_space -f t.ibd -r ./simple_t_describer.rb -d SimpleTDescriber -p 36 page-dump
{:format=>:compact,
:offset=>125,
:header=>
{:next=>11877,
:type=>:node_pointer,
:heap_number=>2,
:n_owned=>0,
:min_rec=>true,
:deleted=>false,
:field_nulls=>nil,
:field_lengths=>[0],
:field_externs=>[false]},
:next=>11877,
:type=>:clustered,
:key=>[{:name=>"i", :type=>"INT UNSIGNED", :value=>252, :extern=>nil}],
:child_page_number=>4}
:键数组存在,虽然它表示最小键而不是精确键,并且不存在:行,因为:child_page_number取代它。
根页面有点特殊 由于在首次创建索引时分配了根页,并且该页号存储在数据字典中,因此根页永远不会重定位或删除。一旦根页填满,它将需要拆分,形成一个小页面的根页加上两个叶页。
但是,根页本身实际上无法拆分,因为它无法重新定位。相反,将分配一个新的空页面,将根目录中的记录移动到那里(根“被提升”一个级别),并将该新页面拆分为两个。然后根页不需要再次拆分,直到它下面的级别有足够的页面,根充满子页面指针(称为“节点指针”),实际上通常意味着数百到超过一千。
B +树级别和增加树深度 作为B + Tree索引效率的一个例子,假设完美的记录打包(每页都满,这在实践中永远不会发生,但对讨论很有用)。对于上面示例中的简单表,InnoDB中的B + Tree索引将能够为每个叶页存储468个记录,或者每个非叶页存储1203个记录。然后,索引树可以是给定树高度的以下大小的最大值:
Height Non-leaf pages Leaf pages Rows Size in bytes
1 0 1 468 16.0 KiB
2 1 1203 > 563 thousand 18.8 MiB
3 1204 1447209 > 677 million 22.1 GiB
4 1448413 1740992427 > 814 billion 25.9 TiB
可以想象,大多数具有明智PRIMARY KEY定义的表是2-3级,有些达到4级。但是,使用过大的PRIMARY KEY会导致B + Tree的效率低得多,因为主键值必须存储在非叶页中。这可以极大地扩大非叶子页面中记录的大小,这意味着每个非叶子页面中的记录更少,这使得整个结构效率降低。