This commit is contained in:
Vonng
2018-03-31 20:22:34 +08:00
parent 0aaf94ff0d
commit c6821d8a14
4 changed files with 66 additions and 62 deletions

74
ch3.md
View File

@@ -13,7 +13,7 @@
在最基本的层次上,一个数据库需要完成两件事情:当你给它数据时,它应该存储起来,而当你提问时,它应该把数据返回给你。
第二章中,我们讨论了数据模型和查询语言,即程序员录入数据库的数据格式,以及你可以再次获取它的机制。在本章中,我们讨论同样的问题,却是从数据库的视角:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。
[第2章](ch2.md)中,我们讨论了数据模型和查询语言,即程序员录入数据库的数据格式,以及你可以再次获取它的机制。在本章中,我们讨论同样的问题,却是从数据库的视角:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。
作为程序员,为什么要关心数据库如何在内部处理存储和检索?你可能不会从头开始实现自己的存储引擎,但是您需要从可用的许多存储引擎中选择适合应用程序的存储引擎。为了调谐一个存储引擎以适应应用工作负载,你需要大致了解存储引擎在做什么。
@@ -21,7 +21,7 @@
但是我们将从您最可能熟悉的两大类数据库传统关系型数据库与所谓的“NoSQL”数据库开始通过介绍它们的存储引擎来开始本章的内容。
我们会研究两大类存储引擎日志结构log-structured的存储引擎以及面向页面page-oriented的存储引擎如B树
我们会研究两大类存储引擎:**日志结构log-structured**的存储引擎,以及**面向页面page-oriented**的存储引擎如B树
@@ -40,7 +40,7 @@ db_get () {
}
```
这两个函数实现了键值存储的功能。执行`db_set key value`,会将`key``value`存储在数据库中。键和值可以是几乎任何你喜欢的东西例如值可以是JSON文档。然后调用`db_get key`,查找与该键关联的最新值并将其返回。麻雀虽小,五脏俱全:
这两个函数实现了键值存储的功能。执行 `db_set key value` ,会将 `key``value` 存储在数据库中。键和值可以是几乎任何你喜欢的东西例如值可以是JSON文档。然后调用 `db_get key` ,查找与该键关联的最新值并将其返回。麻雀虽小,五脏俱全:
```bash
$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}' $ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
@@ -48,16 +48,19 @@ $ db_get 42
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
```
底层的存储格式非常简单一个文本文件每行包含一条逗号分隔的键值对忽略转义问题的话大致类似于CSV文件。每次对`db_set`的调用都会追加记录到文件末尾,所以更新键的时候旧版本的值不会被覆盖。需要查看文件中最后一次出现的键以查找最新值(因此`db_get`中使用了`tail -n 1 `。)
底层的存储格式非常简单一个文本文件每行包含一条逗号分隔的键值对忽略转义问题的话大致类似于CSV文件。每次对 `db_set` 的调用都会追加记录到文件末尾,所以更新键的时候旧版本的值不会被覆盖。需要查看文件中最后一次出现的键以查找最新值(因此 `db_get` 中使用了 `tail -n 1 ` 。)
```bash
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}' $ db_get 42
{"name":"San Francisco","attractions":["Exploratorium"]}
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
$ db_get 42
{"name":"San Francisco","attractions":["Exploratorium"]}
$ cat database
123456,{"name":"London","attractions":["Big Ben","London Eye"]} 42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]} 42,{"name":"San Francisco","attractions":["Exploratorium"]}
```
`db_set`函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似许多数据库内部使用日志log也就是一个Append-Only的数据文件。真正的数据库有更多的问题需要处理如并发控制回收磁盘空间以免日志无限增长处理错误和部分写入记录但基本原理是一样的。日志非常有用我们还将在本书的其它部分见到它。
`db_set` 函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库内部使用**日志log**,也就是一个**仅追加append-only**的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以免日志无限增长,处理错误和部分写入记录),但基本原理是一样的。日志非常有用,我们还将在本书的其它部分见到它。
> 日志这个词通常用来指应用程序日志,应用程序输出描述发生事情的文本。本书中,日志用于更一般的含义上:一个只有追加记录的序列。它不一定是人类可读的记录,它可能是只能由其他程序读取的二进制记录。
@@ -73,7 +76,7 @@ $ cat database
### 哈希索引
让我们从键值数据key-value Data的索引开始。这不是您可以索引的唯一一种数据类型但键值数据是非常常见的。对于更复杂的索引来说这是一个有用的构建模块。
让我们从**键值数据key-value Data**的索引开始。这不是您可以索引的唯一一种数据类型,但键值数据是非常常见的。对于更复杂的索引来说,这是一个有用的构建模块。
键值存储与在大多数编程语言中可以找到的字典类型非常相似通常字典都是用散列表哈希表实现的。哈希映射在许多算法教科书中都有描述【1,2】所以我们不会详细讨论它们工作方式。既然我们已经有内存数据结构——HashMap为什么不使用它们来索引在磁盘上的数据呢
@@ -158,7 +161,7 @@ $ cat database
如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值必须比另一个段中的所有值更新(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。
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)
@@ -174,7 +177,7 @@ $ cat database
到目前为止,但是如何让你的数据首先被按键排序呢?我们的传入写入可以以任何顺序发生。
在磁盘上维护有序结构是可能的(参阅“B-Tree但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构例如红黑树或AVL树【2】。使用这些数据结构您可以按任何顺序插入键并按排序顺序读取它们。
在磁盘上维护有序结构是可能的(参阅“[B树](#B树)但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构例如红黑树或AVL树【2】。使用这些数据结构您可以按任何顺序插入键并按排序顺序读取它们。
现在我们可以使我们的存储引擎工作如下:
@@ -211,7 +214,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
我们前面看到的日志结构索引将数据库分解为可变大小的段通常是几兆字节或更大的大小并且总是按顺序编写段。相比之下B树将数据库分解成固定大小的块或页面传统上大小为4 KB有时会更大并且一次只能读取或写入一个页面。这种设计更接近于底层硬件因为磁盘也被安排在固定大小的块中。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 - 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如[图3-6](img/fig3-6.png)所示。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如[图3-6](img/fig3-6.png)所示。
![](img/fig3-6.png)
@@ -231,17 +234,17 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
**图3-7 通过分割页面来生长B树**
该算法确保树保持平衡:具有n个键的B树总是具有O(log n)的深度。大多数数据库可以放入一个三到四层的B树所以你不需要遵循许多页面引用来找到你正在查找的页面。 分支因子为500的4 KB页面的四级树可以存储多达256 TB。
该算法确保树保持平衡:具有 n 个键的B树总是具有$O(log n)$的深度。大多数数据库可以放入一个三到四层的B树所以你不需要遵循许多页面引用来找到你正在查找的页面。 分支因子为500的4KB页面的四级树可以存储多达256 TB。
#### 让B树更可靠
B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;即当页面被覆盖时对该页面的所有引用保持完整。这与日志结构索引如LSM-trees)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。
B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;即当页面被覆盖时对该页面的所有引用保持完整。这与日志结构索引如LSM)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。
您可以考虑将硬盘上的页面覆盖为实际的硬件操作。在磁性硬盘驱动器上这意味着将磁头移动到正确的位置等待旋转盘上的正确位置出现然后用新的数据覆盖适当的扇区。在固态硬盘上由于SSD必须一次擦除和重写相当大的存储芯片块所以会发生更复杂的事情【19】。
而且,一些操作需要覆盖几个不同的页面。例如,如果因为插入导致页面过度而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。
为了使数据库对崩溃具有韧性B树实现通常会带有一个额外的磁盘数据结构预写式日志WAL也称为重做日志。这是一个只能追加的文件每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时这个日志被用来恢复B树回到一致的状态【5,20】。
为了使数据库对崩溃具有韧性B树实现通常会带有一个额外的磁盘数据结构**预写式日志(write-ahead-log**WAL也称为重做日志。这是一个只能追加的文件每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时这个日志被用来使B树恢复到一致的状态【5,20】。
更新页面的一个额外的复杂情况是如果多个线程要同时访问B树则需要仔细的并发控制 - 否则线程可能会看到树处于不一致的状态。这通常通过使用**锁存器latches**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。
@@ -291,9 +294,9 @@ B树在数据库体系结构中是非常根深蒂固的为许多工作负载
到目前为止,我们只讨论了关键值索引,它们就像关系模型中的**主键primary key**索引。主键唯一标识关系表中的一行或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键或ID引用该行/文档/顶点,并且索引用于解析这样的引用。
有二级索引也很常见。在关系数据库中,您可以使用`CREATE INDEX`命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第2章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在`user_id`列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。
有二级索引也很常见。在关系数据库中,您可以使用 `CREATE INDEX` 命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第2章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。
一个二级索引可以很容易地从一个键值索引构建。主要的不同是Key不是唯一的。即可能有许多行文档顶点具有相同的键。这可以通过两种方式来解决或者通过使索引中的每个值成为匹配行标识符的列表如全文索引中的发布列表或者通过向每个索引添加行标识符来使每个关键字唯一。无论哪种方式B树和日志结构索引都可以用作辅助索引。
一个二级索引可以很容易地从一个键值索引构建。主要的不同是不是唯一的。即可能有许多行文档顶点具有相同的键。这可以通过两种方式来解决或者通过使索引中的每个值成为匹配行标识符的列表如全文索引中的发布列表或者通过向每个索引添加行标识符来使每个关键字唯一。无论哪种方式B树和日志结构索引都可以用作辅助索引。
#### 将值存储在索引中
@@ -302,7 +305,7 @@ B树在数据库体系结构中是非常根深蒂固的为许多工作负载
在某些情况下从索引到堆文件的额外跳跃对读取来说性能损失太大因此可能希望将索引行直接存储在索引中。这被称为聚集索引。例如在MySQL的InnoDB存储引擎中表的主键总是一个聚簇索引二级索引用主键而不是堆文件中的位置【31】。在SQL Server中可以为每个表指定一个聚簇索引【32】。
在**聚集索引clustered index**(在索引中存储所有行数据)和**非聚集索引nonclustered index**(仅在索引中存储对数据的引用)之间的折衷被称为**包含列的索引index with included columns**或**覆盖索引covering index**其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询这种情况叫做索引**覆盖cover**查询【32】。
在**聚集索引clustered index**(在索引中存储所有行数据)和**非聚集索引nonclustered index**(仅在索引中存储对数据的引用)之间的折衷被称为**包含列的索引index with included columns**或**覆盖索引covering index**其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询这种情况叫做索引**覆盖cover**查询【32】。
与任何类型的数据重复一样,聚簇和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应该因为重复而导致不一致。
@@ -321,7 +324,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
一个标准的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】。
@@ -362,14 +365,15 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
在业务数据处理的早期,对数据库的写入通常对应于正在进行的商业交易:进行销售,向供应商下订单,支付员工工资等等。随着数据库扩展到那些没有不涉及钱易手,术语交易仍然卡住,指的是形成一个逻辑单元的一组读写。
事务不一定具有ACID原子性一致性隔离性和持久性属性。事务处理只是意味着允许客户端进行低延迟读取和写入 - 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第7章](ch7.md)中讨论ACID属性在[第10章](ch10.md)中讨论批处理。
即使数据库开始被用于许多不同类型的博客文章游戏中的动作地址簿中的联系人等等基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的因此访问模式被称为在线事务处理OLTP
即使数据库开始被用于许多不同类型的博客文章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,因此访问模式被称为**在线事务处理OLTP, OnLine Transaction Processing**
但是,数据库也开始越来越多地用于数据分析,这些数据分析具有非常不同的访问模式。通常,分析查询需要扫描大量记录,每个记录只读取几列,并计算汇总统计信息(如计数,总和或平均值),而不是将原始数据返回给用户。例如,如果您的数据是一个销售交易表,那么分析查询可能是:
* 一月份我们每个商店的总收入是多少?
* 我们在最近的推广活动中销售多少香蕉?
* 哪种品牌的婴儿食品最常与X品牌的尿布一起购买
这些查询通常由业务分析师编写并提供给帮助公司管理层做出更好决策商业智能的报告。为了区分这种使用数据库的事务处理模式它被称为在线分析处理OLAP。【47】。OLTP和OLAP之间的区别并不总是清晰的但是一些典型的特征在[表3-1]()中列出。
这些查询通常由业务分析师编写,并提供给帮助公司管理层做出更好决策(商业智能)的报告。为了区分这种使用数据库的事务处理模式,它被称为**在线分析处理OLAP, OnLine Analytice Processing**。【47】。OLTP和OLAP之间的区别并不总是清晰的但是一些典型的特征在[表3-1]()中列出。
**表3-1 比较交易处理和分析系统的特点**
@@ -389,7 +393,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
这些OLTP系统通常具有高度的可用性并以低延迟处理事务因为这些系统往往对业务运作至关重要。因此数据库管理员密切关注他们的OLTP数据库他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询因为这些查询通常很昂贵扫描大部分数据集这会损害同时执行的事务的性能。
相比之下数据仓库是一个独立的数据库分析人员可以查询他们心中的内容而不影响OLTP操作【48】。数据仓库包含公司所有各种OLTP系统中的只读数据副本。从OLTP数据库中提取数据使用定期的数据转储或连续的更新流转换成适合分析的模式清理并加载到数据仓库中。将数据存入仓库的过程称为“抽取-转换-加载ETL如[图3-8](img/fig3-8)所示。
相比之下数据仓库是一个独立的数据库分析人员可以查询他们心中的内容而不影响OLTP操作【48】。数据仓库包含公司所有各种OLTP系统中的只读数据副本。从OLTP数据库中提取数据使用定期的数据转储或连续的更新流转换成适合分析的模式清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载ETL**”,如[图3-8](img/fig3-8)所示。
![](img/fig3-8.png)
@@ -401,7 +405,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
#### OLTP数据库和数据仓库之间的分歧
数据仓库的数据模型通常是关系型的因为SQL通常很适合分析查询。有许多图形数据分析工具可以生成SQL查询可视化结果并允许分析人员通过下钻切片和切块等操作探索数据
数据仓库的数据模型通常是关系型的因为SQL通常很适合分析查询。有许多图形数据分析工具可以生成SQL查询可视化结果并允许分析人员探索数据(通过下钻,切片和切块等操作)。
表面上一个数据仓库和一个关系OLTP数据库看起来很相似因为它们都有一个SQL查询接口。然而系统的内部看起来可能完全不同因为它们针对非常不同的查询模式进行了优化。现在许多数据库供应商都将重点放在支持事务处理或分析工作负载上而不是两者都支持。
@@ -423,7 +427,7 @@ TeradataVerticaSAP HANA和ParAccel等数据仓库供应商通常使用昂
事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(允许计算利润余额)。事实表中的其他列是对其他表(称为维表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件的发生地点,时间,方式和原因。
例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product`表中的每一行代表一种待售产品包括库存单位SKU说明品牌名称类别脂肪含量包装尺寸等。`fact_sales`表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。
例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product`表中的每一行代表一种待售产品,包括**库存单位SKU**,说明,品牌名称,类别,脂肪含量,包装尺寸等。`fact_sales`表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。
即使日期和时间通常使用维度表来表示,因为这允许对日期(诸如公共假期)的附加信息进行编码,从而允许查询区分假期和非假期的销售。
@@ -439,7 +443,7 @@ TeradataVerticaSAP 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 分析人们是否更倾向于购买新鲜水果或糖果,这取决于一周中的哪一天**
@@ -462,7 +466,7 @@ GROUP BY
在大多数OLTP数据库中存储都是以面向行的方式进行布局的表格的一行中的所有值都相邻存储。文档数据库是相似的整个文档通常存储为一个连续的字节序列。你可以在[图3-1](img/fig3-1.png)的CSV例子中看到这个。
为了处理像[例3-1]()这样的查询,您可能在`fact_sales.date_key` `fact_sales.product_sk`上有索引它们告诉存储引擎在哪里查找特定日期或特定产品的所有销售情况。但是面向行的存储引擎仍然需要将所有这些行每个包含超过100个属性从磁盘加载到内存中解析它们并过滤掉那些不符合要求的条件。这可能需要很长时间。
为了处理像[例3-1]()这样的查询,您可能在 `fact_sales.date_key` `fact_sales.product_sk`上有索引它们告诉存储引擎在哪里查找特定日期或特定产品的所有销售情况。但是面向行的存储引擎仍然需要将所有这些行每个包含超过100个属性从磁盘加载到内存中解析它们并过滤掉那些不符合要求的条件。这可能需要很长时间。
面向列的存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列,这可以节省大量的工作。这个原理如[图3-10](img/fig3-10.png)所示。
@@ -486,9 +490,9 @@ GROUP BY
**图3-11 压缩位图索引存储布局**
通常情况下一列中不同值的数量与行数相比较小例如零售商可能有数十亿的销售交易但只有100,000个不同的产品。现在我们可以得到一个有n个不同值的列,并把它转换成n个独立的位图每个不同值的一个位图每行一位。如果该行具有该值则该位为1否则为0。
通常情况下一列中不同值的数量与行数相比较小例如零售商可能有数十亿的销售交易但只有100,000个不同的产品。现在我们可以得到一个有 n 个不同值的列,并把它转换成 n 个独立的位图每个不同值的一个位图每行一位。如果该行具有该值则该位为1否则为0。
如果n非常小(例如,国家/地区列可能有大约200个不同的值则这些位图可以每行存储一位。但是如果n更大大部分位图中将会有很多的零我们说它们是稀疏的。在这种情况下位图可以另外进行游程编码如[图3-11](fig3-11.png)底部所示。这可以使列的编码非常紧凑。
如果 n 非常小(例如,国家/地区列可能有大约200个不同的值则这些位图可以每行存储一位。但是如果n更大大部分位图中将会有很多的零我们说它们是稀疏的。在这种情况下位图可以另外进行游程编码如[图3-11](fig3-11.png)底部所示。这可以使列的编码非常紧凑。
这些位图索引非常适合数据仓库中常见的各种查询。例如:
@@ -496,13 +500,13 @@ GROUP BY
WHERE product_sk IN306869
```
加载`product_sk = 30, product_sk = 68, product_sk = 69`的三个位图,并计算三个位图的按位或,这可以非常有效地完成。
加载 `product_sk = 30` , `product_sk = 68` , `product_sk = 69` 的三个位图,并计算三个位图的按位或,这可以非常有效地完成。
```sql
WHERE product_sk = 31 AND store_sk = 3
```
加载`product_sk = 31``store_sk = 3`的位图并逐位计算AND。 这是因为列按照相同的顺序包含行,因此一列的位图中的第k位对应于与另一列的位图中的第k位相同的行。
加载 `product_sk = 31``store_sk = 3` 的位图并逐位计算AND。 这是因为列按照相同的顺序包含行,因此一列的位图中的第 k 位对应于与另一列的位图中的第 k 位相同的行。
对于不同种类的数据也有各种不同的压缩方案但我们不会详细讨论它们参见【58】的概述。
@@ -525,11 +529,11 @@ WHERE product_sk = 31 AND store_sk = 3
注意每列独自排序是没有意义的因为那样我们就不会知道列中的哪些项属于同一行。我们只能重建一行因为我们知道一列中的第k项与另一列中的第k项属于同一行。
相反,即使按列存储数据,也需要一次对整行进行排序。数据库的管理员可以使用他们对常见查询的知识来选择表格应该被排序的列。例如,如果查询通常以日期范围为目标,例如上个月,则可以将`date_key`作为第一个排序键。然后,查询优化器只能扫描上个月的行,这比扫描所有行要快得多。
相反,即使按列存储数据,也需要一次对整行进行排序。数据库的管理员可以使用他们对常见查询的知识来选择表格应该被排序的列。例如,如果查询通常以日期范围为目标,例如上个月,则可以将 `date_key` 作为第一个排序键。然后,查询优化器只能扫描上个月的行,这比扫描所有行要快得多。
第二列可以确定第一列中具有相同值的任何行的排序顺序。例如,如果`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)中的位图一样)可以将该列压缩到几千字节 —— 即使表中有数十亿行。
第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长时间的重复值。排序优先级下面的列以基本上随机的顺序出现,所以它们可能不会被压缩。但前几列排序仍然是一个整体。
@@ -559,7 +563,7 @@ WHERE product_sk = 31 AND store_sk = 3
当底层数据发生变化时物化视图需要更新因为它是数据的非规范化副本。数据库可以自动完成但是这样的更新使得写入成本更高这就是在OLTP数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中它们可能更有意义不管它们是否实际上改善了读取性能取决于个别情况
物化视图的常见特例称为数据立方体或OLAP立方【64】。它是按不同维度分组的聚合网格。[图3-12](img/fig3-12.png)显示了一个例子。
物化视图的常见特例称为数据立方体或OLAP立方【64】。它是按不同维度分组的聚合网格。[图3-12](img/fig3-12.png)显示了一个例子。
![](img/fig3-12.png)
@@ -567,7 +571,7 @@ WHERE product_sk = 31 AND store_sk = 3
想象一下,现在每个事实都只有两个维度表的外键——在[图3-12](img/fig-3-12.png)中,这些是日期和产品。您现在可以绘制一个二维表格,一个轴线上的日期和另一个轴上的产品。每个单元包含具有该日期 - 产品组合的所有事实的属性(例如,`net_price`)的聚集(例如,`SUM`)。然后,您可以沿着每行或每列应用相同的汇总,并获得一个维度减少的汇总(按产品的销售额,无论日期,还是按日期销售,无论产品如何)。
一般来说事实往往有两个以上的维度。在图3-9中有五个维度日期产品商店促销和客户。要想象一个五维超立方体是什么样子是很困难的但是原理是一样的每个单元格都包含特定日期产品 - 商店 - 促销 - 客户组合的销售。这些值可以在每个维度上重复概括。
一般来说事实往往有两个以上的维度。在图3-9中有五个维度日期产品商店促销和客户。要想象一个五维超立方体是什么样子是很困难的但是原理是一样的每个单元格都包含特定日期产品-商店-促销-客户组合的销售。这些值可以在每个维度上重复概括。
物化数据立方体的优点是某些查询变得非常快,因为它们已经被有效地预先计算了。例如,如果您想知道每个商店的总销售额,则只需查看合适维度的总计,无需扫描数百万行。
@@ -586,7 +590,7 @@ WHERE product_sk = 31 AND store_sk = 3
在OLTP方面我们看到了来自两大主流学派的存储引擎
* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 BitcaskSSTablesLSM-treeLevelDBCassandraHBaseLucene等都属于这个组。
* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 BitcaskSSTablesLSMLevelDBCassandraHBaseLucene等都属于这个组。
* 就地更新学派,将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子被用在所有主要的关系数据库中还有许多非关系数据库。
日志结构的存储引擎是相对较新的发展。他们的主要想法是他们系统地将随机访问写入顺序写入磁盘由于硬盘驱动器和固态硬盘的性能特点可以实现更高的写入吞吐量。在完成OLTP方面我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。