本系列为 CMU 15-445 Fall 2022 Database Systems 数据库系统 [卡内基梅隆] 课程重点知识点摘录,附加个人拙见,同样借助CMU 15-445课程内容来完成MIT 6.830 lab内容。
上节提到,DBMS 使用一些特定的数据结构来存储信息:
本节将介绍存储 table index 最常用的树形数据结构:B+ Tree,Skip Lists,Radix Tree
table index 为提供 DBMS 数据查询的快速索引,它本身存储着某表某列排序后的数据,并包含指向相应 tuple 的指针。
DBMS 需要保证表信息与索引信息在逻辑上保持同步。用户可以在 DBMS 中为任意表建立多个索引,DBMS 负责选择最优的索引提升查询效率。但索引自身需要占用存储空间,因此在索引数量与索引存储、维护成本之间存在权衡。
B-Tree 中的 B 指的是 Balanced,实际上 B-Tree Family 中的 Tree 都是 Balanced Tree。B-Tree 就是其中之一,以之为基础又衍生出了 :
B-Tree 与 B+ Tree 最主要的区别有以下几点:
综上所述,B+ Tree 相比于 B-Tree 更适合用于磁盘存储和范围查询等场景,能够更好地利用内存缓存和硬件资源,优化查询性能和吞吐量。
B+ Tree 是一种自平衡树,它将数据有序地存储,且在 search、sequential access、insertions 以及 deletions 操作的复杂度上都满足 O(logn),其中 sequential access 的最终复杂度还与所需数据总量有关。
B+ Tree 可以看作是 BST (Binary Search Tree) 的衍生结构,它的每个节点可以有多个 children,这特别契合 disk-oriented database 的数据存储方式,每个 page 存储一个节点,使得树的结构扁平化,减少获取索引给查询带来的 I/O 成本。其基本结构如下图所示:
以 M-way B+tree 为例,它的特点总结如下:
M/2−1≤#keys≤M−1
B+ Tree 中的每个 node 都包含一列按 key 排好序的 key/value pairs,key 就是 table index 对应的 column,value 的取值与 node 类型相关,在 inner nodes 和 leaf nodes 中存的内容不同。
leaf node 的 values 取值在不同数据库中、不同索引优先级中也不同,但总而言之,通常有两种做法:
此外,leaf node 还需要存储相邻 siblings 的地址以及其它一下元信息,如下图所示:
Insert
Delete
B+Tree 的 Insert、Delete 过程,可参考这里。
综合上面数据能够得出的结论:
填充因子:
average fanout = 2 * degree * fill-factor
空间占用:
每一层级的Page:
Clustered Indexes 规定了 table 本身的物理存储方式,通常即按 primary key 排序存储,因此一个 table 只能建立一个 cluster index。有些 DBMSs 对每个 table 都添加聚簇索引,如果该 table 没有 primary key,则 DBMS 会为其自动生成一个;而有些 DBMSs 则不支持 clustered indexes。
DBMS 支持同时对多个字段建立 table index(B+ Tree),即 compound index,如:
CREATE INDEX compound_idx ON table (a, b, c);
它可以被用在包含 a 的 condition 的查询中,如:
SELECT c
FROM table
WHERE a = 5 AND b >= 42 AND c < 77;
SELECT c
FROM table
WHERE a = 5 AND b >= 42;
SELECT c
FROM table
WHERE a = 5;
尽管它可以被用在不包含 a 相关 condition 的查询中,但这约等于全表扫描,因此 DBMS 通常会使用全表扫描。如果使用 hash index 作为 table index,则必须对 (a, b, c) 完全匹配才可以使用。
通常来说,disk 的数据读取速度越慢,node size 就越大:
Disk Type | Node Size |
---|---|
HDD | ~1MB |
SSD | ~10KB |
In-Memory | ~512B |
具体情境下的最优大小由 workload 决定。
B+ Tree 中存储的 key 经常是变长的,通常有三种手段来应对:
索引针对的 key 可能是非唯一的,通常有两种手段来应对:
分别如下面两张图所示:
在节点内部搜索,就是在排好序的序列中检索元素,手段通常有:
同一个 leaf node 中的 keys 通常有相同的 prefix,如下图所示:
为了节省空间,可以只存所有 keys 的不同的 suffix。
由于 inner nodes 只用于引导搜索,因此没有必要在 inner nodes 中储存完整的 key,我们可以只存储足够的 prefix 即可,如下图所示:
建 B+ Tree 的最快方式是先将 keys 排好序后,再从下往上建树,如下图所示:
因此如果有大量插入操作,可以利用这种方式提高效率
Nodes 使用 page id 来存储其它 nodes 的引用,DBMS 每次需要首先从 page table 中获取对应的内存地址,然后才能获取相应的 nodes 本身,如果 page 已经在 buffer pool 中,我们可以直接存储其它 page 在 buffer pool 中的内存地址作为引用,从而提高访问效率。
许多 DBMSs 会自动创建 index,来帮助施行 integrity constraints(完整性约束),情形包括:
如当我们创建下面的 foo table 时:
CREATE TABLE foo (
id SERIAL PRIMARY KEY,
val1 INT NOT NULL,
val2 VARCHAR(32) UNIQUE
);
CREATE TABLE bar (
id INT REFERENCES foo (val1),
val VARCHAR(32)
);
DBMS 可能会自动建立以下索引:
CREATE UNIQUE INDEX foo_pkey ON foo (id); /* primary keys */
CREATE INDEX foo_val1_key ON foo (val1); /* foreign keys */
CREATE UNIQUE INDEX foo_val2_key ON foo (val2); /* Unique Constraints */
只针对 table 的子集建立 index,这可以大大减少 index 本身的大小,如下所示:
CREATE INDEX idx_foo
ON foo (a, b)
WHERE c = 'WuTang';
一种常见的使用场景就是通过事件来为 indexes 分区,即为不同的年、月、日分别建立索引。
如果 query 所需的所有 column 都存在于 index 中,则 DBMS 甚至不用去获取 tuple 本身即可得到查询结果,如下所示:
CREATE INDEX idx_foo ON foo (a, b);
SELECT b FROM foo
WHERE a = 123;
甚至可以在 index 中有意加入别的 column (INDEX INCLUDE COLUMNS):
CREATE INDEX idx_foo
ON foo (a, b)
INCLUDE (c);
index 中的 key 不一定是 column 中的原始值,也可以是通过计算得到的值,如针对以下查询:
SELECT * FROM users
WHERE EXTRACT(dow FROM login) = 2;
直接针对 login 建立索引是没用的,这时候可以针对计算后的值建立索引:
CREATE INDEX idx_user_login
ON users (EXTRACT(dow FROM login));
从上文的介绍中,可以发现 table index 实际上就是一个 dynamic order-preserving 的数据结构,同时对增删改查的操作应提供较高的性能(B+ Tree,O(log n) )。
dynamic order-preserving 的数据结构中,最简单的就是 sorted linked list,所有操作的复杂度均在 O(n) ,性能较 B+ Tree 相比逊色许多,但如果将多个 sorted linked list 垒起来,就可能提供与 B+ Tree 相媲美的性能。
Skip Lists 是一种随机数据结构(Randomized Data Structure),它是 Set (Ordered Set) 的一种实现。它的各操作复杂度如下表所示:
Operation Name | Time Complexity | Space Complexity |
---|---|---|
find-key(k) | O(lgn) | O(1) |
iter() | O(n) | O(n) |
insert(x) | O(lgn) | O(1) |
delete-key(k) | O(lgn) | O(1) |
delete-min/max() | O(lgn) | O(1) |
find-next/prev(k) | O(lgn) | O(1) |
find-min/max() | O(lgn) | O(1) |
order-iter() | O(n) | O(1) |
One Linked List:
当我们有一个 Linked List 时,Search 操作在最差情况下的复杂度为O(n),我们有什么方式能够提高它的速度呢?
Two Linked Lists:
如图 2 所示,举例如下:
怎么设计快速公交系统的停靠站能使得公交系统的性能达到最大?直觉告诉我们,将快速公交的停靠站平均分布在普通公交停靠站上。那么这时候 Search 的成本为:
求其最小值,可以得到:
如此一来,Search 的成本就是:
More Linked Lists:
使用 个 linked lists 时,已经很像一棵树,如 B 树。
Skip Lists:
Insert(x):
search 的时间复杂度为O(lgn) ,递归插入的时间复杂度同样为O(lgn),因此 insert 的总时间复杂度也为O(lgn)。
Delete(x):
Skip Lists 是一种基于概率的数据结构,它提供的复杂度都是期望的结果。它的 Pros & Cons 总结如下:
Radix Tree vs. Trie:
简介 --> radix tree 将每个 key 拆成一个序列:
为了让 keys 能够合理地拆解成序列,许多类型的 key 都需要特殊处理:
举例如下:
尽管 tree index 非常有利于 point 和 range 查询,如:
但对于 keyword search,tree index 就显得无能为力,如:
尽管 DBMSs 在一定程度上支持这种搜索,但更复杂、灵活的查询就不是它们的专长。有一部分 DBMSs 专门提供复杂、灵活的 keyword search 功能,如 ElasticSearch、Solr、Sphinx 等。
这些高级搜索功能通过利用倒排索引中的词项表和倒排列表来实现。它们提供了更精确和灵活的搜索能力,以满足特定的查询需求,并在许多搜索引擎和数据库系统中被广泛使用。
通过使用暂存或缓冲机制,您可以将更新操作分阶段,将它们暂存起来,然后批量应用于索引。这种方式可以提高更新效率,并减少频繁地更新索引的开销。
不管怎么说B+ Tree依然是Tree Indexes的首选,特殊的关键字搜索场景可以考虑倒排索引。
当然,本文不涉及地理空间树索引,例如R树(R-Tree)、四叉树(Quad-Tree)和KD树(KD-Tree),是用于组织和索引空间数据的数据结构。这些基于树的索引专门设计用于处理多维数据,通常用于表示和查询几何对象或空间坐标。