语境
这个问题与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个块)
- 二进制搜索排序的块以找到密钥
- 从值获取数据文件的偏移量
- 使用偏移量在数据文件中查找记录
- 将数据返回给调用者
问题设定...
好的,这里是问题所在...
步骤#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建议的潜在弊端)
为了最好地与我自己继续进行对话,我意识到了以下缺点:
如果每个“块”都以偏移量数据开头,那么您将无法在以后的配置中调整块大小,因为您可能最终会读取不是以标题正确开头的数据或包含多个标题。
如果您正在索引巨大的键值(例如有人试图索引char(8192)或blob(8192)的列),则键可能不适合单个块,并且需要并排跨两个块溢出。这意味着您的第一个块将具有偏移头,而第二个块将立即以关键数据开始。
所有这些的解决方案是具有一个不可调整的固定数据库块大小,并围绕它开发标题块数据结构...例如,您将所有块大小都固定为4KB(无论如何通常都是最佳的),并写一个很小的块。块头,其开头包括“块类型”。如果它是正常块,则紧随其后的是块头。如果它是“溢出”类型,那么紧跟在块头之后的是原始密钥数据。
更新(可能很棒的方面)
在块被读取为一系列字节并偏移量解码之后;从技术上讲,您可以简单地将要搜索的密钥编码为原始字节,然后对字节流进行直接比较。
找到所需的密钥后,即可对指针进行解码和跟踪。
格雷格想法的另一个令人敬畏的副作用!这里进行CPU时间优化的潜力足够大,以至于获得所有这些都值得设置固定的块大小。