2
0
Fork 0
mirror of https://github.com/Vonng/ddia.git synced 2026-06-21 00:47:05 +08:00

Compare commits

...

4 commits

Author SHA1 Message Date
Gang Yin
f81763bc1e update zh-tw content 2022-10-10 11:51:04 +08:00
Gang Yin
438cd62228 update contribution list and some formatting in ch3.md 2022-10-10 11:50:46 +08:00
YIN, Gang
406b558a0d
Merge pull request #268 from MamaShip/master
优化第三章的部分语句
2022-10-10 11:35:45 +08:00
MamaShip
1398d462ba 优化第三章的部分语句 2022-09-30 16:07:26 +08:00
7 changed files with 136 additions and 132 deletions

View file

@ -143,13 +143,15 @@
5. [词汇表](glossary.md)、[后记](colophon.md)关于野猪的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本与转换脚本 by [@afunTW](https://github.com/afunTW)
7. 多处翻译修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97)
8. 感谢所有作出贡献,提出意见的朋友们:
8. 多处翻译修正 by [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip)
9. 感谢所有作出贡献,提出意见的朋友们:
<details>
<summary><a href="https://github.com/Vonng/ddia/pulls">Pull Requests</a> & <a href="https://github.com/Vonng/ddia/issues">Issues</a></summary>
| ISSUE & Pull Requests | USER | Title |
| ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| [263](https://github.com/Vonng/ddia/pull/263) | [@zydmayday](https://github.com/zydmayday) | ch5: 修正译文中的重复单词 |
| [260](https://github.com/Vonng/ddia/pull/260) | [@haifeiWu](https://github.com/haifeiWu) | ch4: 修正部分不准确的翻译 |
| [258](https://github.com/Vonng/ddia/pull/258) | [@bestgrc](https://github.com/bestgrc) | ch3: 修正一处翻译错误 |
| [257](https://github.com/Vonng/ddia/pull/257) | [@UnderSam](https://github.com/UnderSam) | ch8: 修正一处拼写错误 |

98
ch3.md
View file

@ -78,7 +78,7 @@ $ cat database
### 散列索引
让我们从 **键值数据key-value Data** 的索引开始。这不是你可以索引的唯一数据类型,但键值数据是很常见的。对于更复杂的索引来说,这也是一个有用的构建模块
让我们从 **键值数据key-value Data** 的索引开始。这不是你可以索引的唯一数据类型,但键值数据是很常见的。在引入更复杂的索引之前,它是重要的第一步
键值存储与在大多数编程语言中可以找到的 **字典dictionary** 类型非常相似,通常字典都是用 **散列映射hash map****散列表hash table** 实现的。散列映射在许多算法教科书中都有描述【1,2】所以这里我们不会讨论它的工作细节。既然我们已经可以用散列映射来表示 **内存中** 的数据结构,为什么不使用它来索引 **硬盘上** 的数据呢?
@ -92,7 +92,7 @@ $ cat database
像 Bitcask 这样的存储引擎非常适合每个键的值经常更新的情况。例如键可能是某个猫咪视频的网址URL而值可能是该视频被播放的次数每次有人点击播放按钮时递增。在这种类型的工作负载中有很多写操作但是没有太多不同的键 —— 每个键有很多的写操作,但是将所有键保存在内存中是可行的。
直到现在,我们只是追加写入一个文件 —— 所以如何避免最终用完硬盘空间一种好的解决方案是将日志分为特定大小的段segment当日志增长到特定尺寸时关闭当前段文件并开始写入一个新的段文件。然后我们就可以对这些段进行 **压缩compaction**,如 [图 3-2](img/fig3-2.png) 所示。这里的压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。
到目前为止,我们只是在追加写入一个文件 —— 所以如何避免最终用完硬盘空间?一种好的解决方案是,将日志分为特定大小的 **segment**,当日志增长到特定尺寸时关闭当前段文件,并开始写入一个新的段文件。然后,我们就可以对这些段进行 **压缩compaction**,如 [图 3-2](img/fig3-2.png) 所示。这里的压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。
![](img/fig3-2.png)
@ -136,7 +136,7 @@ $ cat database
但是,散列表索引也有其局限性:
* 散列表必须能放进内存。如果你有非常多的键,那真是倒霉。原则上可以在硬盘上维护一个散列映射,不幸的是硬盘散列映射很难表现优秀。它需要大量的随机访问 I/O当它用满时想要再增长是很昂贵的,并且散列冲突的处理也需要很烦琐的逻辑【5】。
* 散列表必须能放进内存。如果你有非常多的键,那真是倒霉。原则上可以在硬盘上维护一个散列映射,不幸的是硬盘散列映射很难表现优秀。它需要大量的随机访问 I/O而后者耗尽时想要再扩充是很昂贵的,并且需要很烦琐的逻辑去解决散列冲突【5】。
* 范围查询效率不高。例如,你无法轻松扫描 kitty00000 和 kitty99999 之间的所有键 —— 你必须在散列映射中单独查找每个键。
在下一节中,我们将看到一个没有这些限制的索引结构。
@ -191,28 +191,28 @@ $ cat database
这里描述的算法本质上是 LevelDB【6】和 RocksDB【7】这些键值存储引擎库所使用的技术这些存储引擎被设计嵌入到其他应用程序中。除此之外LevelDB 可以在 Riak 中用作 Bitcask 的替代品。在 Cassandra 和 HBase 中也使用了类似的存储引擎【8】而且他们都受到了 Google 的 Bigtable 论文【9】引入了术语 SSTable 和 memtable )的启发。
最初这种索引结构是由 Patrick O'Neil 等人描述的,且被命名为日志结构合并树(或 LSM 树【10】它是基于更早之前的日志结构文件系统【11】来构建的。基于这种合并和压缩排序文件原理的存储引擎通常被称为 LSM 存储引擎。
这种索引结构最早由 Patrick O'Neil 等人发明,且被命名为日志结构合并树(或 LSM 树【10】它是基于更早之前的日志结构文件系统【11】来构建的。基于这种合并和压缩排序文件原理的存储引擎通常被称为 LSM 存储引擎。
Lucene 是 Elasticsearch 和 Solr 使用的一种全文搜索的索引引擎它使用类似的方法来存储它的关键词词典【12,13】。全文索引比键值索引复杂得多但是基于类似的想法在搜索查询中给出一个单词,找到提及单词的所有文档(网页,产品描述等)。这是通过键值结构实现的,其中键是单词(或 **词语**,即 term值是所有包含该单词的文档的 ID 列表(记录列表)。在 Lucene 中,从词语到记录列表的这种映射保存在类似于 SSTable 的有序文件中并根据需要在后台合并【14】。
Lucene,是一种全文搜索的索引引擎,在 Elasticsearch 和 Solr 被使用它使用类似的方法来存储它的关键词词典【12,13】。全文索引比键值索引复杂得多但是基于类似的想法在搜索查询中,由一个给定的单词,找到提及单词的所有文档(网页,产品描述等)。这也是通过键值结构实现的:其中键是 **单词term**,值是所有包含该单词的文档的 ID 列表(**postings list**)。在 Lucene 中,从词语到记录列表的这种映射保存在类似于 SSTable 的有序文件中,并根据需要在后台执行合并【14】。
#### 性能优化
与往常一样要让存储引擎在实践中表现良好涉及到大量设计细节。例如当查找数据库中不存在的键时LSM 树算法可能会很慢你必须先检查内存表然后查看从最近的到最旧的所有的段可能还必须从硬盘读取每一个段文件然后才能确定这个键不存在。为了优化这种访问存储引擎通常使用额外的布隆过滤器Bloom filters【15】。 (布隆过滤器是用于近似集合内容的高效内存数据结构,它可以告诉你数据库中是不是不存在某个键,从而为不存在的键节省掉许多不必要的硬盘读取操作。)
与往常一样要让存储引擎在实践中表现良好涉及到大量设计细节。例如当查找数据库中不存在的键时LSM 树算法可能会很慢你必须先检查内存表然后查看从最近的到最旧的所有的段可能还必须从硬盘读取每一个段文件然后才能确定这个键不存在。为了优化这种访问存储引擎通常使用额外的布隆过滤器Bloom filters【15】。 (布隆过滤器是一种节省内存的数据结构,用于近似表达集合的内容,它可以告诉你数据库中是否存在某个键,从而为不存在的键节省掉许多不必要的硬盘读取操作。)
还有一些不同的策略来确定 SSTables 被压缩和合并的顺序和时间。最常见的选择是 size-tiered 和 leveled compaction。LevelDB 和 RocksDB 使用 leveled compactionLevelDB 因此得名HBase 使用 size-tieredCassandra 同时支持这两种【16】。对于 sized-tiered较新和较小的 SSTables 相继被合并到较旧的和较大的 SSTable 中。对于 leveled compactionkey 范围被拆分到较小的 SSTables而较旧的数据被移动到单独的层级level这使得压缩compaction能够更加增量地进行并且使用较少的硬盘空间。
还有一些不同的策略来确定 SSTables 被压缩和合并的顺序和时间。最常见的选择是 size-tiered 和 leveled compaction。LevelDB 和 RocksDB 使用 leveled compactionLevelDB 因此得名HBase 使用 size-tieredCassandra 同时支持这两种【16】。对于 sized-tiered较新和较小的 SSTables 相继被合并到较旧的和较大的 SSTable 中。对于 leveled compactionkey (按照分布范围被拆分到较小的 SSTables而较旧的数据被移动到单独的层级level这使得压缩compaction能够更加增量地进行并且使用较少的硬盘空间。
即使有许多微妙的东西LSM 树的基本思想 —— 保存一系列在后台合并的 SSTables —— 简单而有效。即使数据集比可用内存大得多,它仍能继续正常工作。由于数据按排序顺序存储,你可以高效地执行范围查询(扫描所有从某个最小值到某个最大值之间的所有键),并且因为硬盘写入是连续的,所以 LSM 树可以支持非常高的写入吞吐量。
### B树
前面讨论的日志结构索引正处在逐渐被接受的阶段,但它们并不是最常见的索引类型。使用最广泛的索引结构和日志结构索引相当不同,它就是我们接下来要讨论的 B 树。
前面讨论的日志结构索引看起来已经相当可用了,但它们却不是最常见的索引类型。使用最广泛的索引结构和日志结构索引相当不同,它就是我们接下来要讨论的 B 树。
从 1970 年被引入【17】仅不到 10 年后就变得 “无处不在”【18】B 树很好地经受了时间的考验。在几乎所有的关系数据库中,它们仍然是标准的索引实现,许多非关系数据库也会使用到 B 树。
像 SSTables 一样B 树保持按键排序的键值对,这允许高效的键值查找和范围查询。但这也就是有的相似之处了B 树有着非常不同的设计理念。
像 SSTables 一样B 树保持按键排序的键值对,这允许高效的键值查找和范围查询。但这也就是有的相似之处了B 树有着非常不同的设计理念。
我们前面看到的日志结构索引将数据库分解为可变大小的段通常是几兆字节或更大的大小并且总是按顺序写入段。相比之下B 树将数据库分解成固定大小的block或页面page,传统上大小为 4KB有时会更大并且一次只能读取或写入一个页面。这种设计更接近于底层硬件因为硬盘空间也是按固定大小的块来组织的。
我们前面看到的日志结构索引将数据库分解为可变大小的段通常是几兆字节或更大的大小并且总是按顺序写入段。相比之下B 树将数据库分解成固定大小的 **块block****分页page**,传统上大小为 4KB有时会更大并且一次只能读取或写入一个页面。这种设计更接近于底层硬件因为硬盘空间也是按固定大小的块来组织的。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在硬盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如 [图 3-6](img/fig3-6.png) 所示。
@ -220,13 +220,13 @@ Lucene 是 Elasticsearch 和 Solr 使用的一种全文搜索的索引引擎,
**图 3-6 使用 B 树索引查找一个键**
一个页面会被指定为 B 树的根;在索引中查找一个键时,就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键,引用之间的键,指明了引用子页面的键范围
一个页面会被指定为 B 树的根;在索引中查找一个键时,就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键,根页面上每两个引用之间的键,表示相邻子页面管理的键的范围(边界)
在 [图 3-6](img/fig3-6.png) 的例子中,我们正在寻找键 251 ,所以我们知道我们需要跟踪边界 200 和 300 之间的页面引用。这将我们带到一个类似的页面,进一步将 200 到 300 的范围拆分到子范围。
最终我们将到达某个包含单个键的页面叶子页面leaf page该页面或者直接包含每个键的值或者包含了对可以找到值的页面的引用。
在 B 树的一个页面中对子页面的引用的数量称为分支因子。例如,在 [图 3-6](img/fig3-6.png) 中,分支因子是 6。在实践中分支因子取决于存储页面引用和范围边界所需的空间,但通常是几百
在 B 树的一个页面中对子页面的引用的数量称为 **分支因子branching factor**。例如,在 [图 3-6](img/fig3-6.png) 中,分支因子是 6。在实践中分支因子的大小取决于存储页面引用和范围边界所需的空间,但这个值通常是几百。
如果要更新 B 树中现有键的值,需要搜索包含该键的叶子页面,更改该页面中的值,并将该页面写回到硬盘(对该页面的任何引用都将保持有效)。如果你想添加一个新的键,你需要找到其范围能包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以反映新的键范围分区,如 [图 3-7](img/fig3-7.png) 所示 [^ii]。
@ -244,39 +244,39 @@ B 树的基本底层写操作是用新数据覆写硬盘上的页面,并假定
你可以把覆写硬盘上的页面对应为实际的硬件操作。在磁性硬盘驱动器上,这意味着将磁头移动到正确的位置,等待旋转盘上的正确位置出现,然后用新的数据覆写适当的扇区。在固态硬盘上,由于 SSD 必须一次擦除和重写相当大的存储芯片块所以会发生更复杂的事情【19】。
而且,一些操作需要覆写几个不同的页面。例如,如果因为插入导致页面过满而拆分页面,则需要写入新拆分的两个页面,并覆写其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有部分页面被写入时崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项
而且,一些操作需要覆写几个不同的页面。例如,如果因为插入导致页面过满而拆分页面,则需要写入新拆分的两个页面,并覆写其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在系列操作进行到一半时崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面没有被任何页面引用
为了使数据库能处理异常崩溃的场景B 树实现通常会带有一个额外的硬盘数据结构:**预写式日志**WAL即 write-ahead log也称为 **重做日志**,即 redo log。这是一个仅追加的文件每个 B 树的修改在其能被应用到树本身的页面之前都必须先写入到该文件。当数据库在崩溃后恢复时,这个日志将被用来使 B 树恢复到一致的状态【5,20】。
另外还有一个更新页面的复杂情况是,如果多个线程要同时访问 B 树,则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常是通过使用 **锁存器**latches轻量级锁保护树的数据结构来完成。日志结构化的方法在这方面更简单因为它们在后台进行所有的合并而不会干扰新接收到的查询并且能够时不时地将旧的段原子交换为新的段
另外还有一个更新页面的复杂情况是,如果多个线程要同时访问 B 树,则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常是通过使用 **锁存器**latches轻量级锁保护树的数据结构来完成。日志结构化的方法在这方面更简单因为它们在后台进行所有的合并而不会干扰新接收到的查询并且能够时不时地将段文件切换为新的(该切换是原子操作)
#### B树的优化
由于 B 树已经存在了很久,所以并不奇怪这么多年下来有很多优化的设计被开发出来,仅举几例:
* 一些数据库(如 LMDB使用写时复制方案【21】,而不是覆盖页面并维护 WAL 以支持崩溃恢复。修改的页面被写入到不同的位置,并且还在树中创建了父页面的新版本,以指向新的位置。这种方法对于并发控制也很有用,我们将在 “[快照隔离和可重复读](ch7.md#快照隔离和可重复读)” 中看到。
* 不同于覆写页面并维护 WAL 以支持崩溃恢复,一些数据库(如 LMDB使用写时复制方案【21】。经过修改的页面被写入到不同的位置,并且还在树中创建了父页面的新版本,以指向新的位置。这种方法对于并发控制也很有用,我们将在 “[快照隔离和可重复读](ch7.md#快照隔离和可重复读)” 中看到。
* 我们可以通过不存储整个键,而是缩短其大小,来节省页面空间。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此也就允许更少的层级 [^iii]。
* 通常,页面可以放置在硬盘上的任何位置;没有什么要求相邻键范围的页面也放在硬盘上相邻的区域。如果某个查询需要按照排序顺序扫描大部分的键范围,那么这种按页面存储的布局可能会效率低下,因为每次页面读取可能都需要进行硬盘查找。因此,许多 B 树的实现在布局树时会尽量使叶子页面按顺序出现在硬盘上。但是,随着树的增长,要维持这个顺序是很困难的。相比之下,由于 LSM 树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在硬盘上彼此靠近
* 额外的指针被添加到树中。例如,每个叶子页面可以引用其左边和右边的兄弟页面,使得不用跳回父页面就能按顺序对键进行扫描。
* B 树的变体如分形树fractal tree【22】借用一些日志结构的思想来减少硬盘查找(而且它们与分形无关)。
* 通常,页面可以放置在硬盘上的任何位置;没有什么要求相邻键范围的页面也放在硬盘上相邻的区域。如果某个查询需要按照排序顺序扫描大部分的键范围,那么这种按页面存储的布局可能会效率低下,因为每个页面的读取都需要执行一次硬盘查找。因此,许多 B 树的实现在布局树时会尽量使叶子页面按顺序出现在硬盘上。但是,随着树的增长,要维持这个顺序是很困难的。相比之下,由于 LSM 树在合并过程中一次性重写一大段存储,所以它们更容易使顺序键在硬盘上连续存储
* 额外的指针被添加到树中。例如,每个叶子页面可以引用其左边和右边的兄弟页面,使得不用跳回父页面就能按顺序对键进行扫描。
* B 树的变体如 **分形树fractal trees**【22】借用了一些日志结构的思想来减少硬盘查找(而且它们与分形无关)。
[^iii]: 这个变种有时被称为 B+ 树,但因为这个优化已被广泛使用,所以经常无法区分于其它的 B 树变种。
### 比较B树和LSM树
尽管 B 树实现通常比 LSM 树实现更成熟,但 LSM 树由于其性能特点也非常有趣。根据经验,通常 LSM 树的写入速度更快,而 B 树的读取速度更快【23】。 LSM 树上的读取通常比较慢因为它们必须检查几种不同的数据结构和不同压缩Compaction层级的 SSTables。
尽管 B 树实现通常比 LSM 树实现更成熟,但 LSM 树由于性能特征也非常有趣。根据经验,通常 LSM 树的写入速度更快,而 B 树的读取速度更快【23】。 LSM 树上的读取通常比较慢因为它们必须检查几种不同的数据结构和不同压缩Compaction层级的 SSTables。
然而,基准测试的结果通常和工作负载的细节相关。你需要用你特有的工作负载来测试系统,以便进行有效的比较。在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
#### LSM树的优点
B 树索引中的每块数据都必须至少写入两次一次写入预先写入日志WAL一次写入树页面本身如果有分页还需要再写入一次。即使在该页面中只有几个字节发生了变化也需要接受写入整个页面的开销。有些存储引擎甚至会覆写同一个页面两次以免在电源故障的情况下导致页面部分更新【24,25】。
B 树索引中的每块数据都必须至少写入两次一次写入预先写入日志WAL一次写入树页面本身如果有分页还需要再写入一次。即使在该页面中只有几个字节发生了变化也需要接受写入整个页面的开销。有些存储引擎甚至会覆写同一个页面两次以免在电源故障的情况下页面未完整更新【24,25】。
由于反复压缩和合并 SSTables日志结构索引也会多次重写数据。这种影响 —— 在数据库的生命周期中每次写入数据库导致对硬盘的多次写入 —— 被称为 **写放大write amplification**。需要特别注意的是固态硬盘,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。
由于反复压缩和合并 SSTables日志结构索引也会多次重写数据。这种影响 —— 在数据库的生命周期中每笔数据导致对硬盘的多次写入 —— 被称为 **写入放大write amplification**。使用固态硬盘的机器需要额外关注这点,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入硬盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入硬盘的次数越多,可用硬盘带宽内它能处理的每秒写入次数就越少。
LSM 树通常能够比 B 树支持更高的写入吞吐量,部分原因是它们有时具有较低的写放大(尽管这取决于存储引擎的配置和工作负载),部分是因为它们顺序地写入紧凑的 SSTable 文件而不是必须覆写树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要,其顺序写入比随机写入要快得多。
LSM 树通常能够比 B 树支持更高的写入吞吐量,部分原因是它们有时具有较低的写放大(尽管这取决于存储引擎的配置和工作负载),部分是因为它们顺序地写入紧凑的 SSTable 文件而不是必须覆写树中的几个页面【26】。这种差异在机械硬盘上尤其重要,其顺序写入比随机写入要快得多。
LSM 树可以被压缩得更好,因此通常能比 B 树在硬盘上产生更小的文件。B 树存储引擎会由于碎片化fragmentation而留下一些未使用的硬盘空间当页面被拆分或某行不能放入现有页面时页面中的某些空间仍未被使用。由于 LSM 树不是面向页面的,并且会通过定期重写 SSTables 以去除碎片所以它们具有较低的存储开销特别是当使用分层压缩leveled compaction时【27】。
@ -292,7 +292,7 @@ LSM 树可以被压缩得更好,因此通常能比 B 树在硬盘上产生更
B 树的一个优点是每个键只存在于索引中的一个位置,而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得 B 树在想要提供强大的事务语义的数据库中很有吸引力:在许多关系数据库中,事务隔离是通过在键范围上使用锁来实现的,在 B 树索引中这些锁可以直接附加到树上【5】。在 [第七章](ch7.md) 中,我们将更详细地讨论这一点。
B 树在数据库架构中是非常根深蒂固的,为许多工作负载都提供了始终如一的良好性能,所以它们不可能很快就会消失。在新的数据存储中,日志结构化索引变得越来越流行。没有快速和容易的规则来确定哪种类型的存储引擎对你的场景更好,所以值得去通过一些测试来得到相关的经验。
B 树在数据库架构中是非常根深蒂固的,为许多工作负载都提供了始终如一的良好性能,所以它们不可能在短期内消失。在新的数据库中,日志结构化索引变得越来越流行。没有简单易行的办法来判断哪种类型的存储引擎对你的使用场景更好,所以需要通过一些测试来得到相关经验。
### 其他索引结构
@ -300,7 +300,7 @@ B 树在数据库架构中是非常根深蒂固的,为许多工作负载都提
次级索引secondary indexes也很常见。在关系数据库中你可以使用 `CREATE INDEX` 命令在同一个表上创建多个次级索引而且这些索引通常对于有效地执行联接join而言至关重要。例如在 [第二章](ch2.md) 中的 [图 2-1](img/fig2-1.png) 中,很可能在 `user_id` 列上有一个次级索引,以便你可以在每个表中找到属于同一用户的所有行。
次级索引可以很容易地从键值索引构建。次级索引主要的不同是键不是唯一的,即可能有许多行(文档,顶点)具有相同的键。这可以通过两种方式来解决:或者将匹配行标识符的列表作为索引里的值就像全文索引中的记录列表或者通过向每个键添加行标识符来使键唯一。无论哪种方式B 树和日志结构索引都可以用作次级索引。
次级索引可以很容易地从键值索引构建。次级索引主要的不同是键不是唯一的即可能有许多行文档顶点具有相同的键。这可以通过两种方式来解决将匹配行标识符的列表作为索引里的值就像全文索引中的记录列表或者通过向每个键添加行标识符来使键唯一。无论哪种方式B 树和日志结构索引都可以用作次级索引。
#### 将值存储在索引中
@ -312,7 +312,7 @@ B 树在数据库架构中是非常根深蒂固的,为许多工作负载都提
**聚集索引**(在索引中存储所有的行数据)和 **非聚集索引**(仅在索引中存储对数据的引用)之间的折衷被称为 **覆盖索引covering index****包含列的索引index with included columns**其在索引内存储表的一部分列【33】。这允许通过单独使用索引来处理一些查询这种情况下可以说索引 **覆盖cover** 了查询【32】。
与任何类型的数据重复一样,聚集索引和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应看到任何因为重复而导致的不一致。
与任何类型的数据重复一样,聚集索引和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应看到任何因为使用副本而导致的不一致。
#### 多列索引
@ -329,15 +329,15 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
一个标准的 B 树或者 LSM 树索引不能够高效地处理这种查询:它可以返回一个纬度范围内的所有餐馆(但经度可能是任意值),或者返回在同一个经度范围内的所有餐馆(但纬度可能是北极和南极之间的任意地方),但不能同时满足两个条件。
一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规 B 树索引【34】。更普遍的是使用特殊化的空间索引例如 R 树。例如PostGIS 使用 PostgreSQL 的通用 GiST 工具【35】将地理空间索引实现为 R 树。这里我们没有足够的地方来描述 R 树,但是有大量的文献可供参考。
一种选择是使用 **空间填充曲线space-filling curve** 将二维位置转换为单个数字,然后使用常规 B 树索引【34】。更普遍的是使用特殊化的空间索引例如 R 树。例如PostGIS 使用 PostgreSQL 的通用 GiST 工具【35】将地理空间索引实现为 R 树。这里我们没有足够的地方来描述 R 树,但是有大量的文献可供参考。
有趣的是,多维索引不仅可以用于地理位置。例如,在电子商务网站上可以使用建立在(红,绿,蓝)维度上的三维索引来搜索特定颜色范围内的产品,也可以在天气观测数据库中建立(日期,温度)的二维索引,以便有效地搜索 2013 年内的温度在 25 至 30°C 之间的所有观测资料。如果使用一维索引,你将不得不扫描 2013 年的所有记录(不管温度如何),然后通过温度进行过滤,或者反之亦然。二维索引可以同时通过时间戳和温度来收窄数据集。这个技术被 HyperDex 所使用【36】。
#### 全文搜索和模糊索引
到目前为止所讨论的所有索引都假定你有确切的数据,并允许你查询键的确切值或具有排序顺序的键的值范围。他们不允许你做的是搜索类似的键,如拼写错误的单词。这种模糊的查询需要不同的技术。
到目前为止所讨论的所有索引都假定你有确切的数据,并允许你查询键的确切值或具有排序顺序的键的值范围。他们不允许你做的是搜索**类似**的键,如拼写错误的单词。这种模糊的查询需要不同的技术。
例如,全文搜索引擎通常允许搜索一个单词扩展为包括该单词的同义词,忽略单词的语法变体,搜索在相同文档中彼此靠近的单词的出现并且支持各种其他取决于文本的语言分析功能。为了处理文档或查询中的拼写错误Lucene 能够在一定的编辑距离(编辑距离 1 意味着添加删除或替换了一个字母内搜索文本【37】
例如,全文搜索引擎通常允许搜索目标从一个单词扩展为包括该单词的同义词,忽略单词的语法变体,搜索在相同文档中的近义词并且支持各种其他取决于文本的语言分析功能。为了处理文档或查询中的拼写错误Lucene 能够在一定的编辑距离内搜索文本【37】编辑距离 1 意味着单词内发生了 1 个字母的添加、删除或替换)
正如 “[用 SSTables 制作 LSM 树](#用SSTables制作LSM树)” 中所提到的Lucene 为其词典使用了一个类似于 SSTable 的结构。这个结构需要一个小的内存索引,告诉查询需要在排序文件中哪个偏移量查找键。在 LevelDB 中,这个内存中的索引是一些键的稀疏集合,但在 Lucene 中,内存中的索引是键中字符的有限状态自动机,类似于 trie 【38】。这个自动机可以转换成 Levenshtein 自动机它支持在给定的编辑距离内有效地搜索单词【39】。
@ -351,7 +351,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
某些内存中的键值存储(如 Memcached仅用于缓存在重新启动计算机时丢失的数据是可以接受的。但其他内存数据库的目标是持久性可以通过特殊的硬件例如电池供电的 RAM来实现也可以将更改日志写入硬盘还可以将定时快照写入硬盘或者将内存中的状态复制到其他机器上。
内存数据库重新启动时,需要从硬盘或通过网络从副本重新加载其状态(除非使用特殊的硬件)。尽管写入硬盘,它仍然是一个内存数据库,因为硬盘仅出于持久性目的进行日志追加,读取请求完全由内存来处理。写入硬盘同时还有运维上的好处:硬盘上的文件可以很容易地由外部实用程序进行备份、检查和分析。
内存数据库重新启动时,需要从硬盘或通过网络从副本重新加载其状态(除非使用特殊的硬件)。尽管写入硬盘,它仍然是一个内存数据库,因为硬盘仅出于持久性目的进行日志追加,读取请求完全由内存来处理。写入硬盘同时还有运维上的好处:硬盘上的文件可以很容易地由外部程序进行备份、检查和分析。
诸如 VoltDB、MemSQL 和 Oracle TimesTen 等产品是具有关系模型的内存数据库供应商声称通过消除与管理硬盘上的数据结构相关的所有开销他们可以提供巨大的性能改进【41,42】。 RAM Cloud 是一个开源的内存键值存储器具有持久性对内存和硬盘上的数据都使用日志结构化方法【43】。 Redis 和 Couchbase 通过异步写入硬盘提供了较弱的持久性。
@ -408,7 +408,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
几乎所有的大型企业都有数据仓库,但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的 OLTP 系统,大多数小公司只有少量的数据 —— 可以在传统的 SQL 数据库中查询,甚至可以在电子表格中分析。在一家大公司里,要做一些在一家小公司很简单的事情,需要很多繁重的工作。
使用单独的数据仓库,而不是直接查询 OLTP 系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明,本章前半部分讨论的索引算法对于 OLTP 来说工作得很好,但对于处理分析查询并不是很好。在本章的其余部分中,我们将研究为分析而优化的存储引擎。
使用单独的数据仓库,而不是直接查询 OLTP 系统进行分析的一大优势是数据仓库可针对分析类的访问模式进行优化。事实证明,本章前半部分讨论的索引算法对于 OLTP 来说工作得很好,但对于处理分析查询并不是很好。在本章的其余部分中,我们将研究为分析而优化的存储引擎。
#### OLTP数据库和数据仓库之间的分歧
@ -416,7 +416,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
表面上,一个数据仓库和一个关系型 OLTP 数据库看起来很相似,因为它们都有一个 SQL 查询接口。然而,系统的内部看起来可能完全不同,因为它们针对非常不同的查询模式进行了优化。现在许多数据库供应商都只是重点支持事务处理负载和分析工作负载这两者中的一个,而不是都支持。
一些数据库(例如 Microsoft SQL Server 和 SAP HANA支持在同一产品中进行事务处理和数据仓库。但是它们也正日益成为两个独立的存储和查询引擎,只是这些引擎正好可以通过一个通用的 SQL 接口访问【49,50,51】。
一些数据库(例如 Microsoft SQL Server 和 SAP HANA支持在同一产品中进行事务处理和数据仓库。但是它们也正日益发展为两套独立的存储和查询引擎,只是这些引擎正好可以通过一个通用的 SQL 接口访问【49,50,51】。
Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用昂贵的商业许可证销售他们的系统。 Amazon RedShift 是 ParAccel 的托管版本。最近,大量的开源 SQL-on-Hadoop 项目已经出现,它们还很年轻,但是正在与商业数据仓库系统竞争,包括 Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo 和 Apache Drill【52,53】。其中一些基于了谷歌 Dremel 的想法【54】。
@ -449,7 +449,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用
如果事实表中有万亿行和数 PB 的数据,那么高效地存储和查询它们就成为一个具有挑战性的问题。维度表通常要小得多(数百万行),所以在本节中我们将主要关注事实表的存储。
尽管事实表通常超过 100 列,但典型的数据仓库查询一次只会访问其中 4 个或 5 个列( “`SELECT *`” 查询很少用于分析【51】。以 [例 3-1]() 中的查询为例:它访问了大量的行(在 2013 日历年中每次都有人购买水果或糖果),但只需访问 `fact_sales` 表的三列:`date_key, product_sk, quantity`。该查询忽略了所有其他的列。
尽管事实表通常超过 100 列,但典型的数据仓库查询一次只会访问其中 4 个或 5 个列( “`SELECT *`” 查询很少用于分析【51】。以 [例 3-1]() 中的查询为例:它访问了大量的行(在 2013 年中所有购买了水果或糖果的记录),但只需访问 `fact_sales` 表的三列:`date_key, product_sk, quantity`。该查询忽略了所有其他的列。
**例 3-1 分析人们是否更倾向于在一周的某一天购买新鲜水果或糖果**
@ -497,7 +497,7 @@ GROUP BY
通常情况下,一列中不同值的数量与行数相比要小得多(例如,零售商可能有数十亿的销售交易,但只有 100,000 个不同的产品)。现在我们可以拿一个有 n 个不同值的列,并把它转换成 n 个独立的位图:每个不同值对应一个位图,每行对应一个比特位。如果该行具有该值,则该位为 1否则为 0。
如果 n 非常小(例如,国家 / 地区列可能有大约 200 个不同的值),则这些位图可以将每行存储成一个比特位。但是,如果 n 更大,大部分位图中将会有很多的零(我们说它们是稀疏的)。在这种情况下,位图可以另外再进行游程编码,如 [图 3-11](fig3-11.png) 底部所示。这可以使列的编码非常紧凑。
如果 n 非常小(例如,国家 / 地区列可能有大约 200 个不同的值),则这些位图可以将每行存储成一个比特位。但是,如果 n 更大,大部分位图中将会有很多的零(我们说它们是稀疏的)。在这种情况下,位图可以另外再进行游程编码run-length encoding一种无损数据压缩技术,如 [图 3-11](fig3-11.png) 底部所示。这可以使列的编码非常紧凑。
这些位图索引非常适合数据仓库中常见的各种查询。例如:
@ -522,28 +522,28 @@ WHERE product_sk = 31 AND store_sk = 3
#### 内存带宽和矢量化处理
对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从硬盘获取数据到内存的带宽。但是,这不是唯一的瓶颈。分析型数据库的开发人员还需要有效地利用主存储器到 CPU 缓存的带宽,避免 CPU 指令处理流水线中的分支预测错误和气泡,以及在现代 CPU 上使用单指令多数据SIMD指令【59,60】。
对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从硬盘获取数据到内存的带宽。但是,这不是唯一的瓶颈。分析型数据库的开发人员还需要有效地利用内存到 CPU 缓存的带宽,避免 CPU 指令处理流水线中的分支预测错误和闲置等待,以及在现代 CPU 上使用单指令多数据SIMD指令来加速运算【59,60】。
除了减少需要从硬盘加载的数据量以外,列式存储布局也可以有效利用 CPU 周期。例如,查询引擎可以将大量压缩的列数据放在 CPU 的 L1 缓存中,然后在紧密的循环(即没有函数调用)中遍历。相比较每个记录的处理都需要大量函数调用和条件判断的代码CPU 执行这样一个循环要快得多。列压缩允许列中的更多行被放进相同数量的 L1 缓存。前面描述的按位 “与” 和 “或” 运算符可以被设计为直接在这样的压缩列数据块上操作。这种技术被称为矢量化处理【58,49】。
除了减少需要从硬盘加载的数据量以外,列式存储布局也可以有效利用 CPU 周期。例如,查询引擎可以将一整块压缩好的列数据放进 CPU 的 L1 缓存中,然后在紧密的循环(即没有函数调用)中遍历。相比于每条记录的处理都需要大量函数调用和条件判断的代码CPU 执行这样一个循环要快得多。列压缩允许列中的更多行被同时放进容量有限的 L1 缓存。前面描述的按位 “与” 和 “或” 运算符可以被设计为直接在这样的压缩列数据块上操作。这种技术被称为矢量化处理vectorized processing【58,49】。
### 列式存储中的排序顺序
在列式存储中,存储行的顺序并不一定很重要。按插入顺序存储它们是最简单的,因为插入一个新行只需要追加到每个列文件。但是,我们可以选择增加一个特定的顺序,就像我们之前对 SSTables 所做的那样,并将其用作索引机制。
在列式存储中,存储行的顺序并不关键。按插入顺序存储它们是最简单的,因为插入一个新行只需要追加到每个列文件。但是,我们也可以选择按某种顺序来排列数据,就像我们之前对 SSTables 所做的那样,并将其用作索引机制。
注意,每列独自排序是没有意义的,因为那样我们就没法知道不同列中的哪些项属于同一行。我们只能在知道一列中的第 k 项与另一列中的第 k 项属于同一行的情况才能重建出完整的行。
注意,对每列分别执行排序是没有意义的,因为那样就没法知道不同列中的哪些项属于同一行。我们只能在明确一列中的第 k 项与另一列中的第 k 项属于同一行的情况下,才能重建出完整的行。
相反,即使按列式存储数据,也需要一次对整行进行排序。数据库的管理员可以根据他们对常用查询的了解来选择表格应该被排序的列。例如,如果查询通常以日期范围为目标,例如上个月,则可以将 `date_key` 作为第一个排序键。这样查询优化器就可以只扫描上个月的行了,这比扫描所有行要快得多。
相反,数据的排序需要对一整行统一操作,即使它们的存储方式是按列的。数据库管理员可以根据他们对常用查询的了解,来选择表格中用来排序的列。例如,如果查询通常以日期范围为目标,例如上个月,则可以将 `date_key` 作为第一个排序键。这样查询优化器就可以只扫描近1个月范围的行了,这比扫描所有行要快得多。
对于第一排序列中具有相同值的行,可以用第二排序列来进一步排序。例如,如果 `date_key` 是 [图 3-10](img/fig3-10.png) 中的第一个排序关键字,那么 `product_sk` 可能是第二个排序关键字,以便同一天的同一产品的所有销售都将在存储中组合在一起。这将有助于需要在特定日期范围内按产品对销售进行分组或过滤的查询。
对于第一排序列中具有相同值的行,可以用第二排序列来进一步排序。例如,如果 `date_key` 是 [图 3-10](img/fig3-10.png) 中的第一个排序关键字,那么 `product_sk` 可能是第二个排序关键字,以便同一天的同一产品的所有销售数据都被存储在相邻位置。这将有助于需要在特定日期范围内按产品对销售进行分组或过滤的查询。
序顺序的另一个好处是它可以帮助压缩列。如果主要排序列没有太多个不同的值,那么在排序之后,它将具有很长的序列,其中相同的值连续重复多次。一个简单的游程编码(就像我们用于 [图 3-11](img/fig3-11.png) 中的位图一样)可以将该列压缩到几千字节 —— 即使表中有数十亿行。
按顺序排序的另一个好处是它可以帮助压缩列。如果主要排序列没有太多个不同的值,那么在排序之后,将会得到一个相同的值连续重复多次的序列。一个简单的游程编码(就像我们用于 [图 3-11](img/fig3-11.png) 中的位图一样)可以将该列压缩到几 KB —— 即使表中有数十亿行。
第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长的连续的重复值。排序优先级更低的列以基本上随机的顺序出现,所以它们可能不会被压缩。但前几列排序在整体上仍然是有好处的。
第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长的连续的重复值。排序优先级更低的列以几乎随机的顺序出现,所以可能不会被压缩。但对前几列做排序在整体上仍然是有好处的。
#### 几个不同的排序顺序
C-Store 中引入了这个想法的一个巧妙扩展,并在商业数据仓库 Vertica 中被采用【61,62】。不同的查询受益于不同的排序顺序,为什么不以几种不同的方式来存储相同的数据呢?无论如何,数据需要复制到多台机器,这样,如果一台机器发生故障,你不会丢失数据。你可能还需要存储以不同方式排序的冗余数据,以便在处理查询时,可以使用最适合查询模式的版本。
对这个想法,有一个巧妙的扩展被 C-Store 发现,并在商业数据仓库 Vertica 中被采用【61,62】既然不同的查询受益于不同的排序顺序,为什么不以几种不同的方式来存储相同的数据呢?反正数据都需要做备份,以防单点故障时丢失数据。因此你可以用不同排序方式来存储冗余数据,以便在处理查询时,调用最适合查询模式的版本。
在一个列式存储中有多个排序顺序有点类似于在一个面向行的存储中有多个次级索引。但最大的区别在于面向行的存储将每一行保存在一个地方(在堆文件或聚集索引中),次级索引只包含指向匹配行的指针。在列式存储中,通常在其他地方没有任何指向数据的指针,只有包含值的列。
@ -555,17 +555,17 @@ C-Store 中引入了这个想法的一个巧妙扩展,并在商业数据仓库
幸运的是本章前面已经看到了一个很好的解决方案LSM 树。所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入硬盘。内存中的存储是面向行还是列的并不重要。当已经积累了足够的写入数据时,它们将与硬盘上的列文件合并,并批量写入新文件。这基本上是 Vertica 所做的【62】。
查询需要检查硬盘上的列数据和最近在内存中的写入,并将两者结合起来。但是,查询优化器对用户隐藏了这个细节。从分析师的角度来看,通过插入、更新或删除操作进行修改的数据会立即反映在后续的查询中。
查询操作需要检查硬盘上的列数据和内存中的最近写入,并将两者的结果合并起来。但是,查询优化器对用户隐藏了这个细节。从分析师的角度来看,通过插入、更新或删除操作进行修改的数据会立即反映在后续的查询中。
### 聚合:数据立方体和物化视图
不是每个数据仓库都必定是一个列式存储传统的面向行的数据库和其他一些架构也被使用。然而列式存储可以显著加快专门的分析查询所以它正在迅速变得流行起来【51,63】。
非所有数据仓库都需要采用列式存储传统的面向行的数据库和其他一些架构也被使用。然而列式存储可以显著加快专门的分析查询所以它正在迅速变得流行起来【51,63】。
数据仓库的另一个值得一提的方面是物化汇总materialized aggregates。如前所述数据仓库查询通常涉及一个聚合函数如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果相同的聚合被许多不同的查询使用那么每次都通过原始数据来处理可能太浪费了。为什么不将一些查询使用最频繁的计数或总和缓存起来
数据仓库的另一个值得一提的方面是物化聚合materialized aggregates。如前所述数据仓库查询通常涉及一个聚合函数如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果相同的聚合被许多不同的查询使用那么每次都通过原始数据来处理可能太浪费了。为什么不将一些查询使用最频繁的计数或总和缓存起来
创建这种缓存的一种方式是物化视图Materialized View。在关系数据模型中它通常被定义为一个标准虚拟视图一个类似于表的对象其内容是一些查询的结果。不同的是物化视图是查询结果的实际副本会被写入硬盘而虚拟视图只是编写查询的一个捷径。从虚拟视图读取时SQL 引擎会将其展开到视图的底层查询中,然后再处理展开的查询。
当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成该操作,但是这样的更新使得写入成本更高,这就是在 OLTP 数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中,它们可能更有意义(它们是否实际上改善了读取性能取决于个别情况)。
当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成该操作,但是这样的更新使得写入成本更高,这就是在 OLTP 数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中,它们可能更有意义(它们是否实际上改善了读取性能取决于使用场景)。
物化视图的常见特例称为数据立方体或 OLAP 立方【64】。它是按不同维度分组的聚合网格。[图 3-12](img/fig3-12.png) 显示了一个例子。
@ -573,7 +573,7 @@ C-Store 中引入了这个想法的一个巧妙扩展,并在商业数据仓库
**图 3-12 数据立方的两个维度,通过求和聚合**
想象一下,现在每个事实都只有两个维度表的外键 —— 在 [图 3-12](img/fig-3-12.png) 中分别是日期和产品。你现在可以绘制一个二维表格,一个轴线上是日期,另一个轴线上是产品。每个单元格包含具有该日期 - 产品组合的所有事实的属性(例如 `net_price`)的聚(例如 `SUM`)。然后,你可以沿着每行或每列应用相同的汇总,并获得减少了一个维度的汇总(按产品的销售额,无论日期,或者按日期的销售额,无论产品)。
想象一下,现在每个事实都只有两个维度表的外键 —— 在 [图 3-12](img/fig-3-12.png) 中分别是日期和产品。你现在可以绘制一个二维表格,一个轴线上是日期,另一个轴线上是产品。每个单元格包含具有该日期 - 产品组合的所有事实的属性(例如 `net_price`)的聚(例如 `SUM`)。然后,你可以沿着每行或每列应用相同的汇总,并获得减少了一个维度的汇总(按产品的销售额,无论日期,或者按日期的销售额,无论产品)。
一般来说,事实往往有两个以上的维度。在图 3-9 中有五个维度:日期、产品、商店、促销和客户。要想象一个五维超立方体是什么样子是很困难的,但是原理是一样的:每个单元格都包含特定日期 - 产品 - 商店 - 促销 - 客户组合的销售额。这些值可以在每个维度上求和汇总。
@ -589,7 +589,7 @@ C-Store 中引入了这个想法的一个巧妙扩展,并在商业数据仓库
在高层次上,我们看到存储引擎分为两大类:针对 **事务处理OLTP** 优化的存储引擎和针对 **在线分析OLAP** 优化的存储引擎。这两类使用场景的访问模式之间有很大的区别:
* OLTP 系统通常面向最终用户,这意味着系统可能会收到大量的请求。为了处理负载,应用程序在每个查询中通常只访问少量的记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。硬盘查找时间往往是这里的瓶颈。
* 数据仓库和类似的分析系统会低调一些,因为它们主要由业务分析人员使用,而不是最终用户。它们的查询量要比 OLTP 系统少得多,但通常每个查询开销高昂,需要在短时间内扫描数百万条记录。硬盘带宽(而不是查找时间)往往是瓶颈,列式存储是针对这种工作负载的日益流行的解决方案。
* 数据仓库和类似的分析系统会少见一些,因为它们主要由业务分析人员使用,而不是最终用户。它们的查询量要比 OLTP 系统少得多,但通常每个查询开销高昂,需要在短时间内扫描数百万条记录。硬盘带宽(而不是查找时间)往往是瓶颈,列式存储是针对这种工作负载的日益流行的解决方案。
在 OLTP 这一边,我们能看到两派主流的存储引擎:
@ -602,7 +602,7 @@ C-Store 中引入了这个想法的一个巧妙扩展,并在商业数据仓库
然后,我们暂时放下了存储引擎的内部细节,查看了典型数据仓库的高级架构,并说明了为什么分析工作负载与 OLTP 差别很大:当你的查询需要在大量行中顺序扫描时,索引的重要性就会降低很多。相反,非常紧凑地编码数据变得非常重要,以最大限度地减少查询需要从硬盘读取的数据量。我们讨论了列式存储如何帮助实现这一目标。
作为一名应用程序开发人员,如果你掌握了有关存储引擎内部的知识,那么你就能更好地了解哪种工具最适合你的特定应用程序。如果你需要调整数据库的调整参数,这种理解可以让你设想一个更高或更低的值可能会产生什么效果。
作为一名应用程序开发人员,如果你掌握了有关存储引擎内部的知识,那么你就能更好地了解哪种工具最适合你的特定应用程序。当你调整数据库的优化参数时,这种理解让你能够设想增减某个值会产生怎样的效果。
尽管本章不能让你成为一个特定存储引擎的调参专家,但它至少大概率使你有了足够的概念与词汇储备去读懂你所选择的数据库的文档。

View file

@ -143,13 +143,15 @@
5. [詞彙表](glossary.md)、[後記](colophon.md)關於野豬的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本與轉換指令碼 by [@afunTW](https://github.com/afunTW)
7. 多處翻譯修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97)
8. 感謝所有作出貢獻,提出意見的朋友們:
8. 多處翻譯修正 by [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip)
9. 感謝所有作出貢獻,提出意見的朋友們:
<details>
<summary><a href="https://github.com/Vonng/ddia/pulls">Pull Requests</a> & <a href="https://github.com/Vonng/ddia/issues">Issues</a></summary>
| ISSUE & Pull Requests | USER | Title |
| ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| [263](https://github.com/Vonng/ddia/pull/263) | [@zydmayday](https://github.com/zydmayday) | ch5: 修正譯文中的重複單詞 |
| [260](https://github.com/Vonng/ddia/pull/260) | [@haifeiWu](https://github.com/haifeiWu) | ch4: 修正部分不準確的翻譯 |
| [258](https://github.com/Vonng/ddia/pull/258) | [@bestgrc](https://github.com/bestgrc) | ch3: 修正一處翻譯錯誤 |
| [257](https://github.com/Vonng/ddia/pull/257) | [@UnderSam](https://github.com/UnderSam) | ch8: 修正一處拼寫錯誤 |

View file

@ -289,7 +289,7 @@
* 可演化性evolvability
使工程師在未來能輕鬆地對系統進行更改,當需求變化時為新應用場景做適配。也稱為 **可擴extensibility**、**可修改性modifiability** 或 **可塑性plasticity**
使工程師在未來能輕鬆地對系統進行更改,當需求變化時為新應用場景做適配。也稱為 **可擴充套件extensibility**、**可修改性modifiability** 或 **可塑性plasticity**
和之前提到的可靠性、可伸縮性一樣,實現這些目標也沒有簡單的解決方案。不過我們會試著想象具有可操作性,簡單性和可演化性的系統會是什麼樣子。

View file

@ -418,7 +418,7 @@ for (var i = 0; i < liElements.length; i++) {
MapReduce 是一個由 Google 推廣的程式設計模型用於在多臺機器上批次處理大規模的資料【33】。一些 NoSQL 資料儲存(包括 MongoDB 和 CouchDB支援有限形式的 MapReduce作為在多個文件中執行只讀查詢的機制。
MapReduce 將 [第十章](ch10.md) 中有更詳細的描述。現在我們將簡要討論一下 MongoDB 使用的模型。
關於 MapReduce 更詳細的介紹在 [第十章](ch10.md)。現在我們只簡要討論一下 MongoDB 使用的模型。
MapReduce 既不是一個宣告式的查詢語言,也不是一個完全命令式的查詢 API而是處於兩者之間查詢的邏輯用程式碼片段來表示這些程式碼片段會被處理框架重複性呼叫。它基於 `map`(也稱為 `collect`)和 `reduce`(也稱為 `fold``inject`)函式,兩個函式存在於許多函數語言程式設計語言中。
@ -504,7 +504,7 @@ db.observations.aggregate([
]);
```
聚合管道語言與 SQL 的子集具有類似表現力,但是它使用基於 JSON 的語法而不是 SQL 的英語句子式語法;這種差異也許是口味問題。這個故事的寓意是 NoSQL 系統可能會發現自己意外地重新發明了 SQL儘管帶著偽裝
聚合管道語言的表現力與(前述 PostgreSQL 例子的SQL 子集相當,但是它使用基於 JSON 的語法而不是 SQL 那種接近英文句式的語法這種差異也許只是口味問題。這個故事的寓意是NoSQL 系統可能會意外發現自己只是重新發明了一套經過喬裝改扮的 SQL
## 圖資料模型
@ -584,13 +584,13 @@ CREATE INDEX edges_heads ON edges (head_vertex);
2. 給定任何頂點,可以高效地找到它的入邊和出邊,從而遍歷圖,即沿著一系列頂點的路徑前後移動(這就是為什麼 [例 2-2]() 在 `tail_vertex``head_vertex` 列上都有索引的原因)。
3. 透過對不同型別的關係使用不同的標籤,可以在一個圖中儲存幾種不同的資訊,同時仍然保持一個清晰的資料模型。
這些特性為資料建模提供了很大的靈活性,如 [圖 2-5](../img/fig2-5.png) 所示。圖中顯示了一些傳統關係模式難以表達的事情,例如不同國家的不同地區結構(法國有省和美國有不同的州和州國中國的怪事先忽略主權國家和國家錯綜複雜的爛攤子不同的資料粒度Lucy 現在的住所被指定為一個城市,而她的出生地點只是在一個州的級別)。
這些特性為資料建模提供了很大的靈活性,如 [圖 2-5](../img/fig2-5.png) 所示。圖中顯示了一些傳統關係模式難以表達的事情,例如不同國家的不同地區結構(法國有省和大區美國有縣和州國中國的怪事先忽略主權國家和民族錯綜複雜的爛攤子不同的資料粒度Lucy 現在的住所記錄具體到城市,而她的出生地點只是在一個州的級別)。
你可以想象延伸圖還能包括許多關於 Lucy 和 Alain,或其他人的其他更多的事實。例如,你可以用它來表示食物過敏(為每個過敏源增加一個頂點,並增加人與過敏源之間的一條邊來指示一種過敏情況),並連結到過敏源,每個過敏源具有一組頂點用來顯示哪些食物含有哪些物質。然後,你可以寫一個查詢,找出每個人吃什麼是安全的。圖在可演化性是富有優勢的:當嚮應用程式新增功能時,可以輕鬆擴充套件圖以適應應用程式資料結構的變化。
你可以想象該圖還能延伸出許多關於 Lucy 和 Alain 的事實,或其他人的其他更多的事實。例如,你可以用它來表示食物過敏(為每個過敏源增加一個頂點,並增加人與過敏源之間的一條邊來指示一種過敏情況),並連結到過敏源,每個過敏源具有一組頂點用來顯示哪些食物含有哪些物質。然後,你可以寫一個查詢,找出每個人吃什麼是安全的。圖在可演化性方面是富有優勢的:當嚮應用程式新增功能時,可以輕鬆擴充套件圖以適應程式資料結構的變化。
### Cypher 查詢語言
Cypher 是屬性圖的宣告式查詢語言,為 Neo4j 圖形資料庫而發明【37】它是以電影 “駭客帝國” 中的一個角色來命名的,而與密碼術中的密碼無關【38】
Cypher 是屬性圖的宣告式查詢語言,為 Neo4j 圖形資料庫而發明【37】它是以電影 “駭客帝國” 中的一個角色來命名的,而與密碼學中的加密演算法無關【38】
[例 2-3]() 顯示了將 [圖 2-5](../img/fig2-5.png) 的左邊部分插入圖形資料庫的 Cypher 查詢。可以類似地新增圖的其餘部分,為了便於閱讀而省略。每個頂點都有一個像 `USA``Idaho` 這樣的符號名稱,查詢的其他部分可以使用這些名稱在頂點之間建立邊,使用箭頭符號:`Idaho - [WITHIN] ->USA` 建立一條標記為 `WITHIN` 的邊,`Idaho` 為尾節點,`USA` 為頭節點。
@ -632,11 +632,11 @@ RETURN person.name
等價地,也可以從兩個 `Location` 頂點開始反向地查詢。假如 `name` 屬性上有索引,則可以高效地找到代表美國和歐洲的兩個頂點。然後,沿著所有 `WITHIN` 入邊,可以繼續查找出所有在美國和歐洲的位置(州,地區,城市等)。最後,查找出那些可以由 `BORN_IN``LIVES_IN` 入邊到那些位置頂點的人。
通常對於宣告式查詢語言來說,在編寫查詢語句時,不需要指定執行細節:查詢最佳化程式會自動選擇預測效率最高的策略,因此你可以繼續編寫應用程式的其他部分。
通常對於宣告式查詢語言來說,在編寫查詢語句時,不需要指定執行細節:查詢最佳化程式會自動選擇預測效率最高的策略,因此你可以專注於編寫應用程式的其他部分。
### SQL 中的圖查詢
[例 2-2]() 建議在關係資料庫中表示圖資料。但是,如果把圖資料放入關係結構中,我們是否也可以使用 SQL 查詢它?
[例 2-2]() 指出,可以在關係資料庫中表示圖資料。但是,如果圖資料已經以關係結構儲存,我們是否也可以使用 SQL 查詢它?
答案是肯定的,但有些困難。在關係資料庫中,你通常會事先知道在查詢中需要哪些連線。在圖查詢中,你可能需要在找到待查詢的頂點之前,遍歷可變數量的邊。也就是說,連線的數量事先並不確定。
@ -704,7 +704,7 @@ WITH RECURSIVE
1. 原始資料型別中的值,例如字串或數字。在這種情況下,三元組的謂語和賓語相當於主語頂點上的屬性的鍵和值。例如,`(lucy, age, 33)` 就像屬性 `{“age”33}` 的頂點 lucy。
2. 圖中的另一個頂點。在這種情況下,謂語是圖中的一條邊,主語是其尾部頂點,而賓語是其頭部頂點。例如,在 `(lucy, marriedTo, alain)` 中主語和賓語 `lucy``alain` 都是頂點,並且謂語 `marriedTo` 是連線他們的邊的標籤。
[例 2-6]() 顯示了與 [例 2-3]() 相同的資料,以稱為 Turtle 的格式Notation3N3【39】的一個子集形式寫成三元組。
[例 2-6]() 展示了與 [例 2-3]() 相同的資料,以稱為 Turtle 的格式Notation3N3【39】的一個子集寫成三元組。
**例 2-6 圖 2-5 中的資料子集,表示為 Turtle 三元組**
@ -742,13 +742,13 @@ _:namerica a :Location; :name "North America"; :type "continent".
#### 語義網
如果你閱讀更多關於三元組儲存的資訊你可能會被捲入關於語義網的文章漩渦中。三元組儲存資料模型完全獨立於語義網例如Datomic【40】是三元組儲存 [^vii],並沒有聲稱與它有任何關係。但是,由於在很多人眼中這兩者緊密相連,我們應該簡要地討論一下。
如果你深入瞭解關於三元組儲存的資訊,可能會陷入關於**語義網**的討論漩渦中。三元組儲存模型其實是完全獨立於語義網存在的例如Datomic【40】作為一種三元組儲存資料庫 [^vii],從未被用於語義網中。但是,由於在很多人眼中這兩者緊密相連,我們應該簡要地討論一下。
[^vii]: 從技術上講Datomic 使用的是五元組而不是三元組,兩個額外的欄位是用於版本控制的元資料
從本質上講語義網是一個簡單且合理的想法:網站已經將資訊釋出為文字和圖片供人類閱讀,為什麼不將資訊作為機器可讀的資料也釋出給計算機呢?**資源描述框架**RDF【41】的目的是作為不同網站以統一的格式釋出資料的一種機制,允許來自不同網站的資料自動合併成 **一個數據網路** - 一種網際網路範圍內的 “通用語義網資料庫 “
從本質上講語義網是一個簡單且合理的想法:網站已經將資訊釋出為文字和圖片供人類閱讀,為什麼不將資訊作為機器可讀的資料也釋出給計算機呢?(基於三元組模型的)**資源描述框架****RDF**【41】被用作不同網站以統一的格式釋出資料的一種機制,允許來自不同網站的資料自動合併成 **一個數據網路** —— 成為一種網際網路範圍內的 “通用語義網資料庫”
不幸的是,這個語義網在二十一世紀初被過度使用,但到目前為止沒有任何跡象表明已在實踐中實現,這使得許多人嗤之以鼻。它還遭受了過多的令人眼花繚亂的縮略詞,過於複雜的標準提議和狂妄自大的苦果
不幸的是,語義網在二十一世紀初被過度炒作,但到目前為止沒有任何跡象表明已在實踐中應用,這使得許多人嗤之以鼻。它還飽受眼花繚亂的縮略詞、過於複雜的標準提案和狂妄自大的困擾
然而,如果從過去的失敗中汲取教訓,語義網專案還是擁有很多優秀的成果。即使你沒有興趣在語義網上釋出 RDF 資料,三元組這種模型也是一種好的應用程式內部資料模型。
@ -786,13 +786,13 @@ _:namerica a :Location; :name "North America"; :type "continent".
RDF 有一些奇怪之處,因為它是為了在網際網路上交換資料而設計的。三元組的主語,謂語和賓語通常是 URI。例如謂語可能是一個 URI`<http://my-company.com/namespace#within>``<http://my-company.com/namespace#lives_in>`,而不僅僅是 `WITHIN``LIVES_IN`。這個設計背後的原因為了讓你能夠把你的資料和其他人的資料結合起來,如果他們賦予單詞 `within` 或者 `lives_in` 不同的含義,兩者也不會衝突,因為它們的謂語實際上是 `<http://other.org/foo#within>``<http://other.org/foo#lives_in>`
從 RDF 的角度來看URL `<http://my-company.com/namespace>` 不一定需要能解析成什麼東西,它只是一個名稱空間。為避免與 `http://URL` 混淆,本節中的示例使用不可解析的 URI`urnexamplewithin`。幸運的是,你只需在檔案頂部指定一個字首,然後就不用再管了。
從 RDF 的角度來看URL `<http://my-company.com/namespace>` 不一定需要能解析成什麼東西,它只是一個名稱空間。為避免與 `http://URL` 混淆,本節中的示例使用不可解析的 URI`urnexamplewithin`。幸運的是,你只需在檔案頂部對這個字首做一次宣告,後續就不用再管了。
### SPARQL 查詢語言
**SPARQL** 是一種用於三元組儲存的面向 RDF 資料模型的查詢語言【43】它是 SPARQL 協議和 RDF 查詢語言的縮寫,發音為 “sparkle”。SPARQL 早於 Cypher並且由於 Cypher 的模式匹配借鑑於 SPARQL這使得它們看起來非常相似【37】。
與之前相同的查詢 - 查詢從美國轉移到歐洲的人 - 使用 SPARQL 比使用 Cypher 甚至更為簡潔(請參閱 [例 2-9]())。
與之前相同的查詢 —— 查詢從美國移民到歐洲的人 —— 使用 SPARQL 比使用 Cypher 甚至更為簡潔(請參閱 [例 2-9]())。
**例 2-9 與示例 2-4 相同的查詢,用 SPARQL 表示**
@ -812,14 +812,14 @@ SELECT ?personName WHERE {
?person :bornIn / :within* ?location. # SPARQL
```
因為 RDF 不區分屬性和邊,而只是將它們作為謂語,所以可以使用相同的語法來匹配屬性。在下面的表示式中,變數 `usa` 被繫結到任意具有值為字串 `"United States"``name` 屬性的頂點:
因為 RDF 不區分屬性和邊,而只是將它們作為謂語,所以可以使用相同的語法來匹配屬性。在下面的表示式中,變數 `usa` 被繫結到任意 `name` 屬性為字串值 `"United States"` 的頂點:
```
(usa {name:'United States'}) # Cypher
?usa :name "United States". # SPARQL
```
SPARQL 是一種很好的查詢語言 — 儘管 SPARQL 從未實現語義網,但是它仍然是一種應用程式內部使用的強大工具。
SPARQL 是一種很好的查詢語言 —— 儘管它構想的語義網從未實現,但它仍然是一種可用於應用程式內部的強大工具。
> #### 圖形資料庫與網狀模型相比較
>
@ -829,8 +829,8 @@ SPARQL 是一種很好的查詢語言 — 儘管 SPARQL 從未實現語義網,
>
> * 在 CODASYL 中,資料庫有一個模式,用於指定哪種記錄型別可以巢狀在其他記錄型別中。在圖形資料庫中,不存在這樣的限制:任何頂點都可以具有到其他任何頂點的邊。這為應用程式適應不斷變化的需求提供了更大的靈活性。
> * 在 CODASYL 中,達到特定記錄的唯一方法是遍歷其中的一個訪問路徑。在圖形資料庫中,可以透過其唯一 ID 直接引用任何頂點,也可以使用索引來查詢具有特定值的頂點。
> * 在 CODASYL,記錄的後續是一個有序集合,所以資料庫的人不得不維持排序(這會影響儲存佈局),並且插入新記錄到資料庫的應用程式不得不擔心的新記錄在這些集合中的位置。在圖形資料庫中,頂點和邊不是有序的(只能在查詢時對結果進行排序)。
> * 在 CODASYL 中,所有查詢都是命令式的,難以編寫,並且很容易因架構中的變化而受到破壞。在圖形資料庫中,如果需要,可以在命令式程式碼中編寫遍歷,但大多數圖形資料庫也支援高階宣告式查詢語言,如 Cypher 或 SPARQL。
> * 在 CODASYL 中,記錄的子專案是一個有序集合,所以資料庫必須去管理它們的次序(這會影響儲存佈局),並且應用程式在插入新記錄到資料庫時必須關注新記錄在這些集合中的位置。在圖形資料庫中,頂點和邊是無序的(只能在查詢時對結果進行排序)。
> * 在 CODASYL 中,所有查詢都是命令式的,難以編寫,並且很容易因架構變化而受到破壞。在圖形資料庫中,你可以在命令式程式碼中手寫遍歷過程,但大多數圖形資料庫都支援高階宣告式查詢,如 Cypher 或 SPARQL。
>
>
@ -838,7 +838,7 @@ SPARQL 是一種很好的查詢語言 — 儘管 SPARQL 從未實現語義網,
**Datalog** 是比 SPARQL、Cypher 更古老的語言,在 20 世紀 80 年代被學者廣泛研究【44,45,46】。它在軟體工程師中不太知名但是它是重要的因為它為以後的查詢語言提供了基礎。
在實踐中Datalog 被用於少數的資料系統中:例如,它是 Datomic 【40】的查詢語言Cascalog 【47】是一種用於查詢 Hadoop 大資料集的 Datalog 實現 [^viii]。
實踐中Datalog 在有限的幾個資料系統中使用:例如,它是 Datomic 【40】的查詢語言Cascalog 【47】是一種用於查詢 Hadoop 大資料集的 Datalog 實現 [^viii]。
[^viii]: Datomic 和 Cascalog 使用 Datalog 的 Clojure S 表示式語法。在下面的例子中使用了一個更容易閱讀的 Prolog 語法,但兩者沒有任何功能差異。
@ -862,7 +862,7 @@ name(lucy, 'Lucy').
born_in(lucy, idaho).
```
既然已經定義了資料,我們可以像之前一樣編寫相同的查詢,如 [例 2-11]() 所示。它看起來有點不同於 Cypher 或 SPARQL 的等價物但是請不要放棄它。Datalog 是 Prolog 的一個子集,如果你學過電腦科學,你可能已經見過
既然已經定義了資料,我們可以像之前一樣編寫相同的查詢,如 [例 2-11]() 所示。它看起來與 Cypher 或 SPARQL 的語法差異較大但請不要抗拒它。Datalog 是 Prolog 的一個子集,如果你是電腦科學專業的學生,可能已經見過 Prolog
**例 2-11 與示例 2-4 相同的查詢,用 Datalog 表示**
@ -881,7 +881,7 @@ migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */
?- migrated(Who, 'United States', 'Europe'). /* Who = 'Lucy'. */
```
Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一小步。我們定義 **規則**,以將新謂語告訴資料庫:在這裡,我們定義了兩個新的謂語,`within_recursive` 和 `migrated`。這些謂語不是儲存在資料庫中的三元組中,而是它們是從資料或其他規則派生而來的。規則可以引用其他規則,就像函式可以呼叫其他函式或者遞迴地呼叫自己一樣。像這樣,複雜的查詢可以一次構建其中的一小塊
Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一小步。我們定義 **規則**,以將新謂語告訴資料庫:在這裡,我們定義了兩個新的謂語,`within_recursive` 和 `migrated`。這些謂語不是儲存在資料庫中的三元組中,而是從資料或其他規則派生而來的。規則可以引用其他規則,就像函式可以呼叫其他函式或者遞迴地呼叫自己一樣。像這樣,複雜的查詢可以藉由小的磚瓦構建起來
在規則中,以大寫字母開頭的單詞是變數,謂語則用 Cypher 和 SPARQL 的方式一樣來匹配。例如,`name(Location, Name)` 透過變數繫結 `Location = namerica``Name ='North America'` 可以匹配三元組 `name(namerica, 'North America')`
@ -906,23 +906,23 @@ Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一
## 本章小結
資料模型是一個巨大的課題,在本章中,我們快速瀏覽了各種不同的模型。我們沒有足夠的空間來詳細介紹每個模型的細節,但是希望這個概述足以激起你的興趣,以更多地瞭解最適合你的應用需求的模型。
資料模型是一個巨大的課題,在本章中,我們快速瀏覽了各種不同的模型。我們沒有足夠的篇幅來詳述每個模型的細節,但是希望這個概述足以激起你的興趣,以更多地瞭解最適合你的應用需求的模型。
在歷史上,資料最開始被表示為一棵大樹(層次資料模型),但是這不利於表示多對多的關係,所以發明了關係模型來解決這個問題。最近,開發人員發現一些應用程式也不適合採用關係模型。新的非關係型 “NoSQL” 資料儲存在兩個主要方向上存在分歧
在歷史上,資料最開始被表示為一棵大樹(層次資料模型),但是這不利於表示多對多的關係,所以發明了關係模型來解決這個問題。最近,開發人員發現一些應用程式也不適合採用關係模型。新的非關係型 “NoSQL” 資料儲存分化為兩個主要方向
1. **文件資料庫** 的應用場景是:資料通常是自我包含的,而且文件之間的關係非常稀少。
2. **圖形資料庫** 用於相反的場景:任意事物都可能與任何事物相關聯。
1. **文件資料庫** 主要關注自我包含的資料文件,而且文件之間的關係非常稀少。
2. **圖形資料庫** 用於相反的場景:任意事物之間都可能存在潛在的關聯。
這三種模型(文件,關係和圖形)在今天都被廣泛使用,並且在各自的領域都發揮很好。一個模型可以用另一個模型來模擬 — 例如,圖資料可以在關係資料庫中表示 — 但結果往往是糟糕的。這就是為什麼我們有著針對不同目的的不同系統,而不是一個單一的萬能解決方案。
這三種模型(文件,關係和圖形)在今天都被廣泛使用,並且在各自的領域都發揮很好。一個模型可以用另一個模型來模擬 — 例如,圖資料可以在關係資料庫中表示 — 但結果往往是糟糕的。這就是為什麼我們有著針對不同目的的不同系統,而不是一個單一的萬能解決方案。
文件資料庫和圖資料庫有一個共同點,那就是它們通常不會為儲存的資料強制一個模式,這可以使應用程式更容易適應不斷變化的需求。但是應用程式很可能仍會假定資料具有一定的結構;這只是模式是明確的(寫入時強制)還是隱含的(讀取時處理)的問題
文件資料庫和圖資料庫有一個共同點,那就是它們通常不會將儲存的資料強制約束為特定模式,這可以使應用程式更容易適應不斷變化的需求。但是應用程式很可能仍會假定資料具有一定的結構;區別僅在於模式是**明確的**(寫入時強制)還是**隱含的**(讀取時處理)
每個資料模型都具有各自的查詢語言或框架我們討論了幾個例子SQLMapReduceMongoDB 的聚合管道CypherSPARQL 和 Datalog。我們也談到了 CSS 和 XSL/XPath它們不是資料庫查詢語言而包含有趣的相似之處。
雖然我們已經覆蓋了很多層面,但仍然有許多資料模型沒有提到。舉幾個簡單的例子:
* 使用基因組資料的研究人員通常需要執行 **序列相似性搜尋**,這意味著需要一個很長的字串(代表一個 DNA 分子),並在一個擁有類似但不完全相同的字串的大型資料庫中尋找匹配。這裡所描述的資料庫都不能處理這種用法,這就是為什麼研究人員編寫了像 GenBank 這樣的專門的基因組資料庫軟體的原因【48】。
* 粒子物理學家數十年來一直在進行大資料型別的大規模資料分析像大型強子對撞機LHC這樣的專案現在可以工作在數百億兆位元組的範圍內在這樣的規模下需要定製解決方案來阻止硬體成本的失控【49】。
* 使用基因組資料的研究人員通常需要執行 **序列相似性搜尋**,這意味著需要一個很長的字串(代表一個 DNA 序列),並在一個擁有類似但不完全相同的字串的大型資料庫中尋找匹配。這裡所描述的資料庫都不能處理這種用法,這就是為什麼研究人員編寫了像 GenBank 這樣的專門的基因組資料庫軟體的原因【48】。
* 粒子物理學家數十年來一直在進行大資料型別的大規模資料分析像大型強子對撞機LHC這樣的專案現在會處理數百 PB 的資料在這樣的規模下需要定製解決方案來阻止硬體成本的失控【49】。
* **全文搜尋** 可以說是一種經常與資料庫一起使用的資料模型。資訊檢索是一個很大的專業課題,我們不會在本書中詳細介紹,但是我們將在第三章和第三部分中介紹搜尋索引。
讓我們暫時將其放在一邊。在 [下一章](ch3.md) 中,我們將討論在 **實現** 本章描述的資料模型時會遇到的一些權衡。

View file

@ -78,7 +78,7 @@ $ cat database
### 雜湊索引
讓我們從 **鍵值資料key-value Data** 的索引開始。這不是你可以索引的唯一資料型別,但鍵值資料是很常見的。對於更複雜的索引來說,這也是一個有用的構建模組
讓我們從 **鍵值資料key-value Data** 的索引開始。這不是你可以索引的唯一資料型別,但鍵值資料是很常見的。在引入更複雜的索引之前,它是重要的第一步
鍵值儲存與在大多數程式語言中可以找到的 **字典dictionary** 型別非常相似,通常字典都是用 **雜湊對映hash map****散列表hash table** 實現的。雜湊對映在許多演算法教科書中都有描述【1,2】所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示 **記憶體中** 的資料結構,為什麼不使用它來索引 **硬碟上** 的資料呢?
@ -92,7 +92,7 @@ $ cat database
像 Bitcask 這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如鍵可能是某個貓咪影片的網址URL而值可能是該影片被播放的次數每次有人點選播放按鈕時遞增。在這種型別的工作負載中有很多寫操作但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。
直到現在,我們只是追加寫入一個檔案 —— 所以如何避免最終用完硬碟空間一種好的解決方案是將日誌分為特定大小的段segment當日志增長到特定尺寸時關閉當前段檔案並開始寫入一個新的段檔案。然後我們就可以對這些段進行 **壓縮compaction**,如 [圖 3-2](../img/fig3-2.png) 所示。這裡的壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
到目前為止,我們只是在追加寫入一個檔案 —— 所以如何避免最終用完硬碟空間?一種好的解決方案是,將日誌分為特定大小的 **segment**,當日志增長到特定尺寸時關閉當前段檔案,並開始寫入一個新的段檔案。然後,我們就可以對這些段進行 **壓縮compaction**,如 [圖 3-2](../img/fig3-2.png) 所示。這裡的壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
![](../img/fig3-2.png)
@ -136,7 +136,7 @@ $ cat database
但是,散列表索引也有其侷限性:
* 散列表必須能放進記憶體。如果你有非常多的鍵,那真是倒楣。原則上可以在硬碟上維護一個雜湊對映,不幸的是硬碟雜湊對映很難表現優秀。它需要大量的隨機訪問 I/O當它用滿時想要再增長是很昂貴的,並且雜湊衝突的處理也需要很煩瑣的邏輯【5】。
* 散列表必須能放進記憶體。如果你有非常多的鍵,那真是倒楣。原則上可以在硬碟上維護一個雜湊對映,不幸的是硬碟雜湊對映很難表現優秀。它需要大量的隨機訪問 I/O而後者耗盡時想要再擴充是很昂貴的,並且需要很煩瑣的邏輯去解決雜湊衝突【5】。
* 範圍查詢效率不高。例如,你無法輕鬆掃描 kitty00000 和 kitty99999 之間的所有鍵 —— 你必須在雜湊對映中單獨查詢每個鍵。
在下一節中,我們將看到一個沒有這些限制的索引結構。
@ -191,28 +191,28 @@ $ cat database
這裡描述的演算法本質上是 LevelDB【6】和 RocksDB【7】這些鍵值儲存引擎庫所使用的技術這些儲存引擎被設計嵌入到其他應用程式中。除此之外LevelDB 可以在 Riak 中用作 Bitcask 的替代品。在 Cassandra 和 HBase 中也使用了類似的儲存引擎【8】而且他們都受到了 Google 的 Bigtable 論文【9】引入了術語 SSTable 和 memtable )的啟發。
最初這種索引結構是由 Patrick O'Neil 等人描述的,且被命名為日誌結構合併樹(或 LSM 樹【10】它是基於更早之前的日誌結構檔案系統【11】來構建的。基於這種合併和壓縮排序檔案原理的儲存引擎通常被稱為 LSM 儲存引擎。
這種索引結構最早由 Patrick O'Neil 等人發明,且被命名為日誌結構合併樹(或 LSM 樹【10】它是基於更早之前的日誌結構檔案系統【11】來構建的。基於這種合併和壓縮排序檔案原理的儲存引擎通常被稱為 LSM 儲存引擎。
Lucene 是 Elasticsearch 和 Solr 使用的一種全文搜尋的索引引擎它使用類似的方法來儲存它的關鍵詞詞典【12,13】。全文索引比鍵值索引複雜得多但是基於類似的想法在搜尋查詢中給出一個單詞,找到提及單詞的所有文件(網頁,產品描述等)。這是透過鍵值結構實現的,其中鍵是單詞(或 **詞語**,即 term值是所有包含該單詞的文件的 ID 列表(記錄列表)。在 Lucene 中,從詞語到記錄列表的這種對映儲存在類似於 SSTable 的有序檔案中並根據需要在後臺合併【14】。
Lucene,是一種全文搜尋的索引引擎,在 Elasticsearch 和 Solr 被使用它使用類似的方法來儲存它的關鍵詞詞典【12,13】。全文索引比鍵值索引複雜得多但是基於類似的想法在搜尋查詢中,由一個給定的單詞,找到提及單詞的所有文件(網頁,產品描述等)。這也是透過鍵值結構實現的:其中鍵是 **單詞term**,值是所有包含該單詞的文件的 ID 列表(**postings list**)。在 Lucene 中,從詞語到記錄列表的這種對映儲存在類似於 SSTable 的有序檔案中,並根據需要在後臺執行合併【14】。
#### 效能最佳化
與往常一樣要讓儲存引擎在實踐中表現良好涉及到大量設計細節。例如當查詢資料庫中不存在的鍵時LSM 樹演算法可能會很慢你必須先檢查記憶體表然後檢視從最近的到最舊的所有的段可能還必須從硬碟讀取每一個段檔案然後才能確定這個鍵不存在。為了最佳化這種訪問儲存引擎通常使用額外的布隆過濾器Bloom filters【15】。 (布隆過濾器是用於近似集合內容的高效記憶體資料結構,它可以告訴你資料庫中是不是不存在某個鍵,從而為不存在的鍵節省掉許多不必要的硬碟讀取操作。)
與往常一樣要讓儲存引擎在實踐中表現良好涉及到大量設計細節。例如當查詢資料庫中不存在的鍵時LSM 樹演算法可能會很慢你必須先檢查記憶體表然後檢視從最近的到最舊的所有的段可能還必須從硬碟讀取每一個段檔案然後才能確定這個鍵不存在。為了最佳化這種訪問儲存引擎通常使用額外的布隆過濾器Bloom filters【15】。 (布隆過濾器是一種節省記憶體的資料結構,用於近似表達集合的內容,它可以告訴你資料庫中是否存在某個鍵,從而為不存在的鍵節省掉許多不必要的硬碟讀取操作。)
還有一些不同的策略來確定 SSTables 被壓縮和合並的順序和時間。最常見的選擇是 size-tiered 和 leveled compaction。LevelDB 和 RocksDB 使用 leveled compactionLevelDB 因此得名HBase 使用 size-tieredCassandra 同時支援這兩種【16】。對於 sized-tiered較新和較小的 SSTables 相繼被合併到較舊的和較大的 SSTable 中。對於 leveled compactionkey 範圍被拆分到較小的 SSTables而較舊的資料被移動到單獨的層級level這使得壓縮compaction能夠更加增量地進行並且使用較少的硬碟空間。
還有一些不同的策略來確定 SSTables 被壓縮和合並的順序和時間。最常見的選擇是 size-tiered 和 leveled compaction。LevelDB 和 RocksDB 使用 leveled compactionLevelDB 因此得名HBase 使用 size-tieredCassandra 同時支援這兩種【16】。對於 sized-tiered較新和較小的 SSTables 相繼被合併到較舊的和較大的 SSTable 中。對於 leveled compactionkey (按照分佈範圍被拆分到較小的 SSTables而較舊的資料被移動到單獨的層級level這使得壓縮compaction能夠更加增量地進行並且使用較少的硬碟空間。
即使有許多微妙的東西LSM 樹的基本思想 —— 儲存一系列在後臺合併的 SSTables —— 簡單而有效。即使資料集比可用記憶體大得多,它仍能繼續正常工作。由於資料按排序順序儲存,你可以高效地執行範圍查詢(掃描所有從某個最小值到某個最大值之間的所有鍵),並且因為硬碟寫入是連續的,所以 LSM 樹可以支援非常高的寫入吞吐量。
### B樹
前面討論的日誌結構索引正處在逐漸被接受的階段,但它們並不是最常見的索引型別。使用最廣泛的索引結構和日誌結構索引相當不同,它就是我們接下來要討論的 B 樹。
前面討論的日誌結構索引看起來已經相當可用了,但它們卻不是最常見的索引型別。使用最廣泛的索引結構和日誌結構索引相當不同,它就是我們接下來要討論的 B 樹。
從 1970 年被引入【17】僅不到 10 年後就變得 “無處不在”【18】B 樹很好地經受了時間的考驗。在幾乎所有的關係資料庫中,它們仍然是標準的索引實現,許多非關係資料庫也會使用到 B 樹。
像 SSTables 一樣B 樹保持按鍵排序的鍵值對,這允許高效的鍵值查詢和範圍查詢。但這也就是有的相似之處了B 樹有著非常不同的設計理念。
像 SSTables 一樣B 樹保持按鍵排序的鍵值對,這允許高效的鍵值查詢和範圍查詢。但這也就是有的相似之處了B 樹有著非常不同的設計理念。
我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序寫入段。相比之下B 樹將資料庫分解成固定大小的block或頁面page,傳統上大小為 4KB有時會更大並且一次只能讀取或寫入一個頁面。這種設計更接近於底層硬體因為硬碟空間也是按固定大小的塊來組織的。
我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序寫入段。相比之下B 樹將資料庫分解成固定大小的 **塊block****分頁page**,傳統上大小為 4KB有時會更大並且一次只能讀取或寫入一個頁面。這種設計更接近於底層硬體因為硬碟空間也是按固定大小的塊來組織的。
每個頁面都可以使用地址或位置來標識,這允許一個頁面引用另一個頁面 —— 類似於指標,但在硬碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹,如 [圖 3-6](../img/fig3-6.png) 所示。
@ -220,13 +220,13 @@ Lucene 是 Elasticsearch 和 Solr 使用的一種全文搜尋的索引引擎,
**圖 3-6 使用 B 樹索引查詢一個鍵**
一個頁面會被指定為 B 樹的根;在索引中查詢一個鍵時,就從這裡開始。該頁面包含幾個鍵和對子頁面的引用。每個子頁面負責一段連續範圍的鍵,引用之間的鍵,指明瞭引用子頁面的鍵範圍
一個頁面會被指定為 B 樹的根;在索引中查詢一個鍵時,就從這裡開始。該頁面包含幾個鍵和對子頁面的引用。每個子頁面負責一段連續範圍的鍵,根頁面上每兩個引用之間的鍵,表示相鄰子頁面管理的鍵的範圍(邊界)
在 [圖 3-6](../img/fig3-6.png) 的例子中,我們正在尋找鍵 251 ,所以我們知道我們需要跟蹤邊界 200 和 300 之間的頁面引用。這將我們帶到一個類似的頁面,進一步將 200 到 300 的範圍拆分到子範圍。
最終我們將到達某個包含單個鍵的頁面葉子頁面leaf page該頁面或者直接包含每個鍵的值或者包含了對可以找到值的頁面的引用。
在 B 樹的一個頁面中對子頁面的引用的數量稱為分支因子。例如,在 [圖 3-6](../img/fig3-6.png) 中,分支因子是 6。在實踐中分支因子取決於儲存頁面引用和範圍邊界所需的空間,但通常是幾百
在 B 樹的一個頁面中對子頁面的引用的數量稱為 **分支因子branching factor**。例如,在 [圖 3-6](../img/fig3-6.png) 中,分支因子是 6。在實踐中分支因子的大小取決於儲存頁面引用和範圍邊界所需的空間,但這個值通常是幾百。
如果要更新 B 樹中現有鍵的值,需要搜尋包含該鍵的葉子頁面,更改該頁面中的值,並將該頁面寫回到硬碟(對該頁面的任何引用都將保持有效)。如果你想新增一個新的鍵,你需要找到其範圍能包含新鍵的頁面,並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵,則將其分成兩個半滿頁面,並更新父頁面以反映新的鍵範圍分割槽,如 [圖 3-7](../img/fig3-7.png) 所示 [^ii]。
@ -244,39 +244,39 @@ B 樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定
你可以把覆寫硬碟上的頁面對應為實際的硬體操作。在磁性硬碟驅動器上,這意味著將磁頭移動到正確的位置,等待旋轉盤上的正確位置出現,然後用新的資料覆寫適當的扇區。在固態硬碟上,由於 SSD 必須一次擦除和重寫相當大的儲存晶片塊所以會發生更複雜的事情【19】。
而且,一些操作需要覆寫幾個不同的頁面。例如,如果因為插入導致頁面過滿而拆分頁面,則需要寫入新拆分的兩個頁面,並覆寫其父頁面以更新對兩個子頁面的引用。這是一個危險的操作,因為如果資料庫在僅有部分頁面被寫入時崩潰,那麼最終將導致一個損壞的索引(例如,可能有一個孤兒頁面不是任何父項的子項
而且,一些操作需要覆寫幾個不同的頁面。例如,如果因為插入導致頁面過滿而拆分頁面,則需要寫入新拆分的兩個頁面,並覆寫其父頁面以更新對兩個子頁面的引用。這是一個危險的操作,因為如果資料庫在系列操作進行到一半時崩潰,那麼最終將導致一個損壞的索引(例如,可能有一個孤兒頁面沒有被任何頁面引用
為了使資料庫能處理異常崩潰的場景B 樹實現通常會帶有一個額外的硬碟資料結構:**預寫式日誌**WAL即 write-ahead log也稱為 **重做日誌**,即 redo log。這是一個僅追加的檔案每個 B 樹的修改在其能被應用到樹本身的頁面之前都必須先寫入到該檔案。當資料庫在崩潰後恢復時,這個日誌將被用來使 B 樹恢復到一致的狀態【5,20】。
另外還有一個更新頁面的複雜情況是,如果多個執行緒要同時訪問 B 樹,則需要仔細的併發控制 —— 否則執行緒可能會看到樹處於不一致的狀態。這通常是透過使用 **鎖存器**latches輕量級鎖保護樹的資料結構來完成。日誌結構化的方法在這方面更簡單因為它們在後臺進行所有的合併而不會干擾新接收到的查詢並且能夠時不時地將舊的段原子交換為新的段
另外還有一個更新頁面的複雜情況是,如果多個執行緒要同時訪問 B 樹,則需要仔細的併發控制 —— 否則執行緒可能會看到樹處於不一致的狀態。這通常是透過使用 **鎖存器**latches輕量級鎖保護樹的資料結構來完成。日誌結構化的方法在這方面更簡單因為它們在後臺進行所有的合併而不會干擾新接收到的查詢並且能夠時不時地將段檔案切換為新的(該切換是原子操作)
#### B樹的最佳化
由於 B 樹已經存在了很久,所以並不奇怪這麼多年下來有很多最佳化的設計被開發出來,僅舉幾例:
* 一些資料庫(如 LMDB使用寫時複製方案【21】,而不是覆蓋頁面並維護 WAL 以支援崩潰恢復。修改的頁面被寫入到不同的位置,並且還在樹中建立了父頁面的新版本,以指向新的位置。這種方法對於併發控制也很有用,我們將在 “[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)” 中看到。
* 不同於覆寫頁面並維護 WAL 以支援崩潰恢復,一些資料庫(如 LMDB使用寫時複製方案【21】。經過修改的頁面被寫入到不同的位置,並且還在樹中建立了父頁面的新版本,以指向新的位置。這種方法對於併發控制也很有用,我們將在 “[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)” 中看到。
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級 [^iii]。
* 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每次頁面讀取可能都需要進行硬碟查詢。因此,許多 B 樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於 LSM 樹在合併過程中一次又一次地重寫儲存的大部分,所以它們更容易使順序鍵在硬碟上彼此靠近
* 額外的指標被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
* B 樹的變體如分形樹fractal tree【22】借用一些日誌結構的思想來減少硬碟查詢(而且它們與分形無關)。
* 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每個頁面的讀取都需要執行一次硬碟查詢。因此,許多 B 樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於 LSM 樹在合併過程中一次性重寫一大段儲存,所以它們更容易使順序鍵在硬碟上連續儲存
* 額外的指標被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
* B 樹的變體如 **分形樹fractal trees**【22】借用了一些日誌結構的思想來減少硬碟查詢(而且它們與分形無關)。
[^iii]: 這個變種有時被稱為 B+ 樹,但因為這個最佳化已被廣泛使用,所以經常無法區分於其它的 B 樹變種。
### 比較B樹和LSM樹
儘管 B 樹實現通常比 LSM 樹實現更成熟,但 LSM 樹由於其效能特點也非常有趣。根據經驗,通常 LSM 樹的寫入速度更快,而 B 樹的讀取速度更快【23】。 LSM 樹上的讀取通常比較慢因為它們必須檢查幾種不同的資料結構和不同壓縮Compaction層級的 SSTables。
儘管 B 樹實現通常比 LSM 樹實現更成熟,但 LSM 樹由於效能特徵也非常有趣。根據經驗,通常 LSM 樹的寫入速度更快,而 B 樹的讀取速度更快【23】。 LSM 樹上的讀取通常比較慢因為它們必須檢查幾種不同的資料結構和不同壓縮Compaction層級的 SSTables。
然而,基準測試的結果通常和工作負載的細節相關。你需要用你特有的工作負載來測試系統,以便進行有效的比較。在本節中,我們將簡要討論一些在衡量儲存引擎效能時值得考慮的事情。
#### LSM樹的優點
B 樹索引中的每塊資料都必須至少寫入兩次一次寫入預先寫入日誌WAL一次寫入樹頁面本身如果有分頁還需要再寫入一次。即使在該頁面中只有幾個位元組發生了變化也需要接受寫入整個頁面的開銷。有些儲存引擎甚至會覆寫同一個頁面兩次以免在電源故障的情況下導致頁面部分更新【24,25】。
B 樹索引中的每塊資料都必須至少寫入兩次一次寫入預先寫入日誌WAL一次寫入樹頁面本身如果有分頁還需要再寫入一次。即使在該頁面中只有幾個位元組發生了變化也需要接受寫入整個頁面的開銷。有些儲存引擎甚至會覆寫同一個頁面兩次以免在電源故障的情況下頁面未完整更新【24,25】。
由於反覆壓縮和合並 SSTables日誌結構索引也會多次重寫資料。這種影響 —— 在資料庫的生命週期中每次寫入資料庫導致對硬碟的多次寫入 —— 被稱為 **寫放大write amplification**。需要特別注意的是固態硬碟,固態硬碟的快閃記憶體壽命在覆寫有限次數後就會耗盡。
由於反覆壓縮和合並 SSTables日誌結構索引也會多次重寫資料。這種影響 —— 在資料庫的生命週期中每筆資料導致對硬碟的多次寫入 —— 被稱為 **寫入放大write amplification**。使用固態硬碟的機器需要額外關注這點,固態硬碟的快閃記憶體壽命在覆寫有限次數後就會耗盡。
在寫入繁重的應用程式中,效能瓶頸可能是資料庫可以寫入硬碟的速度。在這種情況下,寫放大會導致直接的效能代價:儲存引擎寫入硬碟的次數越多,可用硬碟頻寬內它能處理的每秒寫入次數就越少。
LSM 樹通常能夠比 B 樹支援更高的寫入吞吐量,部分原因是它們有時具有較低的寫放大(儘管這取決於儲存引擎的配置和工作負載),部分是因為它們順序地寫入緊湊的 SSTable 檔案而不是必須覆寫樹中的幾個頁面【26】。這種差異在磁性硬碟驅動器上尤其重要,其順序寫入比隨機寫入要快得多。
LSM 樹通常能夠比 B 樹支援更高的寫入吞吐量,部分原因是它們有時具有較低的寫放大(儘管這取決於儲存引擎的配置和工作負載),部分是因為它們順序地寫入緊湊的 SSTable 檔案而不是必須覆寫樹中的幾個頁面【26】。這種差異在機械硬碟上尤其重要,其順序寫入比隨機寫入要快得多。
LSM 樹可以被壓縮得更好,因此通常能比 B 樹在硬碟上產生更小的檔案。B 樹儲存引擎會由於碎片化fragmentation而留下一些未使用的硬碟空間當頁面被拆分或某行不能放入現有頁面時頁面中的某些空間仍未被使用。由於 LSM 樹不是面向頁面的,並且會透過定期重寫 SSTables 以去除碎片所以它們具有較低的儲存開銷特別是當使用分層壓縮leveled compaction時【27】。
@ -292,7 +292,7 @@ LSM 樹可以被壓縮得更好,因此通常能比 B 樹在硬碟上產生更
B 樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得 B 樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在 B 樹索引中這些鎖可以直接附加到樹上【5】。在 [第七章](ch7.md) 中,我們將更詳細地討論這一點。
B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提供了始終如一的良好效能,所以它們不可能很快就會消失。在新的資料儲存中,日誌結構化索引變得越來越流行。沒有快速和容易的規則來確定哪種型別的儲存引擎對你的場景更好,所以值得去透過一些測試來得到相關的經驗。
B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提供了始終如一的良好效能,所以它們不可能在短期內消失。在新的資料庫中,日誌結構化索引變得越來越流行。沒有簡單易行的辦法來判斷哪種型別的儲存引擎對你的使用場景更好,所以需要透過一些測試來得到相關經驗。
### 其他索引結構
@ -300,7 +300,7 @@ B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
次級索引secondary indexes也很常見。在關係資料庫中你可以使用 `CREATE INDEX` 命令在同一個表上建立多個次級索引而且這些索引通常對於有效地執行聯接join而言至關重要。例如在 [第二章](ch2.md) 中的 [圖 2-1](../img/fig2-1.png) 中,很可能在 `user_id` 列上有一個次級索引,以便你可以在每個表中找到屬於同一使用者的所有行。
次級索引可以很容易地從鍵值索引構建。次級索引主要的不同是鍵不是唯一的,即可能有許多行(文件,頂點)具有相同的鍵。這可以透過兩種方式來解決:或者將匹配行識別符號的列表作為索引裡的值就像全文索引中的記錄列表或者透過向每個鍵新增行識別符號來使鍵唯一。無論哪種方式B 樹和日誌結構索引都可以用作次級索引。
次級索引可以很容易地從鍵值索引構建。次級索引主要的不同是鍵不是唯一的即可能有許多行文件頂點具有相同的鍵。這可以透過兩種方式來解決將匹配行識別符號的列表作為索引裡的值就像全文索引中的記錄列表或者透過向每個鍵新增行識別符號來使鍵唯一。無論哪種方式B 樹和日誌結構索引都可以用作次級索引。
#### 將值儲存在索引中
@ -312,7 +312,7 @@ B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
**聚集索引**(在索引中儲存所有的行資料)和 **非聚集索引**(僅在索引中儲存對資料的引用)之間的折衷被稱為 **覆蓋索引covering index****包含列的索引index with included columns**其在索引記憶體儲表的一部分列【33】。這允許透過單獨使用索引來處理一些查詢這種情況下可以說索引 **覆蓋cover** 了查詢【32】。
與任何型別的資料重複一樣,聚集索引和覆蓋索引可以加快讀取速度,但是它們需要額外的儲存空間,並且會增加寫入開銷。資料庫還需要額外的努力來執行事務保證,因為應用程式不應看到任何因為重複而導致的不一致。
與任何型別的資料重複一樣,聚集索引和覆蓋索引可以加快讀取速度,但是它們需要額外的儲存空間,並且會增加寫入開銷。資料庫還需要額外的努力來執行事務保證,因為應用程式不應看到任何因為使用副本而導致的不一致。
#### 多列索引
@ -329,15 +329,15 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
一個標準的 B 樹或者 LSM 樹索引不能夠高效地處理這種查詢:它可以返回一個緯度範圍內的所有餐館(但經度可能是任意值),或者返回在同一個經度範圍內的所有餐館(但緯度可能是北極和南極之間的任意地方),但不能同時滿足兩個條件。
一種選擇是使用空間填充曲線將二維位置轉換為單個數字,然後使用常規 B 樹索引【34】。更普遍的是使用特殊化的空間索引例如 R 樹。例如PostGIS 使用 PostgreSQL 的通用 GiST 工具【35】將地理空間索引實現為 R 樹。這裡我們沒有足夠的地方來描述 R 樹,但是有大量的文獻可供參考。
一種選擇是使用 **空間填充曲線space-filling curve** 將二維位置轉換為單個數字,然後使用常規 B 樹索引【34】。更普遍的是使用特殊化的空間索引例如 R 樹。例如PostGIS 使用 PostgreSQL 的通用 GiST 工具【35】將地理空間索引實現為 R 樹。這裡我們沒有足夠的地方來描述 R 樹,但是有大量的文獻可供參考。
有趣的是,多維索引不僅可以用於地理位置。例如,在電子商務網站上可以使用建立在(紅,綠,藍)維度上的三維索引來搜尋特定顏色範圍內的產品,也可以在天氣觀測資料庫中建立(日期,溫度)的二維索引,以便有效地搜尋 2013 年內的溫度在 25 至 30°C 之間的所有觀測資料。如果使用一維索引,你將不得不掃描 2013 年的所有記錄(不管溫度如何),然後透過溫度進行過濾,或者反之亦然。二維索引可以同時透過時間戳和溫度來收窄資料集。這個技術被 HyperDex 所使用【36】。
#### 全文搜尋和模糊索引
到目前為止所討論的所有索引都假定你有確切的資料,並允許你查詢鍵的確切值或具有排序順序的鍵的值範圍。他們不允許你做的是搜尋類似的鍵,如拼寫錯誤的單詞。這種模糊的查詢需要不同的技術。
到目前為止所討論的所有索引都假定你有確切的資料,並允許你查詢鍵的確切值或具有排序順序的鍵的值範圍。他們不允許你做的是搜尋**類似**的鍵,如拼寫錯誤的單詞。這種模糊的查詢需要不同的技術。
例如,全文搜尋引擎通常允許搜尋一個單詞擴充套件為包括該單詞的同義詞,忽略單詞的語法變體,搜尋在相同文件中彼此靠近的單詞的出現並且支援各種其他取決於文字的語言分析功能。為了處理文件或查詢中的拼寫錯誤Lucene 能夠在一定的編輯距離(編輯距離 1 意味著新增刪除或替換了一個字母內搜尋文字【37】
例如,全文搜尋引擎通常允許搜尋目標從一個單詞擴充套件為包括該單詞的同義詞,忽略單詞的語法變體,搜尋在相同文件中的近義詞並且支援各種其他取決於文字的語言分析功能。為了處理文件或查詢中的拼寫錯誤Lucene 能夠在一定的編輯距離內搜尋文字【37】編輯距離 1 意味著單詞內發生了 1 個字母的新增、刪除或替換)
正如 “[用 SSTables 製作 LSM 樹](#用SSTables製作LSM樹)” 中所提到的Lucene 為其詞典使用了一個類似於 SSTable 的結構。這個結構需要一個小的記憶體索引,告訴查詢需要在排序檔案中哪個偏移量查詢鍵。在 LevelDB 中,這個記憶體中的索引是一些鍵的稀疏集合,但在 Lucene 中,記憶體中的索引是鍵中字元的有限狀態自動機,類似於 trie 【38】。這個自動機可以轉換成 Levenshtein 自動機它支援在給定的編輯距離內有效地搜尋單詞【39】。
@ -351,7 +351,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
某些記憶體中的鍵值儲存(如 Memcached僅用於快取在重新啟動計算機時丟失的資料是可以接受的。但其他記憶體資料庫的目標是永續性可以透過特殊的硬體例如電池供電的 RAM來實現也可以將更改日誌寫入硬碟還可以將定時快照寫入硬碟或者將記憶體中的狀態複製到其他機器上。
記憶體資料庫重新啟動時,需要從硬碟或透過網路從副本重新載入其狀態(除非使用特殊的硬體)。儘管寫入硬碟,它仍然是一個記憶體資料庫,因為硬碟僅出於永續性目的進行日誌追加,讀取請求完全由記憶體來處理。寫入硬碟同時還有運維上的好處:硬碟上的檔案可以很容易地由外部實用程式進行備份、檢查和分析。
記憶體資料庫重新啟動時,需要從硬碟或透過網路從副本重新載入其狀態(除非使用特殊的硬體)。儘管寫入硬碟,它仍然是一個記憶體資料庫,因為硬碟僅出於永續性目的進行日誌追加,讀取請求完全由記憶體來處理。寫入硬碟同時還有運維上的好處:硬碟上的檔案可以很容易地由外部程式進行備份、檢查和分析。
諸如 VoltDB、MemSQL 和 Oracle TimesTen 等產品是具有關係模型的記憶體資料庫供應商聲稱透過消除與管理硬碟上的資料結構相關的所有開銷他們可以提供巨大的效能改進【41,42】。 RAM Cloud 是一個開源的記憶體鍵值儲存器具有永續性對記憶體和硬碟上的資料都使用日誌結構化方法【43】。 Redis 和 Couchbase 透過非同步寫入硬碟提供了較弱的永續性。
@ -408,7 +408,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
幾乎所有的大型企業都有資料倉庫,但在小型企業中幾乎聞所未聞。這可能是因為大多數小公司沒有這麼多不同的 OLTP 系統,大多數小公司只有少量的資料 —— 可以在傳統的 SQL 資料庫中查詢,甚至可以在電子表格中分析。在一家大公司裡,要做一些在一家小公司很簡單的事情,需要很多繁重的工作。
使用單獨的資料倉庫,而不是直接查詢 OLTP 系統進行分析的一大優勢是資料倉庫可針對分析訪問模式進行最佳化。事實證明,本章前半部分討論的索引演算法對於 OLTP 來說工作得很好,但對於處理分析查詢並不是很好。在本章的其餘部分中,我們將研究為分析而最佳化的儲存引擎。
使用單獨的資料倉庫,而不是直接查詢 OLTP 系統進行分析的一大優勢是資料倉庫可針對分析類的訪問模式進行最佳化。事實證明,本章前半部分討論的索引演算法對於 OLTP 來說工作得很好,但對於處理分析查詢並不是很好。在本章的其餘部分中,我們將研究為分析而最佳化的儲存引擎。
#### OLTP資料庫和資料倉庫之間的分歧
@ -416,7 +416,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
表面上,一個數據倉庫和一個關係型 OLTP 資料庫看起來很相似,因為它們都有一個 SQL 查詢介面。然而,系統的內部看起來可能完全不同,因為它們針對非常不同的查詢模式進行了最佳化。現在許多資料庫供應商都只是重點支援事務處理負載和分析工作負載這兩者中的一個,而不是都支援。
一些資料庫(例如 Microsoft SQL Server 和 SAP HANA支援在同一產品中進行事務處理和資料倉庫。但是它們也正日益成為兩個獨立的儲存和查詢引擎,只是這些引擎正好可以透過一個通用的 SQL 介面訪問【49,50,51】。
一些資料庫(例如 Microsoft SQL Server 和 SAP HANA支援在同一產品中進行事務處理和資料倉庫。但是它們也正日益發展為兩套獨立的儲存和查詢引擎,只是這些引擎正好可以透過一個通用的 SQL 介面訪問【49,50,51】。
Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用昂貴的商業許可證銷售他們的系統。 Amazon RedShift 是 ParAccel 的託管版本。最近,大量的開源 SQL-on-Hadoop 專案已經出現,它們還很年輕,但是正在與商業資料倉庫系統競爭,包括 Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo 和 Apache Drill【52,53】。其中一些基於了谷歌 Dremel 的想法【54】。
@ -449,7 +449,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用
如果事實表中有萬億行和數 PB 的資料,那麼高效地儲存和查詢它們就成為一個具有挑戰性的問題。維度表通常要小得多(數百萬行),所以在本節中我們將主要關注事實表的儲存。
儘管事實表通常超過 100 列,但典型的資料倉庫查詢一次只會訪問其中 4 個或 5 個列( “`SELECT *`” 查詢很少用於分析【51】。以 [例 3-1]() 中的查詢為例:它訪問了大量的行(在 2013 日曆年中每次都有人購買水果或糖果),但只需訪問 `fact_sales` 表的三列:`date_key, product_sk, quantity`。該查詢忽略了所有其他的列。
儘管事實表通常超過 100 列,但典型的資料倉庫查詢一次只會訪問其中 4 個或 5 個列( “`SELECT *`” 查詢很少用於分析【51】。以 [例 3-1]() 中的查詢為例:它訪問了大量的行(在 2013 年中所有購買了水果或糖果的記錄),但只需訪問 `fact_sales` 表的三列:`date_key, product_sk, quantity`。該查詢忽略了所有其他的列。
**例 3-1 分析人們是否更傾向於在一週的某一天購買新鮮水果或糖果**
@ -497,7 +497,7 @@ GROUP BY
通常情況下,一列中不同值的數量與行數相比要小得多(例如,零售商可能有數十億的銷售交易,但只有 100,000 個不同的產品)。現在我們可以拿一個有 n 個不同值的列,並把它轉換成 n 個獨立的點陣圖:每個不同值對應一個位圖,每行對應一個位元位。如果該行具有該值,則該位為 1否則為 0。
如果 n 非常小(例如,國家 / 地區列可能有大約 200 個不同的值),則這些點陣圖可以將每行儲存成一個位元位。但是,如果 n 更大,大部分點陣圖中將會有很多的零(我們說它們是稀疏的)。在這種情況下,點陣圖可以另外再進行遊程編碼,如 [圖 3-11](fig3-11.png) 底部所示。這可以使列的編碼非常緊湊。
如果 n 非常小(例如,國家 / 地區列可能有大約 200 個不同的值),則這些點陣圖可以將每行儲存成一個位元位。但是,如果 n 更大,大部分點陣圖中將會有很多的零(我們說它們是稀疏的)。在這種情況下,點陣圖可以另外再進行遊程編碼run-length encoding一種無損資料壓縮技術,如 [圖 3-11](fig3-11.png) 底部所示。這可以使列的編碼非常緊湊。
這些點陣圖索引非常適合資料倉庫中常見的各種查詢。例如:
@ -522,28 +522,28 @@ WHERE product_sk = 31 AND store_sk = 3
#### 記憶體頻寬和向量化處理
對於需要掃描數百萬行的資料倉庫查詢來說,一個巨大的瓶頸是從硬盤獲取資料到記憶體的頻寬。但是,這不是唯一的瓶頸。分析型資料庫的開發人員還需要有效地利用主儲存器到 CPU 快取的頻寬,避免 CPU 指令處理流水線中的分支預測錯誤和氣泡,以及在現代 CPU 上使用單指令多資料SIMD指令【59,60】。
對於需要掃描數百萬行的資料倉庫查詢來說,一個巨大的瓶頸是從硬盤獲取資料到記憶體的頻寬。但是,這不是唯一的瓶頸。分析型資料庫的開發人員還需要有效地利用記憶體到 CPU 快取的頻寬,避免 CPU 指令處理流水線中的分支預測錯誤和閒置等待,以及在現代 CPU 上使用單指令多資料SIMD指令來加速運算【59,60】。
除了減少需要從硬碟載入的資料量以外,列式儲存佈局也可以有效利用 CPU 週期。例如,查詢引擎可以將大量壓縮的列資料放在 CPU 的 L1 快取中,然後在緊密的迴圈(即沒有函式呼叫)中遍歷。相比較每個記錄的處理都需要大量函式呼叫和條件判斷的程式碼CPU 執行這樣一個迴圈要快得多。列壓縮允許列中的更多行被放進相同數量的 L1 快取。前面描述的按位 “與” 和 “或” 運算子可以被設計為直接在這樣的壓縮列資料塊上操作。這種技術被稱為向量化處理【58,49】。
除了減少需要從硬碟載入的資料量以外,列式儲存佈局也可以有效利用 CPU 週期。例如,查詢引擎可以將一整塊壓縮好的列資料放進 CPU 的 L1 快取中,然後在緊密的迴圈(即沒有函式呼叫)中遍歷。相比於每條記錄的處理都需要大量函式呼叫和條件判斷的程式碼CPU 執行這樣一個迴圈要快得多。列壓縮允許列中的更多行被同時放進容量有限的 L1 快取。前面描述的按位 “與” 和 “或” 運算子可以被設計為直接在這樣的壓縮列資料塊上操作。這種技術被稱為向量化處理vectorized processing【58,49】。
### 列式儲存中的排序順序
在列式儲存中,儲存行的順序並不一定很重要。按插入順序儲存它們是最簡單的,因為插入一個新行只需要追加到每個列檔案。但是,我們可以選擇增加一個特定的順序,就像我們之前對 SSTables 所做的那樣,並將其用作索引機制。
在列式儲存中,儲存行的順序並不關鍵。按插入順序儲存它們是最簡單的,因為插入一個新行只需要追加到每個列檔案。但是,我們也可以選擇按某種順序來排列資料,就像我們之前對 SSTables 所做的那樣,並將其用作索引機制。
注意,每列獨自排序是沒有意義的,因為那樣我們就沒法知道不同列中的哪些項屬於同一行。我們只能在知道一列中的第 k 項與另一列中的第 k 項屬於同一行的情況才能重建出完整的行。
注意,對每列分別執行排序是沒有意義的,因為那樣就沒法知道不同列中的哪些項屬於同一行。我們只能在明確一列中的第 k 項與另一列中的第 k 項屬於同一行的情況下,才能重建出完整的行。
相反,即使按列式儲存資料,也需要一次對整行進行排序。資料庫的管理員可以根據他們對常用查詢的瞭解來選擇表格應該被排序的列。例如,如果查詢通常以日期範圍為目標,例如上個月,則可以將 `date_key` 作為第一個排序鍵。這樣查詢最佳化器就可以只掃描上個月的行了,這比掃描所有行要快得多。
相反,資料的排序需要對一整行統一操作,即使它們的儲存方式是按列的。資料庫管理員可以根據他們對常用查詢的瞭解,來選擇表格中用來排序的列。例如,如果查詢通常以日期範圍為目標,例如上個月,則可以將 `date_key` 作為第一個排序鍵。這樣查詢最佳化器就可以只掃描近1個月範圍的行了,這比掃描所有行要快得多。
對於第一排序列中具有相同值的行,可以用第二排序列來進一步排序。例如,如果 `date_key` 是 [圖 3-10](../img/fig3-10.png) 中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,以便同一天的同一產品的所有銷售都將在儲存中組合在一起。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
對於第一排序列中具有相同值的行,可以用第二排序列來進一步排序。例如,如果 `date_key` 是 [圖 3-10](../img/fig3-10.png) 中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,以便同一天的同一產品的所有銷售資料都被儲存在相鄰位置。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
序順序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有太多個不同的值,那麼在排序之後,它將具有很長的序列,其中相同的值連續重複多次。一個簡單的遊程編碼(就像我們用於 [圖 3-11](../img/fig3-11.png) 中的點陣圖一樣)可以將該列壓縮到幾千位元組 —— 即使表中有數十億行。
按順序排序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有太多個不同的值,那麼在排序之後,將會得到一個相同的值連續重複多次的序列。一個簡單的遊程編碼(就像我們用於 [圖 3-11](../img/fig3-11.png) 中的點陣圖一樣)可以將該列壓縮到幾 KB —— 即使表中有數十億行。
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長的連續的重複值。排序優先順序更低的列以基本上隨機的順序出現,所以它們可能不會被壓縮。但前幾列排序在整體上仍然是有好處的。
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長的連續的重複值。排序優先順序更低的列以幾乎隨機的順序出現,所以可能不會被壓縮。但對前幾列做排序在整體上仍然是有好處的。
#### 幾個不同的排序順序
C-Store 中引入了這個想法的一個巧妙擴充套件,並在商業資料倉庫 Vertica 中被採用【61,62】。不同的查詢受益於不同的排序順序,為什麼不以幾種不同的方式來儲存相同的資料呢?無論如何,資料需要複製到多臺機器,這樣,如果一臺機器發生故障,你不會丟失資料。你可能還需要儲存以不同方式排序的冗餘資料,以便在處理查詢時,可以使用最適合查詢模式的版本。
對這個想法,有一個巧妙的擴充套件被 C-Store 發現,並在商業資料倉庫 Vertica 中被採用【61,62】既然不同的查詢受益於不同的排序順序,為什麼不以幾種不同的方式來儲存相同的資料呢?反正資料都需要做備份,以防單點故障時丟失資料。因此你可以用不同排序方式來儲存冗餘資料,以便在處理查詢時,呼叫最適合查詢模式的版本。
在一個列式儲存中有多個排序順序有點類似於在一個面向行的儲存中有多個次級索引。但最大的區別在於面向行的儲存將每一行儲存在一個地方(在堆檔案或聚集索引中),次級索引只包含指向匹配行的指標。在列式儲存中,通常在其他地方沒有任何指向資料的指標,只有包含值的列。
@ -555,17 +555,17 @@ C-Store 中引入了這個想法的一個巧妙擴充套件,並在商業資料
幸運的是本章前面已經看到了一個很好的解決方案LSM 樹。所有的寫操作首先進入一個記憶體中的儲存,在這裡它們被新增到一個已排序的結構中,並準備寫入硬碟。記憶體中的儲存是面向行還是列的並不重要。當已經積累了足夠的寫入資料時,它們將與硬碟上的列檔案合併,並批次寫入新檔案。這基本上是 Vertica 所做的【62】。
查詢需要檢查硬碟上的列資料和最近在記憶體中的寫入,並將兩者結合起來。但是,查詢最佳化器對使用者隱藏了這個細節。從分析師的角度來看,透過插入、更新或刪除操作進行修改的資料會立即反映在後續的查詢中。
查詢操作需要檢查硬碟上的列資料和記憶體中的最近寫入,並將兩者的結果合併起來。但是,查詢最佳化器對使用者隱藏了這個細節。從分析師的角度來看,透過插入、更新或刪除操作進行修改的資料會立即反映在後續的查詢中。
### 聚合:資料立方體和物化檢視
不是每個資料倉庫都必定是一個列式儲存傳統的面向行的資料庫和其他一些架構也被使用。然而列式儲存可以顯著加快專門的分析查詢所以它正在迅速變得流行起來【51,63】。
非所有資料倉庫都需要採用列式儲存傳統的面向行的資料庫和其他一些架構也被使用。然而列式儲存可以顯著加快專門的分析查詢所以它正在迅速變得流行起來【51,63】。
資料倉庫的另一個值得一提的方面是物化彙總materialized aggregates。如前所述資料倉庫查詢通常涉及一個聚合函式如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果相同的聚合被許多不同的查詢使用那麼每次都透過原始資料來處理可能太浪費了。為什麼不將一些查詢使用最頻繁的計數或總和快取起來
資料倉庫的另一個值得一提的方面是物化聚合materialized aggregates。如前所述資料倉庫查詢通常涉及一個聚合函式如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果相同的聚合被許多不同的查詢使用那麼每次都透過原始資料來處理可能太浪費了。為什麼不將一些查詢使用最頻繁的計數或總和快取起來
建立這種快取的一種方式是物化檢視Materialized View。在關係資料模型中它通常被定義為一個標準虛擬檢視一個類似於表的物件其內容是一些查詢的結果。不同的是物化檢視是查詢結果的實際副本會被寫入硬碟而虛擬檢視只是編寫查詢的一個捷徑。從虛擬檢視讀取時SQL 引擎會將其展開到檢視的底層查詢中,然後再處理展開的查詢。
當底層資料發生變化時,物化檢視需要更新,因為它是資料的非規範化副本。資料庫可以自動完成該操作,但是這樣的更新使得寫入成本更高,這就是在 OLTP 資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中,它們可能更有意義(它們是否實際上改善了讀取效能取決於個別情況)。
當底層資料發生變化時,物化檢視需要更新,因為它是資料的非規範化副本。資料庫可以自動完成該操作,但是這樣的更新使得寫入成本更高,這就是在 OLTP 資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中,它們可能更有意義(它們是否實際上改善了讀取效能取決於使用場景)。
物化檢視的常見特例稱為資料立方體或 OLAP 立方【64】。它是按不同維度分組的聚合網格。[圖 3-12](../img/fig3-12.png) 顯示了一個例子。
@ -573,7 +573,7 @@ C-Store 中引入了這個想法的一個巧妙擴充套件,並在商業資料
**圖 3-12 資料立方的兩個維度,透過求和聚合**
想象一下,現在每個事實都只有兩個維度表的外來鍵 —— 在 [圖 3-12](../img/fig-3-12.png) 中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期 - 產品組合的所有事實的屬性(例如 `net_price`)的聚(例如 `SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
想象一下,現在每個事實都只有兩個維度表的外來鍵 —— 在 [圖 3-12](../img/fig-3-12.png) 中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期 - 產品組合的所有事實的屬性(例如 `net_price`)的聚(例如 `SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
一般來說,事實往往有兩個以上的維度。在圖 3-9 中有五個維度:日期、產品、商店、促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的,但是原理是一樣的:每個單元格都包含特定日期 - 產品 - 商店 - 促銷 - 客戶組合的銷售額。這些值可以在每個維度上求和彙總。
@ -589,7 +589,7 @@ C-Store 中引入了這個想法的一個巧妙擴充套件,並在商業資料
在高層次上,我們看到儲存引擎分為兩大類:針對 **事務處理OLTP** 最佳化的儲存引擎和針對 **線上分析OLAP** 最佳化的儲存引擎。這兩類使用場景的訪問模式之間有很大的區別:
* OLTP 系統通常面向終端使用者,這意味著系統可能會收到大量的請求。為了處理負載,應用程式在每個查詢中通常只訪問少量的記錄。應用程式使用某種鍵來請求記錄,儲存引擎使用索引來查詢所請求的鍵的資料。硬碟查詢時間往往是這裡的瓶頸。
* 資料倉庫和類似的分析系統會低調一些,因為它們主要由業務分析人員使用,而不是終端使用者。它們的查詢量要比 OLTP 系統少得多,但通常每個查詢開銷高昂,需要在短時間內掃描數百萬條記錄。硬碟頻寬(而不是查詢時間)往往是瓶頸,列式儲存是針對這種工作負載的日益流行的解決方案。
* 資料倉庫和類似的分析系統會少見一些,因為它們主要由業務分析人員使用,而不是終端使用者。它們的查詢量要比 OLTP 系統少得多,但通常每個查詢開銷高昂,需要在短時間內掃描數百萬條記錄。硬碟頻寬(而不是查詢時間)往往是瓶頸,列式儲存是針對這種工作負載的日益流行的解決方案。
在 OLTP 這一邊,我們能看到兩派主流的儲存引擎:
@ -602,7 +602,7 @@ C-Store 中引入了這個想法的一個巧妙擴充套件,並在商業資料
然後,我們暫時放下了儲存引擎的內部細節,查看了典型資料倉庫的高階架構,並說明了為什麼分析工作負載與 OLTP 差別很大:當你的查詢需要在大量行中順序掃描時,索引的重要性就會降低很多。相反,非常緊湊地編碼資料變得非常重要,以最大限度地減少查詢需要從硬碟讀取的資料量。我們討論了列式儲存如何幫助實現這一目標。
作為一名應用程式開發人員,如果你掌握了有關儲存引擎內部的知識,那麼你就能更好地瞭解哪種工具最適合你的特定應用程式。如果你需要調整資料庫的調整引數,這種理解可以讓你設想一個更高或更低的值可能會產生什麼效果。
作為一名應用程式開發人員,如果你掌握了有關儲存引擎內部的知識,那麼你就能更好地瞭解哪種工具最適合你的特定應用程式。當你調整資料庫的最佳化引數時,這種理解讓你能夠設想增減某個值會產生怎樣的效果。
儘管本章不能讓你成為一個特定儲存引擎的調參專家,但它至少大概率使你有了足夠的概念與詞彙儲備去讀懂你所選擇的資料庫的文件。

View file

@ -407,7 +407,7 @@
> #### 自動衝突解決
>
> 衝突解決規則可能很容易變得變得越來越複雜自定義程式碼可能也很容易出錯。亞馬遜是一個經常被引用的例子由於衝突解決處理程式而產生了令人意外的效果一段時間以來購物車上的衝突解決邏輯將保留新增到購物車的物品但不包括從購物車中移除的物品。因此顧客有時會看到物品重新出現在他們的購物車中即使他們之前已經被移走【37】。
> 衝突解決規則可能很容易變得越來越複雜自定義程式碼可能也很容易出錯。亞馬遜是一個經常被引用的例子由於衝突解決處理程式而產生了令人意外的效果一段時間以來購物車上的衝突解決邏輯將保留新增到購物車的物品但不包括從購物車中移除的物品。因此顧客有時會看到物品重新出現在他們的購物車中即使他們之前已經被移走【37】。
>
> 已經有一些有趣的研究來自動解決由於資料修改引起的衝突。有幾項研究值得一提:
>