数据库如何存储可变长度字段的索引键值(在磁盘上)?


16

语境

这个问题与SQL和NoSQL数据库系统中索引的低级实现细节有关。索引的实际结构(B +树,哈希,SSTable等)无关紧要,因为该问题专门涉及存储在任何这些实现​​的单个节点内的

背景

在SQL(如MySQL的)和NoSQL(CouchDB的,MongoDB的,等等)数据库,如果您在列或数据的JSON文档字段建立索引,你实际上是导致数据库做的就是创建本质上所有的排序列表这些值以及与该值有关的记录所在的主数据文件中的文件偏移量。

(为简单起见,我可能会手动放弃特定展示的其他深奥细节)

简单经典SQL示例

考虑一个标准的SQL表,该表具有一个简单的32位int主键,我们可以在该主键上创建索引,我们最终将获得一个排序后的整数键的索引在磁盘上的索引,并与数据文件中的64位偏移量相关联,其中记录的生命,例如:

id   | offset
--------------
1    | 1375
2    | 1413
3    | 1786

索引中键的磁盘上表示形式类似于以下内容:

[4-bytes][8-bytes] --> 12 bytes for each indexed value

坚持使用文件系统和数据库系统优化磁盘I / O的标准经验法则,假设您将密钥存储在磁盘上的4KB块中,这意味着:

4096 bytes / 12 bytes per key = 341 keys per block

忽略索引的整体结构(B +树,哈希,排序列表等),我们一次将341个键的块读写到内存中,然后根据需要返回到磁盘。

查询范例

使用上一部分中的信息,假设有一个查询“ id = 2”,传统的数据库索引查找如下:

  1. 读取索引的根(在这种情况下为1个块)
  2. 二进制搜索排序的块以找到密钥
  3. 从值获取数据文件的偏移量
  4. 使用偏移量在数据文件中查找记录
  5. 将数据返回给调用者

问题设定...

好的,这里是问题所在...

步骤#2是最重要的部分,它允许这些查询在O(logn)时间内执行...信息必须进行排序,但是您必须能够以快速排序的方式遍历列表...更多特别是,您必须能够随意跳转到定义明确的偏移量,以读取该位置的索引键值。

读完该块后,您必须能够立即跳至第170个位置,读取键值,然后查看所要查找的是该位置的GT还是LT(依此类推等等)。

像这样在上面的示例(每个键4字节然后8字节)的情况下,键值大小都已定义好,因此您能够在块中跳转数据的唯一方法是。

好的,这就是我要进行高效索引设计的地方...对于SQL数据库中的varchar列,或更具体地说,对于文档数据库(如CouchDB或NoSQL)中的完全自由格式的字段,您要索引的任何字段都可以是length 如何实现构建索引所依据的索引结构块内的键值?

例如,假设您在CouchDB中为ID使用顺序计数器,并且正在为推特编制索引...几个月后,您的值将从“ 1”变为“ 100,000,000,000”。

假设您在第1天在数据库上建立索引,而当数据库中只有4条推文时,CouchDB可能会倾向于对索引块内的键值使用以下构造:

[1-byte][8-bytes] <-- 9 bytes
4096 / 9 = 455 keys per block

有时这会中断,您需要可变数量的字节来将键值存储在索引中。

如果您决定索引一个真正可变长度的字段(例如“ tweet_message”之类的东西),则这一点更为明显。

由于密钥本身的长度是完全可变的,并且在创建和更新索引时数据库无法智能地猜测“最大密钥大小”,这些密钥实际上如何存储在这些数据库中代表索引段的块中?

显然,如果您的密钥是可变大小的,并且您读入了一个密钥块,则不仅不知道该块中实际有多少个密钥,而且您也不知道如何跳到列表的中间以进行二进制操作搜索他们。

这就是我被绊倒的地方。

使用经典SQL数据库中的静态类型字段(例如bool,int,char等),我知道索引可以预定义键长度并坚持下去...但是在这个文档数据存储世界中,困扰他们如何有效地在磁盘上建模此数据,以便仍可以在O(logn)时间内对其进行扫描,并且希望在此进行任何澄清。

请让我知道是否需要任何澄清!

更新(格雷格的答案)

请查看我对Greg的回答的评论。经过一个多星期的研究,我认为他确实偶然发现了一个非常简单而高效的建议,即实践会非常容易实现和使用,同时还能避免避免忽略关键值的反序列化,从而获得巨大的性能。

