跳转至

Vertica 冷热数据管理与成本优化

作者:JiangChong | 发布时间:2026-04-30

适用场景: 当你的 Vertica 集群存储使用率持续攀升、查询响应变慢、或者存储账单超出预期时——你很可能存在大量「冷数据」占据昂贵的主存储,却没有被有效识别和分层管理。

关联文章

理解全文脉络: 文章从「什么是冷热数据」的原理讲起(第 1 节),然后教你如何通过系统表全局监控存储状态(第 2 节),接着按排查优先级逐步定位冷表和冗余数据(第 3 节),再分 Enterprise 和 Eon 两种模式给出具体的归档与优化方案(第 4 节),最后通过多个案例和第 6 节的完整演练串联全部知识点。如果你只想快速查 SQL,直接跳到第 7 节的工具箱表格。


1. 原理理解

1.1 什么是冷热数据?

在 Vertica 的语境下,「冷热数据」不是指物理温度,而是指数据被访问的频率

  • 热数据(Hot Data):频繁被查询、更新或参与 ETL 加工的数据。通常是最近 1-7 天的数据或当前分区。
  • 温数据(Warm Data):偶尔被查询(如月末报表),但每日访问频率很低的数据。通常是最近 1-3 个月的数据。
  • 冷数据(Cold Data):极少或从不被查询,仅因合规或归档需求保留。通常是 3 个月以上的历史数据。

为什么冷数据是个问题?因为 Vertica 不会自动区分数据的新旧——冷数据仍然占据主存储空间、仍然在 Catalog 中保留元数据条目、仍然在 Tuple Mover mergeout 时参与 ROS 容器的合并读写(在合并完成前)。

这意味着:一方面,昂贵的 NVMe/SSD 空间被几乎不用的数据占用;另一方面,冷数据的大量 ROS 容器会推高 Catalog 大小,拖慢数据库启动、备份和系统表查询。如果是 Eon 模式,冷数据虽然存储成本低(S3 按量付费),但一旦被意外查询,会产生额外的 API 调用费用并驱逐 Depot 中的热数据缓存。

1.2 为什么 Vertica 会产生冷数据问题?

这与 Vertica 的设计哲学直接相关:Vertica 是为「写一次、读多次」的分析场景优化的,它不做传统数据库那样的行级热/冷自动迁移。数据加载进来之后,除非你主动管理,否则会永久保留在主存储中——占用空间、增加 Catalog 条目、并在 mergeout 时被间歇性地参与读写。

具体来说:

  1. ROS 容器无法自动跨存储层级迁移:Enterprise 模式下所有 ROS 文件都在本地存储上。Tuple Mover mergeout 会周期性地合并 ROS 容器——对于非分区冷表,冷数据和热数据的 ROS 容器被同等对待参与合并;对于分区表,非活跃分区的 ROS 容器在合并为单个 ROS 后就不再参与后续 mergeout。不论哪种情况,冷数据持续占用主存储空间。
  2. Eon 模式的 Depot 缓存 LRU 驱逐:Eon 模式下虽然数据在公共存储,但查询冷数据时会从 S3/HDFS 拉取,产生 API 调用费用和网络延迟。如果 Depot 空间不足,冷数据甚至会反复驱逐热数据。
  3. DELETE 不真正释放空间:如 Vertica 数据删除最佳实践 详述,DELETE 只是标记删除向量,不会立即回收磁盘。冷表中堆积的 delete vector 会让存储占用持续膨胀。
  4. Catalog 元数据膨胀:每个 ROS 容器都在 Catalog 中有元数据条目。大量冷数据的 ROS 容器会让 Catalog 膨胀,拖慢数据库启动、备份和系统表查询。

1.3 Enterprise 模式 vs Eon 模式的存储差异

维度 Enterprise 模式 Eon 模式
数据存储位置 全部在节点本地磁盘(RAID/NVMe) 公共存储(S3/MinIO/HDFS),本地仅 Depot 缓存
存储成本结构 本地磁盘 = 固定成本(硬件折旧) 公共存储按量付费 + API 调用费 + Depot 固定成本
冷数据影响 占用昂贵的 NVMe 空间,拖慢 mergeout 占用公共存储空间(低成本但持续计费),查询时产生 S3 GET 费用
弹性伸缩 扩容需 rebalance(数据重分布) 即扩即缩,存算分离
数据归档方式 分区 + DROP_PARTITIONS / 移到归档表 存储策略(Storage Policy)+ 多位置 + 旧数据迁移到低成本存储

核心差异:Enterprise 模式下冷数据是「物理空间」问题,Eon 模式下冷数据是「按需付费」问题。 这意味着两者的优化策略有本质区别。

1.4 冷数据的成本模型

以实际业务场景估算:

假设你有 100 TB 压缩数据,其中 60% 是 3 个月未访问的冷数据。

Enterprise 模式(国内数据中心托管):60 TB 冷数据占用 NVMe SSD。每 TB 全成本(含服务器 3 年摊销、机柜、电力、运维)约 ¥300-600/月[^1],冷数据年成本约 ¥21.6-43.2 万。若迁移到 HDD 归档存储(约 ¥50-150/TB/月),年成本可降至 ¥3.6-10.8 万,年节约 ¥10.8-32.4 万

Eon 模式(以 AWS 中国宁夏区域官方定价为参考):S3 Standard 阶梯价前 50 TB ¥0.1755/GB/月,60 TB 月费约 ¥10,494[^2]。若迁移到 S3 Glacier Instant Retrieval($0.004/GB/月 ≈ ¥0.029/GB/月,中国区价格可能略有不同),月费降至约 ¥1,740,年节约约 ¥10.5 万。但需注意:Glacier 取回费用约 $0.03/GB,全量取回一次额外产生 ~$1,800(¥13,000)。

核心结论:不管哪种模式,冷热分层的投入产出比都很高——花 1 天做冷数据清理,可能省下每年几万到几十万的存储成本。

[^1]: 2026 年企业级 NVMe SSD 受 AI 需求拉动价格暴涨(TrendForce 数据:Q2 合约价预计涨 48-53%)。此处取合理中位估算,实际成本因采购量、品牌、节点存储密度差异很大,仅供参考量级。

