在数据库的日常使用中,"查看过去某一时刻的数据长什么样"是一个常见但不容易解决的需求。传统做法通常依赖 ETL 定期导出或逻辑备份,链路长、成本高、时效差。列存快照的出现,就是为了在列存索引的基础上,以极低的额外成本提供历史数据的查询能力。
为什么需要列存快照
主要适用于两类场景:
场景一:历史数据分析。 业务需要定期回看过去某个时间点的数据,例如每月初分析上个月的经营情况。传统方式需要在月底单独将数据拉出来存到另一个地方,而有了列存快照,只需在月底打一个快照点,后续随时通过 SELECT ... AS OF TSO 查询即可,无需额外的数据搬运和存储。
场景二:数据变更保护与恢复。 在 AI 时代,Agent 驱动的自动化开发越来越普遍,但 Agent 操作数据库时缺乏人工审查,容易误改或破坏数据。列存快照提供了一种轻量级的"后悔药":在每次变更前打一个快照,一旦数据被搞坏,可以直接基于快照查询变更前的状态,用于数据恢复或问题排查,而不必依赖全量备份回滚。
而列存快照是 PolarDB-X 提供的一种基于列存索引的历史数据查询能力。它允许用户:
- 创建快照点
自动或手动生成快照点(一个历史版本对应的 TSO),后续可以基于快照点查询历史数据。
- 保留快照点
通过指定快照保留时间,系统会保留该时间范围内的快照点及其关联文件,不被 Purge 清理。
- 按时间点查询
通过 SELECT ... AS OF TSO 语法,查询任意保留的快照点对应的历史数据。
具有以下特点:
- 无需数据搬运
快照数据就存储在 OSS 上的列存文件中,不需要额外导出。
- 查询即恢复
可以直接用 SQL 查询历史数据,也可以通过 INSERT ... SELECT ... AS OF TSO 将数据恢复到当前表。
- 成本低
快照只是保留版本链元信息和延迟文件清理,无需复制全量数据。
接下来,我们先了解 PolarDB-X 列存索引的整体架构,再深入了解列存快照的使用方式与实现原理。
PolarDB-X 列存架构速览
2.1 整体架构与核心组件
要理解列存快照的原理,需要先了解 PolarDB-X 列存索引的整体架构。本章不展开所有细节,只介绍与快照机制直接相关的核心概念。
PolarDB-X 采用存算分离的分布式架构,与列存相关的核心组件包括:
- CN(计算节点)
:SQL 入口,负责查询解析、优化和 MPP 执行。列存查询由 CN 协调,构建快照并调度分布式扫描任务。
- DN(存储节点)
:行存引擎,处理 OLTP 事务,产生 Binlog。
- CDC
:采集 DN 产生的 Binlog,合并为全局有序的 Global Binlog,供列存节点消费。
- Columnar(列存节点)
:消费 Global Binlog,将行存数据实时转换为列式存储文件(CSV/ORC/Delete Bitmap),写入 OSS。
- GMS(元数据服务)
:存储全局元数据,包括列存版本链、快照信息。同时提供 TSO(全局授时)服务。
- OSS(对象存储)
:列存数据的持久化介质,存储 CSV、ORC 和 Delete Bitmap 文件。
2.2数据存储模型
列存数据在 OSS 上采用 CSV + ORC 的分层组织方式,涉及三种文件:
- CSV 文件
:增量数据的落盘格式。Columnar 节点消费 Binlog 后,将新写入的数据以追加写的方式写入 CSV 文件。CSV 是无序的行式存储,写入效率高。
- ORC 文件
:列式存储格式,经过压缩和编码,扫描效率高。由后台 Compaction 任务异步转换而来(CSV 转 ORC,或多个 ORC 合并成一个),数据按列有序分布,读取性能好。
- Delete Bitmap
:基于 RoaringBitmap 的删除标记,CSV/ORC中的数据被删除时,在 Bitmap 中标记为已删除,而无需直接修改 CSV/ORC 文件。
三者的关系:CSV 负责"快写",ORC 负责"快读",Delete Bitmap 负责标记哪些行已失效。查询时需要综合三者才能得到正确结果。
列存引擎每次提交数据(group commit),都会在 GMS 中生成一个新的版本节点,形成一条版本链:
V1 → V2 → V3 → V4 → ...
每个版本由一个 TSO 唯一标识。TSO 是 GMS 提供的全局单调递增时间戳,Binlog 中的每个事务都携带 TSO,列存引擎负责消费 Binlog 并推进版本。另外,Compaction 也会产生新的版本。
每个版本记录的信息包括:
- 本次新增的 CSV 文件和 Delete Bitmap,以及这些文件的写入范围(偏移量)
- Compaction 产生的文件变更(删除的旧文件 + 新增的 ORC 文件)
这条版本链是列存快照的基础——每个快照点本质上就是版本链上的一个节点,保留这个节点及其关联的文件,就能还原出该时刻的完整数据视图。
列存快照原理
3.1 一条数据的列存之旅:从行存写入到快照可见
为了直观理解列存快照的工作方式,我们跟踪一条 INSERT 语句,看它如何从行存写入,最终在列存快照中变得可见。
第一步:行存写入,事务提交。 用户执行 INSERT INTO t_order VALUES (...) ,DN 完成行存写入并提交事务。提交时,GMS 分配一个全局唯一的 TSO 作为该事务的提交时间戳,写入 Binlog。
第二步:Binlog 采集与传输。 CDC 组件从各 DN 采集 Binlog,按 TSO 排序后合并为全局有序的 Global Binlog,发送给 Columnar 节点。
第三步:列存引擎消费,数据落盘。 Columnar 节点消费 Global Binlog,将这条 INSERT 对应的数据追加写入 OSS 上的 CSV 文件。如果同一批次中有 DELETE 或 UPDATE(拆分为 DELETE + INSERT),则同时追加 Delete Bitmap。
第四步:版本提交,快照诞生。 当一个 group commit 周期结束,Columnar 节点将一段时间内的所有事务变更写入 CSV 和 Delete Bitmap 文件,持久化到 OSS 中,最后把文件名、偏移量、TSO 等信息注册到 GMS 的版本链上。
此时,版本链从 V(n) 推进到 V(n+1),新版本的 TSO 会大于等于这条 INSERT 事务的 TSO。
第五步:快照可见。 当用户执行 SELECT ... AS OF TSO 并指定 V(n+1) 的 TSO 时,CN 构建列存快照,该快照包含了新版本的文件列表和偏移量,因此这条数据对查询可见。
从写入到列存可见的延迟通常在秒级。
3.2 快照点的生成:columnar_flush 做了什么
在 上文中我们看到,列存引擎会随着 Binlog 消费不断推进版本链。但这些版本默认会被后台 Purge 机制逐步清理,只保留最新的一个。列存快照的核心就是将某个版本标记为"需要保留",使其不被清理,从而可以在未来被查询。
用户可以通过 CALL polardbx.columnar_flush() 手动生成一个快照点。这个调用在内部触发的操作是:
- 触发一次强制 group commit。
列存引擎将当前所有已消费但尚未提交的增量数据立即落盘并提交,生成一个新的版本。这一步保证快照点反映的是调用时刻最新的已提交数据。
- 记录快照点元信息。
系统在 GMS 中将当前最新版本的 TSO 标记为一个快照点,这会告知 Purge 机制不去清理这个历史版本。这个 TSO 就是后续查询时使用的标识。
- 返回 TSO 给用户。
返回这个快照点对应的 TSO,用户可以保存它用于后续查询。
-- 手动生成快照点CALL polardbx.columnar_flush();-- 返回值示例:7206138458723582016-- 也可以指定具体的索引CALL polardbx.columnar_flush('db_name', 'table_name', 'index_name');
除了手动调用,还可以配置自动生成:
- 按固定间隔:设置 auto_gen_columnar_snapshot_interval(单位:分钟,最小 5 分钟)。实现机制为系统每秒检测一次,若距上次生成快照点的时间超过该参数值,则立即生成新快照。
-- 修改为每隔一小时生成一个快照点CALL polardbx.columnar_set_config('db_name', 'table_name', 'index_name', 'auto_gen_columnar_snapshot_interval', 60);-- 关闭自动生成快照点CALL polardbx.columnar_set_config('db_name', 'table_name', 'index_name', 'auto_gen_columnar_snapshot_interval', -1);
- 按 CRON 表达式(更常用)
:通过 columnar_auto_snapshot_config 配置定时任务,例如每小时整点生成。实现机制为按 CRON 表达式定期生成一个全局的快照。
-- 每小时的整点生成一个全局快照CALL polardbx.columnar_auto_snapshot_config('ENABLE', '0 0 * * * ?', '+08:00');-- 查看定期快照配置CALL polardbx.columnar_auto_snapshot_config('SHOW');-- 关闭定期快照功能CALL polardbx.columnar_auto_snapshot_config('DISABLE');
需要注意的是,columnar_flush 的本质是一次强制提交 + 元数据标记,它不会复制数据或创建物理副本。快照的存储成本来自于延迟清理,被快照引用的文件会被保留更长时间,仅此而已。
3.3 快照的保留与过期:版本链的 Purge 机制
列存引擎在运行过程中会持续产生新版本——每次 group commit 和每次 Compaction 都会在版本链上追加节点。如果所有历史版本都被永久保留,版本链的元数据和关联的物理文件会无限增长。因此,系统需要一个 Purge 机制来定期清理不再需要的旧版本。
Purge 的基本逻辑
Purge 定期扫描版本链,确定一个低水位(Low Watermark),将低水位之前、且未被任何快照引用的版本标记为可回收。具体规则:
- 计算低水位
:低水位 = 当前最新版本的 TSO - 保留窗口。保留窗口是一个系统参数,决定了"最近多长时间内的版本默认不清理"。
- 跳过被快照引用的版本
:如果某个版本的 TSO 对应了一个尚未过期的快照点,则该版本及其关联文件不会被清理,这保证了列存快照可以被一直保留。
- 清理可回收版本
:对于可回收的版本,Purge 会删除其独占的物理文件(不被任何版本引用的 CSV、ORC、Delete Bitmap 文件),并从版本链中移除该节点。
快照如何阻止 Purge
在 上文中我们看到,columnar_flush 会在 GMS 中将某个版本标记为快照点。这个标记的实际效果就是:告知 Purge 跳过该版本。快照保留的不仅仅是版本链上的一个节点,也包括该版本指向的所有物理文件。
快照的过期与清理
快照不会永久保留。用户可以通过以下方式控制快照的生命周期:
-- 设置快照保留时长(单位:秒),示例为保留 7 天CALL polardbx.columnar_set_config('db_name', 'table_name', 'index_name', 'columnar_snapshot_retention_time', 604800);
当快照的创建时间超过保留时长后,系统会自动将其标记为过期。下一次 Purge 扫描时,该版本将不再受保护,相关文件可以被正常清理。
3.4 快照查询:快照构建与缓存策略
当用户执行 SELECT ... AS OF TSO 时,CN 根据指定的 TSO 构建一个快照,确定哪些文件、哪些行对这次查询可见:
- 定位版本
:在 GMS 版本链上找到 TSO 小于等于指定值的最大版本节点,作为基准版本。
- 收集文件列表
:从版本链起点到基准版本,累积所有文件变更,得到完整的文件集合——所有可见的 ORC 文件、CSV 文件及其偏移量上界、Delete Bitmap 及其偏移量上界。偏移量截断是关键:同一个 CSV 文件在后续版本中可能被追加了新数据,但快照只读取到基准版本记录的偏移量为止。Delete Bitmap 同理。
- 下发执行
:将文件列表封装为快照,交给 MPP 引擎进行分布式扫描。
-- 查询历史快照数据SELECT * FROM t_order AS OF TSO 7206138458723582016WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';-- 数据恢复:将快照数据写回临时表INSERT INTO t_order_backupSELECT * FROM t_order AS OF TSO 7206138458723582016;
缓存策略方面,快照查询采用 bypass cache 策略:CN 检测到 AS OF TSO 指向历史版本时,读取 OSS 文件不经过本地缓存。这是因为快照查询访问的历史数据通常是一次性的,如果写入缓存会挤出常规查询的热数据,造成缓存污染。以快照查询的单次延迟换取整体缓存效率,对于低频批量分析的典型场景是合理的取舍。
Compaction 与快照的交互
4.1 CSV 到 ORC 的合并
在 2.2 中我们提到,CSV 是增量数据的落盘格式,写入快但扫描慢;ORC 是列式格式,扫描快但不支持追加写。Compaction 的核心任务之一就是将 CSV 数据异步转换为 ORC,提升查询性能。
Compaction 在后台持续运行,主要包括两类操作:
- CSV → ORC:
将一批 CSV 文件中未被删除的数据转换为 ORC 格式,按列有序存储。
- ORC 合并:
将多个 ORC 文件的有效数据合并为一个 ORC 文件,减少文件数量和数据空洞,降低查询时的 IO 开销。
每次 Compaction 完成后,会在版本链上提交一个新版本,记录本次的文件变更:新增了哪些 ORC 文件、删除了哪些旧的 CSV/ORC 文件。
4.2 Compaction 对快照可见性的影响
Compaction 会替换文件,但不能破坏已有快照的数据完整性。
举一个具体的例子:
- 版本 V5 时,数据分布在 csv_1、orc_1、orc_2 三个文件中。用户在 V5 打了一个快照。
- 后台 Compaction 将 csv_1 转换为 orc_3,并在版本 V8 提交了这个变更。从 V8 开始,orc_3 替代了 csv_1。
- 当用户查询 V5 的快照时,快照仍然指向 csv_1 + orc_1 + orc_2,与 Compaction 后的文件无关。
注意到:
- Compaction 产生的文件变更记录在新版本(V8)上,不会修改历史版本的元数据。
- Purge 在清理时会检查 csv_1 是否仍被某个快照引用,如果是,则跳过删除。
因此,Compaction 与快照可以安全地并行运行,互不干扰。代价是被快照引用的旧文件会延迟清理,占用额外的 OSS 存储空间,直到快照过期。由于列存数据本身具有较高的压缩率,且多个版本共享同一批物理文件(快照只额外保留元信息和延迟文件清理),实际的额外存储开销很低。
4.3 Schema Evolution 下的快照兼容
当用户对表执行 DDL(如 ALTER TABLE ADD COLUMN)后,列存索引的 Schema 会发生变化。新版本的 ORC/CSV 文件按新 Schema 写入,但快照可能指向旧 Schema 的文件。
PolarDB-X 通过 Schema 多版本机制解决这个问题:每个版本都能通过 GMS 的元信息找到该版本对应的 Schema 版本号。查询快照时,CN 根据最新版本的 Schema 构建查询计划,读取旧文件时按旧 Schema 解析,保证读取的正确性。
这意味着:
- 快照查询能正确处理打快照之后发生的 ADD COLUMN、DROP COLUMN 等变更。
- 返回的数据始终符合快照时刻的表结构,不会因为后续的 DDL 而出错。
与其他恢复方式的对比
列存快照并不是唯一的历史数据恢复手段。PolarDB-X 还支持传统的备份恢复和行存快照,三者适用于不同场景。
备份恢复 |
行存快照 |
列存快照 |
|
原理 |
全量备份 + 增量 Binlog 回放,恢复到指定时间点 |
保留 Undo 日志,查询时逐行回退到历史版本 |
保留列存版本链上的历史版本,按文件偏移量截断读取 |
恢复粒度 |
整个实例 |
表级别 |
表级别 |
资源开销 |
需要管控任务流创建新实例,占用额外的计算和存储资源 |
无需额外资源,复用当前实例的 Innodb 引擎 |
无需额外资源,复用当前实例的列存引擎 |
存储成本 |
高(全量数据拷贝) |
较高(行存存储成本高于列存) |
低(多版本共享物理文件) |
查询效率 |
不支持直接查询,需恢复完成后才能访问 |
快(数据在本地行存中,点查无需读取无关数据) |
适合批量分析(需远程读取 OSS 上的列存文件,按块读取) |
适用场景 |
灾难恢复、整实例回滚 |
短时间内的行级回查、点查为主的场景 |
长周期历史分析、批量数据恢复 |
简而言之:备份恢复是"重建一个实例",行存快照是"原地回退单行",列存快照是"直接查历史文件"。三者的选择取决于恢复粒度、时间跨度和查询模式。
总结
列存快照的本质是在列存版本链上"钉住"一个历史版本,使其不被 Purge 清理,从而支持按时间点查询历史数据。整个机制具有以下特点:
- 无需数据搬运
快照数据就存储在 OSS 上的列存文件中,不需要额外导出。
- 查询即恢复
可以直接用 SQL 查询历史数据,也可以通过 INSERT ... SELECT ... AS OF TSO 将数据恢复到当前表。
- 成本低
快照只是保留版本链元信息和延迟文件清理,无需复制全量数据。
从使用角度看,用户只需关心两个操作:columnar_flush 生成快照点,SELECT ... AS OF TSO 查询快照。底层的版本链管理、文件保留、Purge 协调、Schema 兼容等复杂性,都由系统透明处理。