列存总是更快吗?
列式存储在很多场景下都具有性能优势,也被不少数据仓库产品采用。大数据量时,硬盘扫描和读取的时间占比很大。采用列存,在总列数很多而计算涉及的列很少时,从硬盘上仅读取需要的列即可,可以减少硬盘访问量,提高性能。但是,列存也并不总是更快,有些情况下性能反而不如行存。
高性能查找常用到索引技术,索引表中存储键值和对应原表记录的位置。行存数据记录的位置可以用一个数值表示,但列存时每列都有自己的位置,如果都存在索引表中,访问成本高且占用空间太大。而且,读取原表时也要分别到各个列的数据区去读,而硬盘有个最小读取单位,这会导致各列的总读取量远远超过行存,表现出来就是列存的查找性能比行存差。
多线程并行可以充分发挥多 CPU(核)的计算能力,要并行必须分段。业界常用的列存分段是分块方案:把数据分成若干块,块内是列存,分段以块为单位。分块数要足够多才能保证平均分段,而分块又要足够大才能让列存产生效果,这两者就是个矛盾。分段没有做好,会造成多线程并行计算时列存的性能不如行存。
因此,行存还是列存,要因地制宜,不能一概做成列存或者行存。但是很多数据仓库都做成了透明机制,不允许用户自由选择。
面对这些复杂的应用需求,可以使用开源的集算器 SPL。SPL 非常灵活,同时支持行存和列存,用户可以在高性能查找时选择行存,在很多列中遍历较少几列时选择列存。两者都需要时,SPL 还支持带值索引,将部分字段冗余在索引中,列存的原表用于遍历,行存的索引表用于查找。
而且,SPL 创新的分段技术很好的解决了列存分段难题,让列存并行也变得容易,从而充分发挥多 CPU(核)的性能优势。
SPL 的部分典型代码如下:
1、 生成列存、行存数据。
l 列存:file("T-c.ctx").create(…).append(cs)
生成列存数据时采用创新的分段技术,非常适合多行程并行读取。
l 行存:file("T-r.ctx").create@r(…).append(cs)
2、 遍历计算。
l 很多列参与计算或总列数不多的遍历用行存:
=file("T-r.ctx").open().cursor@m().groups(f1,f2,f3,…;sum(amt1),avg(amt2),max(amt3+amt4),…) 对行存数据建立游标,多线程并行计算分组汇总。
l 总列数很多,参与计算的列数很少的遍历用列存:
=file("T-c.ctx").open().cursor@m(f1,amt1).groups(f1;sum(amt1))
使用 SPL 创新的分段技术,这里对列存数据做多线程并行计算也很容易。
3、 索引查找。
l 一般情况下用行存:
建立索引:file("T-r.ctx").open().index(index_id;id)
索引预加载:T-r=file("T-r.ctx").open().index@3(index_id)
提前预先加载索引,在查询时可以省去加载时间,性能更好。
用索引查找:=T-r.icursor(…;id=10232 && …,index_id)。
l 查找时取出字段较少时,可以用列存 + 带值索引,兼顾查找和遍历:
建立索引:file("T-c.ctx").open().index(index_id1;id;tdate,amt) 将 tdate 和 amt 存入索引。
索引预加载:T-c=file("T-c.ctx").open().index@3(index_id1)
用索引查找:=T-c.icursor(tdate,amt;id=10232 && …,index_id1) 查询时从索引中取 tdate 和 amt,不需要再访问原表,保证查找性能。列存的原表用来做遍历计算,可以不用生成行存数据,减少硬盘空间占用。
更详细的原理讲解参见这里:倍增分段,组表与列存,行存和带值索引
列存、行存应用案例和操作说明:
我们怎样把 W 银行预计算固定条件查询优化成实时灵活条件查询
英文版