[^2]: AWS 中国宁夏区域 S3 Standard 阶梯价:前 50 TB ¥0.1755/GB/月,50-500 TB ¥0.1719/GB/月。60 TB = 50×1000×0.1755 + 10×1000×0.1719 = ¥10,494/月。数据来源:amazonaws.cn/s3/pricing。


2. 系统级监控(从宏观入手)

2.1 全局存储使用概览

做什么: 先从全局视角了解数据库的总存储使用量、各 schema 的存储分布,建立宏观认知。

-- 全局存储按 schema 汇总(基于 projection_storage)
SELECT
  anchor_table_schema AS schema_name,
  COUNT(DISTINCT anchor_table_name) AS table_count,
  SUM(row_count) AS total_rows,
  SUM(used_bytes) / (1024*1024*1024)::NUMERIC(18,2) AS total_size_gb
FROM v_monitor.projection_storage
WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
GROUP BY 1
ORDER BY total_size_gb DESC;

如何解读结果:

  • 如果某一个 schema 占比超过 50%:这个 schema 是你的主要成本来源,优先排查其内部的大表和冷表。
  • 关注 row_count 大但 total_size_gb 偏小的表:这些是可能被忽略的「隐形大户」。

2.2 表级存储排行(找 TOP 存储大户)

做什么: 找出占用存储最多的表,这些是冷热分析的优先目标。

-- TOP 20 存储大表
SELECT
  anchor_table_schema AS schema_name,
  anchor_table_name AS table_name,
  COUNT(DISTINCT projection_name) AS proj_count,
  SUM(row_count) AS total_rows,
  SUM(used_bytes) / (1024*1024*1024)::NUMERIC(18,2) AS total_size_gb,
  SUM(ros_count) AS total_ros_count
FROM v_monitor.projection_storage
WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
GROUP BY 1, 2
ORDER BY total_size_gb DESC
LIMIT 20;

如何解读结果:

  • proj_count 过多(每张表 > 4 个 projection):可能设计了过多冗余 projection 用于优化不同查询模式,但这些冗余 projection 各自占用完整数据副本,冷数据场景下是纯浪费。
  • total_ros_count 过高(单节点单投影 > 500):这个表可能没有合理分区,或者 Tuple Mover mergeout 跟不上数据加载速度。ROS 容器数过高将直接导致 Catalog 膨胀和查询性能下降。详见 ROS Pushback 故障排查
  • total_rows 很大但很久没有新数据加载:这是冷表的第一信号。

2.3 识别最近无查询活动的表(冷表检测核心)

做什么: 通过 projection_usage 系统表找出长时间未被查询的表。这是识别冷表最关键的一步。

projection_usage 表记录了每次查询访问了哪些 projection——如果一个表的所有 projection 在 projection_usage 中都没有记录,说明它从未被查询过(或记录已被清理)。

-- 找出在 projection_usage 中超过 N 天没有查询记录的表
WITH recently_used_tables AS (
  SELECT DISTINCT anchor_table_schema, anchor_table_name
  FROM v_monitor.projection_usage
  WHERE query_start_timestamp > CURRENT_TIMESTAMP - INTERVAL '30 days'
),
all_user_tables AS (
  SELECT DISTINCT
    table_schema,
    table_name
  FROM v_catalog.tables
  WHERE table_schema NOT IN ('v_catalog','v_monitor','v_internal','v_func','v_internal_tables','v_secret_managers','v_txtindex')
    AND is_system_table = false
    AND is_temp_table = false
)
SELECT
  a.table_schema,
  a.table_name,
  CASE WHEN r.anchor_table_schema IS NOT NULL THEN '热/温' ELSE '冷' END AS hot_or_cold
FROM all_user_tables a
LEFT JOIN recently_used_tables r
  ON a.table_schema = r.anchor_table_schema
 AND a.table_name = r.anchor_table_name
ORDER BY hot_or_cold, a.table_schema, a.table_name;

如何解读结果:

  • 30 天是初筛阈值——目的是快速缩小排查范围,不代表 30 天无查询就一定是冷数据。很多场景(季度报表、年度审计、按需回溯)的数据访问周期超过 30 天。
  • 建议分两级判断:30 天无查询 → 候选观察,结合业务确认后再归档;90 天无查询 → 基本确认冷数据,可以放心执行分层/归档操作。
  • 也可以用表中数据的「最近日期」辅助判断——例如表名含 2020 或分区键最大值为 3 年前,即使 projection_usage 有零星记录,也大概率是冷数据。
  • 注意限制projection_usage 表有数据保留策略(通常保留 30-90 天),且它是按 projection 记录的——如果表使用的是默认 projection 名称,关联是准确的。
  • 如果 projection_usage 表为空或数据很少:说明可能未启用 usage 收集,或者数据已被清理。这时需要结合业务知识判断——哪些表是已知的历史归档表。

2.4 存储趋势监控(判断冷数据是否在膨胀)

做什么: 对比当前存储和历史记录,判断存储增长率是否异常。

-- 当前全局存储总量
SELECT
  SUM(used_bytes) / (1024*1024*1024)::NUMERIC(18,2) AS total_compressed_gb,
  SUM(row_count) AS total_rows,
  COUNT(DISTINCT anchor_table_name) AS total_tables
FROM v_monitor.projection_storage
WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal');

如何解读结果:

  • total_compressed_gb 与 License 审计数据(v_catalog.license_audits)中的 database_size_bytes 对比,确认一致性。
  • 如果 total_rows 增长但查询频率反而下降:说明新数据加载在持续,但大部分是「加载后没人看」的数据——这是冷数据膨胀的典型信号。
  • 通过定期运行此 SQL(如每周)建立趋势基线,做到存储规划可预测

2.5 Linux 层面补充排查:磁盘使用分布

做什么: 了解各节点的物理磁盘使用情况,辅助判断是否有节点磁盘不均衡。

-- 节点级磁盘使用(含 Catalog 和 Data 路径)
SELECT
  node_name,
  storage_path,
  storage_usage,
  disk_space_used_mb,
  disk_space_free_mb,
  disk_space_free_percent
FROM v_monitor.disk_storage
ORDER BY node_name, storage_usage;

