跳转至

MPP 存储分层与冷热数据管理 —— 从不可变存储出发理解为什么冷热分离是所有列存 MPP 的必修课

作者:JiangChong | 撰写时间:2026年06月

适用场景框: 当你看到集群存储水位持续攀升、查询变慢、或者存储账单超出预期——而新增的数据量并没有显著增长时,你很可能撞上了列存 MPP 的通用难题:冷数据无声膨胀。

开篇声明: 本文以 Vertica 为主要剖析对象,但在所有环节对比 ClickHouse、Greenplum、Snowflake、Doris、Redshift 等 MPP 系统的不同实现。MPP 共性与 Vertica 专属将在文中明确区分。

关联文章:

理解全文脉络: 这份文章按「为什么 → 是什么 → 怎么选 → 什么影响 → 真实证据 → 行动原则」的逻辑链组织。第 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_datephone_numberduration 等 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 有几个直接影响冷热分层的属性:

  1. 不可修改:一旦写入,数据永不原地修改。DELETE/UPDATE 产生 delete vector(外部标记),而非修改 container 内容。
  2. 有排序键:container 内数据按 projection sort order 全排序。排序键决定了存储裁剪的效率——谓词列在 sort order 前面,min/max 过滤才能生效。
  3. 有分区归属:如果表定义了 PARTITION BY,同一 container 内的所有行的分区键值相同。分区边界不可跨越 container。
  4. 数量上限:每个节点每个投影最多 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=1000S3DeleteBatchSize=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. 延伸阅读

按推荐阅读顺序排列:

  1. Vertica 冷热数据管理与成本优化Vertica 专属。本文的实操姐妹篇,包含完整的诊断 SQL 工具箱、分步排查流程和成本估算模型。建议读完本文后直接查阅其中的 SQL 工具箱。
  2. MPP 数据删除与存储回收机制Vertica 为主,MPP 对比。深入 DELETE → Delete Vector → AHM → Tuple Mover → PURGE 的完整链路。本文层次二和层次三的详细展开。
  3. Vertica 表分区策略选择指南Vertica 专属。分区粒度选择的决策框架,含 CALENDAR_HIERARCHY_DAY 的完整语法和最佳实践。本文第 3.3 节分区粒度 trade-off 的实操补充。
  4. The Vertica Analytic Database: C-Store 7 Years LaterVertica 专属,MPP 通用。 §3.5(Partitioning)、§3.7(ROS/WOS)、§4(Tuple Mover)、§5.1(AHM)是本文架构论述的原始出处。如果你想知道「这些设计是怎么被论证出来的」,从这里读起。
  5. Analytic Database Design Choices: Vertica's Experience and PerspectivesVertica 专属。 §2.5 记录了 Tuple Mover 在 2010 年重新设计之前的「血泪史」——mergeout 被客户和售后团队视为「significant pain」。理解这段历史有助于理解为什么 Tuple Mover 的 strata 算法如此强调无参数调优。
  6. ROS Pushback 故障排查Vertica 专属。 ROS 容器数超过 1024 上限的 8 种场景及修复方案。本文第 3.3 节问题的实战延伸。