列式存储如何实现高效的数据查询与分析?
摘要:〇、前言 上一篇文章中,博主对 行式存储和列示存储 进行了简单的介绍和对比。 了解完它们的基本信息后,还有疑点,那么本文就针对几个常见的疑点进行详细的解读下,供参考。 一、数据是按照列来分别存储的,那么如何写入? 列式存储数据库通过追加写入
〇、前言
上一篇文章中,博主对 行式存储和列示存储 进行了简单的介绍和对比。
了解完它们的基本信息后,还有疑点,那么本文就针对几个常见的疑点进行详细的解读下,供参考。
一、数据是按照列来分别存储的,那么如何写入?
列式存储数据库通过追加写入(Append-Only)机制结合 LSM-Tree 架构实现高效数据写入。
核心在于将数据按列组织并批量处理,特别适合分析型场景的批量加载需求,但对单行更新和删除操作支持较弱。
1.1 列式存储数据库的写入过程的三个阶段
1)内存缓冲阶段
数据首先写入内存缓冲区(MemTable),并记录预写日志(WAL)以保证数据持久性。
在内存中对数据进行列式转换,将行数据拆分为各列的独立数据流。
批量提交是关键,单次写入多条记录(通常 1000-150000 行)比单行写入效率高 10 倍以上。
2)磁盘落盘阶段
当内存缓冲区达到阈值,数据被压缩并转换为列式结构。
作为不可变数据段(Part/SSTable)刷写到磁盘,每个数据段包含单一列的部分数据。
100% 填满设计:列式存储通常将数据块填满,不留空闲空间,最大化 I/O 效率。
3)后台合并阶段
定期执行 Compaction 任务,将零散的小数据块合并为大数据块。
优化压缩率和查询效率,同时清理标记为删除的数据。
采用分层结构(如 LSM-Tree【Log-Structured Merge-Tree】:更适合写密集型场景)管理数据,减少合并开销。
1.2 列式写入与行式写入的本质区别
特征
列式存储
行式存储
数据组织
按列连续存储(ID 列、Name 列、Age 列分开)
按行连续存储(1:Tom:20→2:Jerry:22)
写入方式
追加写入,数据文件不可变
原地修改,支持 UPDATE/DELETE
写入单位
批量写入,单次写入多行数据
支持单行写入,适合高频事务
更新机制
生成新版本记录,旧数据标记为“已废弃”
直接修改原数据位置
删除机制
生成墓碑标记(Tombstone)
直接删除或标记为删除
注意:列式存储将所有写入操作(INSERT、UPDATE、DELETE)统一转化为追加写入行为,避免了行式存储中的原地修改。
1.3 列式存储的写入限制与应对策略
1)不支持单行更新:更新操作需要重写整个列,效率低下
应对策略:批量加载优先:使用批量工具(如 DataX、yasldr)替代单行插入。
2)删除操作复杂:需生成墓碑标记并在查询时过滤
应对策略:通过墓碑标记后台定期清理。
3)写入放大问题:频繁更新会导致存储空间浪费
应对策略:避免频繁更新,设计数据模型时考虑“写一次、读多次”特性。
4)事务支持弱:通常不支持 ACID 事务,仅支持批量写入
应对策略:采用混合存储架构,如 YashanDB 采用可变列存支持原地更新,实现毫秒级实时写入。
二、主键和列值是如何关联对应的?
列式存储里不会存储一个显式的“顺序索引”(比如存一个 Row_ID: 1),位置即索引(位置指的就是“物理行号”)。
一个数据对象有几列,就会分别保存到几个不同的文件中。通过让所有列文件的第 N 条数据在物理上指向同一行,从而省去了维护复杂索引的开销,并利用同列数据相似的特点进行了极致压缩。
例如,当插入一条包含 5 个字段的记录时:
行式存储:一次性写入 1 个数据块(如:1、张三、25、男、郑州),不同的数据类型仍然存储在连续的物理地址上。
列式存储:需要分别写入 5 个独立的列文件(如:ID.bin→1、姓名.bin→张三、年龄.bin→25、性别.bin→男、地区.bin→郑州),每个文件存储在不同的物理块。
那么如何保证 ID 为 1 的“张三”和 ID 为 1 的“25岁”是属于同一行的呢?
答案就是靠物理偏移量(Offset)或行号对齐。
ID.bin 的第 1 个数据,天然对应 Name.bin 的第 1 个数据。
ID.bin 的第 100 个数据,天然对应 Name.bin 的第 100 个数据。
实际存储结构更像这样:
ID.bin: [1, 2, 3, ...] (第N个位置的值)
Name.bin: ["张三", "李四", "王五", ...] (第N个位置的值)
数据库不需要查索引,它只需要读取“第 N 行”,直接去各个文件的第 N 个位置抓取数据即可。这就像班级中排列整齐的课桌,每一列的第几位属于一排,都是固定的。
三、数据如何编码(Encoding)来优化存储?
在列式存储中,编码是核心“杀手锏”。