如何解读结果:

  • CATALOG 路径的 disk_space_free_percent 低于 20%:Catalog 膨胀风险。Catalog 大小过大会拖慢数据库启动和系统表查询。排查:是否有过多 ROS 容器、过多小表或过多 projection。
  • DATA 路径的 disk_space_free_percent 低于 10%:急迫需要释放空间。优先使用第 3 节的冷表定位方法找出可清理的数据。
  • 各节点磁盘使用不均衡:可能是投影 Segmentation 倾斜或节点间数据分布不均。详见 Vertica Join 重分段倾斜诊断与修复
# Linux 层面确认每个节点的磁盘使用
df -h / 2>/dev/null || df -h /data

3. 逐步定位根因(从宏观到微观)

3.1 第一步:找出「零查询」大表(冷数据第一优先级)

做什么: 结合存储大小和查询记录,找出占用大量空间但从未被访问的表。这些是成本优化的「低垂果实」

-- 冷表存储占用排行:在 projection_usage 中无记录的大表
WITH table_sizes AS (
  SELECT
    anchor_table_schema,
    anchor_table_name,
    SUM(used_bytes) / (1024*1024)::NUMERIC(18,2) AS size_mb,
    SUM(row_count) AS row_count
  FROM v_monitor.projection_storage
  WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
  GROUP BY 1, 2
),
queried_tables AS (
  SELECT DISTINCT anchor_table_schema, anchor_table_name
  FROM v_monitor.projection_usage
  WHERE query_start_timestamp > CURRENT_TIMESTAMP - INTERVAL '90 days'
)
SELECT
  t.anchor_table_schema,
  t.anchor_table_name,
  t.size_mb,
  t.row_count,
  CASE WHEN q.anchor_table_schema IS NOT NULL THEN '90天内有查询' ELSE '90天内无查询' END AS access_status
FROM table_sizes t
LEFT JOIN queried_tables q
  ON t.anchor_table_schema = q.anchor_table_schema
 AND t.anchor_table_name = q.anchor_table_name
WHERE size_mb > 100
ORDER BY access_status, size_mb DESC
LIMIT 50;

如何解读:

  • access_status = '90天内无查询'size_mb 大的表:典型冷数据,优先处理。
  • 对比表名中的日期/业务含义:如果表名叫 *_2022**_bak_*,很可能就是已废弃的归档数据。
  • 阈值建议size_mb > 100 是保守的演示值。在实际生产环境中,可按集群规模调整——小集群(<10 TB)设 1-5 GB,中型集群(10-100 TB)设 10-20 GB,大型集群(>100 TB)设 50 GB 以上。低于阈值的表即使确认是冷数据,节省的空间也可能不抵排查和操作的工作量。

3.2 第二步:检查 DELETE 堆积(隐藏的存储杀手)

做什么: DELETE 在 Vertica 中不会立即释放空间,堆积的 delete vector 是存储膨胀的常见原因。

-- 检查有大量已删除行的 projection
SELECT
  schema_name,
  projection_name,
  SUM(total_row_count) AS total_rows,
  SUM(deleted_row_count) AS deleted_rows,
  SUM(delete_vector_count) AS dv_count,
  (SUM(deleted_row_count)::FLOAT / NULLIF(SUM(total_row_count), 0) * 100)::NUMERIC(5,1) AS deleted_pct
FROM v_monitor.storage_containers
WHERE node_name = (SELECT local_node_name())
GROUP BY 1, 2
HAVING SUM(deleted_row_count) > 0
ORDER BY deleted_pct DESC
LIMIT 20;

如何解读:

  • deleted_pct > 20%:超过了 PurgeMergeoutPercent 默认阈值(20%),Tuple Mover 应该已在自动清理。如果仍然存在,说明 AHM 可能未推进,或者该分区是活跃分区(活跃分区的 delete vector 不会被自动清理)。
  • deleted_pct > 50%:严重浪费。该表可能进行了大规模 DELETE 但未执行 PURGE,或者 AHM 被 HistoryRetentionTime 长期保留。
  • dv_count 很大但 deleted_pct 很小:可能有大量小 delete vector 散落——使用 SELECT DO_TM_TASK('dvmergeout'); 合并它们。

怎么做: 如果在第二步发现了问题,参考第 4 节的解决方案。详细机制见 Vertica 数据删除最佳实践

3.3 第三步:检查分区策略是否合理

做什么: 分区是数据生命周期管理的基础。不合理分区会导致无法按时间清理数据。

-- 检查存在 ROS 容器数过多的分区表
SELECT
  s.projection_name,
  p.table_schema,
  COUNT(DISTINCT s.storage_oid) AS ros_count,
  COUNT(DISTINCT p.partition_key) AS partition_count
FROM v_monitor.storage_containers s
JOIN v_monitor.partitions p ON s.storage_oid = p.ros_id
GROUP BY 1, 2
ORDER BY ros_count DESC
LIMIT 20;

如何解读:

  • 单个 projection 的 ros_count > 1000:已经有 ROS pushback 风险(上限 1024)。应立即优化分区策略或执行 mergeout。
  • partition_count 很大(如按天分区 365 天)但 ros_count 也很大:说明该表的 Tuple Mover 可能跟不上数据加载节奏。考虑改为按周或按月分区。
  • partition_count = 0ros_count 很大:这张表没有分区。对于大表来说没有分区意味着无法 DROP_PARTITIONS 快速删除数据——只能全表 DELETE(产生 delete vector)或 TRUNCATE。

3.4 第四步:检查 Eon 模式的 Depot 效率

做什么: 只适用于 Eon 模式。检查 Depot 缓存命中率和驱逐频率,判断冷数据是否在频繁驱逐热数据。

-- 注意:以下系统表仅在 Eon 模式可用,Enterprise 模式可能返回空
-- 检查 Depot 驱逐事件
SELECT
  node_name,
  COUNT(*) AS eviction_count,
  SUM(file_size_bytes) / (1024*1024)::NUMERIC(18,2) AS evicted_mb
FROM dc_depot_evictions
WHERE "time" > CURRENT_TIMESTAMP - INTERVAL '24 hours'
GROUP BY 1
ORDER BY evicted_mb DESC;

如何解读:

  • 驱逐量很大(每节点 > 10 GB/天):Depot 空间不足,冷数据查询在频繁驱逐热数据。应考虑增大 Depot 空间或使用 Storage Policy 将冷数据迁移到独立的低成本存储桶。
  • 如果某节点驱逐明显高于其他节点:可能是数据分布不均,部分节点承担了更多分片。

