第三章存储和查询-part4,Designing Data-In
面向列的存储
如果你的数据仓库有超过PB级的数据以及有超过几千亿行记录,那如何高效的存储和查询对你来说就是一个技术活了。属性表相对来说就要小很多了,往往也就是百万级。所以我们主要把精力集中在如何处理事实表上。
尽管事实表往往有超过100列,但是一般来说一次查询也就是其中的4、5列,像SELECT * 这种查询分析员一般很少用。举个例子,假设我想知道2013年水果和糖每周都卖出去多少个。那SQL就应该像下面这么写。实际我们只查了事实表当中的3列,date_key,product_sk, quantity 其他列都没有用到
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系统中,数据是以行的方式存储的。一条数据的所有字段在存储的时候也是彼此相邻的。当你要查询的时候,即使你有索引,你也需要把整行数据从内存或者磁盘当中读出来,解析过滤,这个过程相当耗时间。而且对于分析员来说,往往要查的数据要么甚至没索引,有也量极大。面向列的存储思路与之相反,不是把表中每行数据存在一起,而是让每一列的数据存在一起。如果没列数据存成一个文件,那只需要查对应列的文件就很快了。如图Figure 3-10
每个列中属性的顺序是按照表中每行数据的顺序来的,所以如果你想取第23条数据的所有属性,你可以取每个列中的第23个数据取出来,拼成他的所有属性。
列压缩
另外我们还可以通过把列中的内容压缩的方式,减少读写的IO。以上面Figure 3-10为例,你会发现例中的数据有很多重复,这就很容易压缩了。例如Bitmap编码就是在数据仓库中经常用到的压缩方式。以Figure 3-11为例
简单来说,可能每个列值的取值范围要比行数小得多得多,比如一个超市可能有几十亿的销售记录,但只有10万的商品。所以我们可以把n种商品用n个比特位来表示,第i种商品的表示方法就是第i个比特为1,其余为0。如果n很小,比如全世界国家只有不到200个,那可以简单按照Figure 3-11上面的存储方法每位都存进去,但是如果n很大,就会发现有很多0,导致很稀疏。那就可以再给bitmap做一个run-length编码,就好像Figure 3-11 下面那样,比如29就可以用9,1表示前面有9个0,然后1个1,后面全是0。这样压缩后就很小了。
另外就是bitmap索引机制也能够很好的应对数据仓库当中的查询操作,举个例子
比如查询WHERE product_sk IN (30, 68, 69), 就可以找到3个数字对应的bit位,然后将每条product_sk按位或,如果不为0,就表示在这个范围内,就很高效。
Column Family是不是面向列存储
Cassandra和HBase都说自己是面向列存储,但是他们有一个Column Family的概念,概念源自BigTable.如果我们直接叫他们面向列的存储是有问题的,因为在一个Column Family下,一条数据的所有列是存在一起的,而且他们也没有用到列压缩之类的方法。所以其实BigTable总的来说还是面向行的数据库。
内存带宽和矢量处理
太底层了,我是用不上了。将内存和L1cache之间的快速交换和CPU的矢量化处理。
列存储中的数据顺序
其实一般来说不对数据的顺序做要求,最简单的方法就是数据按照插入顺序排序。这样的话插入对于每个列文件来说都是追加操作,比较快。如果有必要其实我们也可以引入一个排序的策略,像之前讲的SSTable一样,用他作为索引。但是有一点需要注意,我们不能每个列都排一个序,因为这样我们就没法知道哪些数据属于同一行了。所以我们只能用一列来对一行数据的所有列排序。选哪一列就需要管理员根据查询的特征来看了,如果查询一般都带有时间特征,比如查上个月的数据,那日期就应该用来排序。这样就只需要在排序后的数据中扫描对应的一段数据就可以了,而不用去扫全表。当第一列相同的时候,我们可以再选一个列作为第二排序字段,以此类推。
排序的一个好处是数据当中会有大量的重复,这对压缩特别有用,可能会有连续很多数字一模一样,这样用前面讲到的run-length 编码就可以用几k的大小表示甚至十亿行的数据。(假设这10亿行的日期都是20180209, 那用20180209,10亿就表示完了)。但是这种好处只有在第一个用于排序的字段对应的文件上特别有效,第二排序字段的数据相对来说就会比较跳跃,效果就没有第一个字段好。但是就算这样收益也已经足够大了。
多个不同的排序顺序
因为不同的查询会希望数据排序的方式是不一样的,比如你查上个月的数据希望数据按日期排序,查某个用户的数据希望数据按用户id排序。所以为什么不把一份数据按照不同的排序顺序多存几个副本呢,反正出于安全考虑,你的数据也需要存多个副本。干脆让每个副本的排序字段不一样,这样查询就可以用最适合的副本去查。这个想法在Vertica上有实现。
面向列的写操作
这方面的优化对于数据仓库来说也很重要,因为前面提到的那些优化都是为了让分析员查起来更快,但是代价就是写入数据的时候很麻烦。
像B树那种原地更新的方式在面向列的时候基本不行,因为一行数据是打散存到每个文件中的。这就要求在写一行数据的是,也要同时对所有列文件写入这条数据。否则顺序就乱了。好在我们还有LSM树,所有写操作先都存到内存里面,数据在内存中排好序。当写入量足够大时,把内存中的排序的数据序列化写到每个列文件。搞定。
这个要求查询的时候不仅要查磁盘上的列文件,还要查内存当中的数据,不过这个已经被查询优化器隐藏在底层了,一般用户是不用关心的。
聚集计算,数据立方和物理视图
数据仓库的查询往往涉及到聚集函数,例如SUM,AVG,MAX,MIN等等。如果在不同的查询中都需要计算这类聚集函数,那每次算一遍其实是很耗资源的。所以为什么不把经常用的一些COUNT, SUM计算结果缓存下来呢?
缓存的一个方法是构建物理视图,在关系数据库中,我们会有视图的概念,但是他其实是一个虚拟的概念。在实际查询中,解析器会把视图代表的SQL进行展开,然后拿着展开后的SQL去做实际查询,也就是说除了写SQL简单了,其他的一点没少。而我们讲的物理视图就不一样了,他是真的把查询结果缓存到某一个地方,如果再查就直接用了。但是这个就有一个问题,当涉及底层数据变化时,虚拟视图是无所谓的,物理视图是需要跟着做更新的。数据库自动做更新倒是不难但是这样会让写操作变得很重,所以线上的OLTP数据库很少这么搞。但是如果你是查询密集型的数据仓库,那就有用了。
一个典型的例子就是数据立方,好像Figure 3-12这样。数据按照不同的维度分到不同的格子,假设你的数据只有2个维度,product_sk和date_key,那么就可以画一个表格,行是date_key, 列是product_sk。每个格子代表对应商品在那一天的售价总和(SUM)。基于此,你可以用这些格子算出每一列和每行的和。当你需要查对应数据的时候,直接从表格里面找就行了,不用再去扫表了。
这样做的一个问题就是他没办法再像原始数据那样灵活,因为当用户有些特殊的没有事先计算好的查询时,这东西就没用了。所以一般来说都会尽可能的保持原始数据以支持各种各样的查询,然后用数据立方的方式使特定的query加速。
总结
这章深入到了数据库的底层来看数据库到底是如何存储和查询数据的。在宏观层面,我们看到数据库分为两大类,面向事务处理的OLTP和面向分析的OLAP。他们是有很大区别的。
- OLTP是面向用户的,他的特点是会有大量查询请求,每个请求只查很少的数据。这类查询要求请求中带有某种key,数据库根据这些key去查数据。谈到如何快速查到这个key,就会有各种索引技术,这个时候往往磁盘的查询速度是性能瓶颈。
- OLAP,数据仓库是面向商务分析人员的。和OLTP相比,他收到的查询请求就少很多,但是每次查询经常涉及大量的数据。磁盘的带宽(这个与查询速度不同)是主要瓶颈。面向列的数据库在逐渐成为主流。
在OLTP领域内,有两个主要流派
- 日志结构(log-structured),只允许像文件追加数据,不允许对文件进行更新。Bitcask, SSTables, LSM-trees, LevelDB, Cassandra, HBase, Lucene都是这一派的。
- 原地更新流派,把磁盘看作一系列特定大小的页,这些页可以被覆盖。B树是这个思路的最成功代表,被广泛用到几乎所有主流的关系数据库中,同时也有很多非关系数据库用到这个技术。
日志结构相对来说是最近开始流行,他把对磁盘的随机写转化成了顺序写,这样可以使得写入数据异常高效。
随后我们深入到了数据库的底层实现方式,也讲到了OLAP和OLTP的不同。当你的请求需要顺序扫描大量数据的时候,索引就变得不再有用。数据如何能够快速编码解析,如何能够减少磁盘的读取量就很重要了。所以我们谈到了面向列的存储以解决这个问题。
作为一个应用开发人员,你并不需要对存储引擎的内部结构了若指掌,但是你需要知道哪个数据库更适合你的应用。以及当你需要调整数据库的某个参数的时候,你能知道这样做的好处和坏处都是什么。