Fractal tree index | |
Type: | tree |
Invented By: | Michael A. Bender, Martin Farach-Colton, Bradley C. Kuszmaul |
Invented Year: | 2007 |
In computer science, a fractal tree index is a tree data structure that keeps data sorted and allows searches and sequential access in the same time as a B-tree but with insertions and deletions that are asymptotically faster than a B-tree. Like a B-tree, a fractal tree index is a generalization of a binary search tree in that a node can have more than two children. Furthermore, unlike a B-tree, a fractal tree index has buffers at each node, which allow insertions, deletions and other changes to be stored in intermediate locations. The goal of the buffers is to schedule disk writes so that each write performs a large amount of useful work, thereby avoiding the worst-case performance of B-trees, in which each disk write may change a small amount of data on disk. Like a B-tree, fractal tree indexes are optimized for systems that read and write large blocks of data. The fractal tree index has been commercialized in databases by Tokutek. Originally, it was implemented as a cache-oblivious lookahead array,[1] but the current implementation is an extension of the Bε tree.[2] The Bε is related to the Buffered Repository Tree.[3] The Buffered Repository Tree has degree 2, whereas the Bε tree has degree Bε. The fractal tree index has also been used in a prototype filesystem.[4] [5] An open source implementation of the fractal tree index is available,[6] which demonstrates the implementation details outlined below.
In fractal tree indexes, internal (non-leaf) nodes can have a variable number of child nodes within some pre-defined range. When data is inserted or removed from a node, its number of child nodes changes. In order to maintain the pre-defined range, internal nodes may be joined or split. Each internal node of a B-tree will contain a number of keys that is one less than its branching factor. The keys act as separation values which divide its subtrees. Keys in subtrees are stored in search tree order, that is, all keys in a subtree are between the two bracketing values. In this regard, they are just like B-trees.
Fractal tree indexes and B-trees both exploit the fact that when a node is fetched from storage, a block of memory, whose size is denoted by
B
B
In a B-tree, this means that the number of keys in a node is targeted to be enough to fill the node, with some variability for node splits and merges. For the purposes of theoretical analysis, if
O(B)
O(logBN)
Fractal trees nodes use a smaller branching factor, say, of
\sqrt{B}
O(log\sqrt{B}N)=O(logBN)
\sqrt{B}+1
B | |
\sqrt{B |
+1} ≈ \sqrt{B}
O(1)
O\left( | 1 |
\sqrt{B}\right) |
Consider the cost of an insertion. Each message gets flushed
O(logBN)
O\left( | 1 |
\sqrt{B}\right) |
O\left( | logBN |
\sqrt{B}\right) |
B\varepsilon
O\left( | 1 |
B1-\varepsilon |
\right)
This section compares fractal tree indexes with other external memory indexing data structures. The theoretical literature on this topic is very large, so this discussion is limited to a comparison with popular data structures that are in use in databases and file systems.
The search time of a B-tree is asymptotically the same as that of a fractal tree index. However, a fractal tree index has deeper trees than a B-tree, and if each node were to require an I/O, say if the cache is cold, then a fractal tree index would induce more IO. However, for many workloads most or all internal nodes of both B-trees and fractal tree indexes are already cached in RAM. In this case, the cost of a search is dominated by the cost of fetching the leaf, which is the same in both cases. Thus, for many workloads, fractal tree indexes can match B-trees in terms of search time.
Where they differ is on insertions, deletions and updates. An insertion in a fractal tree index takes
O\left( | logBN |
\sqrt{B}\right) |
O(logBN)
O(\sqrt{B})
B
O\left( | 1 |
B\right) |
Log-structured merge-trees (LSMs) refer to a class of data structures which consists of two or more index structures of exponentially growing capacities. When a tree at some level reaches its capacity, it is merged into the next bigger level. The IO-complexity of an LSM depends on parameters such as the growth factor between levels and the data structure chosen at each level, so in order to analyze the complexity of LSMs, we need to pick a specific version. For comparison purposes, we select the version of LSMs that match fractal tree indexes on insertion performance.
Suppose an LSM is implemented via
O(logBN)
\sqrt{B}
N
O\left( | N |
B\right) |
N
M
O\left( | N+M |
B\right) |
N
O\left( | N |
B\right) |
O(\sqrt{B})
k
O\left( | k |
\sqrt{B}\right) |
O\left( | logBN |
\sqrt{B}\right) |
The query time is simply the B-tree query time at each level. The query time into the
i
O(logB
| ||||
B |
)=O(i)
i
| ||||
B |
2 | |
O(log | |
B |
N)
A few notes about LSMs: there are ways to make the queries faster. For example, if only membership queries are required and no successor/predecessor/range queries are, then Bloom filters can be used to speed up queries. Also, the growth factor between levels can be set to some other value, giving a range of insertion/query tradeoffs. However, for every choice of insertion rate, the corresponding fractal tree index has faster queries.
The fractal tree index is a refinement of the Bε tree. Like a Bε tree, it consists of nodes with keys and buffers and realizes the optimal insertion/query tradeoff. The fractal tree index differs in including performance optimization and in extending the functionality. Examples of improved functionality include ACID semantics. B-tree implementations of ACID semantics typically involve locking rows that are involved in an active transactions. Such a scheme works well in a B-tree because both insertions and queries involve fetching the same leaf into memory. Thus, locking an inserted row does not incur an IO penalty. However, in fractal tree indexes, insertions are messages, and a row may reside in more than one node at the same time. Fractal tree indexes therefore require a separate locking structure that is IO-efficient or resides in memory in order to implement the locking involved in implementing ACID semantics.
Fractal tree indexes also have several performance optimizations. First, buffers are themselves indexed in order to speed up searches. Second, leaves are much larger than in B-trees, which allows for greater compression. In fact, the leaves are chosen to be large enough that their access time is dominated by the bandwidth time, and therefore amortizes away the seek and rotational latency. Large leaves are an advantage with large range queries but slow down point queries, which require accessing a small portion of the leaf. The solution implemented in fractal tree indexes is to have large leaves that can be fetched as a whole for fast range queries but are broken into smaller pieces call basement nodes which can be fetched individually. Accessing a basement node is faster than accessing a leaf, because of the reduced bandwidth time. Thus the substructure of leaves in fractal tree indexes, as compared to Bε trees allows both range and point queries to be fast.
Insertions, deletions and updates are inserted as message into buffers that make their way towards the leaves. The messaging infrastructure can be exploited to implement a variety of other operations, some of which are discussed below.
An upsert is a statement that inserts a row if it does not exist and updates it if it does. In a B-tree, an upsert is implemented by first searching for the row and then implementing an insertion or an update, depending on the result of the search. This requires fetching the row into memory if it is not already cached. A fractal tree index can implement an upsert by inserting a special upsert message. Such a message can, in theory, implement arbitrary pieces of code during the update. In practice, four update operations are supported:
x:=constant
x:=x+constant
x:=x-constant
x:=\begin{cases}0&ifx=0\ x-1&ifx\ne0\end{cases}
These correspond to the update operations used in LinkBench,[8] a benchmark proposed by Facebook. By avoiding the initial search, upsert messages can improve the speed of upserts by orders of magnitude.
So far, all message types have modified single rows. However, broadcast messages, which are copied to all outgoing buffers, can modify all rows in a database. For example, broadcast messages can be used to change the format of all rows in a database. Although the total work required to change all rows is unchanged over the brute-force method of traversing the table, the latency is improved, since, once the message is injected into the root buffer, all subsequent queries will be able to apply the schema modification to any rows they encounter. The schema change is immediate and the work is deferred to such a time when buffers overflow and leaves would have gotten updated anyway.
The fractal tree index has been implemented and commercialized by Tokutek. It is available as TokuDB as a storage engine for MySQL and MariaDB, and as TokuMX, a more complete integration with MongoDB. Fractal tree indexes have also been used in prototype filesystems, TokuFS[4] and BetrFS.[5]