4. 解决方案(从快速见效到根本治理)

4.1 Enterprise 模式:立即措施(当天可执行)

4.1a 清理 DELETE 堆积(AHM 推进 + PURGE)

适用场景: 第 3.2 步发现 deleted_pct 高。

-- 步骤 1:推进 AHM,使旧删除标记可被永久清理
SELECT MAKE_AHM_NOW();

-- 步骤 2:清理指定表的已删除数据
SELECT PURGE('schema_name.table_name');

为什么这样做: MAKE_AHM_NOW() 将 AHM 推进到当前 epoch,使所有在此之前的 delete vector 可以被 Tuple Mover 清理。然后 PURGE() 强制重写 ROS 容器,去除已删除行。

⚠️ PURGE() 会重写整表的所有 ROS 容器,对 I/O 有较大影响。如果只想清理特定分区,使用 SELECT PURGE_PARTITION('schema.table', partition_key)

4.1b 删除无用分区(快速释放大量空间)

适用场景: 确定某分区的数据不再需要(如 3 年前的数据)。

-- 直接删除单个分区(catalog 操作,不阻塞查询)
SELECT DROP_PARTITIONS('schema.table', '202301', '202301');

-- 批量删除:一次调用删除 2023 年 1 月到 12 月的所有分区
SELECT DROP_PARTITIONS('schema.table', '202301', '202312');

为什么这样做: DROP_PARTITIONS 是纯 catalog 操作——它只修改元数据而不移动数据。数据文件由 Tuple Mover 在后台异步删除。这是 Vertica 中最快、最干净的数据清理方式。相比旧函数 DROP_PARTITION(Vertica 9.0 已废弃),DROP_PARTITIONS 支持一次性删除一个范围的分区,无需逐个调用。

4.1c 清空 staging/临时表

TRUNCATE TABLE temp_staging_table;

TRUNCATEDELETE 快且干净——它不产生 delete vector,直接移除存储并回收空间。

4.2 Enterprise 模式:短期优化(当周执行)

4.2a 按时间分区(为长期管理打基础)

如果一张冷表还没有分区,首先给它分区:

-- 注意:此操作需要业务低峰期执行,会触发后台数据重组
ALTER TABLE schema_name.table_name
  PARTITION BY EXTRACT(year FROM date_column)
  REORGANIZE;

为什么按年分区而不是按天: 对于冷数据的归档场景,按月或按年分区更合理——你不需要保留每天的数据颗粒度,但需要保留「2022 年的所有数据」或「2023 年 Q1」。分区数越少,Catalog 压力越小。

选择分区粒度的原则:

数据保留策略 推荐分区粒度 理由
保留 3-12 个月 按月 每年 12 个分区,数量可控
保留 1-3 年 按月或按季度 36-12 个分区
保留 3 年以上 按年 分区数少,ROS 容器少

4.2b 冷数据迁移到归档表

适用场景: 必须长期保留历史数据,但不需要在主表上查询。

方案:创建归档表 → 将冷分区移动到归档表 → 归档表放在低成本存储上
-- 步骤 1:创建结构相同的归档表
CREATE TABLE archive.table_archive
  LIKE schema.table_name INCLUDING PROJECTIONS;

-- 步骤 2:将旧分区从主表移动到归档表
SELECT MOVE_PARTITIONS_TO_TABLE(
  'schema.table_name',           -- 源表
  min_partition_key,             -- 起始分区键
  max_partition_key,             -- 结束分区键
  'archive.table_archive'        -- 目标表
);

为什么用 MOVE_PARTITIONS_TO_TABLE 而不是 INSERT ... SELECT 前者是 catalog 级别的操作(只修改元数据),不实际移动数据。后者会复制数据产生双倍 I/O。

4.2c 检查 HistoryRetentionTime(值过高会阻塞 DELETE 清理)

HistoryRetentionTime 决定已删除数据在多久之内可通过 AT EPOCH 回滚查看。默认值 -1 表示不保留已删除数据——默认配置下,Tuple Mover 可以随时清理 delete vector(只要DELETE数据的EPOCH小于AHM)。

但有些场景下该参数被设为较大的正值(如 30 天 = 2,592,000 秒),用于提供回滚窗口。问题在于:过高的 HistoryRetentionTime 会阻止 AHM 推进,导致 delete vector 大量堆积——即使 Tuple Mover 想清理也清理不了。

-- 查看当前值(如返回空,说明使用默认值 -1)
SELECT current_value FROM user_configuration_parameters
WHERE parameter_name = 'HistoryRetentionTime';
-- 或者使用
SELECT GET_CONFIG_PARAMETER('HistoryRetentionTime');

-- 如果值过大且不需要长回滚窗口,缩短它
SELECT SET_CONFIG_PARAMETER('HistoryRetentionTime', '172800');  -- 2 天

建议: 如果业务没有「回滚到 N 天前的数据快照」的需求,保持默认 -1 即可。如果需要回滚,设置 1-2 天(86400-172800 秒)通常足够,30 天以上的值会显著阻塞 delete vector 清理,造成虚假的存储膨胀。

4.3 Eon 模式:存储策略驱动的冷数据分层

Eon 模式最大的优势是存储策略(Storage Policy)——你可以定义多级公共存储位置,将不同热度的数据自动放到不同成本的存储桶上。

4.3a 创建第二个公共存储位置(低成本层)

-- 注意:location 配置中的 S3 凭证需要通过 ALTER DATABASE SET S3BucketCredentials 设置
-- 创建标签为 'cold_storage' 的 S3 存储桶位置(仅用于数据,不用于 temp)
CREATE LOCATION 's3://cold-data-bucket'
  COMMUNAL USAGE 'DATA'
  LABEL 'cold_storage';

为什么需要独立的 cold storage 位置? S3 Standard 与 Glacier 的存储成本差异可达 5-6 倍(参见第 1.4 节成本模型)。将冷数据从标准存储迁移到 Glacier,60 TB 月存储费从 ~¥10,500 降至 ~¥1,740。即使算上中国区定价差异,分层归档的投入产出比依然很高。

4.3b 为冷表设置存储策略

