Author: 柒沐
自2012年以来,SQL Server的Column Index在存储层设计上有了几个大方向上的进展:
Functional | Performance | Paper | |
---|---|---|---|
2012 | 只读secondary column index | 略 | 《SQL Server Column Store Indexes》 |
2014 | 可更新primary column index;归档压缩,压缩率好一些,但查询慢一些; | 略 | 《Enhancements to SQL Server Column Stores》 |
2016 | 1.可更新secondary column index;2.可以在primary column index上架secondary btree index来做unique check和seek;3.可根据filter针对部分行加secondary column index; | 1.Snapshot Isolation;2.Defragment(碎片整理);可以在Replica上访问column index; | 《Real-Time Analytical Processing with SQL Server》 |
2017 | 略 | 略 | 略 |
2019 | 可merge多个空洞的row group (Compaction & Flush) | 略 | 略 |
列存和行存不同的点在于数据的物理组织形式,主要体现在以下两点:
基于这两点就可以充分发挥基于单列的压缩能力。同时执行从存储fetch数据时,可以精准fetch某一列的数据,memory和IO的利用效率更高。也更适合batch execution,batch execution一般来说有10倍以上的性能提升。
为支持更新,列存也需要索引结构辅助,列存可以用和行存一样的索引组织形式。行存的索引组织形式可以分为:
思路:还有一种更简单粗暴的方式是简单维护按列为单位的无序数组和一个delete map,数据不会按照键值聚簇,写入时直接追加到数组末尾,现在内存中缓存到一个row group的数据后再压缩写盘。但这种方式的更新删除能力很差,需要扫描全表定位出要删除的行,因此只适合于以插入查询为主的数仓场景。
例子:SQL Server Column Index
删除与更新是列式存储的难点。列式存储往往将一大批数据作为一个row group进行压缩再落盘,因此列式存储的删除往往是标记删除,更新是删除加插入,来避免频繁的解压压缩和写放大。
和行式Heap Table一样我们需要一个额外的索引结构来定位要删除的行号,得到行号后可以通过delete mask或者已删除行号集合来过滤不需要的行。
此时列存作为行存主键索引的二级索引,我们不再需要额外的索引结构来定位要删除的行,只需要将行号维护在主索引的记录里,删除时先根据主键在主键索引中定位到行号,然后再通过delete mask或已删除行号集合来过滤被删除的行。SQL Server基于全内存行存表的列存二级索引就是这种实现方式。
Clickhouse无法支持实时更新,这里就介绍支持实时更新的列式LSM Hologres和TiFlash,并对两者进行一下对比。
Hologres支持KV接口和AP查询,存储层类LSM,主键是主键映射记录,二级索引是二级索引列加主键列为key,value为null。和SQL Server一样,主键索引和二级索引都支持行存形式和列存形式。行存形式就是典型LSM架构,
下面主要介绍下列存形式(以主键索引为例)。Hologres的shard file可以理解为RocksDB的sstable,且只是每列的datablock是单独连续存放的。同时有个delete map的LSM组件用于维护delete操作,该LSM的key为shard file的ID,value中包括了bitmap表示该shard file中哪些记录被删除。读取shard file时会根据read sequence产生一个visible bitmap,同时merge delete map中该shard file ID的bitmap生成一个delete bitmap,merge visible bitmap和delete bitmap来过滤不可见和已删除数据。更新时先seek到旧行,然后还原出更新的新行,按照删除流程删除旧行,然后将新行从前台写入。
并且,shard file间不会有primary key的overlap,因此Hologres扫描时不需要对shard file进行归并,只需要根据bitmask过滤,也就可以并行扫描。
TiFlash的可更新列存引擎DeltaTree设计思路:
Hologres相比于TiFlash的更新开销更大,但扫描时不需要归并,且可以并行扫描。
SQL Server的columnstore index作为主索引或者磁盘行存表的二级索引时就是以这种方式实现的。SQL Server做了几点优化来缓解这种设计的删除劣势:
如果我们需要针对一个现有的行存表上的数据做分析,ETL到data warehouse是最常规的操作模式,但其中有几个挑战点:
这时我们可以选择在现有的行存表上加一个secondary column index支持实时的AP分析。secondary column index和普通的二级索引一样,会跟着primary index保持实时更新。 这种方式的代价是多了一份列式存储数据,但由于列式存储的压缩能力强很多(压缩率基本在10倍以上),column index这份额外的空间开销不是1倍,几乎只是十分之一。带来的优势是:
列存二级索引与普通行存二级索引的区别? 列存二级索引适合scan,不需要保序,maintain cost也更小一点,行存二级索引则更适合seek。
workload以insert和query为主,更新较少,此时我们可以只用一份columnstore index作为primary index,同时支持大量insert/query和少量更新。
理论上上文介绍的几种列存实现都既可以作为primary index也可以作为secondary index,那么我们针对这两种不同的列存实现要如何进行列存实现的选型(列式HeapTable/列式LSM/无序数组+delete map)。
HTAP下会有频繁的数据更新,因此我们可以首先排除无序数组加delete map这种更新能力很差的实现方式。接下来我们就需要对比列式HeapTable和列式LSM。 由于在HTAP场景,列存只是作为行存表的一个二级索引,seek/small range query可以回退到行存上,因此从查询角度上来说我们只需要关注columnstore index的scan能力。
因此,我们只需要从scan/maintain cost两个角度来衡量对比即可:
小结:除了分区对聚簇有依赖,列式堆表比列式LSM更适合HTAP场景
在dataware house场景下,我们以columnstore index为主索引,在这种场景下,我们可以先忽略列存的更新能力的强弱,只要能更新即可,那么这三种实现方式都符合要求。 其次在单一存储上,seek/small range query没有行存能够回退。因此,我们衡量时也需要考虑下seek/小范围查询的能力。因此,我们从seek&small range query/scan/maintain cost三个角度来衡量对比:
SQL Server在无序数组+delete map的实现上添加了nonclustered btree index来弥补seek/小范围查询能力。
小结:在data warehouse场景下,数据量比较大,因此需要对数据分区的可能性更大,也就更适合用列式LSM。此外就需要从各个角度衡量综合判断考量。