mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
finished re-checking the translation of chapter 3
This commit is contained in:
parent
41bbd53153
commit
e3f8bb25ed
@ -61,7 +61,7 @@
|
||||
* [第三章:存储与检索](ch3.md)
|
||||
* [驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)
|
||||
* [事务处理还是分析?](ch3.md#事务处理还是分析?)
|
||||
* [列存储](ch3.md#列存储)
|
||||
* [列式存储](ch3.md#列式存储)
|
||||
* [本章小结](ch3.md#本章小结)
|
||||
* [第四章:编码与演化](ch4.md)
|
||||
* [编码数据的格式](ch4.md#编码数据的格式)
|
||||
|
@ -17,7 +17,7 @@
|
||||
* [第三章:存储与检索](ch3.md)
|
||||
* [驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)
|
||||
* [事务处理还是分析?](ch3.md#事务处理还是分析?)
|
||||
* [列存储](ch3.md#列存储)
|
||||
* [列式存储](ch3.md#列式存储)
|
||||
* [本章小结](ch3.md#本章小结)
|
||||
* [第四章:编码与演化](ch4.md)
|
||||
* [编码数据的格式](ch4.md#编码数据的格式)
|
||||
|
4
ch10.md
4
ch10.md
@ -463,7 +463,7 @@ MapReduce作业的输出处理遵循同样的原理。通过将输入视为不
|
||||
- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。
|
||||
- 与Unix工具类似,MapReduce作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好一件事的作业;而其他团队可以决定何时何地运行这项作业。
|
||||
|
||||
在这些领域,在Unix上表现良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如Avro(请参阅“[Avro](ch4.md#Avro)”)和Parquet(请参阅“[列存储](ch3.md#列存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见[第四章](ch4.md))。
|
||||
在这些领域,在Unix上表现良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如Avro(请参阅“[Avro](ch4.md#Avro)”)和Parquet(请参阅“[列式存储](ch3.md#列式存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见[第四章](ch4.md))。
|
||||
|
||||
### Hadoop与分布式数据库的对比
|
||||
|
||||
@ -673,7 +673,7 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此它们采取了不同
|
||||
|
||||
自由运行任意代码,长期以来都是传统MapReduce批处理系统与MPP数据库的区别所在(请参阅“[Hadoop与分布式数据库的对比](#Hadoop与分布式数据库的对比)”一节)。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统兼容不佳(例如Java的Maven、Javascript的npm以及Ruby的gems)。
|
||||
|
||||
然而数据流引擎已经发现,支持除连接之外的更多**声明式特性**还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外CPU开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(请参阅“[列存储](ch3.md#列存储)”),只从磁盘读取所需的列。 Hive、Spark DataFrames和Impala还使用了向量化执行(请参阅“[内存带宽和向量处理](ch3.md#内存带宽和向量处理)”):在对CPU缓存友好的内部循环中迭代数据,避免函数调用。Spark生成JVM字节码【79】,Impala使用LLVM为这些内部循环生成本机代码【41】。
|
||||
然而数据流引擎已经发现,支持除连接之外的更多**声明式特性**还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外CPU开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用列式存储布局(请参阅“[列式存储](ch3.md#列式存储)”),只从磁盘读取所需的列。 Hive、Spark DataFrames和Impala还使用了向量化执行(请参阅“[内存带宽和向量处理](ch3.md#内存带宽和向量处理)”):在对CPU缓存友好的内部循环中迭代数据,避免函数调用。Spark生成JVM字节码【79】,Impala使用LLVM为这些内部循环生成本机代码【41】。
|
||||
|
||||
通过在高级API中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来越来越像MPP数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。
|
||||
|
||||
|
2
ch12.md
2
ch12.md
@ -18,7 +18,7 @@
|
||||
|
||||
## 数据集成
|
||||
|
||||
本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在[第三章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树,以及列存储。在[第五章](ch5.md)讨论复制时,我们看到了单领导者,多领导者,和无领导者的方法。
|
||||
本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在[第三章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树,以及列式存储。在[第五章](ch5.md)讨论复制时,我们看到了单领导者,多领导者,和无领导者的方法。
|
||||
|
||||
如果你有一个类似于“我想存储一些数据并稍后再查询”的问题,那么并没有一种正确的解决方案。但对于不同的具体环境,总会有不同的合适方法。软件实现通常必须选择一种特定的方法。使单条代码路径能做到稳定健壮且表现良好已经是一件非常困难的事情了 —— 尝试在单个软件中完成所有事情,几乎可以保证,实现效果会很差。
|
||||
|
||||
|
233
ch3.md
233
ch3.md
@ -17,7 +17,7 @@
|
||||
|
||||
作为程序员,为什么要关心数据库内部存储与检索的机理?你可能不会去从头开始实现自己的存储引擎,但是你**确实**需要从许多可用的存储引擎中选择一个合适的。而且为了让存储引擎能在你的工作负载类型上运行良好,你也需要大致了解存储引擎在底层究竟做了什么。
|
||||
|
||||
特别需要注意,针对**事务性**负载优化的和针对**分析性**负载优化的存储引擎之间存在巨大差异。稍后我们将在 “[事务处理还是分析?](#事务处理还是分析?)” 一节中探讨这一区别,并在 “[列存储](#列存储)”中讨论一系列针对分析性负载而优化的存储引擎。
|
||||
特别需要注意,针对**事务性**负载优化的和针对**分析性**负载优化的存储引擎之间存在巨大差异。稍后我们将在 “[事务处理还是分析?](#事务处理还是分析?)” 一节中探讨这一区别,并在 “[列式存储](#列式存储)”中讨论一系列针对分析性负载而优化的存储引擎。
|
||||
|
||||
但首先,我们将从你可能已经很熟悉的两大类数据库(传统的关系型数据库和很多所谓的“NoSQL”数据库)中使用的**存储引擎**来开始本章的内容。我们将研究两大类存储引擎:**日志结构(log-structured)** 的存储引擎,以及**面向页面(page-oriented)** 的存储引擎(例如B树)。
|
||||
|
||||
@ -26,14 +26,14 @@
|
||||
世界上最简单的数据库可以用两个Bash函数实现:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
db_set () {
|
||||
echo "$1,$2" >> database
|
||||
}
|
||||
#!/bin/bash
|
||||
db_set () {
|
||||
echo "$1,$2" >> database
|
||||
}
|
||||
|
||||
db_get () {
|
||||
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
|
||||
}
|
||||
db_get () {
|
||||
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
|
||||
}
|
||||
```
|
||||
|
||||
这两个函数实现了键值存储的功能。执行 `db_set key value` 会将 **键(key)** 和**值(value)** 存储在数据库中。键和值(几乎)可以是你喜欢的任何东西,例如,值可以是JSON文档。然后调用 `db_get key` 会查找与该键关联的最新值并将其返回。
|
||||
@ -41,26 +41,26 @@
|
||||
麻雀虽小,五脏俱全:
|
||||
|
||||
```bash
|
||||
$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
|
||||
$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
|
||||
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
|
||||
|
||||
$ db_get 42
|
||||
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
|
||||
$ db_get 42
|
||||
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
|
||||
```
|
||||
|
||||
底层的存储格式非常简单:一个文本文件,每行包含一条逗号分隔的键值对(忽略转义问题的话,大致与CSV文件类似)。每次对 `db_set` 的调用都会向文件末尾追加记录,所以更新键的时候旧版本的值不会被覆盖 —— 因而查找最新值的时候,需要找到文件中键最后一次出现的位置(因此 `db_get` 中使用了 `tail -n 1 ` 。)
|
||||
|
||||
```bash
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
|
||||
|
||||
$ db_get 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"]}
|
||||
$ 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)** 的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收硬盘空间以避免日志无限增长,处理错误与部分写入的记录),但基本原理是一样的。日志极其有用,我们还将在本书的其它部分重复见到它好几次。
|
||||
@ -88,7 +88,7 @@
|
||||
|
||||
**图3-1 以类CSV格式存储键值对的日志,并使用内存散列映射进行索引。**
|
||||
|
||||
听上去简单,但这是一个可行的方法。现实中,Bitcask实际上就是这么做的(Riak中默认的存储引擎)【3】。 Bitcask提供高性能的读取和写入操作,但要求所有的键必须能放入可用内存中,因为散列映射完全保留在内存中。而数据值可以使用比可用内存更多的空间,因为可以在硬盘上通过一次硬盘寻道操作来加载所需部分,如果数据文件的那部分已经在文件系统缓存中,则读取根本不需要任何硬盘I/O。
|
||||
听上去简单,但这是一个可行的方法。现实中,Bitcask实际上就是这么做的(Riak中默认的存储引擎)【3】。 Bitcask提供高性能的读取和写入操作,但要求所有的键必须能放入可用内存中,因为散列映射完全保留在内存中。而数据值可以使用比可用内存更多的空间,因为可以在硬盘上通过一次硬盘查找操作来加载所需部分,如果数据文件的那部分已经在文件系统缓存中,则读取根本不需要任何硬盘I/O。
|
||||
|
||||
像Bitcask这样的存储引擎非常适合每个键的值经常更新的情况。例如,键可能是某个猫咪视频的网址(URL),而值可能是该视频被播放的次数(每次有人点击播放按钮时递增)。在这种类型的工作负载中,有很多写操作,但是没有太多不同的键 —— 每个键有很多的写操作,但是将所有键保存在内存中是可行的。
|
||||
|
||||
@ -256,9 +256,9 @@ B树的基本底层写操作是用新数据覆写硬盘上的页面,并假定
|
||||
|
||||
* 一些数据库(如LMDB)使用写时复制方案【21】,而不是覆盖页面并维护WAL以支持崩溃恢复。修改的页面被写入到不同的位置,并且还在树中创建了父页面的新版本,以指向新的位置。这种方法对于并发控制也很有用,我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
|
||||
* 我们可以通过不存储整个键,而是缩短其大小,来节省页面空间。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此也就允许更少的层级[^iii]。
|
||||
* 通常,页面可以放置在硬盘上的任何位置;没有什么要求相邻键范围的页面也放在硬盘上相邻的区域。如果某个查询需要按照排序顺序扫描大部分的键范围,那么这种按页面存储的布局可能会效率低下,因为每次页面读取可能都需要进行硬盘寻道。因此,许多B树的实现在布局树时会尽量使叶子页面按顺序出现在硬盘上。但是,随着树的增长,要维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在硬盘上彼此靠近。
|
||||
* 通常,页面可以放置在硬盘上的任何位置;没有什么要求相邻键范围的页面也放在硬盘上相邻的区域。如果某个查询需要按照排序顺序扫描大部分的键范围,那么这种按页面存储的布局可能会效率低下,因为每次页面读取可能都需要进行硬盘查找。因此,许多B树的实现在布局树时会尽量使叶子页面按顺序出现在硬盘上。但是,随着树的增长,要维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在硬盘上彼此靠近。
|
||||
* 额外的指针已被添加到树中。例如,每个叶子页面可以引用其左边和右边的兄弟页面,使得不用跳回父页面就能按顺序对键进行扫描。
|
||||
* B树的变体如分形树(fractal tree)【22】借用一些日志结构的思想来减少硬盘寻道(而且它们与分形无关)。
|
||||
* B树的变体如分形树(fractal tree)【22】借用一些日志结构的思想来减少硬盘查找(而且它们与分形无关)。
|
||||
|
||||
[^iii]: 这个变种有时被称为B+树,但因为这个优化已被广泛使用,所以经常无法区分于其它的B树变种。
|
||||
|
||||
@ -323,8 +323,8 @@ B树在数据库架构中是非常根深蒂固的,为许多工作负载都提
|
||||
**多维索引(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树索引不能够高效地处理这种查询:它可以返回一个纬度范围内的所有餐馆(但经度可能是任意值),或者返回在同一个经度范围内的所有餐馆(但纬度可能是北极和南极之间的任意地方),但不能同时满足两个条件。
|
||||
@ -366,37 +366,39 @@ B树在数据库架构中是非常根深蒂固的,为许多工作负载都提
|
||||
|
||||
## 事务处理还是分析?
|
||||
|
||||
在早期业务数据处理过程中,一次典型的数据库写入通常与一笔 *商业交易(commercial transaction)* 相对应:卖个货,向供应商下订单,支付员工工资等等。但随着数据库应用至那些不涉及到钱的领域,术语 **交易/事务(transaction)** 仍留了下来,用于指代一组读写操作构成的逻辑单元。
|
||||
在早期的业务数据处理过程中,一次典型的数据库写入通常与一笔 *商业交易(commercial transaction)* 相对应:卖个货,向供应商下订单,支付员工工资等等。但随着数据库开始应用到那些不涉及到钱的领域,术语**交易/事务(transaction)** 仍留了下来,用于指代一组读写操作构成的逻辑单元。
|
||||
|
||||
事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入 —— 而不是只能定期运行(例如每天一次)的批量处理作业。我们在[第七章](ch7.md)中讨论ACID属性,在[第十章](ch10.md)中讨论批处理。
|
||||
> 事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟的读取和写入 —— 而不是只能定期运行(例如每天一次)的批处理作业。我们在[第七章](ch7.md)中讨论ACID属性,在[第十章](ch10.md)中讨论批处理。
|
||||
|
||||
即使数据库开始被用于许多不同类型的数据,比如博客文章的评论,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理商业交易。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,这种访问模式被称为 **在线事务处理(OLTP, OnLine Transaction Processing)** 。
|
||||
即使数据库开始被用于许多不同类型的数据,比如博客文章的评论,游戏中的动作,地址簿中的联系人等等,基本的访问模式仍然类似于处理商业交易。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,这种访问模式被称为**在线事务处理(OLTP, OnLine Transaction Processing)**。
|
||||
|
||||
但是,数据库也开始越来越多地用于数据分析,这些数据分析具有非常不同的访问模式。通常,分析查询需要扫描大量记录,每个记录只读取几列,并计算汇总统计信息(如计数,总和或平均值),而不是将原始数据返回给用户。例如,如果你的数据是一个销售交易表,那么分析查询可能是:
|
||||
但是,数据库也开始越来越多地用于数据分析,这些数据分析具有非常不同的访问模式。通常,分析查询需要扫描大量记录,每个记录只读取几列,并计算汇总统计信息(如计数、总和或平均值),而不是将原始数据返回给用户。例如,如果你的数据是一个销售交易表,那么分析查询可能是:
|
||||
|
||||
* 一月份每个商店的总收入是多少?
|
||||
* 在最近的推广活动中多卖了多少香蕉?
|
||||
* 哪个牌子的婴儿食品最常与X品牌的尿布同时购买?
|
||||
|
||||
这些查询通常由业务分析师编写,并提供给帮助公司管理层做出更好决策(商业智能)的报告。为了将这种使用数据库的模式和事务处理区分开,它被称为**在线分析处理(OLAP, OnLine Analytice Processing)**。【47】。OLTP和OLAP之间的区别并不总是清晰的,但是一些典型的特征在[表3-1]()中列出。
|
||||
这些查询通常由业务分析师编写,并提供报告以帮助公司管理层做出更好的决策(商业智能)。为了将这种使用数据库的模式和事务处理区分开,它被称为**在线分析处理(OLAP, OnLine Analytice Processing)**【47】[^iv]。OLTP和OLAP之间的区别并不总是清晰的,但是一些典型的特征在[表3-1]()中列出。
|
||||
|
||||
**表3-1 比较交易处理和分析系统的特点**
|
||||
**表3-1 比较事务处理和分析系统的特点**
|
||||
|
||||
| 属性 | 事务处理 OLTP | 分析系统 OLAP |
|
||||
| 属性 | 事务处理系统 OLTP | 分析系统 OLAP |
|
||||
| :----------: | :--------------------------: | :----------------------: |
|
||||
| 主要读取模式 | 查询少量记录,按键读取 | 在大批量记录上聚合 |
|
||||
| 主要写入模式 | 随机访问,写入要求低延时 | 批量导入(ETL),事件流 |
|
||||
| 主要用户 | 终端用户,通过Web应用 | 内部数据分析师,决策支持 |
|
||||
| 主要写入模式 | 随机访问,写入要求低延时 | 批量导入(ETL)或者事件流 |
|
||||
| 主要用户 | 终端用户,通过Web应用 | 内部数据分析师,用于决策支持 |
|
||||
| 处理的数据 | 数据的最新状态(当前时间点) | 随时间推移的历史事件 |
|
||||
| 数据集尺寸 | GB ~ TB | TB ~ PB |
|
||||
|
||||
起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的:对于OLTP类型的查询以及OLAP类型的查询来说效果很好。尽管如此,在二十世纪八十年代末和九十年代初期,公司有停止使用OLTP系统进行分析的趋势,而是在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库(data warehouse)**。
|
||||
[^iv]: OLAP中的首字母O(online)的含义并不明确,它可能是指查询并不是用来生成预定义好的报告的事实,也可能是指分析师通常是交互式地使用OLAP系统来进行探索式的查询。
|
||||
|
||||
起初,事务处理和分析查询使用了相同的数据库。 SQL在这方面已证明是非常灵活的:对于OLTP类型的查询以及OLAP类型的查询来说效果都很好。尽管如此,在二十世纪八十年代末和九十年代初期,企业有停止使用OLTP系统进行分析的趋势,转而在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库(data warehouse)**。
|
||||
|
||||
### 数据仓库
|
||||
|
||||
一个企业可能有几十个不同的交易处理系统:面向终端客户的网站,控制实体商店的收银系统,跟踪仓库库存,规划车辆路线,供应链管理,员工管理等。这些系统中每一个都很复杂,需要专人维护,所以系统最终都是自动运行的。
|
||||
一个企业可能有几十个不同的交易处理系统:面向终端客户的网站,控制实体商店的收银系统,仓库库存跟踪,车辆路线规划,供应链管理,员工管理等。这些系统中每一个都很复杂,需要专人维护,所以最终这些系统互相之间都是独立运行的。
|
||||
|
||||
这些OLTP系统往往对业务运作至关重要,因而通常会要求 **高可用** 与 **低延迟**。所以DBA会密切关注他们的OLTP数据库,他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询,因为这些查询通常开销巨大,会扫描大部分数据集,这会损害同时执行的事务的性能。
|
||||
这些OLTP系统往往对业务运作至关重要,因而通常会要求 **高可用** 与 **低延迟**。所以DBA会密切关注他们的OLTP数据库,他们通常不愿意让业务分析人员在OLTP数据库上运行临时的分析查询,因为这些查询通常开销巨大,会扫描大部分数据集,这会损害同时在执行的事务的性能。
|
||||
|
||||
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响OLTP操作【48】。数据仓库包含公司各种OLTP系统中所有的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载(ETL)**”,如[图3-8](img/fig3-8.png)所示。
|
||||
|
||||
@ -406,23 +408,23 @@ B树在数据库架构中是非常根深蒂固的,为许多工作负载都提
|
||||
|
||||
几乎所有的大型企业都有数据仓库,但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的OLTP系统,大多数小公司只有少量的数据 —— 可以在传统的SQL数据库中查询,甚至可以在电子表格中分析。在一家大公司里,要做一些在一家小公司很简单的事情,需要很多繁重的工作。
|
||||
|
||||
使用单独的数据仓库,而不是直接查询OLTP系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明,本章前半部分讨论的索引算法对于OLTP来说工作得很好,但对于回答分析查询并不是很好。在本章的其余部分中,我们将研究为分析而优化的存储引擎。
|
||||
使用单独的数据仓库,而不是直接查询OLTP系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明,本章前半部分讨论的索引算法对于OLTP来说工作得很好,但对于处理分析查询并不是很好。在本章的其余部分中,我们将研究为分析而优化的存储引擎。
|
||||
|
||||
#### OLTP数据库和数据仓库之间的分歧
|
||||
|
||||
数据仓库的数据模型通常是关系型的,因为SQL通常很适合分析查询。有许多图形数据分析工具可以生成SQL查询,可视化结果,并允许分析人员探索数据(通过下钻,切片和切块等操作)。
|
||||
数据仓库的数据模型通常是关系型的,因为SQL通常很适合分析查询。有许多图形数据分析工具可以生成SQL查询,可视化结果,并允许分析人员探索数据(通过下钻、切片和切块等操作)。
|
||||
|
||||
表面上,一个数据仓库和一个关系OLTP数据库看起来很相似,因为它们都有一个SQL查询接口。然而,系统的内部看起来可能完全不同,因为它们针对非常不同的查询模式进行了优化。现在许多数据库供应商都将重点放在支持事务处理或分析工作负载上,而不是两者都支持。
|
||||
表面上,一个数据仓库和一个关系型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]的想法。
|
||||
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】。
|
||||
|
||||
### 星型和雪花型:分析的模式
|
||||
|
||||
正如[第二章](ch2.md)所探讨的,根据应用程序的需要,在事务处理领域中使用了大量不同的数据模型。另一方面,在分析型业务中,数据模型的多样性则少得多。许多数据仓库都以相当公式化的方式使用,被称为星型模式(也称为维度建模【55】)。
|
||||
|
||||
图3-9中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 `fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览量或点击量。
|
||||
[图3-9](img/fig3-9.md)中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 `fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览或点击。
|
||||
|
||||
![](img/fig3-9.png)
|
||||
|
||||
@ -430,140 +432,140 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂
|
||||
|
||||
通常情况下,事实被视为单独的事件,因为这样可以在以后分析中获得最大的灵活性。但是,这意味着事实表可以变得非常大。像苹果,沃尔玛或eBay这样的大企业在其数据仓库中可能有几十PB的交易历史,其中大部分保存在事实表中【56】。
|
||||
|
||||
事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(允许计算利润余额)。事实表中的其他列是对其他表(称为维度表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件的发生地点,时间,方式和原因。
|
||||
事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(可以用来计算利润余额)。事实表中的其他列是对其他表(称为维度表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件发生的对象、内容、地点、时间、方式和原因。
|
||||
|
||||
例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product` 表中的每一行代表一种待售产品,包括**库存单位(SKU)**,说明,品牌名称,类别,脂肪含量,包装尺寸等。`fact_sales` 表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。
|
||||
例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product` 表中的每一行代表一种待售产品,包括库存单位(SKU)、产品描述、品牌名称、类别、脂肪含量、包装尺寸等。`fact_sales` 表中的每一行都使用外键表明在特定交易中销售了什么产品。 (简单起见,如果客户一次购买了几种不同的产品,则它们在事实表中被表示为单独的行)。
|
||||
|
||||
甚至日期和时间也通常使用维度表来表示,因为这允许对日期的附加信息(诸如公共假期)进行编码,从而允许查询区分假期和非假期的销售。
|
||||
甚至日期和时间也通常使用维度表来表示,因为这允许对日期的附加信息(诸如公共假期)进行编码,从而允许区分假期和非假期的销售查询。
|
||||
|
||||
“星型模式”这个名字来源于这样一个事实,即当表关系可视化时,事实表在中间,由维度表包围;与这些表的连接就像星星的光芒。
|
||||
“星型模式”这个名字来源于这样一个事实,即当我们对表之间的关系进行可视化时,事实表在中间,被维度表包围;与这些表的连接就像星星的光芒。
|
||||
|
||||
这个模板的变体被称为雪花模式,其中维度被进一步分解为子维度。例如,品牌和产品类别可能有单独的表格,并且 `dim_product` 表格中的每一行都可以将品牌和类别作为外键引用,而不是将它们作为字符串存储在 `dim_product` 表格中。雪花模式比星形模式更规范化,但是星形模式通常是首选,因为分析师使用它更简单【55】。
|
||||
|
||||
在典型的数据仓库中,表格通常非常宽:事实表格通常有100列以上,有时甚至有数百列【51】。维度表也可以是非常宽的,因为它们包括可能与分析相关的所有元数据——例如,`dim_store` 表可以包括在每个商店提供哪些服务的细节,它是否具有店内面包房,店面面积,商店第一次开张的日期,最后一次改造的时间,离最近的高速公路的距离等等。
|
||||
在典型的数据仓库中,表格通常非常宽:事实表通常有100列以上,有时甚至有数百列【51】。维度表也可以是非常宽的,因为它们包括了所有可能与分析相关的元数据——例如,`dim_store` 表可以包括在每个商店提供哪些服务的细节,它是否具有店内面包房,店面面积,商店第一次开张的日期,最近一次改造的时间,离最近的高速公路的距离等等。
|
||||
|
||||
|
||||
## 列存储
|
||||
## 列式存储
|
||||
|
||||
如果事实表中有万亿行和数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 分析人们是否更倾向于购买新鲜水果或糖果,这取决于一周中的哪一天**
|
||||
**例3-1 分析人们是否更倾向于在一周的某一天购买新鲜水果或糖果**
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
dim_date.weekday,
|
||||
dim_product.category,
|
||||
SUM(fact_sales.quantity) AS quantity_sold
|
||||
FROM fact_sales
|
||||
JOIN dim_date ON fact_sales.date_key = dim_date.date_key
|
||||
JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
|
||||
WHERE
|
||||
dim_date.year = 2013 AND
|
||||
dim_product.category IN ('Fresh fruit', 'Candy')
|
||||
GROUP BY
|
||||
dim_date.weekday, dim_product.category;
|
||||
SELECT
|
||||
dim_date.weekday,
|
||||
dim_product.category,
|
||||
SUM(fact_sales.quantity) AS quantity_sold
|
||||
FROM fact_sales
|
||||
JOIN dim_date ON fact_sales.date_key = dim_date.date_key
|
||||
JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
|
||||
WHERE
|
||||
dim_date.year = 2013 AND
|
||||
dim_product.category IN ('Fresh fruit', 'Candy')
|
||||
GROUP BY
|
||||
dim_date.weekday, dim_product.category;
|
||||
```
|
||||
|
||||
我们如何有效地执行这个查询?
|
||||
|
||||
在大多数OLTP数据库中,存储都是以面向行的方式进行布局的:表格的一行中的所有值都相邻存储。文档数据库是相似的:整个文档通常存储为一个连续的字节序列。你可以在[图3-1](img/fig3-1.png)的CSV例子中看到这个。
|
||||
在大多数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)所示。
|
||||
列式存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。如果每个列式存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列,这可以节省大量的工作。这个原理如[图3-10](img/fig3-10.png)所示。
|
||||
|
||||
![](img/fig3-10.png)
|
||||
|
||||
**图3-10 使用列存储关系型数据,而不是行**
|
||||
**图3-10 按列存储关系型数据,而不是行**
|
||||
|
||||
列存储在关系数据模型中是最容易理解的,但它同样适用于非关系数据。例如,Parquet 【57】是一种列式存储格式,支持基于Google的Dremel 【54】的文档数据模型。
|
||||
> 列式存储在关系数据模型中是最容易理解的,但它同样适用于非关系数据。例如,Parquet【57】是一种列式存储格式,支持基于Google的Dremel的文档数据模型【54】。
|
||||
|
||||
面向列的存储布局依赖于每个列文件包含相同顺序的行。 因此,如果你需要重新组装整行,你可以从每个单独的列文件中获取第23项,并将它们放在一起形成表的第23行。
|
||||
列式存储布局依赖于每个列文件包含相同顺序的行。 因此,如果你需要重新组装完整的行,你可以从每个单独的列文件中获取第23项,并将它们放在一起形成表的第23行。
|
||||
|
||||
|
||||
### 列压缩
|
||||
|
||||
除了仅从硬盘加载查询所需的列以外,我们还可以通过压缩数据来进一步降低对硬盘吞吐量的需求。幸运的是,面向列的存储通常很适合压缩。
|
||||
除了仅从硬盘加载查询所需的列以外,我们还可以通过压缩数据来进一步降低对硬盘吞吐量的需求。幸运的是,列式存储通常很适合压缩。
|
||||
|
||||
看看[图3-10](img/fig3-10.png)中每一列的值序列:它们通常看起来是相当重复的,这是压缩的好兆头。根据列中的数据,可以使用不同的压缩技术。在数据仓库中特别有效的一种技术是位图编码,如[图3-11](img/fig3-11.png)所示。
|
||||
|
||||
![](img/fig3-11.png)
|
||||
|
||||
**图3-11 压缩位图索引存储布局**
|
||||
**图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)底部所示。这可以使列的编码非常紧凑。
|
||||
|
||||
这些位图索引非常适合数据仓库中常见的各种查询。例如:
|
||||
|
||||
```sql
|
||||
WHERE product_sk IN(30,68,69)
|
||||
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`这三个位图,并计算三个位图的按位或(OR),这可以非常有效地完成。
|
||||
|
||||
```sql
|
||||
WHERE product_sk = 31 AND store_sk = 3
|
||||
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】的概述。
|
||||
|
||||
> #### 面向列的存储和列族
|
||||
> #### 列式存储和列族
|
||||
>
|
||||
> Cassandra和HBase有一个列族的概念,他们从Bigtable继承【9】。然而,把它们称为面向列是非常具有误导性的:在每个列族中,它们将一行中的所有列与行键一起存储,并且不使用列压缩。因此,Bigtable模型仍然主要是面向行的。
|
||||
> Cassandra和HBase有一个列族(column families)的概念,他们从Bigtable继承【9】。然而,把它们称为列式(column-oriented)是非常具有误导性的:在每个列族中,它们将一行中的所有列与行键一起存储,并且不使用列压缩。因此,Bigtable模型仍然主要是面向行的。
|
||||
>
|
||||
|
||||
#### 内存带宽和向量处理
|
||||
#### 内存带宽和矢量化处理
|
||||
|
||||
对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从硬盘获取数据到内存的带宽。但是,这不是唯一的瓶颈。分析数据库的开发人员也担心有效利用主存储器带宽到CPU缓存中的带宽,避免CPU指令处理流水线中的分支错误预测和泡沫,以及在现代中使用单指令多数据(SIMD)指令CPU 【59,60】。
|
||||
对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从硬盘获取数据到内存的带宽。但是,这不是唯一的瓶颈。分析型数据库的开发人员还需要有效地利用主存储器到CPU缓存的带宽,避免CPU指令处理流水线中的分支预测错误和气泡,以及在现代CPU上使用单指令多数据(SIMD)指令【59,60】。
|
||||
|
||||
除了减少需要从硬盘加载的数据量以外,面向列的存储布局也可以有效利用CPU周期。例如,查询引擎可以将大量压缩的列数据放在CPU的L1缓存中,然后在紧密的循环中循环(即没有函数调用)。相比较每个记录的处理都需要大量函数调用和条件判断的代码,CPU执行这样一个循环要快得多。列压缩允许列中的更多行适合相同数量的L1缓存。前面描述的按位“与”和“或”运算符可以被设计为直接在这样的压缩列数据块上操作。这种技术被称为矢量化处理【58,49】。
|
||||
除了减少需要从硬盘加载的数据量以外,列式存储布局也可以有效利用CPU周期。例如,查询引擎可以将大量压缩的列数据放在CPU的L1缓存中,然后在紧密的循环(即没有函数调用)中遍历。相比较每个记录的处理都需要大量函数调用和条件判断的代码,CPU执行这样一个循环要快得多。列压缩允许列中的更多行被放进相同数量的L1缓存。前面描述的按位“与”和“或”运算符可以被设计为直接在这样的压缩列数据块上操作。这种技术被称为矢量化处理【58,49】。
|
||||
|
||||
|
||||
### 列存储中的排序顺序
|
||||
### 列式存储中的排序顺序
|
||||
|
||||
在列存储中,存储行的顺序并不一定很重要。按插入顺序存储它们是最简单的,因为插入一个新行只需要追加到每个列文件。但是,我们可以选择增加一个特定的顺序,就像我们之前对SSTables所做的那样,并将其用作索引机制。
|
||||
在列式存储中,存储行的顺序并不一定很重要。按插入顺序存储它们是最简单的,因为插入一个新行只需要追加到每个列文件。但是,我们可以选择增加一个特定的顺序,就像我们之前对SSTables所做的那样,并将其用作索引机制。
|
||||
|
||||
注意,每列独自排序是没有意义的,因为那样我们就不会知道列中的哪些项属于同一行。我们只能在知道一列中的第k项与另一列中的第k项属于同一行的情况才能重建出完整的行。
|
||||
注意,每列独自排序是没有意义的,因为那样我们就没法知道不同列中的哪些项属于同一行。我们只能在知道一列中的第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)中的位图一样)可以将该列压缩到几千字节 —— 即使表中有数十亿行。
|
||||
|
||||
第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长时间的重复值。排序优先级更低的列以基本上随机的顺序出现,所以它们可能不会被压缩。但前几列排序在整体上仍然是有好处的。
|
||||
第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长的连续的重复值。排序优先级更低的列以基本上随机的顺序出现,所以它们可能不会被压缩。但前几列排序在整体上仍然是有好处的。
|
||||
|
||||
#### 几个不同的排序顺序
|
||||
|
||||
这个想法的巧妙扩展在C-Store中引入,并在商业数据仓库Vertica【61,62】中被采用。不同的查询受益于不同的排序顺序,为什么不以几种不同的方式来存储相同的数据呢?无论如何,数据需要复制到多台机器,这样,如果一台机器发生故障,你不会丢失数据。你可能还需要存储以不同方式排序的冗余数据,以便在处理查询时,可以使用最适合查询模式的版本。
|
||||
C-Store中引入了这个想法的一个巧妙扩展,并在商业数据仓库Vertica中被采用【61,62】。不同的查询受益于不同的排序顺序,为什么不以几种不同的方式来存储相同的数据呢?无论如何,数据需要复制到多台机器,这样,如果一台机器发生故障,你不会丢失数据。你可能还需要存储以不同方式排序的冗余数据,以便在处理查询时,可以使用最适合查询模式的版本。
|
||||
|
||||
在一个面向列的存储中有多个排序顺序有点类似于在一个面向行的存储中有多个次级索引。但最大的区别在于面向行的存储将每一行保存在一个地方(在堆文件或聚簇索引中),次级索引只包含指向匹配行的指针。在列存储中,通常在其他地方没有任何指向数据的指针,只有包含值的列。
|
||||
在一个列式存储中有多个排序顺序有点类似于在一个面向行的存储中有多个次级索引。但最大的区别在于面向行的存储将每一行保存在一个地方(在堆文件或聚集索引中),次级索引只包含指向匹配行的指针。在列式存储中,通常在其他地方没有任何指向数据的指针,只有包含值的列。
|
||||
|
||||
### 写入列存储
|
||||
### 写入列式存储
|
||||
|
||||
这些优化在数据仓库中是有意义的,因为大多数负载由分析人员运行的大型只读查询组成。面向列的存储,压缩和排序都有助于更快地读取这些查询。然而,他们的缺点是写入更加困难。
|
||||
这些优化在数据仓库中是有意义的,因为其负载主要由分析人员运行的大型只读查询组成。列式存储、压缩和排序都有助于更快地读取这些查询。然而,他们的缺点是写入更加困难。
|
||||
|
||||
使用B树的就地更新方法对于压缩的列是不可能的。如果你想在排序表的中间插入一行,你很可能不得不重写所有的列文件。由于行由列中的位置标识,因此插入必须始终更新所有列。
|
||||
使用B树的就地更新方法对于压缩的列是不可能的。如果你想在排序表的中间插入一行,你很可能不得不重写所有的列文件。由于行由列中的位置标识,因此插入必须对所有列进行一致地更新。
|
||||
|
||||
幸运的是,本章前面已经看到了一个很好的解决方案:LSM树。所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入硬盘。内存中的存储是面向行还是列的,这并不重要。当已经积累了足够的写入数据时,它们将与硬盘上的列文件合并,并批量写入新文件。这基本上是Vertica所做的【62】。
|
||||
幸运的是,本章前面已经看到了一个很好的解决方案:LSM树。所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入硬盘。内存中的存储是面向行还是列的并不重要。当已经积累了足够的写入数据时,它们将与硬盘上的列文件合并,并批量写入新文件。这基本上是Vertica所做的【62】。
|
||||
|
||||
查询需要检查硬盘上的列数据和最近在内存中的写入,并将两者结合起来。但是,查询优化器隐藏了用户的这个区别。从分析师的角度来看,通过插入,更新或删除操作进行修改的数据会立即反映在后续查询中。
|
||||
查询需要检查硬盘上的列数据和最近在内存中的写入,并将两者结合起来。但是,查询优化器对用户隐藏了这个细节。从分析师的角度来看,通过插入、更新或删除操作进行修改的数据会立即反映在后续的查询中。
|
||||
|
||||
### 聚合:数据立方体和物化视图
|
||||
|
||||
并不是每个数据仓库都必定是一个列存储:传统的面向行的数据库和其他一些架构也被使用。然而,对于专门的分析查询,列式存储可以显著加快,所以它正在迅速普及【51,63】。
|
||||
并不是每个数据仓库都必定是一个列式存储:传统的面向行的数据库和其他一些架构也被使用。然而,列式存储可以显著加快专门的分析查询,所以它正在迅速变得流行起来【51,63】。
|
||||
|
||||
数据仓库的另一个值得一提的是物化汇总。如前所述,数据仓库查询通常涉及一个聚合函数,如SQL中的COUNT,SUM,AVG,MIN或MAX。如果相同的聚合被许多不同的查询使用,那么每次都可以通过原始数据来处理。为什么不缓存一些查询使用最频繁的计数或总和?
|
||||
数据仓库的另一个值得一提的方面是物化汇总(materialized aggregates)。如前所述,数据仓库查询通常涉及一个聚合函数,如SQL中的COUNT、SUM、AVG、MIN或MAX。如果相同的聚合被许多不同的查询使用,那么每次都通过原始数据来处理可能太浪费了。为什么不将一些查询使用最频繁的计数或总和缓存起来?
|
||||
|
||||
创建这种缓存的一种方式是物化视图(Materialized View)。在关系数据模型中,它通常被定义为一个标准(虚拟)视图:一个类似于表的对象,其内容是一些查询的结果。不同的是,物化视图是查询结果的实际副本,写入硬盘,而虚拟视图只是写入查询的捷径。从虚拟视图读取时,SQL引擎会将其展开到视图的底层查询中,然后处理展开的查询。
|
||||
创建这种缓存的一种方式是物化视图(Materialized View)。在关系数据模型中,它通常被定义为一个标准(虚拟)视图:一个类似于表的对象,其内容是一些查询的结果。不同的是,物化视图是查询结果的实际副本,会被写入硬盘,而虚拟视图只是编写查询的一个捷径。从虚拟视图读取时,SQL引擎会将其展开到视图的底层查询中,然后再处理展开的查询。
|
||||
|
||||
当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成,但是这样的更新使得写入成本更高,这就是在OLTP数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中,它们可能更有意义(它们是否实际上改善了读取性能取决于个别情况)。
|
||||
当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成该操作,但是这样的更新使得写入成本更高,这就是在OLTP数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中,它们可能更有意义(它们是否实际上改善了读取性能取决于个别情况)。
|
||||
|
||||
物化视图的常见特例称为数据立方体或OLAP立方【64】。它是按不同维度分组的聚合网格。[图3-12](img/fig3-12.png)显示了一个例子。
|
||||
|
||||
@ -571,41 +573,38 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
|
||||
**图3-12 数据立方的两个维度,通过求和聚合**
|
||||
|
||||
想象一下,现在每个事实都只有两个维度表的外键——在[图3-12](img/fig-3-12.png)中,这些是日期和产品。你现在可以绘制一个二维表格,一个轴线上是日期,另一个轴线上是产品。每个单元包含具有该日期 - 产品组合的所有事实的属性(例如,`net_price`)的聚集(例如,`SUM`)。然后,你可以沿着每行或每列应用相同的汇总,并获得一个维度减少的汇总(按产品的销售额,无论日期,还是按日期销售,无论产品如何)。
|
||||
想象一下,现在每个事实都只有两个维度表的外键 —— 在[图3-12](img/fig-3-12.png)中分别是日期和产品。你现在可以绘制一个二维表格,一个轴线上是日期,另一个轴线上是产品。每个单元格包含具有该日期-产品组合的所有事实的属性(例如`net_price`)的聚集(例如`SUM`)。然后,你可以沿着每行或每列应用相同的汇总,并获得减少了一个维度的汇总(按产品的销售额,无论日期,或者按日期的销售额,无论产品)。
|
||||
|
||||
一般来说,事实往往有两个以上的维度。在图3-9中有五个维度:日期,产品,商店,促销和客户。要想象一个五维超立方体是什么样子是很困难的,但是原理是一样的:每个单元格都包含特定日期-产品-商店-促销-客户组合的销售。这些值可以在每个维度上重复概括。
|
||||
一般来说,事实往往有两个以上的维度。在图3-9中有五个维度:日期、产品、商店、促销和客户。要想象一个五维超立方体是什么样子是很困难的,但是原理是一样的:每个单元格都包含特定日期-产品-商店-促销-客户组合的销售额。这些值可以在每个维度上求和汇总。
|
||||
|
||||
物化数据立方体的优点是某些查询变得非常快,因为它们已经被有效地预先计算了。例如,如果你想知道每个商店的总销售额,则只需查看合适维度的总计,无需扫描数百万行。
|
||||
物化数据立方体的优点是可以让某些查询变得非常快,因为它们已经被有效地预先计算了。例如,如果你想知道每个商店的总销售额,则只需查看合适维度的总计,而无需扫描数百万行的原始数据。
|
||||
|
||||
缺点是数据立方体不具有查询原始数据的灵活性。例如,没有办法计算哪个销售比例来自成本超过100美元的项目,因为价格不是其中的一个维度。因此,大多数数据仓库试图保留尽可能多的原始数据,并将聚合数据(如数据立方体)仅用作某些查询的性能提升。
|
||||
数据立方体的缺点是不具有查询原始数据的灵活性。例如,没有办法计算有多少比例的销售来自成本超过100美元的项目,因为价格不是其中的一个维度。因此,大多数数据仓库试图保留尽可能多的原始数据,并将聚合数据(如数据立方体)仅用作某些查询的性能提升手段。
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
在本章中,我们试图深入了解数据库如何处理存储和检索。将数据存储在数据库中会发生什么,以及稍后再次查询数据时数据库会做什么?
|
||||
在本章中,我们试图深入了解数据库是如何处理存储和检索的。将数据存储在数据库中会发生什么?稍后再次查询数据时数据库会做什么?
|
||||
|
||||
在高层次上,我们看到存储引擎分为两大类:优化 **事务处理(OLTP)** 或 **在线分析(OLAP)** 。这些用例的访问模式之间有很大的区别:
|
||||
在高层次上,我们看到存储引擎分为两大类:针对**事务处理(OLTP)** 优化的存储引擎和针对**在线分析(OLAP)** 优化的存储引擎。这两类使用场景的访问模式之间有很大的区别:
|
||||
|
||||
* OLTP系统通常面向用户,这意味着系统可能会收到大量的请求。为了处理负载,应用程序通常只访问每个查询中的少部分记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。硬盘寻道时间往往是这里的瓶颈。
|
||||
* 数据仓库和类似的分析系统会低调一些,因为它们主要由业务分析人员使用,而不是由最终用户使用。它们的查询量要比OLTP系统少得多,但通常每个查询开销高昂,需要在短时间内扫描数百万条记录。硬盘带宽(而不是查找时间)往往是瓶颈,列式存储是这种工作负载越来越流行的解决方案。
|
||||
* OLTP系统通常面向最终用户,这意味着系统可能会收到大量的请求。为了处理负载,应用程序在每个查询中通常只访问少量的记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。硬盘查找时间往往是这里的瓶颈。
|
||||
* 数据仓库和类似的分析系统会低调一些,因为它们主要由业务分析人员使用,而不是最终用户。它们的查询量要比OLTP系统少得多,但通常每个查询开销高昂,需要在短时间内扫描数百万条记录。硬盘带宽(而不是查找时间)往往是瓶颈,列式存储是针对这种工作负载的日益流行的解决方案。
|
||||
|
||||
在OLTP方面,我们能看到两派主流的存储引擎:
|
||||
在OLTP这一边,我们能看到两派主流的存储引擎:
|
||||
|
||||
***日志结构学派***
|
||||
* 日志结构学派:只允许追加到文件和删除过时的文件,但不会更新已经写入的文件。Bitcask、SSTables、LSM树、LevelDB、Cassandra、HBase、Lucene等都属于这个类别。
|
||||
* 就地更新学派:将硬盘视为一组可以覆写的固定大小的页面。 B树是这种理念的典范,用在所有主要的关系数据库和许多非关系型数据库中。
|
||||
|
||||
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个类别。
|
||||
日志结构的存储引擎是相对较新的技术。他们的主要想法是,通过系统性地将随机访问写入转换为硬盘上的顺序写入,由于硬盘驱动器和固态硬盘的性能特点,可以实现更高的写入吞吐量。
|
||||
|
||||
***就地更新学派***
|
||||
关于OLTP,我们最后还介绍了一些更复杂的索引结构,以及针对所有数据都放在内存里而优化的数据库。
|
||||
|
||||
将硬盘视为一组可以覆写的固定大小的页面。 B树是这种哲学的典范,用在所有主要的关系数据库中和许多非关系型数据库。
|
||||
|
||||
日志结构的存储引擎是相对较新的发展。他们的主要想法是,他们系统地将随机访问写入转换为硬盘上的顺序写入,由于硬盘驱动器和固态硬盘的性能特点,可以实现更高的写入吞吐量。在完成OLTP方面,我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。
|
||||
|
||||
然后,我们从存储引擎的内部绕开,看看典型数据仓库的高级架构。这一背景说明了为什么分析工作负载与OLTP差别很大:当你的查询需要在大量行中顺序扫描时,索引的相关性就会降低很多。相反,非常紧凑地编码数据变得非常重要,以最大限度地减少查询需要从硬盘读取的数据量。我们讨论了列式存储如何帮助实现这一目标。
|
||||
然后,我们暂时放下了存储引擎的内部细节,查看了典型数据仓库的高级架构,并说明了为什么分析工作负载与OLTP差别很大:当你的查询需要在大量行中顺序扫描时,索引的重要性就会降低很多。相反,非常紧凑地编码数据变得非常重要,以最大限度地减少查询需要从硬盘读取的数据量。我们讨论了列式存储如何帮助实现这一目标。
|
||||
|
||||
作为一名应用程序开发人员,如果你掌握了有关存储引擎内部的知识,那么你就能更好地了解哪种工具最适合你的特定应用程序。如果你需要调整数据库的调整参数,这种理解可以让你设想一个更高或更低的值可能会产生什么效果。
|
||||
|
||||
尽管本章不能让你成为一个特定存储引擎的调参专家,但它至少有大概率使你有了足够的概念与词汇储备去读懂数据库的文档,从而选择合适的数据库。
|
||||
尽管本章不能让你成为一个特定存储引擎的调参专家,但它至少大概率使你有了足够的概念与词汇储备去读懂你所选择的数据库的文档。
|
||||
|
||||
|
||||
## 参考文献
|
||||
|
@ -61,7 +61,7 @@
|
||||
* [第三章:儲存與檢索](ch3.md)
|
||||
* [驅動資料庫的資料結構](ch3.md#驅動資料庫的資料結構)
|
||||
* [事務處理還是分析?](ch3.md#事務處理還是分析?)
|
||||
* [列儲存](ch3.md#列儲存)
|
||||
* [列式儲存](ch3.md#列式儲存)
|
||||
* [本章小結](ch3.md#本章小結)
|
||||
* [第四章:編碼與演化](ch4.md)
|
||||
* [編碼資料的格式](ch4.md#編碼資料的格式)
|
||||
|
@ -17,7 +17,7 @@
|
||||
* [第三章:儲存與檢索](ch3.md)
|
||||
* [驅動資料庫的資料結構](ch3.md#驅動資料庫的資料結構)
|
||||
* [事務處理還是分析?](ch3.md#事務處理還是分析?)
|
||||
* [列儲存](ch3.md#列儲存)
|
||||
* [列式儲存](ch3.md#列式儲存)
|
||||
* [本章小結](ch3.md#本章小結)
|
||||
* [第四章:編碼與演化](ch4.md)
|
||||
* [編碼資料的格式](ch4.md#編碼資料的格式)
|
||||
|
@ -463,7 +463,7 @@ MapReduce作業的輸出處理遵循同樣的原理。透過將輸入視為不
|
||||
- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
|
||||
- 與Unix工具類似,MapReduce作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。
|
||||
|
||||
在這些領域,在Unix上表現良好的設計原則似乎也適用於Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因為大多數Unix工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用`{print $7}`來提取URL)。在Hadoop上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如Avro(請參閱“[Avro](ch4.md#Avro)”)和Parquet(請參閱“[列儲存](ch3.md#列儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見[第四章](ch4.md))。
|
||||
在這些領域,在Unix上表現良好的設計原則似乎也適用於Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因為大多數Unix工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用`{print $7}`來提取URL)。在Hadoop上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如Avro(請參閱“[Avro](ch4.md#Avro)”)和Parquet(請參閱“[列式儲存](ch3.md#列式儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見[第四章](ch4.md))。
|
||||
|
||||
### Hadoop與分散式資料庫的對比
|
||||
|
||||
@ -673,7 +673,7 @@ Spark,Flink和Tez避免將中間狀態寫入HDFS,因此它們採取了不同
|
||||
|
||||
自由執行任意程式碼,長期以來都是傳統MapReduce批處理系統與MPP資料庫的區別所在(請參閱“[Hadoop與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如Java的Maven、Javascript的npm以及Ruby的gems)。
|
||||
|
||||
然而資料流引擎已經發現,支援除連線之外的更多**宣告式特性**還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外CPU開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用面向列的儲存佈局(請參閱“[列儲存](ch3.md#列儲存)”),只從磁碟讀取所需的列。 Hive、Spark DataFrames和Impala還使用了向量化執行(請參閱“[記憶體頻寬和向量處理](ch3.md#記憶體頻寬和向量處理)”):在對CPU快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark生成JVM位元組碼【79】,Impala使用LLVM為這些內部迴圈生成本機程式碼【41】。
|
||||
然而資料流引擎已經發現,支援除連線之外的更多**宣告式特性**還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外CPU開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用列式儲存佈局(請參閱“[列式儲存](ch3.md#列式儲存)”),只從磁碟讀取所需的列。 Hive、Spark DataFrames和Impala還使用了向量化執行(請參閱“[記憶體頻寬和向量處理](ch3.md#記憶體頻寬和向量處理)”):在對CPU快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark生成JVM位元組碼【79】,Impala使用LLVM為這些內部迴圈生成本機程式碼【41】。
|
||||
|
||||
透過在高階API中引入宣告式的部分,並使查詢最佳化器可以在執行期間利用這些來做最佳化,批處理框架看起來越來越像MPP資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
## 資料整合
|
||||
|
||||
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在[第三章](ch3.md)討論儲存引擎時,我們看到了日誌結構儲存,B樹,以及列儲存。在[第五章](ch5.md)討論複製時,我們看到了單領導者,多領導者,和無領導者的方法。
|
||||
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在[第三章](ch3.md)討論儲存引擎時,我們看到了日誌結構儲存,B樹,以及列式儲存。在[第五章](ch5.md)討論複製時,我們看到了單領導者,多領導者,和無領導者的方法。
|
||||
|
||||
如果你有一個類似於“我想儲存一些資料並稍後再查詢”的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。
|
||||
|
||||
|
233
zh-tw/ch3.md
233
zh-tw/ch3.md
@ -17,7 +17,7 @@
|
||||
|
||||
作為程式設計師,為什麼要關心資料庫內部儲存與檢索的機理?你可能不會去從頭開始實現自己的儲存引擎,但是你**確實**需要從許多可用的儲存引擎中選擇一個合適的。而且為了讓儲存引擎能在你的工作負載型別上執行良好,你也需要大致瞭解儲存引擎在底層究竟做了什麼。
|
||||
|
||||
特別需要注意,針對**事務性**負載最佳化的和針對**分析性**負載最佳化的儲存引擎之間存在巨大差異。稍後我們將在 “[事務處理還是分析?](#事務處理還是分析?)” 一節中探討這一區別,並在 “[列儲存](#列儲存)”中討論一系列針對分析性負載而最佳化的儲存引擎。
|
||||
特別需要注意,針對**事務性**負載最佳化的和針對**分析性**負載最佳化的儲存引擎之間存在巨大差異。稍後我們將在 “[事務處理還是分析?](#事務處理還是分析?)” 一節中探討這一區別,並在 “[列式儲存](#列式儲存)”中討論一系列針對分析性負載而最佳化的儲存引擎。
|
||||
|
||||
但首先,我們將從你可能已經很熟悉的兩大類資料庫(傳統的關係型資料庫和很多所謂的“NoSQL”資料庫)中使用的**儲存引擎**來開始本章的內容。我們將研究兩大類儲存引擎:**日誌結構(log-structured)** 的儲存引擎,以及**面向頁面(page-oriented)** 的儲存引擎(例如B樹)。
|
||||
|
||||
@ -26,14 +26,14 @@
|
||||
世界上最簡單的資料庫可以用兩個Bash函式實現:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
db_set () {
|
||||
echo "$1,$2" >> database
|
||||
}
|
||||
#!/bin/bash
|
||||
db_set () {
|
||||
echo "$1,$2" >> database
|
||||
}
|
||||
|
||||
db_get () {
|
||||
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
|
||||
}
|
||||
db_get () {
|
||||
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
|
||||
}
|
||||
```
|
||||
|
||||
這兩個函式實現了鍵值儲存的功能。執行 `db_set key value` 會將 **鍵(key)** 和**值(value)** 儲存在資料庫中。鍵和值(幾乎)可以是你喜歡的任何東西,例如,值可以是JSON文件。然後呼叫 `db_get key` 會查詢與該鍵關聯的最新值並將其返回。
|
||||
@ -41,26 +41,26 @@
|
||||
麻雀雖小,五臟俱全:
|
||||
|
||||
```bash
|
||||
$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
|
||||
$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
|
||||
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
|
||||
|
||||
$ db_get 42
|
||||
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
|
||||
$ db_get 42
|
||||
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
|
||||
```
|
||||
|
||||
底層的儲存格式非常簡單:一個文字檔案,每行包含一條逗號分隔的鍵值對(忽略轉義問題的話,大致與CSV檔案類似)。每次對 `db_set` 的呼叫都會向檔案末尾追加記錄,所以更新鍵的時候舊版本的值不會被覆蓋 —— 因而查詢最新值的時候,需要找到檔案中鍵最後一次出現的位置(因此 `db_get` 中使用了 `tail -n 1 ` 。)
|
||||
|
||||
```bash
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
|
||||
|
||||
$ db_get 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"]}
|
||||
$ 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)** 的資料檔案。真正的資料庫有更多的問題需要處理(如併發控制,回收硬碟空間以避免日誌無限增長,處理錯誤與部分寫入的記錄),但基本原理是一樣的。日誌極其有用,我們還將在本書的其它部分重複見到它好幾次。
|
||||
@ -88,7 +88,7 @@
|
||||
|
||||
**圖3-1 以類CSV格式儲存鍵值對的日誌,並使用記憶體雜湊對映進行索引。**
|
||||
|
||||
聽上去簡單,但這是一個可行的方法。現實中,Bitcask實際上就是這麼做的(Riak中預設的儲存引擎)【3】。 Bitcask提供高效能的讀取和寫入操作,但要求所有的鍵必須能放入可用記憶體中,因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間,因為可以在硬碟上透過一次硬碟尋道操作來載入所需部分,如果資料檔案的那部分已經在檔案系統快取中,則讀取根本不需要任何硬碟I/O。
|
||||
聽上去簡單,但這是一個可行的方法。現實中,Bitcask實際上就是這麼做的(Riak中預設的儲存引擎)【3】。 Bitcask提供高效能的讀取和寫入操作,但要求所有的鍵必須能放入可用記憶體中,因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間,因為可以在硬碟上透過一次硬碟查詢操作來載入所需部分,如果資料檔案的那部分已經在檔案系統快取中,則讀取根本不需要任何硬碟I/O。
|
||||
|
||||
像Bitcask這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如,鍵可能是某個貓咪影片的網址(URL),而值可能是該影片被播放的次數(每次有人點選播放按鈕時遞增)。在這種型別的工作負載中,有很多寫操作,但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。
|
||||
|
||||
@ -256,9 +256,9 @@ B樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定
|
||||
|
||||
* 一些資料庫(如LMDB)使用寫時複製方案【21】,而不是覆蓋頁面並維護WAL以支援崩潰恢復。修改的頁面被寫入到不同的位置,並且還在樹中建立了父頁面的新版本,以指向新的位置。這種方法對於併發控制也很有用,我們將在“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中看到。
|
||||
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級[^iii]。
|
||||
* 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每次頁面讀取可能都需要進行硬碟尋道。因此,許多B樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於LSM樹在合併過程中一次又一次地重寫儲存的大部分,所以它們更容易使順序鍵在硬碟上彼此靠近。
|
||||
* 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每次頁面讀取可能都需要進行硬碟查詢。因此,許多B樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於LSM樹在合併過程中一次又一次地重寫儲存的大部分,所以它們更容易使順序鍵在硬碟上彼此靠近。
|
||||
* 額外的指標已被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
|
||||
* B樹的變體如分形樹(fractal tree)【22】借用一些日誌結構的思想來減少硬碟尋道(而且它們與分形無關)。
|
||||
* B樹的變體如分形樹(fractal tree)【22】借用一些日誌結構的思想來減少硬碟查詢(而且它們與分形無關)。
|
||||
|
||||
[^iii]: 這個變種有時被稱為B+樹,但因為這個最佳化已被廣泛使用,所以經常無法區分於其它的B樹變種。
|
||||
|
||||
@ -323,8 +323,8 @@ B樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
|
||||
**多維索引(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樹索引不能夠高效地處理這種查詢:它可以返回一個緯度範圍內的所有餐館(但經度可能是任意值),或者返回在同一個經度範圍內的所有餐館(但緯度可能是北極和南極之間的任意地方),但不能同時滿足兩個條件。
|
||||
@ -366,37 +366,39 @@ B樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
|
||||
|
||||
## 事務處理還是分析?
|
||||
|
||||
在早期業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易(commercial transaction)* 相對應:賣個貨,向供應商下訂單,支付員工工資等等。但隨著資料庫應用至那些不涉及到錢的領域,術語 **交易/事務(transaction)** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。
|
||||
在早期的業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易(commercial transaction)* 相對應:賣個貨,向供應商下訂單,支付員工工資等等。但隨著資料庫開始應用到那些不涉及到錢的領域,術語**交易/事務(transaction)** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。
|
||||
|
||||
事務不一定具有ACID(原子性,一致性,隔離性和永續性)屬性。事務處理只是意味著允許客戶端進行低延遲讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批次處理作業。我們在[第七章](ch7.md)中討論ACID屬性,在[第十章](ch10.md)中討論批處理。
|
||||
> 事務不一定具有ACID(原子性,一致性,隔離性和永續性)屬性。事務處理只是意味著允許客戶端進行低延遲的讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批處理作業。我們在[第七章](ch7.md)中討論ACID屬性,在[第十章](ch10.md)中討論批處理。
|
||||
|
||||
即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論,遊戲中的動作,地址簿中的聯絡人等等,基本訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵查詢少量記錄。根據使用者的輸入插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為 **線上事務處理(OLTP, OnLine Transaction Processing)** 。
|
||||
即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論,遊戲中的動作,地址簿中的聯絡人等等,基本的訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵查詢少量記錄。根據使用者的輸入插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為**線上事務處理(OLTP, OnLine Transaction Processing)**。
|
||||
|
||||
但是,資料庫也開始越來越多地用於資料分析,這些資料分析具有非常不同的訪問模式。通常,分析查詢需要掃描大量記錄,每個記錄只讀取幾列,並計算彙總統計資訊(如計數,總和或平均值),而不是將原始資料返回給使用者。例如,如果你的資料是一個銷售交易表,那麼分析查詢可能是:
|
||||
但是,資料庫也開始越來越多地用於資料分析,這些資料分析具有非常不同的訪問模式。通常,分析查詢需要掃描大量記錄,每個記錄只讀取幾列,並計算彙總統計資訊(如計數、總和或平均值),而不是將原始資料返回給使用者。例如,如果你的資料是一個銷售交易表,那麼分析查詢可能是:
|
||||
|
||||
* 一月份每個商店的總收入是多少?
|
||||
* 在最近的推廣活動中多賣了多少香蕉?
|
||||
* 哪個牌子的嬰兒食品最常與X品牌的尿布同時購買?
|
||||
|
||||
這些查詢通常由業務分析師編寫,並提供給幫助公司管理層做出更好決策(商業智慧)的報告。為了將這種使用資料庫的模式和事務處理區分開,它被稱為**線上分析處理(OLAP, OnLine Analytice Processing)**。【47】。OLTP和OLAP之間的區別並不總是清晰的,但是一些典型的特徵在[表3-1]()中列出。
|
||||
這些查詢通常由業務分析師編寫,並提供報告以幫助公司管理層做出更好的決策(商業智慧)。為了將這種使用資料庫的模式和事務處理區分開,它被稱為**線上分析處理(OLAP, OnLine Analytice Processing)**【47】[^iv]。OLTP和OLAP之間的區別並不總是清晰的,但是一些典型的特徵在[表3-1]()中列出。
|
||||
|
||||
**表3-1 比較交易處理和分析系統的特點**
|
||||
**表3-1 比較事務處理和分析系統的特點**
|
||||
|
||||
| 屬性 | 事務處理 OLTP | 分析系統 OLAP |
|
||||
| 屬性 | 事務處理系統 OLTP | 分析系統 OLAP |
|
||||
| :----------: | :--------------------------: | :----------------------: |
|
||||
| 主要讀取模式 | 查詢少量記錄,按鍵讀取 | 在大批次記錄上聚合 |
|
||||
| 主要寫入模式 | 隨機訪問,寫入要求低延時 | 批次匯入(ETL),事件流 |
|
||||
| 主要使用者 | 終端使用者,透過Web應用 | 內部資料分析師,決策支援 |
|
||||
| 主要寫入模式 | 隨機訪問,寫入要求低延時 | 批次匯入(ETL)或者事件流 |
|
||||
| 主要使用者 | 終端使用者,透過Web應用 | 內部資料分析師,用於決策支援 |
|
||||
| 處理的資料 | 資料的最新狀態(當前時間點) | 隨時間推移的歷史事件 |
|
||||
| 資料集尺寸 | GB ~ TB | TB ~ PB |
|
||||
|
||||
起初,相同的資料庫用於事務處理和分析查詢。 SQL在這方面證明是非常靈活的:對於OLTP型別的查詢以及OLAP型別的查詢來說效果很好。儘管如此,在二十世紀八十年代末和九十年代初期,公司有停止使用OLTP系統進行分析的趨勢,而是在單獨的資料庫上執行分析。這個單獨的資料庫被稱為**資料倉庫(data warehouse)**。
|
||||
[^iv]: OLAP中的首字母O(online)的含義並不明確,它可能是指查詢並不是用來生成預定義好的報告的事實,也可能是指分析師通常是互動式地使用OLAP系統來進行探索式的查詢。
|
||||
|
||||
起初,事務處理和分析查詢使用了相同的資料庫。 SQL在這方面已證明是非常靈活的:對於OLTP型別的查詢以及OLAP型別的查詢來說效果都很好。儘管如此,在二十世紀八十年代末和九十年代初期,企業有停止使用OLTP系統進行分析的趨勢,轉而在單獨的資料庫上執行分析。這個單獨的資料庫被稱為**資料倉庫(data warehouse)**。
|
||||
|
||||
### 資料倉庫
|
||||
|
||||
一個企業可能有幾十個不同的交易處理系統:面向終端客戶的網站,控制實體商店的收銀系統,跟蹤倉庫庫存,規劃車輛路線,供應鏈管理,員工管理等。這些系統中每一個都很複雜,需要專人維護,所以系統最終都是自動執行的。
|
||||
一個企業可能有幾十個不同的交易處理系統:面向終端客戶的網站,控制實體商店的收銀系統,倉庫庫存跟蹤,車輛路線規劃,供應鏈管理,員工管理等。這些系統中每一個都很複雜,需要專人維護,所以最終這些系統互相之間都是獨立執行的。
|
||||
|
||||
這些OLTP系統往往對業務運作至關重要,因而通常會要求 **高可用** 與 **低延遲**。所以DBA會密切關注他們的OLTP資料庫,他們通常不願意讓業務分析人員在OLTP資料庫上執行臨時分析查詢,因為這些查詢通常開銷巨大,會掃描大部分資料集,這會損害同時執行的事務的效能。
|
||||
這些OLTP系統往往對業務運作至關重要,因而通常會要求 **高可用** 與 **低延遲**。所以DBA會密切關注他們的OLTP資料庫,他們通常不願意讓業務分析人員在OLTP資料庫上執行臨時的分析查詢,因為這些查詢通常開銷巨大,會掃描大部分資料集,這會損害同時在執行的事務的效能。
|
||||
|
||||
相比之下,資料倉庫是一個獨立的資料庫,分析人員可以查詢他們想要的內容而不影響OLTP操作【48】。資料倉庫包含公司各種OLTP系統中所有的只讀資料副本。從OLTP資料庫中提取資料(使用定期的資料轉儲或連續的更新流),轉換成適合分析的模式,清理並載入到資料倉庫中。將資料存入倉庫的過程稱為“**抽取-轉換-載入(ETL)**”,如[圖3-8](../img/fig3-8.png)所示。
|
||||
|
||||
@ -406,23 +408,23 @@ B樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
|
||||
|
||||
幾乎所有的大型企業都有資料倉庫,但在小型企業中幾乎聞所未聞。這可能是因為大多數小公司沒有這麼多不同的OLTP系統,大多數小公司只有少量的資料 —— 可以在傳統的SQL資料庫中查詢,甚至可以在電子表格中分析。在一家大公司裡,要做一些在一家小公司很簡單的事情,需要很多繁重的工作。
|
||||
|
||||
使用單獨的資料倉庫,而不是直接查詢OLTP系統進行分析的一大優勢是資料倉庫可針對分析訪問模式進行最佳化。事實證明,本章前半部分討論的索引演算法對於OLTP來說工作得很好,但對於回答分析查詢並不是很好。在本章的其餘部分中,我們將研究為分析而最佳化的儲存引擎。
|
||||
使用單獨的資料倉庫,而不是直接查詢OLTP系統進行分析的一大優勢是資料倉庫可針對分析訪問模式進行最佳化。事實證明,本章前半部分討論的索引演算法對於OLTP來說工作得很好,但對於處理分析查詢並不是很好。在本章的其餘部分中,我們將研究為分析而最佳化的儲存引擎。
|
||||
|
||||
#### OLTP資料庫和資料倉庫之間的分歧
|
||||
|
||||
資料倉庫的資料模型通常是關係型的,因為SQL通常很適合分析查詢。有許多圖形資料分析工具可以生成SQL查詢,視覺化結果,並允許分析人員探索資料(透過下鑽,切片和切塊等操作)。
|
||||
資料倉庫的資料模型通常是關係型的,因為SQL通常很適合分析查詢。有許多圖形資料分析工具可以生成SQL查詢,視覺化結果,並允許分析人員探索資料(透過下鑽、切片和切塊等操作)。
|
||||
|
||||
表面上,一個數據倉庫和一個關係OLTP資料庫看起來很相似,因為它們都有一個SQL查詢介面。然而,系統的內部看起來可能完全不同,因為它們針對非常不同的查詢模式進行了最佳化。現在許多資料庫供應商都將重點放在支援事務處理或分析工作負載上,而不是兩者都支援。
|
||||
表面上,一個數據倉庫和一個關係型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]的想法。
|
||||
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】。
|
||||
|
||||
### 星型和雪花型:分析的模式
|
||||
|
||||
正如[第二章](ch2.md)所探討的,根據應用程式的需要,在事務處理領域中使用了大量不同的資料模型。另一方面,在分析型業務中,資料模型的多樣性則少得多。許多資料倉庫都以相當公式化的方式使用,被稱為星型模式(也稱為維度建模【55】)。
|
||||
|
||||
圖3-9中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表(在這個例子中,它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽量或點選量。
|
||||
[圖3-9](../img/fig3-9.md)中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表(在這個例子中,它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽或點選。
|
||||
|
||||
![](../img/fig3-9.png)
|
||||
|
||||
@ -430,140 +432,140 @@ Teradata,Vertica,SAP HANA和ParAccel等資料倉庫供應商通常使用昂
|
||||
|
||||
通常情況下,事實被視為單獨的事件,因為這樣可以在以後分析中獲得最大的靈活性。但是,這意味著事實表可以變得非常大。像蘋果,沃爾瑪或eBay這樣的大企業在其資料倉庫中可能有幾十PB的交易歷史,其中大部分儲存在事實表中【56】。
|
||||
|
||||
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(允許計算利潤餘額)。事實表中的其他列是對其他表(稱為維度表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件的發生地點,時間,方式和原因。
|
||||
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(可以用來計算利潤餘額)。事實表中的其他列是對其他表(稱為維度表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件發生的物件、內容、地點、時間、方式和原因。
|
||||
|
||||
例如,在[圖3-9](../img/fig3-9.md)中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品,包括**庫存單位(SKU)**,說明,品牌名稱,類別,脂肪含量,包裝尺寸等。`fact_sales` 表中的每一行都使用外部表明在特定交易中銷售了哪些產品。 (為了簡單起見,如果客戶一次購買幾種不同的產品,則它們在事實表中被表示為單獨的行)。
|
||||
例如,在[圖3-9](../img/fig3-9.md)中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品,包括庫存單位(SKU)、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外來鍵表明在特定交易中銷售了什麼產品。 (簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
|
||||
|
||||
甚至日期和時間也通常使用維度表來表示,因為這允許對日期的附加資訊(諸如公共假期)進行編碼,從而允許查詢區分假期和非假期的銷售。
|
||||
甚至日期和時間也通常使用維度表來表示,因為這允許對日期的附加資訊(諸如公共假期)進行編碼,從而允許區分假期和非假期的銷售查詢。
|
||||
|
||||
“星型模式”這個名字來源於這樣一個事實,即當表關係視覺化時,事實表在中間,由維度表包圍;與這些表的連線就像星星的光芒。
|
||||
“星型模式”這個名字來源於這樣一個事實,即當我們對錶之間的關係進行視覺化時,事實表在中間,被維度表包圍;與這些表的連線就像星星的光芒。
|
||||
|
||||
這個模板的變體被稱為雪花模式,其中維度被進一步分解為子維度。例如,品牌和產品類別可能有單獨的表格,並且 `dim_product` 表格中的每一行都可以將品牌和類別作為外來鍵引用,而不是將它們作為字串儲存在 `dim_product` 表格中。雪花模式比星形模式更規範化,但是星形模式通常是首選,因為分析師使用它更簡單【55】。
|
||||
|
||||
在典型的資料倉庫中,表格通常非常寬:事實表格通常有100列以上,有時甚至有數百列【51】。維度表也可以是非常寬的,因為它們包括可能與分析相關的所有元資料——例如,`dim_store` 表可以包括在每個商店提供哪些服務的細節,它是否具有店內麵包房,店面面積,商店第一次開張的日期,最後一次改造的時間,離最近的高速公路的距離等等。
|
||||
在典型的資料倉庫中,表格通常非常寬:事實表通常有100列以上,有時甚至有數百列【51】。維度表也可以是非常寬的,因為它們包括了所有可能與分析相關的元資料——例如,`dim_store` 表可以包括在每個商店提供哪些服務的細節,它是否具有店內麵包房,店面面積,商店第一次開張的日期,最近一次改造的時間,離最近的高速公路的距離等等。
|
||||
|
||||
|
||||
## 列儲存
|
||||
## 列式儲存
|
||||
|
||||
如果事實表中有萬億行和數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 分析人們是否更傾向於購買新鮮水果或糖果,這取決於一週中的哪一天**
|
||||
**例3-1 分析人們是否更傾向於在一週的某一天購買新鮮水果或糖果**
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
dim_date.weekday,
|
||||
dim_product.category,
|
||||
SUM(fact_sales.quantity) AS quantity_sold
|
||||
FROM fact_sales
|
||||
JOIN dim_date ON fact_sales.date_key = dim_date.date_key
|
||||
JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
|
||||
WHERE
|
||||
dim_date.year = 2013 AND
|
||||
dim_product.category IN ('Fresh fruit', 'Candy')
|
||||
GROUP BY
|
||||
dim_date.weekday, dim_product.category;
|
||||
SELECT
|
||||
dim_date.weekday,
|
||||
dim_product.category,
|
||||
SUM(fact_sales.quantity) AS quantity_sold
|
||||
FROM fact_sales
|
||||
JOIN dim_date ON fact_sales.date_key = dim_date.date_key
|
||||
JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
|
||||
WHERE
|
||||
dim_date.year = 2013 AND
|
||||
dim_product.category IN ('Fresh fruit', 'Candy')
|
||||
GROUP BY
|
||||
dim_date.weekday, dim_product.category;
|
||||
```
|
||||
|
||||
我們如何有效地執行這個查詢?
|
||||
|
||||
在大多數OLTP資料庫中,儲存都是以面向行的方式進行佈局的:表格的一行中的所有值都相鄰儲存。文件資料庫是相似的:整個文件通常儲存為一個連續的位元組序列。你可以在[圖3-1](../img/fig3-1.png)的CSV例子中看到這個。
|
||||
在大多數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)所示。
|
||||
列式儲存背後的想法很簡單:不要將所有來自一行的值儲存在一起,而是將來自每一列的所有值儲存在一起。如果每個列式儲存在一個單獨的檔案中,查詢只需要讀取和解析查詢中使用的那些列,這可以節省大量的工作。這個原理如[圖3-10](../img/fig3-10.png)所示。
|
||||
|
||||
![](../img/fig3-10.png)
|
||||
|
||||
**圖3-10 使用列儲存關係型資料,而不是行**
|
||||
**圖3-10 按列儲存關係型資料,而不是行**
|
||||
|
||||
列儲存在關係資料模型中是最容易理解的,但它同樣適用於非關係資料。例如,Parquet 【57】是一種列式儲存格式,支援基於Google的Dremel 【54】的文件資料模型。
|
||||
> 列式儲存在關係資料模型中是最容易理解的,但它同樣適用於非關係資料。例如,Parquet【57】是一種列式儲存格式,支援基於Google的Dremel的文件資料模型【54】。
|
||||
|
||||
面向列的儲存佈局依賴於每個列檔案包含相同順序的行。 因此,如果你需要重新組裝整行,你可以從每個單獨的列檔案中獲取第23項,並將它們放在一起形成表的第23行。
|
||||
列式儲存佈局依賴於每個列檔案包含相同順序的行。 因此,如果你需要重新組裝完整的行,你可以從每個單獨的列檔案中獲取第23項,並將它們放在一起形成表的第23行。
|
||||
|
||||
|
||||
### 列壓縮
|
||||
|
||||
除了僅從硬碟載入查詢所需的列以外,我們還可以透過壓縮資料來進一步降低對硬碟吞吐量的需求。幸運的是,面向列的儲存通常很適合壓縮。
|
||||
除了僅從硬碟載入查詢所需的列以外,我們還可以透過壓縮資料來進一步降低對硬碟吞吐量的需求。幸運的是,列式儲存通常很適合壓縮。
|
||||
|
||||
看看[圖3-10](../img/fig3-10.png)中每一列的值序列:它們通常看起來是相當重複的,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是點陣圖編碼,如[圖3-11](../img/fig3-11.png)所示。
|
||||
|
||||
![](../img/fig3-11.png)
|
||||
|
||||
**圖3-11 壓縮點陣圖索引儲存佈局**
|
||||
**圖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)底部所示。這可以使列的編碼非常緊湊。
|
||||
|
||||
這些點陣圖索引非常適合資料倉庫中常見的各種查詢。例如:
|
||||
|
||||
```sql
|
||||
WHERE product_sk IN(30,68,69)
|
||||
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`這三個點陣圖,並計算三個點陣圖的按位或(OR),這可以非常有效地完成。
|
||||
|
||||
```sql
|
||||
WHERE product_sk = 31 AND store_sk = 3
|
||||
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】的概述。
|
||||
|
||||
> #### 面向列的儲存和列族
|
||||
> #### 列式儲存和列族
|
||||
>
|
||||
> Cassandra和HBase有一個列族的概念,他們從Bigtable繼承【9】。然而,把它們稱為面向列是非常具有誤導性的:在每個列族中,它們將一行中的所有列與行鍵一起儲存,並且不使用列壓縮。因此,Bigtable模型仍然主要是面向行的。
|
||||
> Cassandra和HBase有一個列族(column families)的概念,他們從Bigtable繼承【9】。然而,把它們稱為列式(column-oriented)是非常具有誤導性的:在每個列族中,它們將一行中的所有列與行鍵一起儲存,並且不使用列壓縮。因此,Bigtable模型仍然主要是面向行的。
|
||||
>
|
||||
|
||||
#### 記憶體頻寬和向量處理
|
||||
#### 記憶體頻寬和向量化處理
|
||||
|
||||
對於需要掃描數百萬行的資料倉庫查詢來說,一個巨大的瓶頸是從硬盤獲取資料到記憶體的頻寬。但是,這不是唯一的瓶頸。分析資料庫的開發人員也擔心有效利用主儲存器頻寬到CPU快取中的頻寬,避免CPU指令處理流水線中的分支錯誤預測和泡沫,以及在現代中使用單指令多資料(SIMD)指令CPU 【59,60】。
|
||||
對於需要掃描數百萬行的資料倉庫查詢來說,一個巨大的瓶頸是從硬盤獲取資料到記憶體的頻寬。但是,這不是唯一的瓶頸。分析型資料庫的開發人員還需要有效地利用主儲存器到CPU快取的頻寬,避免CPU指令處理流水線中的分支預測錯誤和氣泡,以及在現代CPU上使用單指令多資料(SIMD)指令【59,60】。
|
||||
|
||||
除了減少需要從硬碟載入的資料量以外,面向列的儲存佈局也可以有效利用CPU週期。例如,查詢引擎可以將大量壓縮的列資料放在CPU的L1快取中,然後在緊密的迴圈中迴圈(即沒有函式呼叫)。相比較每個記錄的處理都需要大量函式呼叫和條件判斷的程式碼,CPU執行這樣一個迴圈要快得多。列壓縮允許列中的更多行適合相同數量的L1快取。前面描述的按位“與”和“或”運算子可以被設計為直接在這樣的壓縮列資料塊上操作。這種技術被稱為向量化處理【58,49】。
|
||||
除了減少需要從硬碟載入的資料量以外,列式儲存佈局也可以有效利用CPU週期。例如,查詢引擎可以將大量壓縮的列資料放在CPU的L1快取中,然後在緊密的迴圈(即沒有函式呼叫)中遍歷。相比較每個記錄的處理都需要大量函式呼叫和條件判斷的程式碼,CPU執行這樣一個迴圈要快得多。列壓縮允許列中的更多行被放進相同數量的L1快取。前面描述的按位“與”和“或”運算子可以被設計為直接在這樣的壓縮列資料塊上操作。這種技術被稱為向量化處理【58,49】。
|
||||
|
||||
|
||||
### 列儲存中的排序順序
|
||||
### 列式儲存中的排序順序
|
||||
|
||||
在列儲存中,儲存行的順序並不一定很重要。按插入順序儲存它們是最簡單的,因為插入一個新行只需要追加到每個列檔案。但是,我們可以選擇增加一個特定的順序,就像我們之前對SSTables所做的那樣,並將其用作索引機制。
|
||||
在列式儲存中,儲存行的順序並不一定很重要。按插入順序儲存它們是最簡單的,因為插入一個新行只需要追加到每個列檔案。但是,我們可以選擇增加一個特定的順序,就像我們之前對SSTables所做的那樣,並將其用作索引機制。
|
||||
|
||||
注意,每列獨自排序是沒有意義的,因為那樣我們就不會知道列中的哪些項屬於同一行。我們只能在知道一列中的第k項與另一列中的第k項屬於同一行的情況才能重建出完整的行。
|
||||
注意,每列獨自排序是沒有意義的,因為那樣我們就沒法知道不同列中的哪些項屬於同一行。我們只能在知道一列中的第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)中的點陣圖一樣)可以將該列壓縮到幾千位元組 —— 即使表中有數十億行。
|
||||
|
||||
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長時間的重複值。排序優先順序更低的列以基本上隨機的順序出現,所以它們可能不會被壓縮。但前幾列排序在整體上仍然是有好處的。
|
||||
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長的連續的重複值。排序優先順序更低的列以基本上隨機的順序出現,所以它們可能不會被壓縮。但前幾列排序在整體上仍然是有好處的。
|
||||
|
||||
#### 幾個不同的排序順序
|
||||
|
||||
這個想法的巧妙擴充套件在C-Store中引入,並在商業資料倉庫Vertica【61,62】中被採用。不同的查詢受益於不同的排序順序,為什麼不以幾種不同的方式來儲存相同的資料呢?無論如何,資料需要複製到多臺機器,這樣,如果一臺機器發生故障,你不會丟失資料。你可能還需要儲存以不同方式排序的冗餘資料,以便在處理查詢時,可以使用最適合查詢模式的版本。
|
||||
C-Store中引入了這個想法的一個巧妙擴充套件,並在商業資料倉庫Vertica中被採用【61,62】。不同的查詢受益於不同的排序順序,為什麼不以幾種不同的方式來儲存相同的資料呢?無論如何,資料需要複製到多臺機器,這樣,如果一臺機器發生故障,你不會丟失資料。你可能還需要儲存以不同方式排序的冗餘資料,以便在處理查詢時,可以使用最適合查詢模式的版本。
|
||||
|
||||
在一個面向列的儲存中有多個排序順序有點類似於在一個面向行的儲存中有多個次級索引。但最大的區別在於面向行的儲存將每一行儲存在一個地方(在堆檔案或聚簇索引中),次級索引只包含指向匹配行的指標。在列儲存中,通常在其他地方沒有任何指向資料的指標,只有包含值的列。
|
||||
在一個列式儲存中有多個排序順序有點類似於在一個面向行的儲存中有多個次級索引。但最大的區別在於面向行的儲存將每一行儲存在一個地方(在堆檔案或聚集索引中),次級索引只包含指向匹配行的指標。在列式儲存中,通常在其他地方沒有任何指向資料的指標,只有包含值的列。
|
||||
|
||||
### 寫入列儲存
|
||||
### 寫入列式儲存
|
||||
|
||||
這些最佳化在資料倉庫中是有意義的,因為大多數負載由分析人員執行的大型只讀查詢組成。面向列的儲存,壓縮和排序都有助於更快地讀取這些查詢。然而,他們的缺點是寫入更加困難。
|
||||
這些最佳化在資料倉庫中是有意義的,因為其負載主要由分析人員執行的大型只讀查詢組成。列式儲存、壓縮和排序都有助於更快地讀取這些查詢。然而,他們的缺點是寫入更加困難。
|
||||
|
||||
使用B樹的就地更新方法對於壓縮的列是不可能的。如果你想在排序表的中間插入一行,你很可能不得不重寫所有的列檔案。由於行由列中的位置標識,因此插入必須始終更新所有列。
|
||||
使用B樹的就地更新方法對於壓縮的列是不可能的。如果你想在排序表的中間插入一行,你很可能不得不重寫所有的列檔案。由於行由列中的位置標識,因此插入必須對所有列進行一致地更新。
|
||||
|
||||
幸運的是,本章前面已經看到了一個很好的解決方案:LSM樹。所有的寫操作首先進入一個記憶體中的儲存,在這裡它們被新增到一個已排序的結構中,並準備寫入硬碟。記憶體中的儲存是面向行還是列的,這並不重要。當已經積累了足夠的寫入資料時,它們將與硬碟上的列檔案合併,並批次寫入新檔案。這基本上是Vertica所做的【62】。
|
||||
幸運的是,本章前面已經看到了一個很好的解決方案:LSM樹。所有的寫操作首先進入一個記憶體中的儲存,在這裡它們被新增到一個已排序的結構中,並準備寫入硬碟。記憶體中的儲存是面向行還是列的並不重要。當已經積累了足夠的寫入資料時,它們將與硬碟上的列檔案合併,並批次寫入新檔案。這基本上是Vertica所做的【62】。
|
||||
|
||||
查詢需要檢查硬碟上的列資料和最近在記憶體中的寫入,並將兩者結合起來。但是,查詢最佳化器隱藏了使用者的這個區別。從分析師的角度來看,透過插入,更新或刪除操作進行修改的資料會立即反映在後續查詢中。
|
||||
查詢需要檢查硬碟上的列資料和最近在記憶體中的寫入,並將兩者結合起來。但是,查詢最佳化器對使用者隱藏了這個細節。從分析師的角度來看,透過插入、更新或刪除操作進行修改的資料會立即反映在後續的查詢中。
|
||||
|
||||
### 聚合:資料立方體和物化檢視
|
||||
|
||||
並不是每個資料倉庫都必定是一個列儲存:傳統的面向行的資料庫和其他一些架構也被使用。然而,對於專門的分析查詢,列式儲存可以顯著加快,所以它正在迅速普及【51,63】。
|
||||
並不是每個資料倉庫都必定是一個列式儲存:傳統的面向行的資料庫和其他一些架構也被使用。然而,列式儲存可以顯著加快專門的分析查詢,所以它正在迅速變得流行起來【51,63】。
|
||||
|
||||
資料倉庫的另一個值得一提的是物化彙總。如前所述,資料倉庫查詢通常涉及一個聚合函式,如SQL中的COUNT,SUM,AVG,MIN或MAX。如果相同的聚合被許多不同的查詢使用,那麼每次都可以透過原始資料來處理。為什麼不快取一些查詢使用最頻繁的計數或總和?
|
||||
資料倉庫的另一個值得一提的方面是物化彙總(materialized aggregates)。如前所述,資料倉庫查詢通常涉及一個聚合函式,如SQL中的COUNT、SUM、AVG、MIN或MAX。如果相同的聚合被許多不同的查詢使用,那麼每次都透過原始資料來處理可能太浪費了。為什麼不將一些查詢使用最頻繁的計數或總和快取起來?
|
||||
|
||||
建立這種快取的一種方式是物化檢視(Materialized View)。在關係資料模型中,它通常被定義為一個標準(虛擬)檢視:一個類似於表的物件,其內容是一些查詢的結果。不同的是,物化檢視是查詢結果的實際副本,寫入硬碟,而虛擬檢視只是寫入查詢的捷徑。從虛擬檢視讀取時,SQL引擎會將其展開到檢視的底層查詢中,然後處理展開的查詢。
|
||||
建立這種快取的一種方式是物化檢視(Materialized View)。在關係資料模型中,它通常被定義為一個標準(虛擬)檢視:一個類似於表的物件,其內容是一些查詢的結果。不同的是,物化檢視是查詢結果的實際副本,會被寫入硬碟,而虛擬檢視只是編寫查詢的一個捷徑。從虛擬檢視讀取時,SQL引擎會將其展開到檢視的底層查詢中,然後再處理展開的查詢。
|
||||
|
||||
當底層資料發生變化時,物化檢視需要更新,因為它是資料的非規範化副本。資料庫可以自動完成,但是這樣的更新使得寫入成本更高,這就是在OLTP資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中,它們可能更有意義(它們是否實際上改善了讀取效能取決於個別情況)。
|
||||
當底層資料發生變化時,物化檢視需要更新,因為它是資料的非規範化副本。資料庫可以自動完成該操作,但是這樣的更新使得寫入成本更高,這就是在OLTP資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中,它們可能更有意義(它們是否實際上改善了讀取效能取決於個別情況)。
|
||||
|
||||
物化檢視的常見特例稱為資料立方體或OLAP立方【64】。它是按不同維度分組的聚合網格。[圖3-12](../img/fig3-12.png)顯示了一個例子。
|
||||
|
||||
@ -571,41 +573,38 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
|
||||
**圖3-12 資料立方的兩個維度,透過求和聚合**
|
||||
|
||||
想象一下,現在每個事實都只有兩個維度表的外來鍵——在[圖3-12](../img/fig-3-12.png)中,這些是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元包含具有該日期 - 產品組合的所有事實的屬性(例如,`net_price`)的聚集(例如,`SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得一個維度減少的彙總(按產品的銷售額,無論日期,還是按日期銷售,無論產品如何)。
|
||||
想象一下,現在每個事實都只有兩個維度表的外來鍵 —— 在[圖3-12](../img/fig-3-12.png)中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期-產品組合的所有事實的屬性(例如`net_price`)的聚集(例如`SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
|
||||
|
||||
一般來說,事實往往有兩個以上的維度。在圖3-9中有五個維度:日期,產品,商店,促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的,但是原理是一樣的:每個單元格都包含特定日期-產品-商店-促銷-客戶組合的銷售。這些值可以在每個維度上重複概括。
|
||||
一般來說,事實往往有兩個以上的維度。在圖3-9中有五個維度:日期、產品、商店、促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的,但是原理是一樣的:每個單元格都包含特定日期-產品-商店-促銷-客戶組合的銷售額。這些值可以在每個維度上求和彙總。
|
||||
|
||||
物化資料立方體的優點是某些查詢變得非常快,因為它們已經被有效地預先計算了。例如,如果你想知道每個商店的總銷售額,則只需檢視合適維度的總計,無需掃描數百萬行。
|
||||
物化資料立方體的優點是可以讓某些查詢變得非常快,因為它們已經被有效地預先計算了。例如,如果你想知道每個商店的總銷售額,則只需檢視合適維度的總計,而無需掃描數百萬行的原始資料。
|
||||
|
||||
缺點是資料立方體不具有查詢原始資料的靈活性。例如,沒有辦法計算哪個銷售比例來自成本超過100美元的專案,因為價格不是其中的一個維度。因此,大多數資料倉庫試圖保留儘可能多的原始資料,並將聚合資料(如資料立方體)僅用作某些查詢的效能提升。
|
||||
資料立方體的缺點是不具有查詢原始資料的靈活性。例如,沒有辦法計算有多少比例的銷售來自成本超過100美元的專案,因為價格不是其中的一個維度。因此,大多數資料倉庫試圖保留儘可能多的原始資料,並將聚合資料(如資料立方體)僅用作某些查詢的效能提升手段。
|
||||
|
||||
|
||||
## 本章小結
|
||||
|
||||
在本章中,我們試圖深入瞭解資料庫如何處理儲存和檢索。將資料儲存在資料庫中會發生什麼,以及稍後再次查詢資料時資料庫會做什麼?
|
||||
在本章中,我們試圖深入瞭解資料庫是如何處理儲存和檢索的。將資料儲存在資料庫中會發生什麼?稍後再次查詢資料時資料庫會做什麼?
|
||||
|
||||
在高層次上,我們看到儲存引擎分為兩大類:最佳化 **事務處理(OLTP)** 或 **線上分析(OLAP)** 。這些用例的訪問模式之間有很大的區別:
|
||||
在高層次上,我們看到儲存引擎分為兩大類:針對**事務處理(OLTP)** 最佳化的儲存引擎和針對**線上分析(OLAP)** 最佳化的儲存引擎。這兩類使用場景的訪問模式之間有很大的區別:
|
||||
|
||||
* OLTP系統通常面向使用者,這意味著系統可能會收到大量的請求。為了處理負載,應用程式通常只訪問每個查詢中的少部分記錄。應用程式使用某種鍵來請求記錄,儲存引擎使用索引來查詢所請求的鍵的資料。硬碟尋道時間往往是這裡的瓶頸。
|
||||
* 資料倉庫和類似的分析系統會低調一些,因為它們主要由業務分析人員使用,而不是由終端使用者使用。它們的查詢量要比OLTP系統少得多,但通常每個查詢開銷高昂,需要在短時間內掃描數百萬條記錄。硬碟頻寬(而不是查詢時間)往往是瓶頸,列式儲存是這種工作負載越來越流行的解決方案。
|
||||
* OLTP系統通常面向終端使用者,這意味著系統可能會收到大量的請求。為了處理負載,應用程式在每個查詢中通常只訪問少量的記錄。應用程式使用某種鍵來請求記錄,儲存引擎使用索引來查詢所請求的鍵的資料。硬碟查詢時間往往是這裡的瓶頸。
|
||||
* 資料倉庫和類似的分析系統會低調一些,因為它們主要由業務分析人員使用,而不是終端使用者。它們的查詢量要比OLTP系統少得多,但通常每個查詢開銷高昂,需要在短時間內掃描數百萬條記錄。硬碟頻寬(而不是查詢時間)往往是瓶頸,列式儲存是針對這種工作負載的日益流行的解決方案。
|
||||
|
||||
在OLTP方面,我們能看到兩派主流的儲存引擎:
|
||||
在OLTP這一邊,我們能看到兩派主流的儲存引擎:
|
||||
|
||||
***日誌結構學派***
|
||||
* 日誌結構學派:只允許追加到檔案和刪除過時的檔案,但不會更新已經寫入的檔案。Bitcask、SSTables、LSM樹、LevelDB、Cassandra、HBase、Lucene等都屬於這個類別。
|
||||
* 就地更新學派:將硬碟視為一組可以覆寫的固定大小的頁面。 B樹是這種理念的典範,用在所有主要的關係資料庫和許多非關係型資料庫中。
|
||||
|
||||
只允許附加到檔案和刪除過時的檔案,但不會更新已經寫入的檔案。 Bitcask,SSTables,LSM樹,LevelDB,Cassandra,HBase,Lucene等都屬於這個類別。
|
||||
日誌結構的儲存引擎是相對較新的技術。他們的主要想法是,透過系統性地將隨機訪問寫入轉換為硬碟上的順序寫入,由於硬碟驅動器和固態硬碟的效能特點,可以實現更高的寫入吞吐量。
|
||||
|
||||
***就地更新學派***
|
||||
關於OLTP,我們最後還介紹了一些更復雜的索引結構,以及針對所有資料都放在記憶體裡而最佳化的資料庫。
|
||||
|
||||
將硬碟視為一組可以覆寫的固定大小的頁面。 B樹是這種哲學的典範,用在所有主要的關係資料庫中和許多非關係型資料庫。
|
||||
|
||||
日誌結構的儲存引擎是相對較新的發展。他們的主要想法是,他們系統地將隨機訪問寫入轉換為硬碟上的順序寫入,由於硬碟驅動器和固態硬碟的效能特點,可以實現更高的寫入吞吐量。在完成OLTP方面,我們透過一些更復雜的索引結構和為保留所有資料而最佳化的資料庫做了一個簡短的介紹。
|
||||
|
||||
然後,我們從儲存引擎的內部繞開,看看典型資料倉庫的高階架構。這一背景說明了為什麼分析工作負載與OLTP差別很大:當你的查詢需要在大量行中順序掃描時,索引的相關性就會降低很多。相反,非常緊湊地編碼資料變得非常重要,以最大限度地減少查詢需要從硬碟讀取的資料量。我們討論了列式儲存如何幫助實現這一目標。
|
||||
然後,我們暫時放下了儲存引擎的內部細節,查看了典型資料倉庫的高階架構,並說明了為什麼分析工作負載與OLTP差別很大:當你的查詢需要在大量行中順序掃描時,索引的重要性就會降低很多。相反,非常緊湊地編碼資料變得非常重要,以最大限度地減少查詢需要從硬碟讀取的資料量。我們討論了列式儲存如何幫助實現這一目標。
|
||||
|
||||
作為一名應用程式開發人員,如果你掌握了有關儲存引擎內部的知識,那麼你就能更好地瞭解哪種工具最適合你的特定應用程式。如果你需要調整資料庫的調整引數,這種理解可以讓你設想一個更高或更低的值可能會產生什麼效果。
|
||||
|
||||
儘管本章不能讓你成為一個特定儲存引擎的調參專家,但它至少有大概率使你有了足夠的概念與詞彙儲備去讀懂資料庫的文件,從而選擇合適的資料庫。
|
||||
儘管本章不能讓你成為一個特定儲存引擎的調參專家,但它至少大概率使你有了足夠的概念與詞彙儲備去讀懂你所選擇的資料庫的文件。
|
||||
|
||||
|
||||
## 參考文獻
|
||||
|
Loading…
Reference in New Issue
Block a user