-- 将指定表的所有新写入数据定向到 cold_storage 位置
SELECT SET_OBJECT_STORAGE_POLICY(
  'schema.table_name',    -- 目标表
  'cold_storage',         -- 使用上一步创建的 location label
  true                    -- enforce: 强制执行
);

enforce = true 的含义: 该表的所有新数据(包括 Tuple Mover mergeout 产生的文件)都写入 cold_storage 位置,即使该表有数据在之前的默认位置。

4.3c 检查存储策略状态

-- 查看所有存储策略
SELECT * FROM storage_policies;

-- 查看每个 ROS 容器当前所在的存储位置
SELECT schema_name, projection_name, storage_oid,
       total_row_count, used_bytes, location_label
FROM v_monitor.storage_containers
WHERE location_label != ''
ORDER BY used_bytes DESC
LIMIT 20;

4.3d 弹性缩减计算资源(Eon 独有的成本优势)

Eon 模式支持在不丢数据的情况下缩减子集群节点,实现真正意义上的计算资源按需伸缩。当确认某批数据为冷数据后,如果不需要查询这些数据,你可以:

  1. 将冷数据放在低速存储桶(Storage Policy)
  2. 缩减计算节点数量(只需保留最小节点数以维持 K-safety)
  3. 当需要查询冷数据时,临时扩容子集群

这个过程不需要数据 rebalance——因为数据在公共存储中,节点只是临时加载 Depot 缓存。

4.4 两模式通用:投影设计与数据效率优化

4.4a 删除冗余投影

每多一个投影就多一份完整数据副本。检查是否有不必要的冗余投影:

-- 找出有冗余投影的表(超过 K-safety 所需数量的投影)
SELECT
  anchor_table_schema,
  anchor_table_name,
  COUNT(DISTINCT projection_name) AS projection_count,
  SUM(used_bytes) / (1024*1024)::NUMERIC(18,2) AS total_mb
FROM v_monitor.projection_storage
WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
GROUP BY 1, 2
HAVING COUNT(DISTINCT projection_name) > 4
ORDER BY total_mb DESC;

为什么关注: Enterprise 模式下 K-safety=1 的集群只需 2 个 buddy projection。额外的投影(如为不同排序建的优化投影)虽然在查询时有加速作用,但冷数据根本不会被查询——这些额外的投影是纯粹的存储浪费。

4.4b 优化列编码(减少冷数据占用)

列编码选择直接影响压缩比。对于几乎不会被查询的冷数据列,优先使用压缩率最高的编码:

-- 查看某表的当前编码
SELECT
  projection_column_name,
  encoding_type,
  data_type
FROM v_catalog.projection_columns
WHERE projection_name LIKE '%your_table%_b0'
ORDER BY ordinal_position;

编码选择建议:

  • 冷数据中低基数列(如 statusflag):使用 RLE(Run Length Encoding),压缩率极高。
  • 冷数据中的时间列:使用 GCDDELTA(适合单调递增的 timestamp/int 序列)。
  • 高基数列如 UUID/字符串:使用 GLOBAL_DICTBLOCK_DICT

5. 深入案例

说明:以下案例包含虚构案例(标注 📝)和真实案例(标注 📋)。虚构案例用于覆盖真实案例中不存在的场景,但其技术背景(系统表查询、参数配置、诊断逻辑)均来源于 vault 中的已有知识。


5.1 📝 虚构案例 1:Enterprise 模式——运营商话单表存储暴涨

场景描述: 某省级运营商的经分系统,Vertica Enterprise 模式、12 节点集群,存储 3.5 PB 压缩数据。近 3 个月存储增长率从正常的 15 TB/月飙升到 40 TB/月,但每日 ETL 加载量并未增加。

诊断过程:

-- 步骤 1:检查全局存储增长(结合历史巡检记录对比)
SELECT anchor_table_schema, anchor_table_name,
  SUM(used_bytes)/(1024^3)::NUMERIC(18,2) AS gb
FROM v_monitor.projection_storage
WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
GROUP BY 1,2 ORDER BY gb DESC LIMIT 10;

输出显示 cdr_detail 表占 1.8 PB,比上季度增加了 600 TB。

-- 步骤 2:检查该表的 DELETE 堆积
SELECT schema_name, projection_name,
  SUM(total_row_count) AS rows,
  SUM(deleted_row_count) AS del_rows,
  (SUM(deleted_row_count)::FLOAT / NULLIF(SUM(total_row_count),0) * 100)::NUMERIC(5,1) AS del_pct
FROM v_monitor.storage_containers
WHERE schema_name = 'cdr' AND projection_name LIKE 'cdr_detail%'
  AND node_name = (SELECT local_node_name())
GROUP BY 1,2
ORDER BY del_pct DESC;

输出:cdr_detail_b0del_pct = 57.3%,近 6 成数据是 delete vector。

根因分析:

业务部门每天运行一个「错误数据修复」程序——先 DELETE 错误日期的全量数据,然后重新加载修正后的数据。DELETE 产生了大量 delete vector,但 HistoryRetentionTime 被设置为 30 天,导致 AHM 长期滞留在 30 天前,Tuple Mover 无法清理这些 delete vector。日积月累,delete vector 反而占了存储大头。

修复方案:

-- 1. 缩短已删除数据保留时间到 2 天
SELECT SET_CONFIG_PARAMETER('HistoryRetentionTime', '172800');

-- 2. 推进 AHM 并强制清理
SELECT MAKE_AHM_NOW();
SELECT PURGE('cdr.cdr_detail');

效果对比:

  • 修复前:cdr_detail 表占用 1.8 PB,查询平均响应 8.3 秒
  • 修复后:cdr_detail 表占用 0.8 PB(释放 1 PB),查询平均响应 3.1 秒
  • 释放的 1 PB 空间足够供未来 2 年的正常数据增长

复盘: 根本问题是「DELETE + 重新加载」的 ETL 模式与长 HistoryRetentionTime 的组合。更好的做法是:将错误数据所在的分区 DROP_PARTITIONS,然后重新加载该分区,根本不产生 delete vector。


5.2 📝 虚构案例 2:Eon 模式——金融客户报表系统的 S3 账单失控

场景描述:

某基金公司使用 Vertica Eon 模式(6 节点,AWS S3 公共存储),存储约 50 TB 数据。月度 AWS 账单显示 S3 API 调用费用异常——从正常的 $300/月飙升到 $2,100/月,但数据量没有明显增加。

