数据库存储系列(1)列式存储

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

OLTP vs OLAP

从数据的存储和检索角度看,我们通常会将数据库系统分为两类:

  1. 针对事务处理的系统 Online Transaction Processing,即 OLTP;
  2. 针对数据分析的系统 Online analytical processing,即 OLAP。

这个划分体现了我们对于数据存储效率,查询效率在不同场景下的预期。

TP 的场景大家经常遇到,你在电商app上下单,打开抖音看视频都归为 TP,这类场景对于数据库操作的要求是:高可用,低延时,查询一般较为直接。

AP 的场景侧重于大规模数据分析,我们经常需要看一些统计性质的指标,比如某个区域用户的年龄,喜好,开销等,这类场景的计算量是远大于 TP 的,有时还涉及复杂的外键关联,但对于延时要求不太高,可以容忍计算时长。

按照传统的解法看,TP 和 AP 所需要的技术是不同的,比如用 Oracle,MySQL 等关系型数据库解决 TP 问题,而使用 MapReduce,Spark 等大数据基建来解决 AP 问题。

随着数据时代的发展和技术的进步,我们开始遇到越来越多的 TP 和 AP 交叉的场景,需要基础的计算能力,但不一定很复杂,同时需要保证可用性和低延迟。这里我们先抛开 OceanBase, TiDB 等复杂的 HTAP 方案不谈,从数据本身思考一下解法。

场景分析

想象一下这个场景:

现在你有一张关系型数据表,语义上代表了 User 实体,包含姓名 Name, 身高 Height,年龄 Age, 薪资 Salary,家庭住址 Address 这些属性。你需要基于(身高,年龄,薪资)这三个统计字段,对外提供高效的公式计算服务,要求在插入新数据时能立刻算出来公式结果。

例如:

  • 计算平均年龄;
  • 计算总收入;
  • 计算平均身高。

如果数据规模小,我们把所有数据都捞回来,在内存里计算就 ok 了,照样低延迟,几万行数据也没什么压力。但从 I/O 的角度看,采用传统关系型数据库的【行存】模式效率是很低的。

比如计算总收入,我们需要把所有行记录全都从磁盘读出来,然后再计算总额。事实上我们只需要【薪资】这一列就够了。想象一下假设你现在使用的是一个经典的 Key Value 存储,要算这个收入的成本是多大呢?

答案是全表!你需要把所有记录都加载出来,虽然你要的数据只有一列,但数据在磁盘上的组织形式是【行】,除非吧所有行都加载出来,你不可能拿到那一列的所有值。

这样的 I/O 成本显然是过高的,比如我还有很多个统计指标的列,难不成做任何一个公式计算,我都需要全表扫描?这个磁盘和网络的 I/O 都是不可接受的。

这也很容易理解,否则大家就不会把 TP 和 AP 一开始分开了。要求计算的场景,势必是逃不开大量的数据加载的,这与低延迟从直观上看就是互斥的。

由此,业界提出了【列存】的概念。

列式存储

image.png

与行存将每一行的数据连续存储不同,列存将每一列的数据连续存储。

分析场景中往往需要读大量行但是少数几个列,列存模式下,只需要读取参与计算的列即可,极大的 减低了IO cost,加速了查询。

同一列中的数据属于同一类型,压缩效果显著,同等大小的内存能够存放更多数据。

经典的行存数据库:Oracle, MySQL, SQL Server。而 HBase,Cassandra, Clickhouse 则是列存数据库中的佼佼者。

当你需要针对某一列进行排序,聚合运算时,可以直接加载这一列的数据。存储层面看,由于压缩效果更好,占用磁盘空间更小。同时你需要加载的数据量,也降低了甚至不止一个级别。

但同时列存也有其开销:

  1. 写放大:原本在行存场景下的一条记录更新,到了列存的场景需要更新多个列。因为列式存储将不同列存储在磁盘上不连续的空间,导致更新多个列时磁盘是随机写操作;而行式存储时同一行多个列都存储在连续的空间,一次磁盘写操作就可以完成,列式存储的随机写效率要远远低于行式存储的写效率。
  2. 列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘。

行存 vs 列存

image.png

image.png

行存在insert/update/delete/point lookup query的场景是比较优的,因为涉及的行数据是连续存储的,理论上不存在读写放大。如处理一个query,通过使用table索引,可以快速寻址到页,然后根据页尾的索引能快速寻址到行首,将数据返回。这个特点非常符合OLTP的workload场景,所以在OLTP场景主要使用行存。

但是行存不是完美的,例如需要遍历全表获取符合要求的行,但只取部分列进行分组/排序/聚合等操作,行存就不太适合了,在读取时,由于会读取大量的无效的列的数据,且数据量很大,在存储是系统瓶颈的时代无疑是一大灾难,而且会影响内存中cache的使用效率。在计算时,由于行数据在内存中是顺序存储在一起的,所以对 cpu cache 也很不友好。

列存就是解决上述问题的好办法,首先读取时只需要读取关心的列数据,在计算时也对cpu cache非常友好,所以存在大量复杂查询的数据分析场景(OLAP)主要使用列存。列存在更新场景明显存在缺陷,每insert/update/delete 一行数据,由于会去更新存在在不同位置的column,会带来I/O放大,且为随机I/O。

基于上述列式存储的优缺点,一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。

总结

行存和列存没有绝对的高下之分,主要看开发者使用场景。你的计算所需要的【列】越多,你的写入频率越高,使用列存的成本就越高。行式存储的优势是在特定的业务场景下才能体现,如果不存在这样的业务场景,那么行式存储的优势也将不复存在,甚至成为劣势,典型的场景就是海量数据进行统计。

猜你喜欢

转载自juejin.im/post/7124856750695514142
今日推荐