CMU15-445:Lecture #08 笔记
Lecture #08: Tree Indexes & B+Tree
本文是对CMU15-445课程第8节笔记的一个粗略总结和翻译。仅供个人(M1kanN)复习使用。
1. Table Indexes
- 在数据库系统中,有许多不同的数据结构,可以用于内部数据、核心数据存储、临时数据结构或者表索引等目的。对于表的索引,可能涉及到到带有范围扫描的查询。
- 表索引是一个表的列的子集的复制品,它被组织或排序以使用这些属性的子集,进行有效的访问。因此,DBMS可以查找表索引的辅助数据结构,而不是执行顺序扫描,以更快的找到tuple。DBMS确保表和索引的内容在逻辑上总是同步的。
- 在每个数据库创建的索引数量之间存在这一种权衡。尽管更多的索引会让查询更快,但是也会使用存储空间并且需要维护。DBMS的工作就是找出最佳的索引来执行查询。
2. B+Tree
-
B+树是一种自我平衡的树数据结构,而且保持数据有序,且允许查询、线性访问,插入,删除等操作在\(O(log(n))\)内执行。它针对读、写大数据块的面向磁盘的DBMS进行了优化。
-
几乎所有支持order-preserving的现代DBMS都使用B+Tree。有一种特定的数据结构叫做B树,但用来泛指一种数据结构。原始的B树与B+树的区别在于,B树在所有结点存储key和value,但是B+树只在叶子结点存储。现在B+树还结合了其他B树变体的特征,如B-link-Tree中的兄弟指针。
-
形式上,一个B+树是一个M路搜索树(M代表一个结点可以拥有的最大孩子数),且具有以下特征:
- 完美平衡(所有叶子都在同一层)
- 除根以外的每个内部节点至少有一半是满的(M/2 - 1 <= 键的数量 <= M - 1)。
- 每个有k个键的内部节点都有k+1个非空的子节点。
-
B+Tree中的每个节点都包含一个键/值对数组。这些对中的键是由索引所基于的属性派生的。这些值将根据一个节点是内部节点还是叶子节点而有所不同。对于内部节点,值数组将包含指向其他节点的指针。叶子节点值的两种方法是记录ID和元组数据。记录ID指的是一个指向元组位置的指针。有元组数据的叶子节点在每个节点中存储元组的实际内容。
-
虽然根据B+树的定义,这不是必须的,但每个节点的数组几乎都是按键排序的。
-
从概念上讲,内部节点上的键可以被认为是引导柱。它们指导树的遍历,但不代表叶子节点上的键(以及它们的值)。这意味着你有可能在一个内部节点中拥有一个在叶子节点上找不到的键(作为一个引导帖guide post)。尽管必须注意到,传统上内部节点只拥有叶子节点中存在的那些键。
Insertion
-
在B+树上插入一个元素,首先要借助中间结点向下遍历树找到要插入哪个叶子结点。
1. Find correct leaf L. 2. Add new entry into L in sorted order: • If L has enough space, the operation is done. • Otherwise split L into two nodes L and L2. Redistribute entries evenly and copy up middle key. Insert index entry pointing to L2 into parent of L. 3. To split an inner node, redistribute entries evenly, but push up the middle key.
-
例子: (B+树模拟网站:https://dichchankinh.com/~galles/visualization/BPlusTree.html )
-
插入5后:
-
Deletion
-
在插入过程中,当树变得太满时,我们偶尔不得不分割叶子,而如果删除导致树少于半满,我们必须进行合并,以重新平衡树。
-
算法:
1. Find correct leaf L. 2. Remove the entry: • If L is at least half full, the operation is done. • Otherwise, you can try to redistribute, borrowing from sibling. • If redistribution fails, merge L and sibling. 3. If merge occurred, you must delete entry in parent pointing to L.
-
例子:
删除5后:
删除6后:
Selection Conditions
- 因为B+树有序,所以查找可以很快,也不需要整个key。DBMS可以使用B+树索引。这与哈希索引不同,哈希索引需要搜索key中的所有属性。
Non-Unique Indexes
- 像散列表一样,B+Trees可以通过重复键或存储值列表来处理非唯一的索引。在重复键的方法中,使用相同的叶子节点布局,但重复的键被多次存储。在值列表方法中,每个键只存储一次,并保持一个唯一值的链接列表。
Duplicate Keys
-
有两种方法存储重复value
-
append record IDs :
因为每一个record ID都是唯一的,所以可以保证所有的key都可辨识 -
allow leaf nodes to spill into overflow nodes that contain the duplicate keys:
允许叶子节点溢出到包含重复键的溢出节点。虽然没有多余的信息被存储,但这种方法的维护和修改更加复杂。
-
Clustered Indexes
-
一般设置主键索引就为聚集索引。
该表按照主键指定的排序顺序存储,作为堆组织或索引组织的存储:
由于一些DBMS总是使用聚类索引,所以如果一个表没有明确的主键,它们会自动做一个隐藏的行id主键,但是其他的DBMS根本不能使用它们。 -
聚类索引: 参考:聚集索引(聚类索引)与非聚集索引(非聚类索引)
-
重点:
非聚集索引和聚集索引的区别在于:
通过聚集索引可以一次查到需要查找的数据, 而通过非聚集索引第一次只能查到记录对应的主键值 , 再使用主键的值通过聚集索引查找到需要的数据。
聚集索引一张表只能有一个,而非聚集索引一张表可以有多个。
Heap Clustering
- tuple在堆的页面中使用聚类索引指定的顺序进行排序。如果聚类索引的属性被用来访问tuple,DBMS可以直接跳到这些页上。
Index Scan Page Sorting
- 由于直接从非聚类索引中检索tuple的效率很低,DBMS可以首先找出它所需要的所有tuples,然后根据它们的页面id对它们进行排序。
3. B+Tree Design Choices
3.1 Node Size
- 根据存储介质的不同,我们会选择更大或者更小的结点尺寸。例如:存储在硬盘上的结点通常是以MB为单位的,以减少寻找数据所需要的次数,并将昂贵的磁盘读取分摊到一大块数据上,而内存数据库可能使用小到512字节的页面大小,以便将整个页面放入CPU缓存,并减少数据碎片(data fragmentation)。这种选择也取决于工作负载的类型,如点查询会喜欢尽可能小的页面,以减少不必要的额外信息的加载量,而大的顺序扫描更喜欢大的页面,以减少它需要做的检索的数量。
3.2 Merge Threshold
- 虽然B+树有一条规则,即在删除后合并underflowed的结点,但有时候暂时违反该规则以减少删除操作的数量可能更好。例如:急于合并可能会导致thrashing,大量连续的删除和插入操作会导致不断的分裂和合并。所以batched merging可能会对这种情况改善,即多个合并操作同时发生,减少在树上进行昂贵的写锁存的时间。
3.3 Variable Length Keys
- 目前都是讨论的是固定长度key的B+树,当然也可以支持变长的key。如large key的小子集会导致大量的空间浪费。对此有几种方法:
- Pointers
不是存储key,而是存储key的指针。由于每个key都要一个指针的效率很低,在工业界唯一使用这种方法的是嵌入式设备,其微小的寄存器和缓存可能从这种空间的节省中受益。 - Variable Length Nodes
可以直接支持变长,但这样会带来很大的内存管理开销。 - Padding:
我们也可以不改变key的大小,而是将每个key都设置为最大key的大小,然后有多余的就补齐(padding)。但这样会带来巨大的内存浪费 - Key Map / Indirection
最广泛的使用方法,是在一个separate dictionary里,用一个索引到key/value对的索引(index)来代替key。可以大大节省空间,并有可能缩短点查询(因为索引指向的key/value对与叶子结点指向的key/value对完全相同)。由于字典index的大小较小,所以有足够的空间将每个key的前缀放在index旁边,可能允许一些索引搜索和叶子扫描,甚至不必chase the pointer(如果前缀和search key完全不同)- 例子:
- 例子:
- Pointers
3.4 Intra-Node Search
-
一旦我们到达一个结点,我们仍然需要在结点内进行搜索(要么从内部结点找到下一个结点,要么在叶子结点中找到我们的键值)。虽然这相对简单,但还是要考虑:
- 方法一:线性搜索
- 方法二:二分查找
- 方法三:插值(估算大概位置)
某些情况下,我们可能会利用插值法来寻找key。这种方法利用了存储在结点上的任何元数据(最大值,最小值,平均数等等),并利用它来生成钥匙的大致位置。例如,如果我们在一个结点中寻找8,我们知道10是最大的key,10-(n+1)是最小的key(其中n是每个结点中的key的数量),那么我们知道从最大的key向下2个slot开始搜索。因为在这种情况下,离最大的key的slot的键必须是9。
这种方法一般不用。因为适用性有限而且复杂度高。
4. Optimizations
4.1 Pointer Swizzling
- 因为B+树的每个结点都存储在缓冲池的一个页面中,所以每次我们加载一个新的页面的时候,都需要从缓冲池中获取它,这需要latching和lookups。为了完全跳过这一步,我们可以用实际的原始指针来代替页面ID(并称为swizzling),完全避免缓冲池的获取。与其手动获取整个树并手动放置指针,我们可以在正常遍历索引的时候简单地存储从页面查询中所得到的指针。请注意,我们必须跟踪哪些指针被swizzled了或者deswizzle,并在它们所指向的页面被unpinned和victimized前back to page ids (原文:Note that we must track which pointers are swizzled and deswizzle them back to page ids when the page they point to is unpinned and victimized.)
4.2 Bulk Insert
- 当B+最初建立的时候,必须以通畅的方式插入每个键,这将导致不断的分裂操作。由于我们已经给叶子提供了兄弟指针,如果我们构建一个叶子结点的排序链表,然后使用每个叶子结点的第一个键,从下往上建立索引,那么初始插入数据的效率就高得多。请注意:我们可能希望尽可能紧密的打包叶子以节省空间,或者在每个叶子中留出空间,以便在有必要进行分割之前进行更多的操作。
- 就是把一个个插入,变为打包插入,打包之前要排序,互相指针!
4.3 Prefix Compression
- 就是前缀压缩
4.4 De-duplication
- 就是可以将相同key不同值的key/value对,都存到一个key下
4.5 Suffix Truncation
- 不需要全部的key。仅仅需要部分的key