诊断过程:

-- 步骤 1:检查 Depot 缓存命中率(Eon 特有)
SELECT node_name,
  COUNT(*) AS fetch_count,
  SUM(file_size_bytes)/(1024^3)::NUMERIC(18,2) AS fetched_gb
FROM dc_depot_fetches
WHERE "time" > CURRENT_TIMESTAMP - INTERVAL '7 days'
GROUP BY 1
ORDER BY fetched_gb DESC;

输出显示每个节点每周从 S3 拉取了 ~1.2 TB 数据(正常应 < 200 GB)。

-- 步骤 2:检查哪些表频繁触发 S3 读取
SELECT
  anchor_table_schema,
  anchor_table_name,
  COUNT(*) AS access_count
FROM v_monitor.projection_usage
WHERE query_start_timestamp > CURRENT_TIMESTAMP - INTERVAL '7 days'
GROUP BY 1, 2
ORDER BY access_count DESC
LIMIT 10;

输出显示 hist_market_data_2021 被访问了 15,000+ 次/周。这张表是 2021 年的历史行情数据,应该是冷数据。

-- 步骤 3:检查 Depot 存储容量
SELECT node_name,
  SUM(used_bytes) / (1024^3)::NUMERIC(18,2) AS depot_used_gb
FROM v_monitor.depot_sizes
GROUP BY 1;

输出显示每个节点的 Depot 仅 200 GB(6 节点 × 200 GB = 1.2 TB 总缓存),但活跃数据集约 15 TB。Depot 严重不足,导致热数据和冷数据互相驱逐。

根因分析:

有一个自动化报表程序每天凌晨遍历全部 5 年的历史数据做月度趋势分析。这张 2021 年的 hist_market_data 表(8 TB)每次查询都需要从 S3 拉取全量数据填充 Depot,而 Depot 总容量只有 1.2 TB,导致频繁的缓存驱逐和重新拉取。每 GB S3 GET 费用约 $0.0004,每月额外产生了约 $1,800 的 API 费用。

修复方案:

  1. 增大 Depot(每节点从 200 GB 扩展到 1 TB):
-- 修改 depot 大小
SELECT ALTER_LOCATION_SIZE('/depot/path', '', '1T');
  1. 将冷数据迁移到低成本 S3 存储桶
CREATE LOCATION 's3://cold-historical-data'
  COMMUNAL USAGE 'DATA' LABEL 'cold_tier';
SELECT SET_OBJECT_STORAGE_POLICY('market.hist_market_data_2021', 'cold_tier', true);
  1. 优化报表 SQL——增加日期过滤条件,只查最近 30 天 + 同比数据,而非全 5 年扫描。

效果对比:

  • 修复前:S3 API 费用 $2,100/月,Depot 驱逐 1.2 TB/天/节点
  • 修复后:S3 API 费用 $280/月,Depot 驱逐 80 GB/天/节点
  • 查询性能提升 60%(热数据命中 Depot 缓存)

5.3 📋 真实案例:Eon 模式——公共存储死文件不清理

场景描述:

某省级运营商的 Eon 模式数据库(Vertica 10.1.1-7),公共存储使用 HDFS(3 副本)。从 Vertica 系统表 projection_storage 统计总量为 160 TB(单副本),但从 HDFS 统计为 227 TB(单副本),两者差约 67 TB——即有 67 TB 的死文件泄漏在 HDFS 中

根因:

Eon 模式的 Reaper 进程负责异步清理已删除的公共存储文件。当数据库异常终止或 Reaper 队列堆积时,文件不会被清理。

修复方案:

-- 步骤 1:调整清理参数,加快清理速度
SELECT SET_CONFIG_PARAMETER('TombstoneProcessingBatchSize', 1);  /* default 1000 */
SELECT SET_CONFIG_PARAMETER('S3DeleteBatchSize', 0);            /* default 1000 */

-- 步骤 2:执行清理
SELECT MAKE_AHM_NOW();
SELECT SYNC_CATALOG();
SELECT CLEAN_COMMUNAL_STORAGE(true);
SELECT FLUSH_REAPER_QUEUE();

效果: 执行后 1-2 分钟 HDFS 空间释放 67 TB。

教训:

  1. Eon 模式数据库应定期执行 CLEAN_COMMUNAL_STORAGE 检查死文件
  2. 数据库必须正常关闭(admintools -t stop_db),避免异常 kill 导致 Reaper 队列未处理
  3. 监控 HDFS/S3 实际使用量和 Vertica 系统表统计量的差异,差异过大即存在死文件泄漏

5.4 📝 虚构案例 3:混合场景——宽表冷数据 + 冗余投影的双重浪费

场景描述:

某电商平台的用户画像表(user_profile),500 列,3 TB 压缩数据,创建了 6 个投影(4 个为不同分析场景优化的排序投影)。业务变更后该表已 6 个月无查询,但数据仍保留在主存储中。

诊断过程:

-- 确认零查询
SELECT DISTINCT anchor_table_schema, anchor_table_name
FROM v_monitor.projection_usage
WHERE anchor_table_name = 'user_profile'
  AND query_start_timestamp > CURRENT_TIMESTAMP - INTERVAL '180 days';
-- 返回 0 行:确认 180 天内零查询

-- 确认冗余投影数量
SELECT anchor_table_name, projection_name,
  used_bytes / (1024^3)::NUMERIC(18,2) AS size_gb
FROM v_monitor.projection_storage
WHERE anchor_table_name = 'user_profile';

输出 6 个投影,每个约 3 TB——即总占用 18 TB(6 个投影 × 3 TB 数据副本)。

修复方案:

-- 1. 删除 4 个冗余投影(只保留 K-safety 必须的 2 个 buddy 投影)
DROP PROJECTION user_profile_opt_sort1;
DROP PROJECTION user_profile_opt_sort2;
DROP PROJECTION user_profile_opt_sort1_b1;
DROP PROJECTION user_profile_opt_sort2_b1;

-- 2. 将表移动到归档 schema 或直接删除(如确定不再需要)
-- 如果合规要求保留,创建归档表
CREATE TABLE archive.user_profile_archive LIKE marketing.user_profile INCLUDING PROJECTIONS;
SELECT MOVE_PARTITIONS_TO_TABLE('marketing.user_profile',
  min_partition_key, max_partition_key, 'archive.user_profile_archive');

