continue to re-check the translation of chapter 3

This commit is contained in:
Gang Yin 2021-12-01 12:15:15 +08:00
parent fc565f1202
commit 313d25ace1
2 changed files with 96 additions and 92 deletions

94
ch3.md
View File

@ -88,7 +88,7 @@
**图3-1 以类CSV格式存储键值对的日志并使用内存散列映射进行索引。**
听上去简单但这是一个可行的方法。现实中Bitcask实际上就是这么做的Riak中默认的存储引擎【3】。 Bitcask提供高性能的读取和写入操作但要求所有的键必须能放入可用内存中因为散列映射完全保留在内存中。而数据值可以使用比可用内存更多的空间因为可以在磁盘上通过一次`seek`操作来加载所需部分如果数据文件的那部分已经在文件系统缓存中则读取根本不需要任何磁盘I/O。
听上去简单但这是一个可行的方法。现实中Bitcask实际上就是这么做的Riak中默认的存储引擎【3】。 Bitcask提供高性能的读取和写入操作但要求所有的键必须能放入可用内存中因为散列映射完全保留在内存中。而数据值可以使用比可用内存更多的空间因为可以在磁盘上通过一次磁盘寻道操作来加载所需部分如果数据文件的那部分已经在文件系统缓存中则读取根本不需要任何磁盘I/O。
像Bitcask这样的存储引擎非常适合每个键的值经常更新的情况。例如键可能是某个猫咪视频的网址URL而值可能是该视频被播放的次数每次有人点击播放按钮时递增。在这种类型的工作负载中有很多写操作但是没有太多不同的键 —— 每个键有很多的写操作,但是将所有键保存在内存中是可行的。
@ -158,7 +158,7 @@ CSV不是日志的最佳格式。使用二进制格式更快更简单
如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值一定比另一个段中的所有值都更近(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。
2. 为了在文件中找到一个特定的键,你不再需要在内存中保存所有键的索引。以[图3-5](img/fig3-5.png)为例:假设你正在内存中寻找键 `handiwork`,但是你不知道段文件中该关键字的确切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由于排序特性,你知道 `handiwork` 必须出现在这两者之间。这意味着你可以跳到 `handbag` 的偏移位置并从那里扫描,直到你找到 `handiwork`(或没找到,如果该文件中没有该键)。
2. 为了在文件中找到一个特定的键,你不再需要在内存中保存所有键的索引。以[图3-5](img/fig3-5.png)为例:假设你正在内存中寻找键 `handiwork`,但是你不知道这个键在段文件中的确切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由于排序特性,你知道 `handiwork` 必须出现在这两者之间。这意味着你可以跳到 `handbag` 的偏移位置并从那里扫描,直到你找到 `handiwork`(或没找到,如果该文件中没有该键)。
![](img/fig3-5.png)
@ -222,11 +222,11 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
一个页面会被指定为B树的根在索引中查找一个键时就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键引用之间的键指明了引用子页面的键范围。
在[图3-6](img/fig3-6.png)的例子中,我们正在寻找251 ,所以我们知道我们需要跟踪边界 200 300 之间的页面引用。这将我们带到一个类似的页面进一步将200到300的范围拆分到子范围。
在[图3-6](img/fig3-6.png)的例子中我们正在寻找键251 所以我们知道我们需要跟踪边界200和300之间的页面引用。这将我们带到一个类似的页面进一步将200到300的范围拆分到子范围。
最终我们将到达某个包含单个键的页面叶子页面leaf page该页面或者直接包含每个键的值或者包含了对可以找到值的页面的引用。
在B树的一个页面中对子页面的引用的数量称为分支因子。例如在[图3-6](img/fig3-6.png)中,分支因子是 6 。在实践中,分支因子取决于存储页面引用和范围边界所需的空间量,但通常是几百个。
在B树的一个页面中对子页面的引用的数量称为分支因子。例如在[图3-6](img/fig3-6.png)中分支因子是6。在实践中分支因子取决于存储页面引用和范围边界所需的空间量但通常是几百个。
如果要更新B树中现有键的值需要搜索包含该键的叶子页面更改该页面中的值并将该页面写回到磁盘对该页面的任何引用都将保持有效。如果你想添加一个新的键你需要找到其范围能包含新键的页面并将其添加到该页面。如果页面中没有足够的可用空间容纳新键则将其分成两个半满页面并更新父页面以反映新的键范围分区如[图3-7](img/fig3-7.png)所示[^ii]。
@ -236,7 +236,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
[^ii]: 向B树中插入一个新的键是相当符合直觉的但删除一个键同时保持树平衡就会牵扯很多其他东西了【2】。
这个算法可以确保树保持平衡具有n个键的B树总是具有 $O(log n)$ 的深度。大多数数据库可以放入一个三到四层的B树所以你不需要追踪多个页面引用来找到你正在查找的页面。分支因子为500的4KB页面的四树可以存储多达256TB的数据。
这个算法可以确保树保持平衡具有n个键的B树总是具有 $O(log n)$ 的深度。大多数数据库可以放入一个三到四层的B树所以你不需要追踪多个页面引用来找到你正在查找的页面。分支因子为500的4KB页面的四树可以存储多达256TB的数据。
#### 让B树更可靠
@ -250,116 +250,118 @@ B树的基本底层写操作是用新数据覆写硬盘上的页面并假定
另外还有一个更新页面的复杂情况是如果多个线程要同时访问B树则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常是通过使用**锁存器latches**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰新接收到的查询,并且能够时不时地将旧的段原子交换为新的段。
#### B树优化
#### B树优化
由于B树已经存在了很久所以并不奇怪这么多年下来有很多优化的设计被开发出来仅举几例
* 一些数据库如LMDB使用写时复制方案【21】而不是覆盖页面并维护WAL进行崩溃恢复。修改的页面被写入到不同的位置并且树中的父页面的新版本被创建指向新的位置。这种方法对于并发控制也很有用我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
* 我们可以通过不存储整个键,而是缩短其大小,来节省页面空间。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次。
* 通常页面可以放置在磁盘上的任何位置没有什么要求附近的键放在页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围那么这种按页面存储的布局可能会效率低下因为每个读取的页面都可能需要磁盘寻道。因此许多B树实现尝试布局树使得叶子页面按顺序出现在磁盘上。但是随着树的增长维持这个顺序是很困难的。相比之下由于LSM树在合并过程中一次又一次地重写存储的大部分所以它们更容易使顺序键在磁盘上彼此靠近。
* 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
* B树的变体如分形树【22】借用一些日志结构的思想来减少磁盘寻道而且它们与分形无关
* 一些数据库如LMDB使用写时复制方案【21】而不是覆盖页面并维护WAL以支持崩溃恢复。修改的页面被写入到不同的位置并且还在树中创建了父页面的新版本以指向新的位置。这种方法对于并发控制也很有用我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
* 我们可以通过不存储整个键,而是缩短其大小,来节省页面空间。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此也就允许更少的层级[^iii]。
* 通常页面可以放置在磁盘上的任何位置没有什么要求相邻键范围的页面也放在磁盘上相邻的区域。如果某个查询需要按照排序顺序扫描大部分的键范围那么这种按页面存储的布局可能会效率低下因为每次页面读取可能都需要进行磁盘寻道。因此许多B树的实现在布局树时会尽量使叶子页面按顺序出现在磁盘上。但是随着树的增长要维持这个顺序是很困难的。相比之下由于LSM树在合并过程中一次又一次地重写存储的大部分所以它们更容易使顺序键在磁盘上彼此靠近。
* 额外的指针已被添加到树中。例如,每个叶子页面可以引用其左边和右边的兄弟页面,使得不用跳回父页面就能按顺序对键进行扫描。
* B树的变体如分形树fractal tree【22】借用一些日志结构的思想来减少磁盘寻道而且它们与分形无关
[^iii]: 这个变种有时被称为B+树但因为这个优化已被广泛使用所以经常无法区分于其它的B树变种。
### 比较B树和LSM树
尽管B树实现通常比LSM树实现更成熟但LSM树由于其性能特点也非常有趣。根据经验通常LSM树的写入速度更快而B树的读取速度更快【23】。 LSM树上的读取通常比较慢因为它们必须检查几种不同的数据结构和不同压缩Compaction层级的SSTables。
然而,基准测试通常对工作负载的细节不确定且敏感。 你需要测试具有特定工作负载的系统,以便进行有效的比较。在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
然而,基准测试的结果通常和工作负载的细节相关。你需要用你特有的工作负载来测试系统,以便进行有效的比较。在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
#### LSM树的优点
B树索引必须至少两次写入每一段数据:一次写入预先写入日志,一次写入树页面本身(也许再次分页)。即使在该页面中只有几个字节发生了变化,也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次以免在电源故障的情况下导致页面部分更新【24,25】。
B树索引中的每块数据都必须至少写入两次一次写入预先写入日志WAL一次写入树页面本身如果有分页还需要再写入一次。即使在该页面中只有几个字节发生了变化也需要接受写入整个页面的开销。有些存储引擎甚至会覆写同一个页面两次以免在电源故障的情况下导致页面部分更新【24,25】。
由于反复压缩和合并SSTables日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中每次写入数据库导致对磁盘的多次写入 —— 被称为**写放大write amplification**。需要特别注意的是固态硬盘,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。
由于反复压缩和合并SSTables日志结构索引也会多次重写数据。这种影响 —— 在数据库的生命周期中每次写入数据库导致对磁盘的多次写入 —— 被称为**写放大write amplification**。需要特别注意的是固态硬盘,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入磁盘的次数越多,可用磁盘带宽内它能处理的每秒写入次数越少。
而且LSM树通常能够比B树支持更高的写入吞吐量部分原因是它们有时具有较低的写放大尽管这取决于存储引擎配置和工作负载部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要顺序写入比随机写入快得多。
而且LSM树通常能够比B树支持更高的写入吞吐量部分原因是它们有时具有较低的写放大尽管这取决于存储引擎配置和工作负载部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要顺序写入比随机写入快得多。
LSM树可以被压缩得更好因此经常比B树在磁盘上产生更小的文件。 B树存储引擎会由于分割而留下一些未使用的磁盘空间当页面被拆分或某行不能放入现有页面时页面中的某些空间仍未被使用。由于LSM树不是面向页面的并且定期重写SSTables以去除碎片所以它们具有较低的存储开销特别是当使用分层压缩leveled compaction时【27】。
LSM树可以被压缩得更好因此通常能比B树在磁盘上产生更小的文件。B树存储引擎会由于碎片化fragmentation而留下一些未使用的磁盘空间当页面被拆分或某行不能放入现有页面时页面中的某些空间仍未被使用。由于LSM树不是面向页面的并且会通过定期重写SSTables以去除碎片所以它们具有较低的存储开销特别是当使用分层压缩leveled compaction时【27】。
在许多固态硬盘上固件内部使用日志结构化算法将随机写入转变为顺序写入底层存储芯片因此存储引擎写入模式的影响不太明显【19】。但是较低的写入放大率和减少的碎片对SSD仍然有利更紧凑地表示数据可在可用的I/O带宽内提供更多的读取和写入请求。
在许多固态硬盘上,固件内部使用日志结构化算法,将随机写入转变为顺序写入底层存储芯片因此存储引擎写入模式的影响不太明显【19】。但是较低的写入放大率和减少的碎片仍然对固态硬盘更有利更紧凑地表示数据允许在可用的I/O带宽内处理更多的读取和写入请求。
#### LSM树的缺点
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(请参阅“[描述性能](ch1.md#描述性能)”),对日志结构化存储引擎的查询响应时间有时会相当长而B树的行为则相对更具可预测性【28】。
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试增量地执行压缩以尽量不影响并发访问,但是磁盘资源有限,所以很容易发生某个请求需要等待磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是日志结构化存储引擎在更高百分位的响应时间(请参阅“[描述性能](ch1.md#描述性能)”有时会相当长而B树的行为则相对更具可预测性【28】。
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录日志和刷新内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
如果写入吞吐量很高并且压缩没有仔细配置压缩跟不上写入速率。在这种情况下磁盘上未合并段的数量不断增加直到磁盘空间用完读取速度也会减慢因为它们需要检查更多段文件。通常情况下即使压缩无法跟上基于SSTable的存储引擎也不会限制传入写入的速率所以你需要进行明确的监控来检测这种情况【29,30】。
如果写入吞吐量很高,并且压缩没有仔细配置有可能导致压缩跟不上写入速率。在这种情况下,磁盘上未合并段的数量不断增加,直到磁盘空间用完,读取速度也会减慢,因为它们需要检查更多段文件。通常情况下即使压缩无法跟上基于SSTable的存储引擎也不会限制传入写入的速率所以你需要进行明确的监控来检测这种情况【29,30】。
B树的一个优点是每个键只存在于索引中的一个位置而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得B树在想要提供强大的事务语义的数据库中很有吸引力在许多关系数据库中事务隔离是通过在键范围上使用锁来实现的在B树索引中这些锁可以直接连接到树【5】。在[第七章](ch7.md)中,我们将更详细地讨论这一点。
B树的一个优点是每个键只存在于索引中的一个位置而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得B树在想要提供强大的事务语义的数据库中很有吸引力在许多关系数据库中事务隔离是通过在键范围上使用锁来实现的在B树索引中这些锁可以直接附加到树上【5】。在[第七章](ch7.md)中,我们将更详细地讨论这一点。
B树在数据库体系结构中是非常根深蒂固的,为许多工作负载提供始终如一的良好性能,所以它们不可能很快就会消失。在新的数据存储中,日志结构化索引变得越来越流行。没有快速和容易的规则来确定哪种类型的存储引擎对你的场景更好,所以值得进行一些经验上的测试
B树在数据库构中是非常根深蒂固的,为许多工作负载提供始终如一的良好性能,所以它们不可能很快就会消失。在新的数据存储中,日志结构化索引变得越来越流行。没有快速和容易的规则来确定哪种类型的存储引擎对你的场景更好,所以值得去通过一些测试来得到相关的经验
### 其他索引结构
到目前为止,我们只讨论了键值索引,它们就像关系模型中的**主键primary key** 索引。主键唯一标识关系表中的一行或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键或ID引用该行/文档/顶点,并且索引用于解析这样的引用。
到目前为止,我们只讨论了键值索引,它们就像关系模型中的**主键primary key** 索引。主键唯一标识关系表中的一行或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键或ID引用该行/文档/顶点,索引就被用于解析这样的引用。
次级索引也很常见。在关系数据库中,你可以使用 `CREATE INDEX` 命令在同一个表上创建多个次级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第二章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个次级索引,以便你可以在每个表中找到属于同一用户的所有行。
次级索引secondary indexes也很常见。在关系数据库中,你可以使用 `CREATE INDEX` 命令在同一个表上创建多个次级索引,而且这些索引通常对于有效地执行联接join而言至关重要。例如,在[第二章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个次级索引,以便你可以在每个表中找到属于同一用户的所有行。
一个次级索引可以很容易地从一个键值索引构建。主要的不同是键不是唯一的。即可能有许多行(文档,顶点)具有相同的键。这可以通过两种方式来解决:或者通过使索引中的每个值,成为匹配行标识符的列表(如全文索引中的发布列表),或者通过向每个索引添加行标识符来使每个关键字唯一。无论哪种方式B树和日志结构索引都可以用作次级索引。
次级索引可以很容易地从键值索引构建。次级索引主要的不同是键不是唯一的,即可能有许多行(文档,顶点)具有相同的键。这可以通过两种方式来解决:或者将匹配行标识符的列表作为索引里的值(就像全文索引中的记录列表),或者通过向每个键添加行标识符来使键唯一。无论哪种方式B树和日志结构索引都可以用作次级索引。
#### 将值存储在索引中
索引中的键是查询搜索的内容,而其值可以是以下两种情况之一:它可以是所讨论的实际行(文档,顶点),也可以是对存储在别处的行的引用。在后一种情况下,行被存储的地方被称为**堆文件heap file**,并且存储的数据没有特定的顺序(它可以是仅追加的,或者可以跟踪被删除的行以便用新数据覆盖它们后来)。堆文件方法很常见,因为它避免了在存在多个次级索引时复制数据:每个索引只引用堆文件中的一个位置,实际的数据保存在一个地方。
索引中的键是查询搜索的内容,而其值可以是以下两种情况之一:它可以是实际行(文档,顶点),也可以是对存储在别处的行的引用。在后一种情况下,行被存储的地方被称为**堆文件heap file**,并且存储的数据没有特定的顺序(它可以是仅追加的,或者可以跟踪被删除的行以便后续可以用新数据进行覆盖)。堆文件方法很常见,因为它避免了在存在多个次级索引时对数据的复制:每个索引只引用堆文件中的一个位置,实际的数据保存在一个地方。
在不更改键的情况下更新值时堆文件方法可以非常高效只要新值的字节数不大于旧值就可以覆盖该记录。如果新值更大情况会更复杂因为它可能需要移到堆中有足够空间的新位置。在这种情况下要么所有的索引都需要更新以指向记录的新堆位置或者在旧堆位置留下一个转发指针【5】。
在某些情况下从索引到堆文件的额外跳跃对读取来说性能损失太大因此可能希望将索引行直接存储在索引中。这被称为聚集索引。例如在MySQL的InnoDB存储引擎中表的主键总是一个聚集索引次级索引则引用主键而不是堆文件中的位置【31】。在SQL Server中可以为每个表指定一个聚集索引【32】。
在某些情况下,从索引到堆文件的额外跳跃对读取来说性能损失太大,因此可能希望将索引行直接存储在索引中。这被称为聚集索引clustered index。例如在MySQL的InnoDB存储引擎中表的主键总是一个聚集索引次级索引则引用主键而不是堆文件中的位置【31】。在SQL Server中可以为每个表指定一个聚集索引【32】。
**聚集索引clustered index** (在索引中存储所有行数据)和 **非聚集索引nonclustered index** (仅在索引中存储对数据的引用)之间的折衷被称为 **包含列的索引index with included columns** 或**覆盖索引covering index**其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询(这种情况叫做:索引 **覆盖cover** 了查询【32】。
**聚集索引**(在索引中存储所有行数据)和 **非聚集索引**(仅在索引中存储对数据的引用)之间的折衷被称为 **覆盖索引covering index** 或 **包含列的索引index with included columns**其在索引内存储表的一部分列【33】。这允许通过单独使用索引来处理一些查询(这种情况叫做:索引 **覆盖cover** 了查询【32】。
与任何类型的数据重复一样,聚集和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应看到任何因为重复而导致的不一致。
与任何类型的数据重复一样,聚集索引和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应看到任何因为重复而导致的不一致。
#### 多列索引
至今讨论的索引只是将一个键映射到一个值。如果我们需要同时查询一个表中的多个列(或文档中的多个字段),这显然是不够的。
最常见的多列索引被称为 **连接索引concatenated index** ,它通过将一列的值追加到另一列后面,简单地将多个字段组合成一个键(索引定义中指定了字段的连接顺序)。这就像一个老式的纸质电话簿,它提供了一个从(姓,名)到电话号码的索引。由于排序顺序,索引可以用来查找所有具有特定姓氏的人,或所有具有特定姓-名组合的人。**然而,如果你想找到所有具有特定名字的人,这个索引是没有用的**
最常见的多列索引被称为 **连接索引concatenated index** ,它通过将一列的值追加到另一列后面,简单地将多个字段组合成一个键(索引定义中指定了字段的连接顺序)。这就像一个老式的纸质电话簿,它提供了一个从(姓,名)到电话号码的索引。由于排序顺序,索引可以用来查找所有具有特定姓氏的人,或所有具有特定姓氏-名字组合的人。但如果你想找到所有具有特定名字的人,这个索引是没有用的。
**多维索引multi-dimensional index** 是一种查询多个列的更一般的方法,这对于地理空间数据尤为重要。例如,餐厅搜索网站可能有一个数据库,其中包含每个餐厅的经度和纬度。当用户在地图上查看餐馆时,网站需要搜索用户正在查看的矩形地图区域内的所有餐馆。这需要一个二维范围查询,如下所示:
```sql
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
AND longitude > -0.1162 AND longitude < -0.1004;
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
AND longitude > -0.1162 AND longitude < -0.1004;
```
一个标准的B树或者LSM树索引不能够高效地响应这种查询:它可以返回一个纬度范围内的所有餐馆(但经度可能是任意值),或者返回在同一个经度范围内的所有餐馆(但纬度可能是北极和南极之间的任意地方),但不能同时满足。
一个标准的B树或者LSM树索引不能够高效地处理这种查询:它可以返回一个纬度范围内的所有餐馆(但经度可能是任意值),或者返回在同一个经度范围内的所有餐馆(但纬度可能是北极和南极之间的任意地方),但不能同时满足两个条件
一种选择是使用空间填充曲线将二维位置转换为单个数字然后使用常规B树索引【34】。更普遍的是使用特殊化的空间索引例如R树。例如PostGIS使用PostgreSQL的通用Gist工具【35】将地理空间索引实现为R树。这里我们没有足够的地方来描述R树但是有大量的文献可供参考。
一种选择是使用空间填充曲线将二维位置转换为单个数字然后使用常规B树索引【34】。更普遍的是使用特殊化的空间索引例如R树。例如PostGIS使用PostgreSQL的通用GiST工具【35】将地理空间索引实现为R树。这里我们没有足够的地方来描述R树但是有大量的文献可供参考。
一个有趣的主意是,多维索引不仅可以用于地理位置。例如,在电子商务网站上可以使用维度红色绿色蓝色上的三维索引来搜索特定颜色范围内的产品也可以在天气观测数据库中搜索二维日期温度的指数以便有效地搜索2013年的温度在25至30°C之间的所有观测资料。使用一维索引你将不得不扫描2013年的所有记录不管温度如何然后通过温度进行过滤反之亦然。 二维索引可以同时通过时间戳和温度来收窄数据集。这个技术被HyperDex使用【36】。
有趣的是,多维索引不仅可以用于地理位置。例如,在电子商务网站上可以使用建立在绿维度上的三维索引来搜索特定颜色范围内的产品也可以在天气观测数据库中建立日期温度的二维索引以便有效地搜索2013年内的温度在25至30°C之间的所有观测资料。如果使用一维索引你将不得不扫描2013年的所有记录不管温度如何然后通过温度进行过滤或者反之亦然。 二维索引可以同时通过时间戳和温度来收窄数据集。这个技术被HyperDex使用【36】。
#### 全文搜索和模糊索引
到目前为止所讨论的所有索引都假定你有确切的数据,并允许你查询键的确切值或具有排序顺序的键的值范围。他们不允许你做的是搜索类似的键,如拼写错误的单词。这种模糊的查询需要不同的技术。
例如,全文搜索引擎通常允许搜索一个单词以扩展为包括该单词的同义词,忽略单词的语法变体,并且搜索在相同文档中彼此靠近的单词的出现,并且支持各种其他功能取决于文本的语言分析。为了处理文档或查询中的拼写错误Lucene能够在一定的编辑距离内搜索文本编辑距离1意味着添加删除或替换了一个字母【37】。
例如,全文搜索引擎通常允许搜索一个单词以扩展为包括该单词的同义词,忽略单词的语法变体,搜索在相同文档中彼此靠近的单词的出现,并且支持各种其他取决于文本的语言分析功能。为了处理文档或查询中的拼写错误Lucene能够在一定的编辑距离编辑距离1意味着添加删除或替换了一个字母内搜索文本【37】。
正如“[用SSTables制作LSM树](#用SSTables制作LSM树)”中所提到的Lucene为其词典使用了一个类似于SSTable的结构。这个结构需要一个小的内存索引告诉查询需要在排序文件中哪个偏移量查找键。在LevelDB中这个内存中的索引是一些键的稀疏集合但在Lucene中内存中的索引是键中字符的有限状态自动机类似于trie 【38】。这个自动机可以转换成Levenshtein自动机它支持在给定的编辑距离内有效地搜索单词【39】。
其他的模糊搜索技术正朝着文档分类和机器学习的方向发展。有关更多详细信息请参阅信息检索教科书例如【40】。
其他的模糊搜索技术正朝着文档分类和机器学习的方向发展。更多详细信息请参阅信息检索教科书例如【40】。
#### 在内存中存储一切
本章到目前为止讨论的数据结构都是对磁盘限制的回答。与主内存相比,磁盘处理起来很尴尬。对于磁盘和SSD如果要在读取和写入时获得良好性能则需要仔细地布置磁盘上的数据。但是我们容忍这种尴尬因为磁盘有两个显著的优点它们是持久的它们的内容在电源关闭时不会丢失并且每GB的成本比RAM低。
本章到目前为止讨论的数据结构都是对磁盘限制的应对。与主内存相比,磁盘处理起来很麻烦。对于磁盘和SSD如果要在读取和写入时获得良好性能则需要仔细地布置磁盘上的数据。但是我们能容忍这种麻烦因为磁盘有两个显著的优点它们是持久的它们的内容在电源关闭时不会丢失并且每GB的成本比RAM低。
随着RAM变得更便宜每GB成本的论据被侵蚀了。许多数据集不是那么大所以将它们全部保存在内存中是非常可行的可能分布在多个机器上。这导致了内存数据库的发展。
随着RAM变得更便宜每GB成本的论据被侵蚀了。许多数据集不是那么大所以将它们全部保存在内存中是非常可行的包括可能分布在多个机器上。这导致了内存数据库的发展。
某些内存中的键值存储如Memcached仅用于缓存在重新启动计算机时丢失的数据是可以接受的。但其他内存数据库的目标是持久性可以通过特殊的硬件例如电池供电的RAM将更改日志写入磁盘将定时快照写入磁盘或者将内存中的状态复制到其他机器上。
某些内存中的键值存储如Memcached仅用于缓存在重新启动计算机时丢失的数据是可以接受的。但其他内存数据库的目标是持久性可以通过特殊的硬件例如电池供电的RAM来实现也可以将更改日志写入磁盘,还可以将定时快照写入磁盘或者将内存中的状态复制到其他机器上。
内存数据库重新启动时,需要从磁盘或通过网络从副本重新加载其状态(除非使用特殊的硬件)。尽管写入磁盘,它仍然是一个内存数据库,因为磁盘仅出于持久性目的进行日志追加,读取完全由内存提供。写入磁盘也具有运维优势:磁盘上的文件可以很容易地由外部实用程序进行备份,检查和分析。
内存数据库重新启动时,需要从磁盘或通过网络从副本重新加载其状态(除非使用特殊的硬件)。尽管写入磁盘,它仍然是一个内存数据库,因为磁盘仅出于持久性目的进行日志追加,读取请求完全由内存来处理。写入磁盘同时还有运维上的好外:磁盘上的文件可以很容易地由外部实用程序进行备份、检查和分析。
诸如VoltDBMemSQL和Oracle TimesTen等产品是具有关系模型的内存数据库供应商声称通过消除与管理磁盘上的数据结构相关的所有开销他们可以提供巨大的性能改进【41,42】。 RAM Cloud是一个开源的内存键值存储器具有持久性存储器中的数据以及磁盘上的数据使用日志结构化方法【43】。 Redis和Couchbase通过异步写入磁盘提供了较弱的持久性。
诸如VoltDBMemSQL和Oracle TimesTen等产品是具有关系模型的内存数据库供应商声称通过消除与管理磁盘上的数据结构相关的所有开销他们可以提供巨大的性能改进【41,42】。 RAM Cloud是一个开源的内存键值存储器具有持久性内存和磁盘上的数据都使用日志结构化方法【43】。 Redis和Couchbase通过异步写入磁盘提供了较弱的持久性。
反直觉的是,内存数据库的性能优势并不是因为它们不需要从磁盘读取的事实。即使是基于磁盘的存储引擎也可能永远不需要从磁盘读取,因为操作系统在内存中缓存了最近使用的磁盘块。相反,它们更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销【44】。
反直觉的是,内存数据库的性能优势并不是因为它们不需要从磁盘读取的事实。只要有足够的内存即使是基于磁盘的存储引擎也可能永远不需要从磁盘读取因为操作系统在内存中缓存了最近使用的磁盘块。相反它们更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销【44】。
除了性能,内存数据库的另一个有趣的领域是提供难以用基于磁盘的索引实现的数据模型。例如Redis为各种数据结构如优先级队列和集合提供了类似数据库的接口。因为它将所有数据保存在内存中所以它的实现相对简单。
除了性能,内存数据库的另一个有趣的地方是提供了难以用基于磁盘的索引实现的数据模型。例如Redis为各种数据结构如优先级队列和集合提供了类似数据库的接口。因为它将所有数据保存在内存中所以它的实现相对简单。
最近的研究表明内存数据库体系结构可以扩展到支持比可用内存更大的数据集而不必重新采用以磁盘为中心的体系结构【45】。所谓的 **反缓存anti-caching** 方法通过在内存不足的情况下将最近最少使用的数据从内存转移到磁盘并在将来再次访问时将其重新加载到内存中。这与操作系统对虚拟内存和交换文件的操作类似但数据库可以比操作系统更有效地管理内存因为它可以按单个记录的粒度工作而不是整个内存页面。尽管如此这种方法仍然需要索引能完全放入内存中就像本章开头的Bitcask例子
如果 **非易失性存储器(NVM** 技术得到更广泛的应用可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域值得关注。
如果 **非易失性存储器(non-volatile memory, NVM** 技术得到更广泛的应用可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域值得关注。
## 事务处理还是分析?

View File

@ -88,7 +88,7 @@
**圖3-1 以類CSV格式儲存鍵值對的日誌並使用記憶體雜湊對映進行索引。**
聽上去簡單但這是一個可行的方法。現實中Bitcask實際上就是這麼做的Riak中預設的儲存引擎【3】。 Bitcask提供高效能的讀取和寫入操作但要求所有的鍵必須能放入可用記憶體中因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間因為可以在磁碟上透過一次`seek`操作來載入所需部分如果資料檔案的那部分已經在檔案系統快取中則讀取根本不需要任何磁碟I/O。
聽上去簡單但這是一個可行的方法。現實中Bitcask實際上就是這麼做的Riak中預設的儲存引擎【3】。 Bitcask提供高效能的讀取和寫入操作但要求所有的鍵必須能放入可用記憶體中因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間因為可以在磁碟上透過一次磁碟尋道操作來載入所需部分如果資料檔案的那部分已經在檔案系統快取中則讀取根本不需要任何磁碟I/O。
像Bitcask這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如鍵可能是某個貓咪影片的網址URL而值可能是該影片被播放的次數每次有人點選播放按鈕時遞增。在這種型別的工作負載中有很多寫操作但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。
@ -158,7 +158,7 @@ CSV不是日誌的最佳格式。使用二進位制格式更快更簡單
如果在幾個輸入段中出現相同的鍵,該怎麼辦?請記住,每個段都包含在一段時間內寫入資料庫的所有值。這意味著一個輸入段中的所有值一定比另一個段中的所有值都更近(假設我們總是合併相鄰的段)。當多個段包含相同的鍵時,我們可以保留最近段的值,並丟棄舊段中的值。
2. 為了在檔案中找到一個特定的鍵,你不再需要在記憶體中儲存所有鍵的索引。以[圖3-5](../img/fig3-5.png)為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道段檔案中該關鍵字的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著你可以跳到 `handbag` 的偏移位置並從那裡掃描,直到你找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
2. 為了在檔案中找到一個特定的鍵,你不再需要在記憶體中儲存所有鍵的索引。以[圖3-5](../img/fig3-5.png)為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道這個鍵在段檔案中的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著你可以跳到 `handbag` 的偏移位置並從那裡掃描,直到你找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
![](../img/fig3-5.png)
@ -222,11 +222,11 @@ Lucene是Elasticsearch和Solr使用的一種全文搜尋的索引引擎它使
一個頁面會被指定為B樹的根在索引中查詢一個鍵時就從這裡開始。該頁面包含幾個鍵和對子頁面的引用。每個子頁面負責一段連續範圍的鍵引用之間的鍵指明瞭引用子頁面的鍵範圍。
在[圖3-6](../img/fig3-6.png)的例子中,我們正在尋找251 ,所以我們知道我們需要跟蹤邊界 200 300 之間的頁面引用。這將我們帶到一個類似的頁面進一步將200到300的範圍拆分到子範圍。
在[圖3-6](../img/fig3-6.png)的例子中我們正在尋找鍵251 所以我們知道我們需要跟蹤邊界200和300之間的頁面引用。這將我們帶到一個類似的頁面進一步將200到300的範圍拆分到子範圍。
最終我們將到達某個包含單個鍵的頁面葉子頁面leaf page該頁面或者直接包含每個鍵的值或者包含了對可以找到值的頁面的引用。
在B樹的一個頁面中對子頁面的引用的數量稱為分支因子。例如在[圖3-6](../img/fig3-6.png)中,分支因子是 6 。在實踐中,分支因子取決於儲存頁面引用和範圍邊界所需的空間量,但通常是幾百個。
在B樹的一個頁面中對子頁面的引用的數量稱為分支因子。例如在[圖3-6](../img/fig3-6.png)中分支因子是6。在實踐中分支因子取決於儲存頁面引用和範圍邊界所需的空間量但通常是幾百個。
如果要更新B樹中現有鍵的值需要搜尋包含該鍵的葉子頁面更改該頁面中的值並將該頁面寫回到磁碟對該頁面的任何引用都將保持有效。如果你想新增一個新的鍵你需要找到其範圍能包含新鍵的頁面並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵則將其分成兩個半滿頁面並更新父頁面以反映新的鍵範圍分割槽如[圖3-7](../img/fig3-7.png)所示[^ii]。
@ -236,7 +236,7 @@ Lucene是Elasticsearch和Solr使用的一種全文搜尋的索引引擎它使
[^ii]: 向B樹中插入一個新的鍵是相當符合直覺的但刪除一個鍵同時保持樹平衡就會牽扯很多其他東西了【2】。
這個演算法可以確保樹保持平衡具有n個鍵的B樹總是具有 $O(log n)$ 的深度。大多數資料庫可以放入一個三到四層的B樹所以你不需要追蹤多個頁面引用來找到你正在查詢的頁面。分支因子為500的4KB頁面的四樹可以儲存多達256TB的資料。
這個演算法可以確保樹保持平衡具有n個鍵的B樹總是具有 $O(log n)$ 的深度。大多數資料庫可以放入一個三到四層的B樹所以你不需要追蹤多個頁面引用來找到你正在查詢的頁面。分支因子為500的4KB頁面的四樹可以儲存多達256TB的資料。
#### 讓B樹更可靠
@ -250,116 +250,118 @@ B樹的基本底層寫操作是用新資料覆寫硬碟上的頁面並假定
另外還有一個更新頁面的複雜情況是如果多個執行緒要同時訪問B樹則需要仔細的併發控制 —— 否則執行緒可能會看到樹處於不一致的狀態。這通常是透過使用**鎖存器latches**(輕量級鎖)保護樹的資料結構來完成。日誌結構化的方法在這方面更簡單,因為它們在後臺進行所有的合併,而不會干擾新接收到的查詢,並且能夠時不時地將舊的段原子交換為新的段。
#### B樹最佳化
#### B樹最佳化
由於B樹已經存在了很久所以並不奇怪這麼多年下來有很多最佳化的設計被開發出來僅舉幾例
* 一些資料庫如LMDB使用寫時複製方案【21】而不是覆蓋頁面並維護WAL進行崩潰恢復。修改的頁面被寫入到不同的位置並且樹中的父頁面的新版本被建立指向新的位置。這種方法對於併發控制也很有用我們將在“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中看到。
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此更少的層次。
* 通常頁面可以放置在磁碟上的任何位置沒有什麼要求附近的鍵放在頁面附近的磁碟上。如果查詢需要按照排序順序掃描大部分關鍵字範圍那麼這種按頁面儲存的佈局可能會效率低下因為每個讀取的頁面都可能需要磁碟尋道。因此許多B樹實現嘗試佈局樹使得葉子頁面按順序出現在磁碟上。但是隨著樹的增長維持這個順序是很困難的。相比之下由於LSM樹在合併過程中一次又一次地重寫儲存的大部分所以它們更容易使順序鍵在磁碟上彼此靠近。
* 額外的指標已新增到樹中。例如,每個葉子頁面可以在左邊和右邊具有對其兄弟頁面的引用,這允許不跳回父頁面就能順序掃描。
* B樹的變體如分形樹【22】借用一些日誌結構的思想來減少磁碟尋道而且它們與分形無關
* 一些資料庫如LMDB使用寫時複製方案【21】而不是覆蓋頁面並維護WAL以支援崩潰恢復。修改的頁面被寫入到不同的位置並且還在樹中建立了父頁面的新版本以指向新的位置。這種方法對於併發控制也很有用我們將在“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中看到。
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級[^iii]。
* 通常頁面可以放置在磁碟上的任何位置沒有什麼要求相鄰鍵範圍的頁面也放在磁碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍那麼這種按頁面儲存的佈局可能會效率低下因為每次頁面讀取可能都需要進行磁碟尋道。因此許多B樹的實現在佈局樹時會盡量使葉子頁面按順序出現在磁碟上。但是隨著樹的增長要維持這個順序是很困難的。相比之下由於LSM樹在合併過程中一次又一次地重寫儲存的大部分所以它們更容易使順序鍵在磁碟上彼此靠近。
* 額外的指標已被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
* B樹的變體如分形樹fractal tree【22】借用一些日誌結構的思想來減少磁碟尋道而且它們與分形無關
[^iii]: 這個變種有時被稱為B+樹但因為這個最佳化已被廣泛使用所以經常無法區分於其它的B樹變種。
### 比較B樹和LSM樹
儘管B樹實現通常比LSM樹實現更成熟但LSM樹由於其效能特點也非常有趣。根據經驗通常LSM樹的寫入速度更快而B樹的讀取速度更快【23】。 LSM樹上的讀取通常比較慢因為它們必須檢查幾種不同的資料結構和不同壓縮Compaction層級的SSTables。
然而,基準測試通常對工作負載的細節不確定且敏感。 你需要測試具有特定工作負載的系統,以便進行有效的比較。在本節中,我們將簡要討論一些在衡量儲存引擎效能時值得考慮的事情。
然而,基準測試的結果通常和工作負載的細節相關。你需要用你特有的工作負載來測試系統,以便進行有效的比較。在本節中,我們將簡要討論一些在衡量儲存引擎效能時值得考慮的事情。
#### LSM樹的優點
B樹索引必須至少兩次寫入每一段資料:一次寫入預先寫入日誌,一次寫入樹頁面本身(也許再次分頁)。即使在該頁面中只有幾個位元組發生了變化,也需要一次編寫整個頁面的開銷。有些儲存引擎甚至會覆蓋同一個頁面兩次以免在電源故障的情況下導致頁面部分更新【24,25】。
B樹索引中的每塊資料都必須至少寫入兩次一次寫入預先寫入日誌WAL一次寫入樹頁面本身如果有分頁還需要再寫入一次。即使在該頁面中只有幾個位元組發生了變化也需要接受寫入整個頁面的開銷。有些儲存引擎甚至會覆寫同一個頁面兩次以免在電源故障的情況下導致頁面部分更新【24,25】。
由於反覆壓縮和合並SSTables日誌結構索引也會重寫資料。這種影響 —— 在資料庫的生命週期中每次寫入資料庫導致對磁碟的多次寫入 —— 被稱為**寫放大write amplification**。需要特別注意的是固態硬碟,固態硬碟的快閃記憶體壽命在覆寫有限次數後就會耗盡。
由於反覆壓縮和合並SSTables日誌結構索引也會多次重寫資料。這種影響 —— 在資料庫的生命週期中每次寫入資料庫導致對磁碟的多次寫入 —— 被稱為**寫放大write amplification**。需要特別注意的是固態硬碟,固態硬碟的快閃記憶體壽命在覆寫有限次數後就會耗盡。
在寫入繁重的應用程式中,效能瓶頸可能是資料庫可以寫入磁碟的速度。在這種情況下,寫放大會導致直接的效能代價:儲存引擎寫入磁碟的次數越多,可用磁碟頻寬內的每秒寫入次數越少。
在寫入繁重的應用程式中,效能瓶頸可能是資料庫可以寫入磁碟的速度。在這種情況下,寫放大會導致直接的效能代價:儲存引擎寫入磁碟的次數越多,可用磁碟頻寬內它能處理的每秒寫入次數越少。
而且LSM樹通常能夠比B樹支援更高的寫入吞吐量部分原因是它們有時具有較低的寫放大儘管這取決於儲存引擎配置和工作負載部分是因為它們順序地寫入緊湊的SSTable檔案而不是必須覆樹中的幾個頁面【26】。這種差異在磁性硬碟驅動器上尤其重要順序寫入比隨機寫入快得多。
而且LSM樹通常能夠比B樹支援更高的寫入吞吐量部分原因是它們有時具有較低的寫放大儘管這取決於儲存引擎配置和工作負載部分是因為它們順序地寫入緊湊的SSTable檔案而不是必須覆樹中的幾個頁面【26】。這種差異在磁性硬碟驅動器上尤其重要順序寫入比隨機寫入快得多。
LSM樹可以被壓縮得更好因此經常比B樹在磁碟上產生更小的檔案。 B樹儲存引擎會由於分割而留下一些未使用的磁碟空間當頁面被拆分或某行不能放入現有頁面時頁面中的某些空間仍未被使用。由於LSM樹不是面向頁面的並且定期重寫SSTables以去除碎片所以它們具有較低的儲存開銷特別是當使用分層壓縮leveled compaction時【27】。
LSM樹可以被壓縮得更好因此通常能比B樹在磁碟上產生更小的檔案。B樹儲存引擎會由於碎片化fragmentation而留下一些未使用的磁碟空間當頁面被拆分或某行不能放入現有頁面時頁面中的某些空間仍未被使用。由於LSM樹不是面向頁面的並且會透過定期重寫SSTables以去除碎片所以它們具有較低的儲存開銷特別是當使用分層壓縮leveled compaction時【27】。
在許多固態硬碟上韌體內部使用日誌結構化演算法將隨機寫入轉變為順序寫入底層儲存晶片因此儲存引擎寫入模式的影響不太明顯【19】。但是較低的寫入放大率和減少的碎片對SSD仍然有利更緊湊地表示資料可在可用的I/O頻寬內提供更多的讀取和寫入請求。
在許多固態硬碟上,韌體內部使用日誌結構化演算法,將隨機寫入轉變為順序寫入底層儲存晶片因此儲存引擎寫入模式的影響不太明顯【19】。但是較低的寫入放大率和減少的碎片仍然對固態硬碟更有利更緊湊地表示資料允許在可用的I/O頻寬內處理更多的讀取和寫入請求。
#### LSM樹的缺點
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試逐步執行壓縮而不影響併發訪問,但是磁碟資源有限,所以很容易發生請求需要等待磁碟完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是在更高百分比的情況下(請參閱“[描述效能](ch1.md#描述效能)”),對日誌結構化儲存引擎的查詢響應時間有時會相當長而B樹的行為則相對更具可預測性【28】。
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試增量地執行壓縮以儘量不影響併發訪問,但是磁碟資源有限,所以很容易發生某個請求需要等待磁碟完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是日誌結構化儲存引擎在更高百分位的響應時間(請參閱“[描述效能](ch1.md#描述效能)”有時會相當長而B樹的行為則相對更具可預測性【28】。
壓縮的另一個問題出現在高寫入吞吐量:磁碟的有限寫入頻寬需要在初始寫入(記錄和重新整理記憶體表到磁碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全磁碟頻寬進行初始寫入,但資料庫越大,壓縮所需的磁碟頻寬就越多。
壓縮的另一個問題出現在高寫入吞吐量:磁碟的有限寫入頻寬需要在初始寫入(記錄日誌和重新整理記憶體表到磁碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全磁碟頻寬進行初始寫入,但資料庫越大,壓縮所需的磁碟頻寬就越多。
如果寫入吞吐量很高並且壓縮沒有仔細配置壓縮跟不上寫入速率。在這種情況下磁碟上未合併段的數量不斷增加直到磁碟空間用完讀取速度也會減慢因為它們需要檢查更多段檔案。通常情況下即使壓縮無法跟上基於SSTable的儲存引擎也不會限制傳入寫入的速率所以你需要進行明確的監控來檢測這種情況【29,30】。
如果寫入吞吐量很高,並且壓縮沒有仔細配置有可能導致壓縮跟不上寫入速率。在這種情況下,磁碟上未合併段的數量不斷增加,直到磁碟空間用完,讀取速度也會減慢,因為它們需要檢查更多段檔案。通常情況下即使壓縮無法跟上基於SSTable的儲存引擎也不會限制傳入寫入的速率所以你需要進行明確的監控來檢測這種情況【29,30】。
B樹的一個優點是每個鍵只存在於索引中的一個位置而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得B樹在想要提供強大的事務語義的資料庫中很有吸引力在許多關係資料庫中事務隔離是透過在鍵範圍上使用鎖來實現的在B樹索引中這些鎖可以直接連線到樹【5】。在[第七章](ch7.md)中,我們將更詳細地討論這一點。
B樹的一個優點是每個鍵只存在於索引中的一個位置而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得B樹在想要提供強大的事務語義的資料庫中很有吸引力在許多關係資料庫中事務隔離是透過在鍵範圍上使用鎖來實現的在B樹索引中這些鎖可以直接附加到樹上【5】。在[第七章](ch7.md)中,我們將更詳細地討論這一點。
B樹在資料庫體系結構中是非常根深蒂固的,為許多工作負載提供始終如一的良好效能,所以它們不可能很快就會消失。在新的資料儲存中,日誌結構化索引變得越來越流行。沒有快速和容易的規則來確定哪種型別的儲存引擎對你的場景更好,所以值得進行一些經驗上的測試
B樹在資料庫構中是非常根深蒂固的,為許多工作負載提供始終如一的良好效能,所以它們不可能很快就會消失。在新的資料儲存中,日誌結構化索引變得越來越流行。沒有快速和容易的規則來確定哪種型別的儲存引擎對你的場景更好,所以值得去透過一些測試來得到相關的經驗
### 其他索引結構
到目前為止,我們只討論了鍵值索引,它們就像關係模型中的**主鍵primary key** 索引。主鍵唯一標識關係表中的一行或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵或ID引用該行/文件/頂點,並且索引用於解析這樣的引用。
到目前為止,我們只討論了鍵值索引,它們就像關係模型中的**主鍵primary key** 索引。主鍵唯一標識關係表中的一行或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵或ID引用該行/文件/頂點,索引就被用於解析這樣的引用。
次級索引也很常見。在關係資料庫中,你可以使用 `CREATE INDEX` 命令在同一個表上建立多個次級索引,而且這些索引通常對於有效地執行聯接而言至關重要。例如,在[第二章](ch2.md)中的[圖2-1](../img/fig2-1.png)中,很可能在 `user_id` 列上有一個次級索引,以便你可以在每個表中找到屬於同一使用者的所有行。
次級索引secondary indexes也很常見。在關係資料庫中,你可以使用 `CREATE INDEX` 命令在同一個表上建立多個次級索引,而且這些索引通常對於有效地執行聯接join而言至關重要。例如,在[第二章](ch2.md)中的[圖2-1](../img/fig2-1.png)中,很可能在 `user_id` 列上有一個次級索引,以便你可以在每個表中找到屬於同一使用者的所有行。
一個次級索引可以很容易地從一個鍵值索引構建。主要的不同是鍵不是唯一的。即可能有許多行(文件,頂點)具有相同的鍵。這可以透過兩種方式來解決:或者透過使索引中的每個值,成為匹配行識別符號的列表(如全文索引中的釋出列表),或者透過向每個索引新增行識別符號來使每個關鍵字唯一。無論哪種方式B樹和日誌結構索引都可以用作次級索引。
次級索引可以很容易地從鍵值索引構建。次級索引主要的不同是鍵不是唯一的,即可能有許多行(文件,頂點)具有相同的鍵。這可以透過兩種方式來解決:或者將匹配行識別符號的列表作為索引裡的值(就像全文索引中的記錄列表),或者透過向每個鍵新增行識別符號來使鍵唯一。無論哪種方式B樹和日誌結構索引都可以用作次級索引。
#### 將值儲存在索引中
索引中的鍵是查詢搜尋的內容,而其值可以是以下兩種情況之一:它可以是所討論的實際行(文件,頂點),也可以是對儲存在別處的行的引用。在後一種情況下,行被儲存的地方被稱為**堆檔案heap file**,並且儲存的資料沒有特定的順序(它可以是僅追加的,或者可以跟蹤被刪除的行以便用新資料覆蓋它們後來)。堆檔案方法很常見,因為它避免了在存在多個次級索引時複製資料:每個索引只引用堆檔案中的一個位置,實際的資料儲存在一個地方。
索引中的鍵是查詢搜尋的內容,而其值可以是以下兩種情況之一:它可以是實際行(文件,頂點),也可以是對儲存在別處的行的引用。在後一種情況下,行被儲存的地方被稱為**堆檔案heap file**,並且儲存的資料沒有特定的順序(它可以是僅追加的,或者可以跟蹤被刪除的行以便後續可以用新資料進行覆蓋)。堆檔案方法很常見,因為它避免了在存在多個次級索引時對資料的複製:每個索引只引用堆檔案中的一個位置,實際的資料儲存在一個地方。
在不更改鍵的情況下更新值時堆檔案方法可以非常高效只要新值的位元組數不大於舊值就可以覆蓋該記錄。如果新值更大情況會更復雜因為它可能需要移到堆中有足夠空間的新位置。在這種情況下要麼所有的索引都需要更新以指向記錄的新堆位置或者在舊堆位置留下一個轉發指標【5】。
在某些情況下從索引到堆檔案的額外跳躍對讀取來說效能損失太大因此可能希望將索引行直接儲存在索引中。這被稱為聚集索引。例如在MySQL的InnoDB儲存引擎中表的主鍵總是一個聚集索引次級索引則引用主鍵而不是堆檔案中的位置【31】。在SQL Server中可以為每個表指定一個聚集索引【32】。
在某些情況下,從索引到堆檔案的額外跳躍對讀取來說效能損失太大,因此可能希望將索引行直接儲存在索引中。這被稱為聚集索引clustered index。例如在MySQL的InnoDB儲存引擎中表的主鍵總是一個聚集索引次級索引則引用主鍵而不是堆檔案中的位置【31】。在SQL Server中可以為每個表指定一個聚集索引【32】。
**聚集索引clustered index** (在索引中儲存所有行資料)和 **非聚集索引nonclustered index** (僅在索引中儲存對資料的引用)之間的折衷被稱為 **包含列的索引index with included columns** 或**覆蓋索引covering index**其儲存表的一部分在索引內【33】。這允許透過單獨使用索引來回答一些查詢(這種情況叫做:索引 **覆蓋cover** 了查詢【32】。
**聚集索引**(在索引中儲存所有行資料)和 **非聚集索引**(僅在索引中儲存對資料的引用)之間的折衷被稱為 **覆蓋索引covering index** 或 **包含列的索引index with included columns**其在索引記憶體儲表的一部分列【33】。這允許透過單獨使用索引來處理一些查詢(這種情況叫做:索引 **覆蓋cover** 了查詢【32】。
與任何型別的資料重複一樣,聚集和覆蓋索引可以加快讀取速度,但是它們需要額外的儲存空間,並且會增加寫入開銷。資料庫還需要額外的努力來執行事務保證,因為應用程式不應看到任何因為重複而導致的不一致。
與任何型別的資料重複一樣,聚集索引和覆蓋索引可以加快讀取速度,但是它們需要額外的儲存空間,並且會增加寫入開銷。資料庫還需要額外的努力來執行事務保證,因為應用程式不應看到任何因為重複而導致的不一致。
#### 多列索引
至今討論的索引只是將一個鍵對映到一個值。如果我們需要同時查詢一個表中的多個列(或文件中的多個欄位),這顯然是不夠的。
最常見的多列索引被稱為 **連線索引concatenated index** ,它透過將一列的值追加到另一列後面,簡單地將多個欄位組合成一個鍵(索引定義中指定了欄位的連線順序)。這就像一個老式的紙質電話簿,它提供了一個從(姓,名)到電話號碼的索引。由於排序順序,索引可以用來查詢所有具有特定姓氏的人,或所有具有特定姓-名組合的人。**然而,如果你想找到所有具有特定名字的人,這個索引是沒有用的**
最常見的多列索引被稱為 **連線索引concatenated index** ,它透過將一列的值追加到另一列後面,簡單地將多個欄位組合成一個鍵(索引定義中指定了欄位的連線順序)。這就像一個老式的紙質電話簿,它提供了一個從(姓,名)到電話號碼的索引。由於排序順序,索引可以用來查詢所有具有特定姓氏的人,或所有具有特定姓氏-名字組合的人。但如果你想找到所有具有特定名字的人,這個索引是沒有用的。
**多維索引multi-dimensional index** 是一種查詢多個列的更一般的方法,這對於地理空間資料尤為重要。例如,餐廳搜尋網站可能有一個數據庫,其中包含每個餐廳的經度和緯度。當用戶在地圖上檢視餐館時,網站需要搜尋使用者正在檢視的矩形地圖區域內的所有餐館。這需要一個二維範圍查詢,如下所示:
```sql
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
AND longitude > -0.1162 AND longitude < -0.1004;
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
AND longitude > -0.1162 AND longitude < -0.1004;
```
一個標準的B樹或者LSM樹索引不能夠高效地響應這種查詢:它可以返回一個緯度範圍內的所有餐館(但經度可能是任意值),或者返回在同一個經度範圍內的所有餐館(但緯度可能是北極和南極之間的任意地方),但不能同時滿足。
一個標準的B樹或者LSM樹索引不能夠高效地處理這種查詢:它可以返回一個緯度範圍內的所有餐館(但經度可能是任意值),或者返回在同一個經度範圍內的所有餐館(但緯度可能是北極和南極之間的任意地方),但不能同時滿足兩個條件
一種選擇是使用空間填充曲線將二維位置轉換為單個數字然後使用常規B樹索引【34】。更普遍的是使用特殊化的空間索引例如R樹。例如PostGIS使用PostgreSQL的通用Gist工具【35】將地理空間索引實現為R樹。這裡我們沒有足夠的地方來描述R樹但是有大量的文獻可供參考。
一種選擇是使用空間填充曲線將二維位置轉換為單個數字然後使用常規B樹索引【34】。更普遍的是使用特殊化的空間索引例如R樹。例如PostGIS使用PostgreSQL的通用GiST工具【35】將地理空間索引實現為R樹。這裡我們沒有足夠的地方來描述R樹但是有大量的文獻可供參考。
一個有趣的主意是,多維索引不僅可以用於地理位置。例如,在電子商務網站上可以使用維度紅色綠色藍色上的三維索引來搜尋特定顏色範圍內的產品也可以在天氣觀測資料庫中搜索二維日期溫度的指數以便有效地搜尋2013年的溫度在25至30°C之間的所有觀測資料。使用一維索引你將不得不掃描2013年的所有記錄不管溫度如何然後透過溫度進行過濾反之亦然。 二維索引可以同時透過時間戳和溫度來收窄資料集。這個技術被HyperDex使用【36】。
有趣的是,多維索引不僅可以用於地理位置。例如,在電子商務網站上可以使用建立在維度上的三維索引來搜尋特定顏色範圍內的產品也可以在天氣觀測資料庫中建立日期溫度的二維索引以便有效地搜尋2013年內的溫度在25至30°C之間的所有觀測資料。如果使用一維索引你將不得不掃描2013年的所有記錄不管溫度如何然後透過溫度進行過濾或者反之亦然。 二維索引可以同時透過時間戳和溫度來收窄資料集。這個技術被HyperDex使用【36】。
#### 全文搜尋和模糊索引
到目前為止所討論的所有索引都假定你有確切的資料,並允許你查詢鍵的確切值或具有排序順序的鍵的值範圍。他們不允許你做的是搜尋類似的鍵,如拼寫錯誤的單詞。這種模糊的查詢需要不同的技術。
例如,全文搜尋引擎通常允許搜尋一個單詞以擴充套件為包括該單詞的同義詞,忽略單詞的語法變體,並且搜尋在相同文件中彼此靠近的單詞的出現,並且支援各種其他功能取決於文字的語言分析。為了處理文件或查詢中的拼寫錯誤Lucene能夠在一定的編輯距離內搜尋文字編輯距離1意味著新增刪除或替換了一個字母【37】。
例如,全文搜尋引擎通常允許搜尋一個單詞以擴充套件為包括該單詞的同義詞,忽略單詞的語法變體,搜尋在相同文件中彼此靠近的單詞的出現,並且支援各種其他取決於文字的語言分析功能。為了處理文件或查詢中的拼寫錯誤Lucene能夠在一定的編輯距離編輯距離1意味著新增刪除或替換了一個字母內搜尋文字【37】。
正如“[用SSTables製作LSM樹](#用SSTables製作LSM樹)”中所提到的Lucene為其詞典使用了一個類似於SSTable的結構。這個結構需要一個小的記憶體索引告訴查詢需要在排序檔案中哪個偏移量查詢鍵。在LevelDB中這個記憶體中的索引是一些鍵的稀疏集合但在Lucene中記憶體中的索引是鍵中字元的有限狀態自動機類似於trie 【38】。這個自動機可以轉換成Levenshtein自動機它支援在給定的編輯距離內有效地搜尋單詞【39】。
其他的模糊搜尋技術正朝著文件分類和機器學習的方向發展。有關更多詳細資訊請參閱資訊檢索教科書例如【40】。
其他的模糊搜尋技術正朝著文件分類和機器學習的方向發展。更多詳細資訊請參閱資訊檢索教科書例如【40】。
#### 在記憶體中儲存一切
本章到目前為止討論的資料結構都是對磁碟限制的回答。與主記憶體相比,磁碟處理起來很尷尬。對於磁碟和SSD如果要在讀取和寫入時獲得良好效能則需要仔細地佈置磁碟上的資料。但是我們容忍這種尷尬因為磁碟有兩個顯著的優點它們是持久的它們的內容在電源關閉時不會丟失並且每GB的成本比RAM低。
本章到目前為止討論的資料結構都是對磁碟限制的應對。與主記憶體相比,磁碟處理起來很麻煩。對於磁碟和SSD如果要在讀取和寫入時獲得良好效能則需要仔細地佈置磁碟上的資料。但是我們能容忍這種麻煩因為磁碟有兩個顯著的優點它們是持久的它們的內容在電源關閉時不會丟失並且每GB的成本比RAM低。
隨著RAM變得更便宜每GB成本的論據被侵蝕了。許多資料集不是那麼大所以將它們全部儲存在記憶體中是非常可行的可能分佈在多個機器上。這導致了記憶體資料庫的發展。
隨著RAM變得更便宜每GB成本的論據被侵蝕了。許多資料集不是那麼大所以將它們全部儲存在記憶體中是非常可行的包括可能分佈在多個機器上。這導致了記憶體資料庫的發展。
某些記憶體中的鍵值儲存如Memcached僅用於快取在重新啟動計算機時丟失的資料是可以接受的。但其他記憶體資料庫的目標是永續性可以透過特殊的硬體例如電池供電的RAM將更改日誌寫入磁碟將定時快照寫入磁碟或者將記憶體中的狀態複製到其他機器上。
某些記憶體中的鍵值儲存如Memcached僅用於快取在重新啟動計算機時丟失的資料是可以接受的。但其他記憶體資料庫的目標是永續性可以透過特殊的硬體例如電池供電的RAM來實現也可以將更改日誌寫入磁碟,還可以將定時快照寫入磁碟或者將記憶體中的狀態複製到其他機器上。
記憶體資料庫重新啟動時,需要從磁碟或透過網路從副本重新載入其狀態(除非使用特殊的硬體)。儘管寫入磁碟,它仍然是一個記憶體資料庫,因為磁碟僅出於永續性目的進行日誌追加,讀取完全由記憶體提供。寫入磁碟也具有運維優勢:磁碟上的檔案可以很容易地由外部實用程式進行備份,檢查和分析。
記憶體資料庫重新啟動時,需要從磁碟或透過網路從副本重新載入其狀態(除非使用特殊的硬體)。儘管寫入磁碟,它仍然是一個記憶體資料庫,因為磁碟僅出於永續性目的進行日誌追加,讀取請求完全由記憶體來處理。寫入磁碟同時還有運維上的好外:磁碟上的檔案可以很容易地由外部實用程式進行備份、檢查和分析。
諸如VoltDBMemSQL和Oracle TimesTen等產品是具有關係模型的記憶體資料庫供應商聲稱透過消除與管理磁碟上的資料結構相關的所有開銷他們可以提供巨大的效能改進【41,42】。 RAM Cloud是一個開源的記憶體鍵值儲存器具有永續性儲存器中的資料以及磁碟上的資料使用日誌結構化方法【43】。 Redis和Couchbase透過非同步寫入磁碟提供了較弱的永續性。
諸如VoltDBMemSQL和Oracle TimesTen等產品是具有關係模型的記憶體資料庫供應商聲稱透過消除與管理磁碟上的資料結構相關的所有開銷他們可以提供巨大的效能改進【41,42】。 RAM Cloud是一個開源的記憶體鍵值儲存器具有永續性記憶體和磁碟上的資料都使用日誌結構化方法【43】。 Redis和Couchbase透過非同步寫入磁碟提供了較弱的永續性。
反直覺的是,記憶體資料庫的效能優勢並不是因為它們不需要從磁碟讀取的事實。即使是基於磁碟的儲存引擎也可能永遠不需要從磁碟讀取,因為作業系統在記憶體中快取了最近使用的磁碟塊。相反,它們更快的原因在於省去了將記憶體資料結構編碼為磁碟資料結構的開銷【44】。
反直覺的是,記憶體資料庫的效能優勢並不是因為它們不需要從磁碟讀取的事實。只要有足夠的記憶體即使是基於磁碟的儲存引擎也可能永遠不需要從磁碟讀取因為作業系統在記憶體中快取了最近使用的磁碟塊。相反它們更快的原因在於省去了將記憶體資料結構編碼為磁碟資料結構的開銷【44】。
除了效能,記憶體資料庫的另一個有趣的領域是提供難以用基於磁碟的索引實現的資料模型。例如Redis為各種資料結構如優先順序佇列和集合提供了類似資料庫的介面。因為它將所有資料儲存在記憶體中所以它的實現相對簡單。
除了效能,記憶體資料庫的另一個有趣的地方是提供了難以用基於磁碟的索引實現的資料模型。例如Redis為各種資料結構如優先順序佇列和集合提供了類似資料庫的介面。因為它將所有資料儲存在記憶體中所以它的實現相對簡單。
最近的研究表明記憶體資料庫體系結構可以擴充套件到支援比可用記憶體更大的資料集而不必重新採用以磁碟為中心的體系結構【45】。所謂的 **反快取anti-caching** 方法透過在記憶體不足的情況下將最近最少使用的資料從記憶體轉移到磁碟並在將來再次訪問時將其重新載入到記憶體中。這與作業系統對虛擬記憶體和交換檔案的操作類似但資料庫可以比作業系統更有效地管理記憶體因為它可以按單個記錄的粒度工作而不是整個記憶體頁面。儘管如此這種方法仍然需要索引能完全放入記憶體中就像本章開頭的Bitcask例子
如果 **非易失性儲存器(NVM** 技術得到更廣泛的應用可能還需要進一步改變儲存引擎設計【46】。目前這是一個新的研究領域值得關注。
如果 **非易失性儲存器(non-volatile memory, NVM** 技術得到更廣泛的應用可能還需要進一步改變儲存引擎設計【46】。目前這是一個新的研究領域值得關注。
## 事務處理還是分析?