MPP 存储分层与冷热数据管理 —— 从不可变存储出发理解为什么冷热分离是所有列存 MPP 的必修课¶
作者:JiangChong | 撰写时间:2026年06月
适用场景框: 当你看到集群存储水位持续攀升、查询变慢、或者存储账单超出预期——而新增的数据量并没有显著增长时,你很可能撞上了列存 MPP 的通用难题:冷数据无声膨胀。
开篇声明: 本文以 Vertica 为主要剖析对象,但在所有环节对比 ClickHouse、Greenplum、Snowflake、Doris、Redshift 等 MPP 系统的不同实现。MPP 共性与 Vertica 专属将在文中明确区分。
关联文章:
- Vertica 冷热数据管理与成本优化 — 实操诊断与工具箱(本文偏理论,那篇偏操作)
- MPP 数据删除与存储回收机制 — DELETE → Delete Vector → AHM → Tuple Mover 完整链路
- MPP 数据分布策略 — 分段(Segmentation)与分区(Partitioning)的设计哲学
- Vertica 表分区策略选择指南 — 分区粒度选择与层次化分区
- ROS Pushback 故障排查 — ROS 容器超限的根因与修复
- Tuple Mover 最佳实践完全指南 — Mergeout 与存储回收机制
理解全文脉络: 这份文章按「为什么 → 是什么 → 怎么选 → 什么影响 → 真实证据 → 行动原则」的逻辑链组织。第 1 节描述所有列存 MPP 面临的存储分层困境;第 2 节拆解分层机制的四层模型,每层对比 5+ 系统的不同实现;第 3 节是全文核心——围绕一个「不可变存储单元」带来的一组 trade-off 做跨系统深度对比;第 4 节分析这些设计对查询、加载、运维的实际影响;第 5 节用虚构案例和真实故障验证结论;第 6 节给出可操作的设计原则。如果你只有 5 分钟,直接读第 6 节的原则清单;如果想理解背后的架构哲学,从第 1 节开始。
1. 问题背景 —— 这个架构要解决什么问题¶
1.1 所有列存 MPP 面临的共同困境¶
在列存 MPP 出现之前,传统行存数据库(Oracle、PostgreSQL)处理数据老化有一套成熟方案:表分区 → 将老分区移动到归档表空间 → truncate 原分区。这个过程之所以简单,是因为行存数据库可以原地修改数据页——删除一行就是真的从页中抹去,空间立即回收。
列存 MPP 推翻了这个前提。为了追求极致的压缩率和扫描性能,列存引擎普遍采用不可变存储单元(immutable storage unit)的设计:
- Vertica 的 ROS Container:写入后永不原地修改
- ClickHouse 的 Data Part:MergeTree 的 part 写入后 immutable,合并产生新 part
- Snowflake 的 Micro-partition:16MB 压缩列存块,DML 产生新版本而非修改旧版本
- Greenplum AOCO 的 Append-Optimized 表:删除通过辅助的可见性位图标记
- Doris / StarRocks 的 Tablet / Segment:写入后不可变,compaction 合并产生新文件
来源:C-Store 7 Years §3.7 明确声明 "Data in Vertica is never modified in place"。这一原则是列存压缩率和扫描性能的根源,也是冷热分层复杂性的根源。
不可变存储给冷热分层带来了一个根本矛盾:数据一旦写入,就永远占据那块物理空间——除非整个存储单元被重写(合并)或删除。冷数据不会自己消失,也不会自己迁移到便宜的存储上。如果表没有按时间分区,冷数据和热数据就物理混杂在同一个 ROS container 中——即使查询只需要昨天的数据,引擎也不得不扫过包含几年前数据的 container(至少检查其 min/max 索引)。
下表总结了各 MPP 系统对这个困境的不同应对:
| 系统 | 不可变存储单元 | 标记删除方式 | 物理回收方式 | 冷热分层机制 |
|---|---|---|---|---|
| Vertica | ROS Container | Delete Vector(外置独立文件) | Tuple Mover mergeout | Enterprise: 分区+表空间; Eon: 多位置存储策略 |
| ClickHouse | Data Part | Mutation 文件 / _row_exists 伪列 |
后台 Merge | TTL TO VOLUME + storage_policy |
| Snowflake | Micro-partition | 新版本替换旧版本 | 自动后台 GC | Time Travel 窗口 + 外部表卸载 |
| Greenplum | AOCO segment | 辅助可见性位图 | VACUUM(全表扫描) | 表空间 + EXCHANGE PARTITION |
| Doris | Tablet/Rowset | Delete Bitmap | 后台 Compaction | Storage Policy + cooldown TTL |
| Redshift | Block | 内部标记 | VACUUM(自动/手动) | RA3 自动 SSD→S3 / Spectrum 外部表 |
1.2 一个具体场景:535TB 的表,三分之二的冷数据占了最贵的存储¶
假设你有一张运营商的通话详单表 cdr_detail,每天新增 500GB 压缩数据,按月分区,保留 3 年(36 个分区)。每一行数据有 call_date、phone_number、duration 等 50 列。
场景 A:不做冷热分层
3 年下来,这张表占用了 500GB × 365 × 3 ≈ 535TB。其中最近 30 天的数据只占 15TB(不到 3%),但它和其余 520TB 的 3 年老数据共同参与 Tuple Mover 的 mergeout、共同存储在昂贵的 NVMe SSD 上。当你查询「昨天通话时长超过 1 小时的记录」时,虽然 Vertica 的分区裁剪能跳过 35 个非目标分区的 ROS 容器,但:
- 该表的 catalog 元数据仍然包含了全部 36 个分区的条目,DDL 和系统表查询都可能变慢
- 3 年前的老数据仍然占据 NVMe,每 TB 年成本数万元
- Tuple Mover mergeout 需要扫描和重写这些老数据(尽管不活跃分区只合并一次),消耗 I/O 和 CPU
场景 B:做了冷热分层
最近 30 天的数据留在 NVMe SSD 上(热层),30 天到 1 年的数据迁移到 HDD(温层),1 年以上的数据迁移到 S3/HDFS 上的低成本存储(冷层)。此时:
- 主存储只保留 ~15TB 热数据 + ~170TB 温数据
- mergeout 只作用于热数据和温数据的不活跃分区
- 冷数据查询仍然可能(通过外部表或跨存储桶查询),但不再占用昂贵的 NVMe
- 存储成本:NVMe ¥300-600/TB/月 vs S3 Glacier ¥30-50/TB/月,差 5-10 倍
这就是存储分层的核心价值:让成本跟随访问频率。
1.3 各系统对同一问题的策略一览¶
不同 MPP 系统对「如何把冷数据从热存储中分离出去」给出了截然不同的答案,差异背后反映了架构哲学的不同:
| 维度 | Vertica Enterprise | Vertica Eon | ClickHouse | Snowflake | Doris |
|---|---|---|---|---|---|
| 存储耦合度 | 存算一体 | 存算分离 | 存算一体(S3 disk 可选) | 存算分离 | 存算一体(远程 tier 可选) |
| 分层粒度 | 分区(partition key range) | 分区 + 存储位置 label | 分区内的 part(后台按 TTL 移动) | Micro-partition 版本过期 | Rowset(compaction 触发) |
| 自动化程度 | 手动 DDL(MOVE_PARTITIONS_TO_TABLE) | 半自动(SET_OBJECT_STORAGE_POLICY 后 TM 自动移动) | 全自动(TTL + move_factor 阈值) | 全自动(版本过期) | 全自动(cooldown daemon) |
| 冷数据可查 | 需要查归档表或跨表 UNION | 直接查(S3 读取) | 直接查(S3 disk 读取,更高延迟) | 7 天 Fail-safe 不可查,外部表可查 | 直接查(S3 读取 + 本地缓存) |
| 反向迁移 | MOVE_PARTITIONS_TO_TABLE 反向 | 修改存储策略 label | ALTER TABLE MOVE PARTITION | COPY INTO | 不支持自动提升 |
关键差异:存算一体的系统(Vertica Enterprise、ClickHouse 本地模式)需要物理移动数据文件来实现分层;存算分离的系统(Vertica Eon、Snowflake、Doris 远程模式)只需要修改元数据指针——因为数据本来就在远程存储上,分层只是换了个 bucket。
2. 核心概念与机制 —— MPP 存储分层的四层模型¶
任何列存 MPP 的存储分层都可以拆解为四个层次的机制,每层解决一个子问题。本节以 Vertica 为主要案例展开,每层最后附跨系统对比。
层次一:不可变存储单元的组织方式¶
存储分层的「原子操作对象」是什么?——这是所有后续讨论的基础。
Vertica 的 ROS Container
ROS container(Read Optimized Store container,读取优化存储容器)是 Vertica 的不可变存储单元。每个 ROS container 包含按 projection sort order 排序的一组完整行,在磁盘上以列文件形式存储。Vertica 7.2 起,每列的数据和位置索引合并存储在一个文件中,位置索引记录每个磁盘 block 的 start position、min/max value,大小约数据本身的 1/1000(来源:C-Store 7 Years §3.7;文件格式演进见 ROS Bundling 最佳实践)。
概念映射:ROS container ≈ ClickHouse 的 data part ≈ Snowflake 的 micro-partition ≈ Doris 的 rowset。它们都是不可变的、排序的、压缩的列存数据块。关键差异在于合并策略——Vertica 的 Tuple Mover 按 strata 分层合并,Snowflake 的 micro-partition 用新版本替换旧版本。
ROS container 有几个直接影响冷热分层的属性:
- 不可修改:一旦写入,数据永不原地修改。DELETE/UPDATE 产生 delete vector(外部标记),而非修改 container 内容。
- 有排序键:container 内数据按 projection sort order 全排序。排序键决定了存储裁剪的效率——谓词列在 sort order 前面,min/max 过滤才能生效。
- 有分区归属:如果表定义了
PARTITION BY,同一 container 内的所有行的分区键值相同。分区边界不可跨越 container。 - 数量上限:每个节点每个投影最多 1024 个 ROS container。接近上限时 Tuple Mover 停止工作,触发 ROS pushback 错误。
其他系统对比:
| 系统 | 不可变单元 | 排序是否强制 | 合并触发方式 |
|---|---|---|---|
| Vertica | ROS container | 是(projection sort order) | Tuple Mover strata 算法 |
| ClickHouse | Data Part | 是(ORDER BY) | 后台 merge(按 size 层级) |
| Snowflake | Micro-partition | 自动(按 clustering key) | 自动后台 |
| Greenplum AOCO | Segment file | 否(仅 append) | VACUUM 手动 |
| Doris | Rowset | 是(DUPLICATE/AGGREGATE key) | Compaction 后台 |
打个比方:ROS container 就像一本按日期装订好的纸质账本——写过的那一页就不能改了,要改只能在旁边贴一张"此条作废"的便签(delete vector)。账本多了就需要定期合并整理(mergeout),把有效的条目誊抄到新账本里。
层次二:标记删除的方式与可见性窗口¶
列存 MPP 的 DELETE 不会立即移除数据,而是先标记。标记的方式决定了冷数据何时能被物理回收。
Vertica 的 Delete Vector + AHM 机制
Vertica 将 delete vector 存储为独立文件:先在内存中累积(DVWOS),再由 Tuple Mover moveout 到磁盘(DVROS)(来源:C-Store 7 Years §3.7.1)。每个 delete vector 记录了「哪些位置的行被删除了」,被标记为删除的行在查询时被过滤掉,但物理上仍然占据磁盘空间。
AHM(Ancient History Mark,古老历史标记) 是控制删除标记何时可以被物理清除的关键机制。AHM 之前的 delete vector 可以在 mergeout 时被安全丢弃——因为超过 AHM 的数据不可能被任何查询引用(包括 AT EPOCH 时间旅行查询)。AHM 的推进受 HistoryRetentionTime 参数控制:默认值 -1 表示不保留历史,AHM 可以自由推进;设为正值则 AHM 被锁定在「当前时间 - HistoryRetentionTime 秒」之前。
Vertica 专属:AHM 是一个全局 epoch 值,不是每个表独立的。这意味着如果有一张表的
HistoryRetentionTime设得很大,整个数据库的 AHM 都会被拖住——所有表的 delete vector 都无法清理。这个全局约束在其他 MPP 中不存在。
跨系统对比:
| 系统 | 标记删除方式 | 可见性窗口机制 | 窗口对存储回收的影响 |
|---|---|---|---|
| Vertica | Delete Vector(独立文件) | AHM + HistoryRetentionTime(全局) | AHM 不推进 → 所有表的 delete vector 无法清理 |
| ClickHouse | Mutation 文件标记 | mutation 完成即清理 | 轻量级 DELETE(ALTER DELETE)异步执行 |
| Snowflake | 新 micro-partition 版本 | Time Travel 0-90天 + Fail-safe 7天 | 冷数据持续计费直到完全退出 Fail-safe |
| Greenplum | 可见性位图 | 事务 ID horizon | VACUUM 扫描删除位图并回收 |
| Doris | Delete Bitmap | Compaction 合并删除 | compaction 频率决定回收速度 |
| Redshift | 内部行标记 | VACUUM 回收 | 自动 VACUUM 在后台运行 |
层次三:物理回收的触发方式¶
标记删除之后,物理空间何时被回收?这是各系统差异最大的地方。
Vertica 的 Tuple Mover Mergeout
Tuple Mover 的 mergeout 操作同时完成两件事:合并小 ROS container 为大 container 和 过滤掉 AHM 之前的 delete vector(来源:C-Store 7 Years §4)。这带来了一个关键收益:存储回收和存储整理是同一趟 I/O——不需要额外扫描。
mergeout 使用 strata 算法:ROS container 按大小被划分为指数增长的层级(strata),输出 container 至少比输入高一个层级。这保证了每个 tuple 在整个生命周期内被重写的次数是有界的(等于 strata 数量)。
Vertica 专属:mergeout 尊重分区边界——不跨分区合并 ROS container。每个非活跃分区最终会成为 1 个 ROS container。这就是为什么分区数直接决定了 ROS container 总数,进而影响 catalog 大小。
跨系统对比:
| 系统 | 回收机制 | 是否复用合并I/O | 是否全自动 | 运维负担 |
|---|---|---|---|---|
| Vertica | Tuple Mover mergeout 过滤 AHM 前的 delete vector | ✅ 复用 | ✅ 自动(需 AHM 推进) | 低(除非 AHM 被阻塞) |
| ClickHouse | 后台 merge + mutation 执行 | ✅ 复用 | ✅ 全自动 | 极低 |
| Snowflake | 后台 GC 自动清理过期 micro-partition | ❌ 独立 | ✅ 全自动 | 零运维 |
| Greenplum | VACUUM(全表扫描回收) | ❌ 独立 | ❌ 需手动/定时 | 高 |
| Doris | Compaction 合并 + 删除位图清理 | ✅ 复用 | ✅ 全自动 | 极低 |
层次四:冷热分层——数据在各存储层级间迁移¶
前三层解决了「如何标记删除」和「如何回收删除」,但冷热分层的核心问题是另一种:数据没有被删除,只是访问频率低——如何把它从昂贵存储迁移到便宜存储?
Vertica Enterprise 的分层方案:MOVE_PARTITIONS_TO_TABLE + 不同表空间
Enterprise 模式的核心思路是:创建归档表 → 将冷分区 catalog 级移动到归档表 → 归档表放在 HDD/低速存储位置。
-- 将冷分区从主表移动到归档表(纯 catalog 操作,秒级完成)
-- 若目标表不存在,函数内部自动调用 CREATE TABLE LIKE ... INCLUDING PROJECTIONS 创建
SELECT MOVE_PARTITIONS_TO_TABLE(
'cdr.cdr_detail', -- 源表
'2021', '2023', -- 分区键范围(min-range-value, max-range-value,均为字符串)
'archive.cdr_archive' -- 目标表
);
MOVE_PARTITIONS_TO_TABLE 是纯 catalog 操作——不移动任何数据文件,只修改元数据中的所属关系。这是 Vertica 独有的高效设计。
Vertica Eon 的分层方案:多位置存储策略
Eon 模式利用存储策略(Storage Policy)实现真正的自动分层:
-- 步骤 1:在低成本 S3 bucket 创建第二公共存储位置
CREATE LOCATION 's3://cold-historical-bucket'
COMMUNAL USAGE 'DATA'
LABEL 'cold_tier';
-- 步骤 2:为冷表设置存储策略(新数据写入 cold_tier)
-- enforce-storage-move 是第 5 个参数,前两个占位 '' 为分区键范围(不指定则整表生效)
SELECT SET_OBJECT_STORAGE_POLICY('archive.cdr_archive', 'cold_tier', '', '', 'true');
存储策略的优先级层次(由低到高):数据库 → Schema → 表 → 表分区(来源:v26.2 官方文档 §17.3)。这意味着你可以为同表的不同分区设置不同的存储位置——最新的分区在 hot tier,老分区在 cold tier。
跨系统对比:
| 系统 | 分层命令/操作 | 是否物理移动数据 | 迁移粒度 | 冷数据查询路径 |
|---|---|---|---|---|
| Vertica Enterprise | MOVE_PARTITIONS_TO_TABLE |
❌ 纯 catalog | 分区 | 查归档表或 UNION |
| Vertica Eon | SET_OBJECT_STORAGE_POLICY |
❌ 元数据(内部) | 分区/表 | 直接查(S3 读取) |
| ClickHouse | TTL ... TO VOLUME |
✅ 物理移动 part 文件 | Part | 直接查(更高延迟) |
| Snowflake | COPY INTO @stage + CREATE EXTERNAL TABLE |
✅ 导出数据 | 表/分区 | 查外部表(性能较低) |
| Doris | CREATE STORAGE POLICY + cooldown_ttl |
✅ 物理上传 rowset | Rowset | 直接查(读取 S3 + 本地缓存) |
| Greenplum | ALTER TABLE EXCHANGE PARTITION |
❌ 纯 catalog | 分区 | 查目标表 |
3. 设计决策与 Trade-off —— 为什么 A 选了这个,B 选了那个?¶
存储分层的设计选择可以归结为一个核心问题:不可变存储单元决定了删除和迁移都不能原地操作——代价付在哪里?
3.1 核心 trade-off:标记删除 → 物理回收之间的「真空期」¶
所有列存 MPP 都选择将标记删除与物理清除解耦(这是 MPP 共性)。解耦的方式不同直接决定了三样东西:
- DELETE 执行速度:标记是 O(1) 还是 O(n)
- 存储回收延迟:标记后多久空间真正释放
- 历史查询支持:能否查询「昨天的数据快照」
下表是全文最核心的一张对比表:
| 系统 | DELETE 行为 | 回收延迟 | 历史查询 | 回收机制 I/O | 分区级清理(冷数据首选) |
|---|---|---|---|---|---|
| Vertica | 写 delete vector,需扫描匹配行 | 分钟~天(取决于 mergeout + AHM) | ✅ AT EPOCH(AHM 之前) | 复用合并 I/O | DROP_PARTITIONS 秒级,纯 catalog |
| ClickHouse | mutation 异步排序 | 分钟~小时 | ❌ 不支持时间旅行 | 额外 | ALTER TABLE DROP PARTITION 秒级 |
| Snowflake | 新 micro-partition 版本 | 0~90+7天(Time Travel + Fail-safe) | ✅ Time Travel (0-90天) | 零(新版本替换) | ALTER TABLE DROP PARTITION / SWAP WITH |
| Greenplum | 可见性位图 | 不定(取决于 VACUUM 频率) | ❌ | 额外(全表扫描) | ALTER TABLE EXCHANGE PARTITION + DROP |
| Doris | delete bitmap | 分钟~小时(compaction) | ❌ | 复用合并 I/O | ALTER TABLE DROP PARTITION 秒级 |
为什么表里同时列出 DELETE 和分区级清理?因为面向冷数据管理时,DELETE 几乎总是错误选择——删除 18 个月的老数据应该用
DROP_PARTITIONS(秒级、无 delete vector、无 mergeout 负担)。这里保留 DELETE 对比是为了展示:如果你误用了 DELETE,不同系统的「惩罚」各不相同。
Vertica 的选择哲学:用 delete vector 换取 DELETE 瞬时完成,但代价是空间回收依赖 mergeout 周期——而 mergeout 又可能被 AHM 阻塞。这是一个「写时快速、回收懒惰」的策略。
Snowflake 的选择哲学:用 micro-partition 替换换取 DELETE 的简洁性(没有 delete vector 概念),代价是 Time Travel + Fail-safe 数据保留期内持续计费——即使你已经 DROP 了表。这是一个「按时间付费换回滚安全」的策略。
ClickHouse 的选择哲学:mutation 异步执行,适合大数据量的批量删除,不适合高频小量 DELETE。这是一个「吞吐优先、延迟容忍」的策略。
打个比方:Vertica 像在书页上贴"此条作废"标签(delete vector),标签贴得多了就需要定期重新装订(mergeout);Snowflake 像直接印新版书然后回收旧版——快是快,但旧书要过一段时间才能当废纸卖。
3.2 Eon 模式的分层 trade-off:公共存储 + Depot 缓存¶
Eon 模式将存储与计算分离,数据永久存储在 S3/HDFS 上,节点本地只有 Depot 缓存。这带来了分层的新维度:
收益:
- 冷数据分层只需修改 S3 bucket——不涉及数据文件移动
- 子集群可以弹性扩缩——增删节点无需 rebalance
- 多个 communal storage location 可以指向不同成本的对象存储(S3 Standard → S3 Glacier)
代价:
- 查询冷数据要从 S3 拉取 → API 费用 + 网络延迟
- Depot 缓存空间有限 → 冷数据查询可能驱逐热数据缓存
- 公共存储死文件泄漏 → 需要定期
CLEAN_COMMUNAL_STORAGE
[推测] 基于共同架构原理,存算分离的优势是所有将计算与存储解耦的系统(Snowflake、Doris 远程模式、Redshift RA3)共享的——分层简化为 bucket 切换。存算一体的系统(ClickHouse 本地模式、Greenplum)必须承受物理数据迁移的 I/O 成本。
3.3 分区粒度 trade-off:精细裁剪 vs 容器爆炸¶
Vertica 的 ROS container 数量与分区数一一对应(每个非活跃分区贡献 1 个容器)。这创造了一个硬约束——分区粒度必须在查询裁剪效率和管理成本之间取得平衡:
| 分区粒度 | 3 年分区数 | ROS 容器数 | 存储裁剪效率 | ROS Pushback 风险 |
|---|---|---|---|---|
| 按天 | 1095 | ~1095 | 极精细 | ❌ 超过 1024 上限 |
| 按周 | ~156 | ~156 | 精细 | ✅ 安全 |
| 按月 | 36 | 36 | 中等 | ✅ 安全 |
| 按年 | 3 | 3 | 粗 | ✅ 安全但裁剪极弱 |
Vertica 的折衷方案:CALENDAR_HIERARCHY_DAY
CALENDAR_HIERARCHY_DAY 通过层次化分区在精细度和容器数之间取得平衡:最近 2 个月按天分区(精细裁剪),2 个月到 2 年按月分区,2 年以上按年分区。3 年总分区数约 40(而非 1095),同时保持了对近期数据的高效裁剪。
[推测] ClickHouse 的 partition + TTL 设计避免了类似问题——因为 TTL 不仅移动数据,还可以 TTL ... DELETE 直接删除过期分区,不需要无限期保留所有的 part。Doris 的 remote tiering 将数据移到 S3 后,本地只保留元数据,容器数不是瓶颈。
4. 设计对实际使用的影响¶
4.1 查询维度¶
【通用】分区裁剪失效时的性能退化
如果你的查询谓词没有包含分区键,即使 95% 的数据是冷数据,查询也要扫描所有分区的 min/max 索引——虽然最终可能跳过大部分 container,但 min/max 检查本身就有开销。教训:分区键必须对齐最高频的查询谓词。
【通用】冷数据查询在存算分离系统中的额外成本
在 Eon 模式(以及 Snowflake、Doris remote)中,查询冷数据意味着从对象存储拉取数据——产生 API 调用费用(S3 GET ~$0.0004/1000 次)并消耗网络带宽。如果 Depot 空间不足,冷数据查询还可能驱逐热数据缓存,使后续热查询也变慢。
【Vertica】AHM 全局阻塞的连锁效应
如果 HistoryRetentionTime 被设置为 30 天,整个数据库的 AHM 被锁在 30 天前。这意味着 Tuple Mover 在 mergeout 时无法过滤掉任何 delete vector——所有表的 delete vector 都堆积在磁盘上。常见误解:「HistoryRetentionTime 只影响我想回滚的表」——错误。它是全局参数,影响所有表。
4.2 加载维度¶
【通用】分区表的数据加载路径更长
COPY 到分区表时,Vertica 需要计算每一行的分区键值,并将其路由到对应分区的 ROS container。非分区表没有这一步。对于高频小批量加载(trickle load),分区表的分区路由可能成为性能瓶颈。
【Vertica】MOVE_PARTITIONS_TO_TABLE 后源表的 projection 不会自动删除
当你将冷分区从主表移动到归档表后,主表的物理存储大小减少了,但 projection 定义仍然存在。如果主表不再需要这些 projection 的某些特殊排序键优化,需要手动评估并清理冗余投影。
4.3 运维维度¶
【通用】冷数据不会自己消失
这是最常见的误解:以为数据「不查了」就等于「不占空间了」。实际上,只要数据没有被 DROP_PARTITIONS 删除或没有被 mergeout 过滤,它就一直占据存储和 catalog 条目。需要主动管理。
【Vertica】Enterprise 模式没有自动冷热迁移
Enterprise 模式的分层完全是手动操作——你需要创建归档表、执行 MOVE_PARTITIONS_TO_TABLE、在 HDD 上创建 storage location。Eon 模式通过 Storage Policy 实现了半自动化,但仍需手动设置策略。
【Eon 专属】公共存储死文件泄漏
Eon 模式的 Reaper 进程负责异步清理已删除的公共存储文件。当数据库异常终止(如 kill -9)或 Reaper 队列堆积时,死文件可能永久留在 S3/HDFS 中——继续计费但不包含在任何系统表统计中。参考 Eon 公共存储文件不删除问题 中的真实案例:67TB 死文件被 HDFS 计量但不被 Vertica 统计。
5. 案例验证¶
5.1 虚构案例¶
📝 虚构案例 1:冷数据清理的正确方式 —— 分区级操作 vs DELETE¶
场景: 一张 500 亿行的物联网传感器表,按月分区,保留 2 年数据。需要删除其中 ~200 亿行(40%)超过 18 个月的老数据。
正确做法: 18 个月的数据 = 18 个分区。在任何一个 MPP 系统中,正确的操作都是按分区删除——Vertica 用 DROP_PARTITIONS,ClickHouse 用 ALTER TABLE DROP PARTITION,Snowflake 用 ALTER TABLE DROP PARTITION。分区删除是纯元数据操作:秒级完成,不产生 delete vector,不触发 mergeout/compaction,空间立即回收账单。
如果误用了 DELETE: 下表展示在「用错工具」的情况下,各系统分别付出什么代价,帮助理解其底层设计差异——
| 系统 | DELETE 行为 | 空间立即释放? | 回收延迟 | 回收期间对查询的影响 |
|---|---|---|---|---|
| Vertica | 扫描匹配行 + 写 delete vector(扫描量与数据分布成正比) | ❌ 空间增长(delete vector 占用) | mergeout 周期 + AHM 推进 | 低(TM 在后台) |
| ClickHouse | mutation 异步排序合并 | ❌ mutation 完成前不释放 | 数分钟~数小时 | 中等(mutation 消耗 I/O) |
| Snowflake | 新 micro-partition 版本替换 | ❌ 旧版本仍在 Time Travel | 0~90天(取决于 DATA_RETENTION_TIME) | 零 |
| Greenplum | 可见性位图标记 | ❌ 需要 VACUUM | 取决于 VACUUM 频率(可能数天) | 高(VACUUM 全表扫描) |
| Doris | delete bitmap 标记 | ❌ compaction 完成后释放 | 分钟~小时 | 低 |
⚠️ 上表的操作在冷数据场景下是反模式——删除 40% 的数据不应逐行 DELETE。但它揭示了一个重要事实:分区不仅是查询裁剪的工具,更是数据生命周期管理的唯一正确入口。没有分区的表,冷数据清理只能走 DELETE 这条昂贵路径。
📝 虚构案例 2:混合场景——Eon 模式冷数据查询驱逐热数据缓存¶
场景: 某电商平台 Eon 模式 6 节点集群,Depot 总量 1.2TB,活跃热数据 ~800GB。一个自动化的月度报表在凌晨遍历全部 5 年的交易数据。每次执行时,查询从 S3 拉取 4TB 历史数据填充 Depot,将 800GB 热数据全部驱逐出缓存。
后果: 第二天早高峰,所有常规查询都需要从 S3 重新拉取热数据——响应时间从 2 秒飙升到 30 秒。S3 API 费用额外增加 ¥15,000/月。
回溯到原理: Depot 是 LRU 缓存——冷数据查询会驱逐热数据。解决方案:(1)增大 Depot 空间或使用 SET_DEPOT_ANTI_PIN_POLICY_PARTITION 标记冷分区优先驱逐;(2)将报表 SQL 优化为仅查最近 30 天数据,冷数据单独处理;(3)使用 Storage Policy 将 1 年以上的数据迁移到独立的低成本 bucket,减少与热数据的缓存竞争。
5.2 真实案例¶
📋 真实案例 · 来源:Eon 公共存储文件不删除问题¶
行业: 某省级运营商 | 集群: Eon 模式,HDFS 公共存储,Vertica 10.1.1-7
问题: Vertica 系统表 projection_storage 统计单副本总量 160TB,但从 HDFS 用 hadoop fs -du -s 统计为 227TB——差值 67TB。这 67TB 是泄漏在 HDFS 中的死文件,不在任何 Vertica 系统表的统计范围内,但仍然占用 HDFS 空间和 3 副本存储成本。
根因: Eon 模式的 Reaper 进程负责异步清理已删除的公共存储文件。数据库异常终止导致 Reaper 队列未被处理,死文件永久残留。默认的 Tombstone 清理参数(TombstoneProcessingBatchSize=1000、S3DeleteBatchSize=1000)过于保守,清理速度跟不上产生速度。
修复:
SELECT SET_CONFIG_PARAMETER('TombstoneProcessingBatchSize', 1); --这个配置过于激进,生产环境中如没有出现泄漏问题,可以保持缺省配置或进行适当的调整
SELECT SET_CONFIG_PARAMETER('S3DeleteBatchSize', 0);--这个配置过于激进,生产环境中如没有出现泄漏问题,可以保持缺省配置或进行适当的调整
SELECT MAKE_AHM_NOW();
SELECT SYNC_CATALOG();
SELECT CLEAN_COMMUNAL_STORAGE(true);
SELECT FLUSH_REAPER_QUEUE();
效果: 执行后 1-2 分钟,HDFS 释放 67TB。
跨系统启示: 死文件泄漏是所有使用对象存储作为数据持久层的 MPP 系统的共同风险——Snowflake 的 Fail-safe 数据在 7 天后自动清理、Doris 的 remote rowset 通过 compaction 触发 GC、Redshift RA3 的 S3 数据由 AWS 托管。Vertica Eon 的独特之处在于 Reaper 进程的异步清理机制——它不保证即时清理,因此在异常关闭后容易堆积。如果你运维的是自建对象存储(非托管服务)上的 MPP,死文件监控是必须建立的巡检项。
6. 设计原则总结¶
以下原则按适用范围分组:先 MPP 通用,后 Vertica 专属。组内按重要性排列。
【通用】原则 1:不可变存储 × 无自动分层 = 冷数据必须主动管理¶
为什么: 所有列存 MPP 的数据一旦写入就永不原地修改。冷数据不会自动消失、不会自动迁移到低成本存储。如果你不主动管理,它会永远占据最昂贵的存储层。
反例: 一张 2020 年的月度汇总表,3 年无人查询,但由于没有删除也没有归档,仍然占据 NVMe SSD。在 Snowflake 中,即使你 DROP 了这张表,Fail-safe 仍会持续计费 7 天。
【通用】原则 2:分区键必须对齐数据生命周期策略¶
为什么: 分区是 DROP_PARTITIONS(Vertica)、TTL DELETE(ClickHouse)、表空间交换(Greenplum)的前置条件。没有分区的表只能用 DELETE(产生大量标记)或 TRUNCATE(全表清空)——两者都无法精细化管理冷数据。
反例: 一张非分区大表,想删 3 年前的数据只能 DELETE FROM t WHERE date < '2023-01-01'——产生数十亿条 delete vector,空间不但不释放反而膨胀,Tuple Mover 需要数小时甚至数天才能完成 mergeout。在 Greenplum 中类似操作后不做 VACUUM,空间永远不回收。
【通用】原则 3:分区粒度要匹配保留周期,不是越细越好¶
为什么: 对于保留 3 年以上的数据,按天分区会产生 1095+ 个 ROS 容器(Vertica)/ 大量 part(ClickHouse),推高 catalog 大小和管理成本。按月分区足以满足大多数归档需求。
反例: Vertica 按天分区 × 3 年 = 1095 个 ROS 容器,超过 1024 上限触发 ROS pushback——Tuple Mover 彻底停工,所有后续 load 都失败。ClickHouse 按天分区 × 3 年 = 1095 个 partition,system.parts 表查询变慢。
【通用】原则 4:DROP_PARTITIONS 永远优于 DELETE 用于数据清理¶
为什么: DROP_PARTITIONS(Vertica)/ ALTER TABLE DROP PARTITION(ClickHouse)/ ALTER TABLE EXCHANGE PARTITION(Greenplum)是纯 catalog/metadata 操作——不扫描数据、不产生 delete vector、不触发 mergeout。DELETE 需要标记每一行,空间回收延迟从秒级变为分钟到小时级别。
反例: 某运营商每天 DELETE 老数据再重新加载修正数据(参见 Vertica 冷热数据管理与成本优化 虚构案例 1),产生 57% 的 delete vector 堆积——1.8PB 表中有 1PB 是 delete vector。改为 DROP_PARTITIONS + 重新加载该分区后,存储降至 0.8PB。
【通用】原则 5:设计存储分层前先量化访问模式¶
为什么: 冷热的定义不是拍脑袋的——需要基于 projection_usage(Vertica)/ system.query_log(ClickHouse)/ QUERY_HISTORY(Snowflake)的实际查询记录。30 天无查询的表不一定全是冷数据——季度报表的数据访问周期是 90 天。
反例: 某团队将所有 30 天无查询的分区迁移到冷存储,结果季度报表跑不动——因为冷存储的读取延迟从 5ms 变成了 200ms。应先用 90 天作为冷数据判定阈值,再结合业务确认。
【Vertica】原则 6:不要让 HistoryRetentionTime 过大地拖住 AHM¶
为什么: HistoryRetentionTime 是全局参数,过大的值会阻止 AHM 推进,进而阻止所有表的 delete vector 在 mergeout 时被清理。默认 -1(不保留历史)已经是最优设置。
反例: 某金融机构为了支持 30 天数据回滚,设置 HistoryRetentionTime = 2592000(30 天)。结果 cluster 中所有表的 delete vector 都堆积了 30 天以上——存储膨胀了 40%,而真正需要回滚的场景一年只有 2-3 次。
【Vertica】原则 7:Eon 模式下定期对比系统表与对象存储的实际用量¶
为什么: Reaper 进程可能因异常终止而堆积死文件。如果不定期对比 projection_storage 的统计值和 S3/HDFS 的实际计量值,死文件泄漏可能持续数月不被发现。
反例: 案例中的运营商 67TB 死文件泄漏——如果每月执行一次 CLEAN_COMMUNAL_STORAGE(false)(仅检查不删除),可以第一时间发现并处理。
7. 延伸阅读¶
按推荐阅读顺序排列:
- Vertica 冷热数据管理与成本优化 — Vertica 专属。本文的实操姐妹篇,包含完整的诊断 SQL 工具箱、分步排查流程和成本估算模型。建议读完本文后直接查阅其中的 SQL 工具箱。
- MPP 数据删除与存储回收机制 — Vertica 为主,MPP 对比。深入 DELETE → Delete Vector → AHM → Tuple Mover → PURGE 的完整链路。本文层次二和层次三的详细展开。
- Vertica 表分区策略选择指南 — Vertica 专属。分区粒度选择的决策框架,含 CALENDAR_HIERARCHY_DAY 的完整语法和最佳实践。本文第 3.3 节分区粒度 trade-off 的实操补充。
- The Vertica Analytic Database: C-Store 7 Years Later — Vertica 专属,MPP 通用。 §3.5(Partitioning)、§3.7(ROS/WOS)、§4(Tuple Mover)、§5.1(AHM)是本文架构论述的原始出处。如果你想知道「这些设计是怎么被论证出来的」,从这里读起。
- Analytic Database Design Choices: Vertica's Experience and Perspectives — Vertica 专属。 §2.5 记录了 Tuple Mover 在 2010 年重新设计之前的「血泪史」——mergeout 被客户和售后团队视为「significant pain」。理解这段历史有助于理解为什么 Tuple Mover 的 strata 算法如此强调无参数调优。
- ROS Pushback 故障排查 — Vertica 专属。 ROS 容器数超过 1024 上限的 8 种场景及修复方案。本文第 3.3 节问题的实战延伸。