效果对比:

  • 修复前:6 个投影 × 3 TB = 18 TB
  • 修复后:2 个投影 × 3 TB = 6 TB(归档后)
  • 释放 12 TB 主存储,同时 Catalog 条目数减少 60%

6. 完整诊断流程实战(虚构场景,充当总复习)

📝 虚构场景 · 完整演练

场景: 你是某金融机构的 DBA,收到告警——某 Vertica Enterprise 集群的 DATA 磁盘使用率达到 92%。集群 8 节点,存储约 400 TB 压缩数据,每天新增约 1 TB 数据。

时间线:

时间 动作 工具/SQL
09:00 收到磁盘告警 监控系统
09:15 宏观排查:找出存储大户 SQL 1
09:20 发现 trade_detail 表吃掉了 210 TB SQL 1
09:30 检查 DELETE 堆积 SQL 2
09:35 trade_detail_b0deleted_pct = 38% SQL 2
09:45 检查冷表:是否有表长期未被查询 SQL 3
09:50 发现 trade_detailtrade_date < 2020 分区 180 天无查询 SQL 3
10:00 制定方案:清理 DELETE + 归档冷分区
10:30 执行修复(在维护窗口) SQL 4-5
12:00 效果验证 SQL 6

SQL 1:找出存储大户(09:15)

SELECT anchor_table_schema, anchor_table_name,
  SUM(used_bytes)/(1024^4)::NUMERIC(18,2) AS size_tb,
  SUM(row_count) AS rows
FROM v_monitor.projection_storage
WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
GROUP BY 1,2 ORDER BY size_tb DESC LIMIT 10;

输出:

 anchor_table_schema | anchor_table_name | size_tb | rows
---------------------+-------------------+---------+----------
 trade               | trade_detail      | 210.50  | 58.3B
 trade               | trade_summary     | 95.20   | 12.1B
 risk                | risk_metrics      | 45.80   | 8.5B

trade_detail 一张表占了集群 52% 的存储。

SQL 2:检查 DELETE 堆积(09:30)

SELECT schema_name, projection_name,
  SUM(total_row_count) AS rows,
  SUM(deleted_row_count) AS del_rows,
  SUM(delete_vector_count) AS dv_count,
  (SUM(deleted_row_count)::FLOAT / NULLIF(SUM(total_row_count),0) * 100)::NUMERIC(5,1) AS del_pct
FROM v_monitor.storage_containers
WHERE schema_name = 'trade' AND projection_name LIKE 'trade_detail%'
  AND node_name = (SELECT local_node_name())
GROUP BY 1,2 ORDER BY del_pct DESC;

输出:

 schema_name | projection_name      | rows      | del_rows  | dv_count | del_pct
-------------+----------------------+-----------+-----------+----------+--------
 trade       | trade_detail_b0      | 6.2B      | 2.4B      | 350      | 38.2
 trade       | trade_detail_b1      | 6.2B      | 2.3B      | 348      | 37.8

38% 的行是已删除的——这意味着 210 TB × 38% ≈ 80 TB 是「幽灵数据」。

SQL 3:检查冷数据分区(09:45)

WITH partitioned_storage AS (
  SELECT p.table_schema, p.projection_name, p.partition_key,
    SUM(p.ros_row_count) AS row_count,
    SUM(p.ros_size_bytes) / (1024^4)::NUMERIC(18,2) AS size_tb,
    MAX(s.start_epoch) AS latest_epoch
  FROM v_monitor.partitions p
  JOIN v_monitor.storage_containers s
    ON p.ros_id = s.storage_oid
    AND s.node_name = (SELECT local_node_name())
  WHERE p.table_schema = 'trade'
  GROUP BY 1,2,3
)
SELECT * FROM partitioned_storage
ORDER BY partition_key
LIMIT 30;

输出显示前 120 个分区(对应 10 年前数据)每个约 0.5 TB,共计 60 TB。这些分区在 SQL 1 中已经确认是冷数据。

SQL 4:修复——清理 DELETE + 归档冷数据(10:30)

-- 第 1 步:缩短历史保留时间
SELECT SET_CONFIG_PARAMETER('HistoryRetentionTime', '86400');  -- 1 天

-- 第 2 步:推进 AHM
SELECT MAKE_AHM_NOW();

-- 第 3 步:清理 trade_detail 表的已删除数据
SELECT PURGE('trade.trade_detail');

-- 第 4 步:将 2017 年及以前的冷分区移到归档表
CREATE TABLE archive.trade_detail_archive
  LIKE trade.trade_detail INCLUDING PROJECTIONS;

-- 假设分区键为年份:2011 到 2017
SELECT MOVE_PARTITIONS_TO_TABLE(
  'trade.trade_detail', 2011, 2017, 'archive.trade_detail_archive');

SQL 5:效果验证(12:00)

-- 重新检查存储
SELECT anchor_table_schema, anchor_table_name,
  SUM(used_bytes)/(1024^3)::NUMERIC(18,2) AS gb
FROM v_monitor.projection_storage
WHERE anchor_table_name = 'trade_detail'
  AND anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal')
GROUP BY 1,2;

效果:

  • trade_detail 表从 210 TB → 122 TB(PURGE 释放 80 TB DELETE 堆积 + 归档 60 TB 冷分区)
  • 集群磁盘使用率从 92% → 67%
  • 紧急告警解除。后续计划:将 archive.trade_detail_archive 对应的存储路径指向 HDD 盘组。

7. 快速诊断 SQL 工具箱

