mirror of
https://github.com/Vonng/ddia.git
synced 2026-07-29 17:49:57 +08:00
fmt fix
This commit is contained in:
74
ch3.md
74
ch3.md
@@ -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`(或没找到,如果该文件中没有该键)。
|
||||
|
||||

|
||||
|
||||
@@ -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)所示。
|
||||
|
||||

|
||||
|
||||
@@ -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)所示。
|
||||
|
||||

|
||||
|
||||
@@ -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 @@ Teradata,Vertica,SAP 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 @@ 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 分析人们是否更倾向于购买新鲜水果或糖果,这取决于一周中的哪一天**
|
||||
|
||||
@@ -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 IN(30,68,69)
|
||||
```
|
||||
|
||||
加载`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)显示了一个例子。
|
||||
|
||||

|
||||
|
||||
@@ -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方面,我们看到了来自两大主流学派的存储引擎:
|
||||
|
||||
* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM-tree,LevelDB,Cassandra,HBase,Lucene等都属于这个组。
|
||||
* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个组。
|
||||
* 就地更新学派,将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子,被用在所有主要的关系数据库中,还有许多非关系数据库。
|
||||
|
||||
日志结构的存储引擎是相对较新的发展。他们的主要想法是,他们系统地将随机访问写入顺序写入磁盘,由于硬盘驱动器和固态硬盘的性能特点,可以实现更高的写入吞吐量。在完成OLTP方面,我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。
|
||||
|
||||
Reference in New Issue
Block a user