存储空间这件事,在数据量小的时候没人关心,等数据量上去了就是真金白银。一个TB的存储空间,如果通过存储引擎的优化能省掉一半,就意味着硬件成本减半、备份时间减半、IO扫描量减半。很多数据库产品在宣传时会强调自己的压缩率,但压缩率这个数字跟数据特征、压缩算法、存储格式都有关系,不能简单地拿来比较。这篇文章从技术角度分析TeleDB存储引擎的设计,看看它到底用了什么手段来节省存储空间,以及这些手段在实际数据上效果如何。
行存与列存的区别
要理解TeleDB怎么省空间,先得搞清楚行存和列存的区别。传统数据库大多采用行式存储,一条记录的所有字段连续存放。这种方式对事务处理很友好,读取一条完整记录只需一次IO。但对于分析型查询,往往只需要几个字段,行存会把不需要的列也读进来,造成IO浪费。
列式存储把同一列的数据连续存放。这样做有几个好处:第一,相同类型的数据放在一起,压缩效率更高,比如一个性别字段只有"男"和"女"两种值,列存可以用极高的压缩率存储;第二,查询时只读取涉及的列,减少IO量;第三,列式格式适合向量化计算,CPU缓存命中率更高。列存的缺点是对单行数据的更新不友好,因为一条记录的各列分散在不同位置。
TeleDB的存储引擎同时支持行存和列存两种格式。对于事务型表,默认使用行存;对于分析型表,可以指定使用列存。在HTAP场景下,TeleDB会同时维护行存和列存两份数据,通过内部同步机制保持一致。虽然存储了两份,但由于列存的高压缩率,总空间并不一定比单份行存大很多。
压缩算法的选择
压缩是节省空间最直接的手段。TeleDB的列存引擎支持多种压缩算法,根据数据类型和特征自动选择最优方案。
对于整数类型,使用的是变长编码和字典编码。很多业务场景中的整数列基数不高,比如状态码、类别ID等,字典编码可以把这些重复值压缩到极小的空间。对于字符串类型,使用的是LZ4或ZSTD算法。LZ4压缩和解压速度快,适合对性能敏感的场景;ZSTD压缩率更高但消耗更多CPU,适合冷数据。
时间戳和日期类型也有专门的编码方案。这类数据通常有规律性,比如递增的日志时间戳,使用Delta编码可以大幅压缩。布尔类型更简单,一个bit就够了。
实测中,用一批真实业务数据做了对比测试。数据包含订单表、用户表、日志表等,总量约100GB(行存格式)。转换为列存后,存储空间降到了约35GB,压缩比接近3:1。其中订单表的效果最好,因为它的很多列基数低、重复值多;日志表的效果稍差,因为日志内容是长文本,压缩率有限。
存储格式优化
除了压缩算法,存储格式本身的设计也影响空间占用。TeleDB的列存格式采用了一些优化设计来减少空间浪费。
一个是数据对齐的优化。传统存储格式为了保证CPU访问效率,会对数据做字节对齐,这会浪费一些空间。TeleDB的列存格式在保证访问效率的前提下,尽量减少对齐填充。对于分析型查询,大量数据是批量读取的,不需要严格的单值对齐。
另一个是稀疏列的处理。很多业务表有不少列大部分时间是空的,比如可选字段、扩展字段等。行存格式中即使值为空也要占用空间(至少需要标记位),列存格式中空值可以用位图来标记,几乎不占额外空间。对于空值比例高的列,这种处理方式能省下不少空间。
还有数据页的合并。小数据页会导致页头元数据的开销占比过高。TeleDB的列存使用较大的数据页(默认1MB),减少了元数据开销。同时,大页面也有利于压缩算法发挥效果,因为可供分析的上下文更多。
实际效果与注意事项
综合压缩算法和格式优化,TeleDB的列存引擎在典型业务数据上的空间节省效果是显著的。与行存相比,列存通常能节省50%到70%的存储空间,具体取决于数据特征。重复值多、基数低的列压缩效果好;内容唯一的文本列压缩效果差。
但使用列存也有需要注意的地方。第一,列存不适合频繁更新的表,每次更新可能触发数据页的重写,性能开销大。第二,列存对单行查询不友好,如果业务主要是按主键查单条记录,行存更合适。第三,虽然列存省了磁盘空间,但解压数据需要消耗CPU,在CPU密集型的场景下可能成为瓶颈。
合理的使用方式是根据表的访问模式来选择存储格式。事务表用行存,分析表用列存,既有事务又有分析的表可以同时维护两种格式。TeleDB的存储引擎提供了这种灵活性,让用户在不同场景下都能得到较优的空间效率和访问性能。