诊断目标 SQL 模式
全局存储按 schema 分布 SELECT anchor_table_schema, SUM(used_bytes)/(1024^3)::NUMERIC(18,2) AS gb FROM v_monitor.projection_storage WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal') GROUP BY 1 ORDER BY gb DESC; 通用
TOP 20 大表 SELECT anchor_table_schema, anchor_table_name, SUM(used_bytes)/(1024^3)::NUMERIC(18,2) AS gb, SUM(row_count) AS rows FROM v_monitor.projection_storage WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal') GROUP BY 1,2 ORDER BY gb DESC LIMIT 20; 通用
最近 30 天有查询的表 SELECT DISTINCT anchor_table_schema, anchor_table_name FROM v_monitor.projection_usage WHERE query_start_timestamp > CURRENT_TIMESTAMP - INTERVAL '30 days'; 通用
DELETE 堆积检查 SELECT schema_name, projection_name, SUM(deleted_row_count) AS del_rows, (SUM(deleted_row_count)::FLOAT/NULLIF(SUM(total_row_count),0)*100)::NUMERIC(5,1) AS del_pct FROM v_monitor.storage_containers WHERE node_name = (SELECT local_node_name()) GROUP BY 1,2 HAVING SUM(deleted_row_count) > 0 ORDER BY del_pct DESC LIMIT 20; 通用
ROS 容器数检查 SELECT s.node_name, s.schema_name, s.projection_name, COUNT(DISTINCT s.storage_oid) AS ros_count FROM v_monitor.storage_containers s GROUP BY 1,2,3 ORDER BY ros_count DESC LIMIT 20; Enterprise
分区数 vs ROS 分布 SELECT p.table_schema, s.projection_name, COUNT(DISTINCT p.partition_key) AS partition_cnt, COUNT(DISTINCT s.storage_oid) AS ros_cnt FROM v_monitor.storage_containers s JOIN v_monitor.partitions p ON s.storage_oid = p.ros_id GROUP BY 1,2 ORDER BY ros_cnt DESC LIMIT 20; 通用
冗余投影检测 SELECT anchor_table_schema, anchor_table_name, COUNT(DISTINCT projection_name) AS proj_cnt, SUM(used_bytes)/(1024^2)::NUMERIC(18,2) AS mb FROM v_monitor.projection_storage WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal') GROUP BY 1,2 HAVING COUNT(DISTINCT projection_name) > 4 ORDER BY mb DESC; 通用
磁盘空间(节点级) SELECT node_name, storage_usage, disk_space_used_mb, disk_space_free_mb, disk_space_free_percent FROM v_monitor.disk_storage ORDER BY node_name, storage_usage; 通用
Eon 存储策略检查 SELECT * FROM storage_policies; Eon
Eon 存储位置检查 SELECT * FROM storage_locations; Eon
Eon Depot 驱逐量 SELECT node_name, COUNT(*) AS cnt, SUM(file_size_bytes)/(1024^2)::NUMERIC(18,2) AS mb FROM dc_depot_evictions WHERE "time" > CURRENT_TIMESTAMP - INTERVAL '24 hours' GROUP BY 1 ORDER BY mb DESC; Eon
Eon 公共存储死文件检查 SELECT CLEAN_COMMUNAL_STORAGE(false);false 参数仅检查不删除 Eon
全局压缩数据总量 SELECT SUM(used_bytes)/(1024^3)::NUMERIC(18,2) AS total_gb FROM v_monitor.projection_storage WHERE anchor_table_schema NOT IN ('v_catalog','v_monitor','v_internal'); 通用
存储位置分布(ROS 按 location) SELECT location_label, COUNT(*) AS ros_cnt, SUM(used_bytes)/(1024^3)::NUMERIC(18,2) AS gb FROM v_monitor.storage_containers GROUP BY 1 ORDER BY gb DESC; Eon

8. 最佳实践清单

按投入产出比从高到低排列:

  1. 先做 DELETE 清理再做归档 — 如果表中有大量 delete vector(deleted_pct > 20%),PURGE 能立即回收空间,比任何归档方案都快。为什么: DELETE 堆积是最常见的「看起来用了很多空间但其实没有」的场景。
  2. 大表必须分区(按时间) — 所有超过 10 GB 的明细表都应该按时间分区。为什么: 分区是 DROP_PARTITIONS 的前提,也是 Eon 模式存储策略的对象。没有分区意味着没有快速清理手段。
  3. DROP_PARTITIONS 优于 DELETE — 需要删除过期数据时,永远优先用 DROP_PARTITIONS 而非 DELETE。为什么: DROP_PARTITIONS 是纯 catalog 操作,不产生 delete vector,不重写数据。
  4. 按月分区,不要按天 — 对于数据保留超过 1 年的大表,按月分区足以满足管理需求。为什么: 按天分区会产生 365+ 个分区和大量 ROS 容器,推高 Catalog 大小。一个 10 年的按天分区表会产生 3650 个分区,每个投影的上限是 1024 个 ROS 容器。
  5. Eon 模式:为冷数据创建独立的低成本存储桶 — 使用 Storage Policy 将 3 个月以上无查询的表定向到 S3 Glacier 或等效低成本存储。为什么: S3 Standard vs Glacier 的存储成本差异可达 5-6 倍,对于 TB 级别的冷数据效果显著。
  6. 定期检查 projection_usage 识别冷表 — 每月运行一次冷表检测 SQL(第 3.1 步)。为什么: 冷表不会自己消失。业务需求变化后,曾经的热表可能变成冷表,需要主动发现。
  7. 不要给 HistoryRetentionTime 设过大的值 — 默认 -1 已经是最优的(不保留已删除数据)。如果业务有回滚需求才设正值,建议 1-2 天(86400-172800 秒),30 天以上的值会严重阻塞 AHM 推进和 delete vector 清理。为什么: 这个参数的值等于 AHM 被挡在「当前时间 - N 秒」之外的天数,值越大 Tuple Mover 越无法回收空间。
  8. 监控公共存储的「死文件」 — Eon 模式下定期比较 projection_storage 的统计值和 S3/HDFS 的实际值。为什么: Reaper 队列可能因为异常终止而堆积,导致死文件泄漏并持续计费。
  9. 删除冷表中的冗余投影 — 不需要查询的表只需要 buddy 投影(K-safety 所需的最小数量)。为什么: 每个额外投影都是完整数据副本,冷数据场景下是纯浪费。
  10. archive schema 放在 HDD 存储上(Enterprise 模式)— 使用 ALTER_LOCATION 为归档 schema 指定 HDD 路径。为什么: 归档数据几乎不被查询,NVMe SSD 的性能优势完全用不上,而 HDD 每 TB 成本仅为 NVMe 的 1/5-1/10。
  11. 建立存储增长基线 — 每周记录一次全局存储量(第 2.4 步的 SQL),绘制趋势图。为什么: 没有基线就无法判断增长是否异常。告警阈值建议:月增长 > 预期的 150% 时触发排查。

扩展阅读