列式存储如何实现高效的数据查询与分析?

摘要:〇、前言 上一篇文章中,博主对 行式存储和列示存储 进行了简单的介绍和对比。 了解完它们的基本信息后,还有疑点,那么本文就针对几个常见的疑点进行详细的解读下,供参考。 一、数据是按照列来分别存储的,那么如何写入? 列式存储数据库通过追加写入
〇、前言 上一篇文章中,博主对 行式存储和列示存储 进行了简单的介绍和对比。 了解完它们的基本信息后,还有疑点,那么本文就针对几个常见的疑点进行详细的解读下,供参考。 一、数据是按照列来分别存储的,那么如何写入? 列式存储数据库通过追加写入(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)来优化存储? 在列式存储中,编码是核心“杀手锏”。
阅读全文