MPP 数据删除与存储回收机制 —— 为什么列存数据库的 DELETE 都不直接删除数据¶
作者:JiangChong | 撰写时间:2026年06月
适用场景: 当你发现 DELETE 后磁盘空间没有释放、查询越来越慢、或节点恢复卡住几小时无法完成时,就需要理解本文的内容。这些并非某一个数据库的 bug,而是所有列存 MPP 系统的架构选择所带来的固有权衡。
关联文章:
- Vertica DELETE 相关问题 — Vertica DELETE 生命周期每一步的实操 FAQ
- Vertica 数据删除最佳实践 — DELETE 类型对比、Purge 策略、替代方案
- Vertica 数据库 Replay Delete 算法 — Replay Delete 的两种算法详解
- Tuple Mover 最佳实践完全指南 — Vertica Tuple Mover 的完整架构与配置速查
- Vertica Epoch 机制详解 — Vertica 五种 Epoch 的关系与推进机制
- MPP 列存引擎的架构设计哲学 — 理解为什么列存需要特殊的删除机制
理解全文脉络¶
本文以所有列存 MPP 系统共同面临的「数据删除与空间回收」问题为主线,以 Vertica 为主要剖析对象,同时在各环节对比 Greenplum、Redshift、ClickHouse、Doris/StarRocks、Snowflake 的不同实现选择。
- 如果你想快速知道「为什么我用的 MPP 数据库空间不释放」:先看第 2.1 节了解共同原因,再跳到对应系统的对比表。
- 如果你想对比不同 MPP 的删除策略差异:第 2 节贯穿全节 + 第 3 节的综合对比表。
- 如果你正在排查 ROS Pushback 或节点恢复卡死(Vertica 用户):先看第 5 节案例,再回头看第 2 节机制。
1. 问题背景 —— 所有列存 MPP 面临的共同困境¶
1.1 行存数据库的删除为什么「简单」¶
在传统行存数据库(PostgreSQL、MySQL InnoDB、Oracle)中,删除一条记录的过程天然与「原地修改」相容:数据以行为单位存储在数据页中,找到目标行的位置、标记为删除、后续由 VACUUM 或 Purge 线程回收空间。虽然也涉及 MVCC 的多版本管理,但至少定位被删除行是一件低成本的事——因为一行所有列的数据物理上相邻,一次页查找就能覆盖所有列。
1.2 为什么所有列存 MPP 都无法这样删除¶
考虑一个几乎所有列存 MPP 用户都熟悉的场景:某运营商有一张通话记录表,每天新增 50 亿行,保留 6 个月数据,总行数约 9000 亿。每月需要清理最旧月份的数据,约 1500 亿行。如果用行存式的「逐行标记删除」,以每秒 100 万行的处理速度,需要约 42 小时,且持有排他锁。
但更根本的问题在于列存物理组织——这一点对 Vertica / ClickHouse / Greenplum / Redshift / Doris / StarRocks / Snowflake 全部成立:
- 无法原地修改:每列的数据存储在独立的文件(或 micro-partition)中。删除其中几行意味着解压→修改→重新排序→重新压缩所有列文件。一张 100 列的宽表,删除 1 行需要重写 100 个列段。
- 行重建代价高:列存数据库中「一行」是一个逻辑概念,它的各列值通过位置编号(或 Row ID)在不同列文件中隐式关联。要删除某个满足条件的行,必须先穿透各列文件定位该行在各列中的位置编号,跨列做映射。
因此,所有列存 MPP 系统不约而同地选择了同一种策略:把「标记删除」和「物理清除」彻底解耦。 标记删除只记录「哪些行被删了」,不做任何物理修改;物理清除留给后台异步进程,在重组数据时顺便过滤掉标记为删除的行。
这一设计根植于 C-Store 论文的核心原则——"数据一旦写入就永不被原地修改"。Vertica 是这一理念最忠实的商业化实现,但 ClickHouse 的 MergeTree、Snowflake 的 immutability of micro-partitions、Doris 的 Tablet Compaction,全部遵循同一条设计线索:immutable storage units。
| 系统 | 存储单元与可变性 | 标记删除方式 | 物理回收方式 |
|---|---|---|---|
| Vertica | ROS Container | Delete Vector(独立文件) | Tuple Mover Mergeout |
| Greenplum | Heap Page(可变,但标记为 dead) | MVCC dead tuple(页内标记) | VACUUM |
| Redshift | Block(1MB immutable) | MVCC row marker | VACUUM + Sort |
| ClickHouse | Part(immutable directory) | _row_exists 伪列 / Mutation 文件 |
MergeTree 后台 merge |
| Doris / StarRocks | Tablet Segment(immutable) | Delete-on-Read / Delete Bitmap | Tablet Compaction |
| Snowflake | Micro-partition(immutable) | 重写 entire micro-partition | Time Travel 窗口过期自动 GC |
关于 Greenplum: Greenplum 支持两种存储格式——默认的 Heap 表(行存) 和可选的 AO 列存表。本文对 Greenplum 删除机制的描述(页内 dead tuple + VACUUM)均指其默认 Heap 表行为。AO 列存表使用块级删除位图(block header 标记已删除行号),与 Vertica 的独立标记方案更接近,但非默认存储格式,此处不展开。
说明: 本文后续以 Vertica 为主要剖析对象,每一节都会明确标注「哪些是 MPP 共性、哪些是 Vertica 特有的实现选择」。
2. 核心概念与机制 —— 从 MPP 共性问题到 Vertica 具体实现¶
2.1 共同的架构模式:标记 → 保留 → 合并时回收¶
所有列存 MPP 的删除回收都遵循同一条链路:
DELETE 语句
│
├─► 创建「删除标记」
│ (Vertica: Delete Vector / Greenplum: dead tuple / ClickHouse: mutation 文件)
│
├─► 标记保留期(可见性窗口)
│ (Vertica: AHM / Greenplum: transaction horizon / Snowflake: Time Travel window)
│ 在此期间标记只过滤查询结果,不触发物理删除
│
└─► 后台合并/重组时物理清除
(Vertica: Tuple Mover / Greenplum: VACUUM / ClickHouse: Merge / Doris: Compaction)
这条链路上每个 MPP 系统都要回答三个问题:
- 删除标记放在哪里? 与数据文件的关系是什么?
- 标记保留多久才能被回收? 用什么机制控制可见性?
- 由谁触发物理回收? 回收的条件是什么?
以下逐层比较。
2.2 第一层:删除标记放哪里?¶
这是列存 MPP 之间分歧最大的环节,也是最直接影响查询性能、恢复速度和运维复杂度的设计决策。
方案 A:独立标记文件(外置)—— Vertica¶
Vertica 将删除标记存放在与数据列文件完全独立的 Delete Vector 文件中。Delete Vector 记录了:
- 被删除行在 ROS 容器中的位置编号列表
- DELETE 提交时的 epoch(时间戳)
Delete Vector 本身也是数据——随 DELETE 提交直接写入磁盘(DVROS 格式,同样是排序压缩存储)。查询执行时,Vertica 引擎打开 ROS 列文件,同时检查关联的 Delete Vector,跳过标记位置。
比喻: 在纸质账本上贴便利贴——「第 5 页第 3 行、第 12 页第 7 行已作废」,原始记录始终完好。
优点: 数据文件真正 immutable,压缩效率不受删除影响;增量恢复可以从 buddy 直接拷贝 ROS 文件。 缺点: Delete Vector 本身需要管理和存储;当 Delete Vector 数量膨胀时,查询和恢复的开销会指数级增长(见第 5 节真实案例)。
方案 B:数据页内标记(混存)—— Greenplum / Redshift¶
与 Vertica 的外置标记形成鲜明对比的,是 Greenplum 和 Redshift。它们继承了 PostgreSQL 的 MVCC 传统:删除标记(dead tuple)直接存储在数据页内,每个 tuple 携带 xmin/xmax 事务 ID,查询时通过事务可见性判断哪些行有效。这省去了额外的标记文件结构,但与数据混存也意味着空间回收必须逐一扫描数据页(VACUUM)——Redshift 的大表 VACUUM 可能持续数小时且与查询竞争 I/O,Greenplum 的 heap 表原地更新 page 需要事务日志(WAL),与 Vertica 的「数据永不修改」哲学背道而驰。
方案 C:写入时合并标记(Merge-on-Write)—— ClickHouse / Doris / StarRocks¶
ClickHouse 的 DELETE 本质上是一个异步 mutation:它先创建一个临时的 mutation 文件记录删除条件,后台 merge 时将匹配的行物理删除再写回新 part。(ClickHouse 部分基于 MergeTree 公开文档;Doris/StarRocks 部分为基于共同架构原理的推断)。Doris/StarRocks 支持两种模式:
- Delete-on-Read:删除标记(Delete Bitmap)独立存储,查询时过滤
- Merge-on-Write:写入时同步合并删除标记,查询无需额外过滤
优点: Merge-on-Write 模式下查询无额外过滤开销。 缺点: Merge-on-Write 会阻塞写入;Delete-on-Read 模式下的 Delete Bitmap 管理复杂度与 Vertica 的 Delete Vector 类似,但没有独立的 epoch 机制来辅助 GC。
方案 D:重写整块(immutable partition)—— Snowflake¶
Snowflake 的 micro-partition 是完全不可变的。DELETE 不创建标记——它直接重写受影响的 micro-partition,新 micro-partition 中不包含被删除的行;旧的 micro-partition 保留到 Time Travel 窗口(默认 1 天,Enterprise 最大 90 天)过期后自动 GC。
优点: 查询无需过滤已删除行(新文件里本来就没有),空间回收完全自动化,无运维负担。 缺点: 大规模 DELETE(如删除全表一半数据)需要重写大量 micro-partitions,写放大极高;Time Travel 窗口内的存储成本较高。
标记方式对比总结¶
| 系统 | 标记存放 | 独立性 | 查询开销 | 对恢复的影响 |
|---|---|---|---|---|
| Vertica | 独立 Delete Vector 文件 | ✅ 完全独立 | 需过滤 DV | 恢复需 Replay Delete |
| Greenplum | 数据页内 dead tuple | ❌ 与数据混存 | 可见性判断 | WAL 重放 |
| Redshift | Block 内 MVCC marker | ❌ 与数据混存 | 可见性判断 | 从 snapshot 恢复 |
| ClickHouse | 独立 mutation 文件 → merge 后消失 | ✅ merge 前独立 | merge 后无开销 | 从副本复制 |
| Doris/StarRocks | Delete Bitmap(独立,或 merge 合并) | ✅ 可独立 | 需过滤 bitmap | Tablet Compaction 恢复 |
| Snowflake | 无标记,直接重写 micro-partition | — | 无额外开销 | 不适用 |
2.3 第二层:标记保留多久才能回收?¶
无论哪种标记方式,都需要回答一个问题:已删除的行什么时候可以被安全地物理清除? 清除太早,可能影响历史查询、增量恢复或 MVCC 可见性。清除太晚,空间浪费。
Vertica:AHM(Ancient History Mark)+ Epoch¶
Vertica 用 Epoch(64 位逻辑时间戳)标记每一行和每一个 Delete Vector 的时间。AHM(Ancient History Mark)是「数据保质期」的截止线——早于 AHM 的已删除数据可以被物理清除。默认每 3 分钟自动推进 AHM,但存在 DOWN 节点或未刷新 projection 时不会推进。
关键约束: AHM 不会超过 LGE(Last Good Epoch)——LGE 是集群所有节点数据完全持久化到磁盘的最小 epoch。这保证了恢复的安全性:在 AHM 之前的所有数据都已被完整持久化。
SELECT get_ahm_epoch() AS ahm,
get_last_good_epoch() AS lge,
get_current_epoch() AS ce;
ahm | lge | ce
-----+-----+-----
240 | 277 | 278
Greenplum / Redshift:事务 ID 水位线¶
Greenplum 和 Redshift 没有独立的 epoch/AHM 机制。它们沿用了 PostgreSQL 的 MVCC 设计:每个 tuple 携带 xmin(插入事务 ID)和 xmax(删除事务 ID),通过事务 ID 可见性判断来决定哪些行对当前查询可见。当一个 dead tuple 的 xmax 小于当前所有活跃事务的最小 xid 时(即不再被任何事务看到),它在 VACUUM 时可被回收。(这一设计源于 Greenplum 和 Redshift 都是 PostgreSQL 的衍生分支)。
区别:Vertica 的 AHM 是一个全局统一、跨节点一致的时间点,而 Greenplum 的事务水位线是节点内局部计算的。这导致 Vertica 的回收时机跨节点严格一致,适合增量恢复;Greenplum 则更灵活但不提供跨节点的删除一致性保证。
ClickHouse / Doris:Part 级 / Tablet 级可见性¶
ClickHouse 的 mutation 机制没有全局的时间戳概念——删除标记在 merge 后直接消失,merge 策略完全由 MergeTree 的后台参数(如 merge_with_ttl_timeout)控制。(基于 MergeTree 公开架构文档的推断)。Doris/StarRocks 的 Delete Bitmap 由 version 号管理,Compaction 时清理过期 bitmap。(推测,基于共同架构原理)。
Snowflake:Time Travel 窗口¶
Snowflake 最简单——删除后旧数据保留到 Time Travel 窗口过期(默认 1 天),超时后自动从云存储 GC。没有手动推进回收的操作,也没有「AHM 卡住」的概念——但也意味着你无法控制回收时机,存储成本在窗口期内是固定的。
可见性控制对比¶
| 系统 | 机制 | 配置方式 | 「卡住」风险 |
|---|---|---|---|
| Vertica | AHM(全局 epoch 水位线) | HistoryRetentionTime / HistoryRetentionEpochs |
✅ 高风险(DOWN 节点、未刷新 projection 会导致 AHM 不推进) |
| Greenplum | 事务 ID horizon | vacuum_freeze_min_age 等 |
低风险(VACUUM 可手动调度) |
| ClickHouse | Part 级 mutation 状态 | merge_with_ttl_timeout |
低风险(merge 自动管理) |
| Doris | Tablet version / bitmap | 后台 Compaction 自动控制 | 低风险 |
| Snowflake | Time Travel window | DATA_RETENTION_TIME_IN_DAYS |
无(全自动 GC,不可手动干预) |
2.4 第三层:由谁触发物理回收?¶
这是列存 MPP 之间其次大的分歧。不同 MPP 系统的回收机制选择,反映了它们在「运维自动化程度」和「用户控制力度」之间的不同倾向。
Vertica:Tuple Mover(自动 + 手动)¶
Tuple Mover 是 Vertica 的自动后台管家,核心职责是 Mergeout:将多个小 ROS 容器合并为大容器,同时完成两项附带工作——过滤掉 AHM 之前的已删除行(物理清除的唯一自动途径),以及触发 Replay Delete 重定位残留的删除标记。
Mergeout 的自动清除有条件:仅对非活跃分区的 ROS 容器生效,且已删除行占比须超过 PurgeMergeoutPercent(默认 20%)。活跃分区永不触发自动清除。
当自动清除不工作时,需要手动干预:
-- 步骤 1:推进 AHM,使 delete marker 可以被物理清除
SELECT MAKE_AHM_NOW();
-- 步骤 2:清理指定分区
SELECT PURGE_PARTITION('schema.table_name', partition_key);
-- 或清理整表(最后手段)
SELECT PURGE('schema.table_name');
PURGE无条件重写整个 ROS 容器——哪怕只有 1 条已删除记录也全部重写。I/O 代价远高于自动 Mergeout,属于应急手段。
Greenplum / Redshift:VACUUM(半自动)¶
Greenplum 和 Redshift 都需要 VACUUM 来回收空间。VACUUM 是一个全表扫描操作,逐页检查 dead tuple 是否可以回收。Redshift 的 VACUUM 还附带排序功能(RE-SORT),因为 Redshift 不像 Vertica 那样有 Tuple Mover 自动维持排序。
关键差异:VACUUM 是独立操作,与常规查询竞争 I/O 和 CPU。而 Vertica 的 Mergeout 本身是存储管理的例行后台任务,删除回收只是顺带完成——没有额外的全表扫描。
ClickHouse / Doris:后台 Compaction(全自动)¶
ClickHouse 的 MergeTree 引擎自动后台 merge,没有手动 PURGE 的概念。Doris/StarRocks 的 Tablet Compaction 同样全自动。这些系统的运维成本更低——你通常不需要关心「删除数据什么时候回收」——但相应的,你对回收时机和优先级的控制也更弱。
Snowflake:全自动 GC(零运维)¶
Snowflake 的回收完全自动化——你甚至无法手动触发。旧 micro-partition 在 Time Travel 窗口过期后被云存储的后台 GC 回收。这对运维友好,但在大规模 DELETE 后如果想立即释放存储空间,你没有办法。
回收机制对比¶
| 系统 | 回收方式 | 触发模式 | 是否需要手动干预 | 回收 I/O 来源 |
|---|---|---|---|---|
| Vertica | Tuple Mover + PURGE | 自动(Mergeout 条件触发)+ 手动 | ✅ 有时需要 | 复用 Mergeout I/O |
| Greenplum | VACUUM | 手动 / 定时任务 | ✅ 必须调度 | 独立全表扫描 |
| Redshift | VACUUM(含 SORT) | 手动 / 自动(auto VACUUM) | ⚠️ 视情况 | 独立全表扫描 + 排序 |
| ClickHouse | Merge | 全自动 | 极少 | 复用 merge I/O |
| Doris/StarRocks | Compaction | 全自动 | 极少 | 复用 compaction I/O |
| Snowflake | GC(云存储后端) | 全自动(不可干预) | ❌ 不需要 | 无客户端 I/O |
2.5 Vertica 专属机制:Replay Delete¶
虽然标记和回收是所有 MPP 的共同问题,但 Vertica 有一个独特机制是其他系统没有的:Replay Delete。
当 Tuple Mover 的 Mergeout 重写数据后,被删除行在新容器中的物理位置发生了变化。Replay Delete 的任务是在新容器中重新找到这些被删除行,更新 Delete Vector 中的位置编号。这发生在 Mergeout、节点恢复、投影刷新、集群 Rebalance 等任何会重写存储容器的操作中。
为什么其他 MPP 不需要 Replay Delete?因为:
- Greenplum/Redshift:删除标记在数据页内,数据迁移时标记跟着一起走
- ClickHouse:mutation 在 merge 后直接被物理删除,没有残留标记需要重定位
- Doris:Delete Bitmap 绑定在 Tablet 上,tablet 的 segment 重组后 bitmap 跟着重组
- Snowflake:直接重写 micro-partition,没有残留标记
Vertica 之所以需要 Replay Delete,恰恰是因为 Delete Vector 与数据文件物理隔离——这是方案 A(独立标记文件)的结构性代价。
Replay Delete 有两种算法,Vertica 自动选择:
| 算法 | 原理 | 复杂度 | 适用场景 |
|---|---|---|---|
| 传统算法 | 先匹配排序键找到候选位置,再逐列精确匹配 | O(N²) | 排序键基数高、删除比例低 |
| 新算法 | 将被删除数据与容器数据做 JOIN,全部列作为 JOIN 键 | O(N log N) | 排序键基数低、删除比例高 |
2.6 完整链路:Vertica 从 DELETE 到存储回收¶
SQL DELETE
│
├─► 创建 Delete Vector 写入磁盘
│ 记录:{位置=[row3, row7, row12], epoch=105}
│
├─► Tuple Mover: Mergeout
│ 合并小 ROS 容器为大容器
│ 触发 Replay Delete:重新映射删除行位置
│ 同时过滤掉 AHM 前的已删除行(物理清除)
│ 条件:非活跃分区 + 删除占比 > PurgeMergeoutPercent(20%)
│
├─► AHM 推进(每 3 分钟自动,或手动 MAKE_AHM_NOW)
│ AHM < DELETE epoch → 不能清除
│ AHM ≥ DELETE epoch → 可以清除
│
└─► 或手动 PURGE_PARTITION / PURGE_TABLE
无条件重写 ROS 容器,立即清除 AHM 前的已删除数据
存储空间释放 ✓
3. 设计决策与 Trade-off —— 跨系统对比¶
3.1 核心权衡:查询性能 vs 写入性能 vs 存储空间 vs 运维复杂度¶
所有列存 MPP 的删除回收设计,本质上是在这四个维度之间做取舍:
| 维度 | Vertica | Greenplum | ClickHouse | Snowflake |
|---|---|---|---|---|
| DELETE 执行速度 | 极快(只写 DV) | 快(页内标记) | 慢(异步 mutation,可能排队) | 小范围快,大范围慢(重写 micro-partition) |
| 查询过滤开销 | 有(需查 DV,>20% 删行时明显) | 有(可见性判断) | 无(merge 后已物理清除) | 无 |
| 存储空间回收 | 延迟(依赖 Mergeout+AHM) | 延迟(依赖 VACUUM 调度) | 延迟(依赖 merge 周期) | 延迟(固定 Time Travel 窗口) |
| 运维负担 | 中-高(需监控 DV/AHM,可能需手动 PURGE) | 中(需调度 VACUUM) | 低(全自动) | 极低(零运维) |
| 历史查询 | ✅ AHM 控制,灵活 | ❌ 不支持(需外部备份) | ❌ 不支持 | ✅ Time Travel |
| 大表 DELETE 后果 | DV 堆积→恢复灾难(见 §5) | 需长时间 VACUUM | mutation 排队可能阻塞新写入 | 重写海量 micro-partitions |
共同结论: 没有任何一个系统在「DELETE 后立即回收空间」这个维度上拿满分——这是列存 immutable 存储单元的必然代价。差异在于系统选择在哪一步让你支付这个代价:Vertica 是恢复时(企业模式)的 Replay Delete 代价,Greenplum 是 VACUUM 时的 I/O 代价,ClickHouse 是 mutation 排队时的延迟代价,Snowflake 是 Time Travel 窗口期的额外存储成本。
3.2 为什么不能没有 DELETE?¶
在所有列存 MPP 系统中,DELETE 时同步物理删除都是不可选的。 原因在 1.2 节已论述——解压→修改→重排→重压全列文件的代价极高。这种忽略是基于物理存储格式的,与具体实现无关。
但这引出一个更本质的问题:既然物理删除代价高,为什么不干脆不提供 DELETE 能力——只让用户 TRUNCATE / DROP PARTITION?
Vertica 的论文(C-Store 7 Years §3.5)直接回答了这个问题:分区是批量删除的最优方式(秒级 catalog 操作,空间立即回收),但在以下场景不可或缺:
- 修正少量错误数据(如某条记录的金额错了)
- 删除无法通过分区键裁剪的数据子集
- 符合数据合规要求的「被遗忘权」操作
因此所有列存 MPP 都选择了:同时提供分区裁剪(最优)和 DELETE(兜底),把选择权留给用户。 出问题的场景几乎都是用户用 DELETE 做批量清理——这在所有系统上都是反模式。
3.3 AHM vs Time Travel vs 事务水位线 —— 回收策略的三种哲学¶
| 对比维度 | Vertica AHM | Snowflake Time Travel | Greenplum 事务水位线 |
|---|---|---|---|
| 控制粒度 | 全局统一(跨节点一致) | Per-object(表/Schema/Account) | Per-page(局部计算) |
| 手动推进 | ✅ MAKE_AHM_NOW() |
❌ 无法手动加速 GC | ✅ 手动 VACUUM |
| 下限延迟 | 3 分钟(可配) | 最少 0 天(可配) | 取决于 VACUUM 频率 |
| 「卡住」风险 | ✅ 有 DOWN 节点或未刷新 projection 时卡住 | ❌ 不会卡住(窗口到期自动 GC) | ✅ 长事务阻止 horizon 推进 |
| 恢复联动 | AHM 直接决定增量恢复的范围 | 恢复不依赖 Time Travel 窗口 | 恢复依赖 WAL,与 horizon 弱相关 |
4. 设计对实际使用的影响 —— 以 Vertica 为主,兼顾其他 MPP¶
4.1 查询性能:删除标记是查询的「隐形税」¶
这是所有使用分离式标记方案(Vertica、Doris Delete-on-Read)的系统的共同代价: 每次查询扫描数据时都需要检查删除标记并过滤。而当已删除行占比超过 20-30% 时,这笔税的代价会变得明显——因为引擎需要处理大量删除标记,且有效数据密度降低。
对于 Merge-on-Write 方案(ClickHouse merge 后、Doris Merge-on-Write、Snowflake),查询不受已删除数据的影响,因为物理文件里根本没有被删的行。代价转移到了写入侧。
-- Vertica:识别已删除行占比 >20% 的投影
-- deleted_pct 比例天然正确(分子分母同被 buddy 膨胀)
SELECT sc.projection_name,
sum(sc.deleted_row_count) * 100 / sum(sc.total_row_count) AS deleted_pct
FROM v_monitor.storage_containers sc
JOIN (SELECT DISTINCT projection_id, is_segmented
FROM v_catalog.projections) p
ON sc.projection_id = p.projection_id
GROUP BY sc.projection_name
HAVING CASE WHEN bool_and(p.is_segmented)
THEN sum(sc.total_row_count) / count(DISTINCT sc.projection_name)
ELSE max(sc.total_row_count) END > 100000000
AND sum(sc.deleted_row_count) * 100 / sum(sc.total_row_count) > 20
ORDER BY 2 DESC;
4.2 不同 MPP 的实际选择:DELETE 还是 DROP PARTITION?¶
这是一个对所有 MPP 系统都成立的铁律:按时间清理历史数据时,DROP PARTITION 远远优于 DELETE。
| 维度 | DELETE(所有 MPP) | DROP PARTITION(所有 MPP) |
|---|---|---|
| 操作类型 | DML,涉及数据标记 | DDL / metadata 操作 |
| 执行时间 | 与数据量成正比 | 秒级(Vertica/Redshift/Snowflake);ClickHouse 也是秒级(删除 part 目录) |
| 空间回收 | 异步延迟回收 | 大多数系统立即回收(或极短延迟) |
| 可回滚 | ✅(MVCC 保证可见性) | ❌(数据立即不可见,多数系统不可回滚) |
| 产生删除标记 | ✅ 是(后续需要回收) | ❌ 不产生标记 |
| 阻塞其他操作 | 视系统而异(Vertica 持 X 锁;ClickHouse mutation 不阻塞读但可能排队) | 多数不阻塞(catalog/metadata 操作) |
由此衍生一条所有 MPP 通用的设计原则:表的分区键设计,第一考量应该是数据清理策略,其次才是查询性能。
4.3 Vertica 专属:Projection 设计对 DELETE 性能的影响¶
以下两条是 Vertica 特有的设计考量(其他 MPP 不适用),但对 Vertica 用户至关重要:
- DELETE 谓词列必须存在于所有 projection 中:如果某些 projection 缺少谓词列,Vertica 需要跨 projection 搜索被删除行的位置,性能断崖式下降。
- ORDER BY 末尾放高基数列可优化 Replay Delete:高基数排序键让 Replay Delete 的定位从 O(N²) 降为近似 O(log N)。
4.4 常见误解¶
「所有 MPP 的 DELETE 都不会释放空间,这是 bug」 → 这是故意设计的架构选择,不是 bug。但不同系统释放空间的速度和确定性差异很大:Snowflake 最可预测(固定 Time Travel 窗口),Vertica 最不可预测(依赖 AHM + Mergeout 多个条件同时满足),ClickHouse 中 Merge 后最彻底(merge 后数据物理消失)。
「PURGE / VACUUM / MERGE 是日常维护操作」 → 都不是。DROP PARTITION 才是。在 Vertica 中用 PURGE 替代分区设计、在 Greenplum 中用大量小 DELETE 然后 VACUUM,都是在用最贵的工具做本该便宜完成的事。
「COMMIT 后空间就会释放」 → 在所有列存 MPP 中,COMMIT 只让删除标记对查询可见,与空间释放完全没有直接关系。空间释放永远是一个异步的、不可精确预测时间的过程。
「Greenplum 的 VACUUM 和 Vertica 的 Tuple Mover 是一回事」 → 完全不同。VACUUM 是独立全表扫描,不干别的只回收空间。Tuple Mover 的 Mergeout 首要目标是合并文件以提高查询效率,顺带过滤已删除行。前者是专项清洁工,后者是整理货架的库管员,顺便把过期商品下架。
5. 案例验证¶
说明: 以下案例均为 Vertica,但这些案例揭示的问题模式——删除标记堆积导致系统不可用——是任何使用分离式删除标记方案的 MPP 系统都可能面临的。
5.1 虚构案例:小表每日 Trickle Delete 的慢性自杀¶
📝 虚构案例(Vertica)
某电商平台的用户行为表 user_events,每天新增约 2 亿行,同时每天用 DELETE 清理 30 天前的数据(每天约删 2 亿行)。表没有分区。
前 3 个月一切正常。 第 4 个月起查询响应时间从 2 秒涨到 8 秒;ROS 容器数从 ~50 涨到 ~400;Delete Vector 数量超过 3000。
根因: 部分 ROS 容器删除占比 18-19%,始终低于 PurgeMergeoutPercent=20% 阈值,永远不会被自动清除。Delete Vector 持续累积。
解决方案: 按 event_date 分区,用 DROP PARTITION 替代 DELETE。操作耗时从 4 小时降为 2 秒。
推测 — MPP 通用版: 基于共同的 immutable storage 设计原则,在 ClickHouse/Doris 中如果也每天用 DELETE 清理而非 DROP PARTITION,可能产生类似的 mutation/compaction 积压,只是表现形式不同(mutation 队列过长而非 Delete Vector 堆积)。
5.2 虚构案例:跨系统对比——同场景下不同 MPP 的行为¶
📝 虚构案例(跨系统模拟)
场景:一张 500 亿行的交易表,需要删除某产品线(约 40% 数据 = 200 亿行)的历史数据,删除条件与分区键不对齐,无法使用 DROP PARTITION。
| 系统 | 操作 | DELETE 阶段 | 空间回收阶段 |
|---|---|---|---|
| Vertica | DIRECT DELETE → MAKE_AHM_NOW() → PURGE_PARTITION |
快(只写 Delete Vector 到磁盘,不修改数据文件) | PURGE 重写高删除占比分区的 ROS 容器,I/O 集中在受影响分区;回收后空间立即释放 |
| Greenplum | DELETE → VACUUM FULL |
快(页内标记 dead tuple) | VACUUM FULL 全表重写——仅删 40% 数据也要重写 100% 的表,I/O 与表总大小成正比 |
| Redshift | DELETE → VACUUM |
快(Block 内 MVCC 标记) | VACUUM 扫描全表回收 dead row 空间并附带 RE-SORT,I/O 与表大小成正比 |
| ClickHouse | ALTER TABLE ... DELETE WHERE(异步 mutation) |
mutation 提交秒级,但可能排队等待已有 mutation | 后台 merge 逐步重写受影响 part,回收分步完成、不可手动干预 |
| Snowflake | DELETE FROM ... WHERE |
重写受影响 micro-partitions,写放大与删除行分布的 partition 数成正比 | 旧 micro-partitions 由 Time Travel 窗口到期后云存储 GC 回收,完全自动、不可干预 |
核心结论: 不管哪个系统,200 亿行的 DELETE 都不是小事。区别在于你把代价付在哪里——Vertica 是 PURGE 时的 I/O + 恢复时的 Replay Delete,Greenplum/Redshift 是 VACUUM 全表扫描,ClickHouse 是后台 merge 排队,Snowflake 是写入时重写 micro-partition + 存储窗口期成本。
5.3 真实案例:Delete Vector 堆积导致集群宕机¶
📋 真实案例 · 来源:某运营商省级 Vertica 地集市宕机故障处理报告
背景: 某运营商省级数据仓库,Vertica Enterprise Mode 集群。
故障: 一张约 2193 亿行的通话记录表,长期用 DELETE 清理数据,积累了海量 Delete Vector。节点因硬件故障宕机后,恢复过程需要 Replay Delete 数月的删除历史。在千亿级数据量下,传统算法陷入 O(N²) 复杂度陷阱,恢复持续 2 天无法完成。期间其他节点线程资源耗尽,集群完全宕机。
修复: 调整系统 cgroup 参数后恢复,根本性修复仍需将 DELETE 改为 DROP PARTITION。
跨系统启示: 这个案例的极端后果(集群宕机)与 Vertica 的 Replay Delete 机制直接挂钩——Greenplum 没有 Replay Delete 所以不会因同样原因宕机(但 VACUUM 可能堵塞查询),ClickHouse/Snowflake 没有这种风险(它们压根不用删除标记做恢复)。但删除标记堆积导致性能恶化这一点,是所有分离式标记方案(Vertica / Doris Delete-on-Read)共同面临的风险。
5.4 真实案例:AHM 推进阻塞导致全库性能问题¶
📋 真实案例 · 来源:某运营商 Vertica 数据库性能问题分析处理报告
故障: AHM 自动推进任务(内部任务名 "Manage Epochs: Advance AHM")持有 Global Catalog X 锁,阻塞所有需要访问 catalog 的操作。根因是节点故障恢复后 AHM 远远落后于 LGE,推进 epoch 时需要跨越大量 epoch 对 catalog 做全局排他操作,耗时过长。(原案例发生于 Vertica 7.2.3,当时日志记录为 advance_epoch function call。)
跨系统启示: 这个问题是 Vertica AHM 独占的——Snowflake/Greenplum 没有全局 epoch,ClickHouse 没有 catalog 级别的排他锁。但背后的问题是通用的:回收机制的「全局屏障」如果和系统一致性协议耦合过紧,运维风险会被放大。
6. 设计原则总结¶
以下前 6 条为跨系统通用原则,后 2 条为 Vertica 专属。
1(通用). 用分区 DROP 替代 DELETE 做批量清理¶
- 为什么:DELETE 在任何列存 MPP 中都产生删除标记,需要后续回收。DROP PARTITION 在几乎所有系统中都是秒级 metadata 操作,空间立即释放。
- 反例:第 5.3 节——2193 亿行表用 DELETE 日常清理 → 节点恢复 2 天无法完成 → 集群宕机。在 Greenplum 中同样的场景会导致 VACUUM 数小时无法完成;在 ClickHouse 中会导致 mutation 队列积压。
2(通用). 分区键的设计,第一考量是清理策略,其次才是查询性能¶
- 为什么:如果清理策略无法对齐分区键,就只能用 DELETE,随之而来的删除标记和回收开销是任何系统都无法避免的。
- 反例:表按月分区但需要按产品线删除 → 无法用 DROP PARTITION → 被迫使用 DELETE → 200 亿行级别的删除标记堆积。
3(通用). 空间回收是异步的——不存在「DELETE 后执行一条命令就立即释放」的捷径¶
- 为什么:所有列存 MPP 都将标记删除与物理清除解耦。COMMIT 只让标记对查询可见;实际的空间回收(Mergeout/VACUUM/Compaction)取决于系统后台调度,时间不可精确预测。
- 反例:「我昨天 DELETE 了,做了一次 COMMIT,空间没释放,是不是 bug?」——在每个 MPP 系统上都不是 bug。
4(通用). 删除标记的清理周期应当纳入运维监控¶
- 为什么:不管系统是否「全自动」,删除标记堆积的速度超过了后台回收的速度就会出问题。Vertica 看
delete_vectors,Greenplum 看pg_stat_user_tables.n_dead_tup,Doris 看 delete bitmap 占用率。 - 反例:Vertica 的
PurgeMergeoutPercent=20%下,占比 18% 的 ROS 容器永不回收;Greenplum 的autovacuum_vacuum_scale_factor设置过高导致 dead tuple 堆积到数十亿还未触发。
5(通用). 批量化你的 DELETE,避免逐行提交¶
- 为什么:单行 DELETE 每一条都生成独立的删除标记,导致标记碎片化,回收效率极低。
- 反例:一次删除 10000 行用了 10000 条单独 DELETE 逐行提交 → 产生 10000 次独立的标记操作。而用一条
DELETE FROM t WHERE key IN (v1, v2, ..., v10000)或DELETE FROM t USING source WHERE t.key = source.key将所有待删行合并到一个事务中,每个 ROS 容器仅产生一个 Delete Vector,后续回收效率远高于碎片化标记。
6(通用). 选择 MPP 系统时,把删除回收策略纳入评估¶
- 为什么:如果你有日常大规模 DELETE 的需求(无法全部用分区覆盖),你应该选择一个回收策略与你的工作负载匹配的系统:如果要可预测的回收时间 → Snowflake;如果要回收后查询无开销 → ClickHouse/Merge-on-Write;如果要手动精确控制 → Vertica 或 Greenplum。
- 反例:选了一个全自动回收的系统(如 ClickHouse/Snowflake),但又需要在精确的时间点立即回收空间 → 做不到,会失望。
7(Vertica). AHM 推进是空间回收的闸门,必须保证其畅通¶
- 为什么:AHM 不推进 → 所有已删除数据都无法清除 → Delete Vector 累积 → ROS Pushback → 恢复灾难。
- 反例:有 DOWN 节点或未刷新 projection 时 AHM 卡住数周,导致已删除数据永远无法自动回收。
8(Vertica). Projection 设计决定了 DELETE 和 Replay Delete 的性能边界¶
- 为什么:谓词列缺失 → 跨 projection 搜索 → DELETE 性能断崖式下降。排序键无高基数列 → Replay Delete O(N²) → 恢复慢到不可接受。
- 反例:仅按
status(3 个值)排序的表,每次 Mergeout 后 Replay Delete 需扫描几乎全部数据来匹配。
7. 延伸阅读¶
按推荐阅读顺序排列:
- MPP 列存引擎的架构设计哲学 — 理解列存的 immutable 存储单元是所有删除回收策略的「第一因」。建议先读这篇建立列存物理存储的心智模型。【MPP 通用】
- 论文 The Vertica Analytic Database — CStore 7 Years Later — 删除回收的理论源头【MPP 通用 — 理论源头,Vertica 为主要载体】:
- §3.5:为什么分区是批量删除的首选方案
- §3.7.1:Delete Vector 的原始设计决策与 immutable storage 的关联
- §4:Tuple Mover(Moveout / Mergeout / STRATA)的完整设计逻辑
- §5.1-5.2:AHM + 增量恢复——为什么删除回收与容错机制是耦合的
- Vertica 数据删除最佳实践 — 五种删除类型的全面对比 + 三种 purge 策略的决策树。当你面对一个具体的删除需求时,先看这篇文章选择正确的方式。【Vertica 专属操作指南,其中「替代 DELETE 的方法」通用】
- Vertica DELETE 相关问题 — DELETE 生命周期每一步的实操 FAQ,补充本文省略的运维细节。【Vertica 专属】
- Vertica 数据库 Replay Delete 算法 — 深入两种 Replay Delete 算法的原理和切换逻辑。这是 Vertica 独有的概念,但了解它对理解「独立标记文件方案的结构性代价」很有价值。【Vertica 专属 — 理解独立标记方案的代价】
- Tuple Mover 最佳实践完全指南 — 完整的 TM 操作手册,覆盖本文未展开的 STRATA 执行逻辑、资源池配置和监控查询。【Vertica 专属操作指南】
- Vertica Epoch 机制详解 — 五种 Epoch(CE/LE/CPE/LGE/AHM)的完整推演和故障排查。【Vertica 专属,但 Epoch 原理对理解其他系统的全局时钟设计有参考价值】
- 论文 C-Store: A Column-oriented DBMS — 原始 C-Store 论文 §6.1.1 和 §7,了解 Delete Vector(DRV)和 Low Water Mark(AHM 前身)最早的设计形态。【MPP 通用 — 理论起源】