【数据蒋堂】第 8 期:列式存储的另一面

列存是常见的数据存储技术,在许多场景下也确实很有效,因而也被不少数据仓库类产品采用,在业内列存也常常就意味着高性能。

可是,列存真有这么好吗?搜索一下,容易找到的列存缺点一般是针对数据修改的,而对于只读的分析计算任务,却很少能见到较详细的讨论。我们在这里来研究一下这个问题。

对内存计算意义不大

列存的原理很简单:由于磁盘不适合跳动式读取,采用行式存储时在读取数据时会扫描所有列,而一次运算可能只涉及很少的列,这样就会多读很多用不上的数据。采用列存则只需要读取需要用到的列,数据访问量大概率会大幅减少,而大数据计算中磁盘扫描时间的占比很大,减少访问量就能节约大量时间。另外,同一列数据相同值情况较多,采用列存更容易做合并压缩,从而进一步减少数据存储量,提高性能。

从原理可以看出,列存能提高性能主要是因为减少了磁盘访问量,但对于计算量减少并没有帮助。如果数据已经被加载进内存,再采用列存就没多大意义了。普通结构化数据运算都是以行为单位的,在内存中使用列存反而会加大构造完整记录的复杂度,降低性能。所以,除了专业的向量式运算(数据挖掘中常用,运算本身就是以列为单位的)外,类似关系数据库型的内存运算(包括内存数据库)并不合适采用列式存储。

加剧硬盘的不连续访问程度

列式存储时,各列是连续存储的,这样同时访问多个列进行计算时,就会导致造成不连续的随机访问,访问的列越多造成的不连续性就越强。而针对机械硬盘的不连续读取会严重影响性能,在访问列数较多或总列数并不多时,就可能发生还不如行存的性能好的现象,因为行存是连续访问的,跳动的成本有可能超过读取成本。如果有并发任务(以及下面会说到的并行计算)还会严重加剧这一问题,当然行存并发时也会发生磁盘跳动,但程度比列存轻得多,列存时每多一个并发计算任务会多出几个(涉及列数)对磁盘的并发访问请求,行存则只会多一个磁盘并发请求。

一个办法是加大读取缓存区以减少磁盘寻道时间的占比,但这样为每个涉及列都设置缓存区,列较多时会占用大量内存。另一个办法是增加磁盘数量,把不同的列存储到不同的磁盘上,不过列存一般应用场景都是总数列很多的情况,常常远大于机器可以接受的硬盘数量,还会较大概率地造成磁盘随机访问冲突。

固态硬盘没有寻道时间的问题,列式存储更适合采用固态硬盘。

索引效率低

索引也是常用技术,用于从大数据集中按键值找出指定记录。我们在以前文章中讲过,索引的本质是排序,索引表中将存储有序的键值及该键值对应的原表记录位置。对于行式存储来说,整条记录的位置可以用一个数表示;但列存就不一样了,整条记录的每个列分别有各自的位置,原则上需要都记录下来,这样一来,索引表几乎和原表一样大,访问成本变高很多,空间占用也太大,这和复制原表后排序区别并不大了。

每条记录只存储一个序号,然后用乘法计算出位置,这样可以吗?有些数据类型的字段值的长度本身就是不固定的(串型),而固定长度的字段值(整数、日期)也可能因为要压缩编码(列存中常用的技术)而变成不固定,一定要用定长方式存储,索引倒是简单了,访问也很快,但会加大存储量,遍历时又不划算了,而这是列存更主要的应用场景。

实际常用的手段是把数据分块,块内数据采用列存,索引只建立在块上。这样可以用索引迅速定位中所需要的数据在哪个块中,然后只要块内进行扫描即可。

这种索引比行存索引会多一个块内扫描的过程,性能要低一些。如果原数据按索引键值有序(索引键常常就是原表主键),那可以很容易地定位出目标数据所在的少量的几个块(大概率只在一块中),这时性能损失还可以容忍,可适用于按唯一 ID 值找出指定记录的场景。但如果原数据对索引键无序,那这个索引几乎没有用处,目标数据可能落在几乎所有的块中,这就和全表扫描区别不大了。

分段并行麻烦

要充分利用多 CPU(核),多线程并行能力是个必须考虑的问题,而要并行这就需要先把数据分段。

分段有两个基本需求:每段数据量基本相同(每线程处理能力相当),可以较灵活的分段(事先不能预测线程数)。行式存储时相对容易实现分段,只要每条(也可以每 N 条)记录后做一个结束标记,在分段时按字节数平均分成 K 段,然后在每段中寻找到结束标记后作为开始点即可。但列式存储不能采用同样的办法,由于前述原因,字段值是不定长的,某个列的分段点未必和另一个列的同样的分段点同步落在同一条记录上,这会错位导致错误的数据。

列式存储的分段一般也是采用前述的分块方案:分段必须以块为单位,在块内不再分段并行。这样就会有一个矛盾,首先,分块数不能太少了,否则就无法做到灵活分段了(只有 5 个分块时不可能做出 10 个分段),按现代服务器的 CPU(核)数,要有上百个分块才能比较自由地平衡分段;但是,分块数又不能太多,列数据在物理上会被拆成多个不连续的小块,不仅使得遍历代码复杂很多,而且还会多读入少量两块之间的无用数据,对于机械硬盘还有寻道时间问题,分块数越多这些问题就越严重。只有分块内列数据占用空间比读入缓冲区大很多时,无用数据读入时间和寻道时间的占比才会比较小,这就要求每个分块中有足够多的记录数,也就是说,实现列存并行,数据量要足够大才有意义,对于机械硬盘(包括用机械硬盘构成的阵列)上一般得达到单机单表十亿记录、空间约在百 G 以上。规模较小的数据量就不容易获得并行计算的性能提升,而特别适合使用列存的多维分析业务的数据量就处于这种尴尬的规模中。另外,分块容量在数据追加前就要确定下来,随着数据的不断追加,相邻分块却不能物理上合并,分块数就会越来越多,这将给管理造成不少麻烦,需要可扩展的空间专门存储分块的索引信息。

我们在这里介绍列存的另一面,并非要否定列存在许多计算场景时的巨大优势 ,但完全不区分情况地全面采用列存也是不负责任的。对于数据仓库类产品,正确的做法应当将这个自由度留给系统管理员,由用户来决定是否采用列存、如何分块、哪些数据采用列存、有些数据甚至会行存和列存共存,以冗余换取更高的性能。