我研究了3个单独的DBMS实现(CouchDB,kivaloo和InnoDB),它们通过在执行环境(erlang / C)中搜索值之前将整个块反序列化为内部数据结构来处理此问题。

我认为这对Greg的建议是如此出色。正常的2048块大小通常具有50个或更小的偏移量,因此需要读入的数字非常小。

更新(Greg建议的潜在弊端)

为了最好地与我自己继续进行对话,我意识到了以下缺点:

  1. 如果每个“块”都以偏移量数据开头,那么您将无法在以后的配置中调整块大小,因为您可能最终会读取不是以标题正确开头的数据或包含多个标题。

  2. 如果您正在索引巨大的键值(例如有人试图索引char(8192)或blob(8192)的列),则键可能不适合单个块,并且需要并排跨两个块溢出。这意味着您的第一个块将具有偏移头,而第二个块将立即以关键数据开始。

所有这些的解决方案是具有一个不可调整的固定数据库块大小,并围绕它开发标题块数据结构...例如,您将所有块大小都固定为4KB(无论如何通常都是最佳的),并写一个很小的块。块头,其开头包括“块类型”。如果它是正常块,则紧随其后的是块头。如果它是“溢出”类型,那么紧跟在块头之后的是原始密钥数据。

更新(可能很棒的方面)

在块被读取为一系列字节并偏移量解码之后;从技术上讲,您可以简单地将要搜索的密钥编码为原始字节,然后对字节流进行直接比较。

找到所需的密钥后,即可对指针进行解码和跟踪。

格雷格想法的另一个令人敬畏的副作用!这里进行CPU时间优化的潜力足够大,以至于获得所有这些都值得设置固定的块大小。


对于对此主题感兴趣的任何人,Redis的首席开发人员都在尝试为Redis实现已失效的“磁盘存储”组件时遇到了这个确切的问题。他最初选择了32字节的“足够大”的静态密钥大小,但意识到潜在的问题,而是选择存储密钥的哈希值(sha1或md5)以保持大小一致。这杀死了执行远程查询的能力,但确实可以很好地平衡树的FWIW。此处的详细信息redis.hackyhack.net/2011-01-12.html
Riyad Kalla

我发现了一些更多信息。看起来SQLite可以限制键的大小,或者实际上它会在某个上限处截断键值,并将其余的值放在磁盘上的“溢出页”中。当随机I / O加倍时,这可能会使对巨大键的查询变得令人恐惧。在此处向下滚动到“ B树页面”部分sqlite.org/fileformat2.html
Riyad Kalla

Answers:


7

您可以将索引作为固定大小的偏移量列表存储在包含关键数据的块中。例如:

+--------------+
| 3            | number of entries
+--------------+
| 16           | offset of first key data
+--------------+
| 24           | offset of second key data
+--------------+
| 39           | offset of third key data
+--------------+
| key one |
+----------------+
| key number two |
+-----------------------+
| this is the third key |
+-----------------------+

(好吧,关键数据将在一个真实的示例中进行排序,但是您会明白的)。

请注意,这不一定反映任何数据库中实际上如何构造索引块。这仅仅是你如何一个例子可以组织索引数据的块,其中关键数据的长度是可变的。


Greg,我尚未选择您的答案作为实际答案,因为我希望获得更多反馈,并对其他DBMS进行更多研究(我将评论添加到原始Q中)。到目前为止,最常见的方法似乎是上限,然后是溢出表中的其余键,仅在需要完整键时才检查该键。没有那么优雅。您的解决方案具有我喜欢的优雅之处,但是在某些极端情况下,键会破坏您的页面大小,您的方式仍然需要一个溢出表或只是不允许它。
里亚德·卡拉

我没有足够的空间...简而言之,如果数据库设计人员可以在密钥大小上设置一些硬性限制,那么我认为您的方法是最有效和灵活的。空间和CPU效率的完美结合。溢出表更灵活,但是对于向不断溢出的键的查询中添加随机I / O来说可能很棒。感谢您的投入!
里亚德·卡拉

格雷格,我一直在思考这个问题,正在寻找替代解决方案,我想您已将它与偏移头想法联系在一起。如果将块保持较小,则可以使用8位(1字节)的偏移量,而对于更大的块,即使是128KB或256KB的块(应该是4或8字节的密钥),16位也是最安全的。最大的胜利是,您可以读取偏移数据多么便宜又快速,以及因此节省了多少反序列化。很好的建议,再次感谢您。
里亚德·卡拉

这也是UpscaleDB使用的方法:upscaledb.com/about.html#varlength
马修罗迪奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.