跳转至

Vertica 表使用情况分析与资源优化

作者:JiangChong | 发布时间:2026-05-20

适用场景框: 当你需要清理数据库中不再使用的表以释放存储空间、减少 Catalog 膨胀、降低 Tuple Mover mergeout 开销,或者当你收到「磁盘空间不足」告警却不知道哪些表可以被安全清理时——本文提供从诊断到执行的全流程方法。

关联文章:

理解全文脉络: 文章从「为什么 Vertica 需要主动管理表生命周期」的原理讲起(第 1 节),然后从全局存储监控切入(第 2 节),接着按「表访问频率 → 存储占用 → 删除残留」的顺序逐步定位问题表(第 3 节),再给出从快速见效到根本治理的分层解决方案(第 4 节),最后通过多个案例和第 6 节的完整演练串联全部知识点。如果你只想快速查 SQL,直接跳到第 7 节的工具箱表格。


第 1 节:原理理解

1.1 Vertica 为什么需要关注表使用情况?

在传统数据库中,表通常只是「数据容器」——创建了就一直在那里,偶尔删几张旧表也不会对性能产生显著影响。但在 Vertica 中,每张表不只是数据,它还会生成投影(projection)、在 Catalog 中注册元数据、在 Tuple Mover mergeout 时参与 ROS 容器的合并读写。

这意味着:一张创建后从未被查询的表,仍然实实在在地影响着数据库:

  • 存储成本:数据在主存储(NVMe/SSD)中占位,Eon 模式虽然数据在 S3 上,但 Depot 缓存仍可能被意外驱逐
  • Catalog 膨胀:每张表在 Catalog 中都有对应的元数据条目,Catalog 越大,数据库启动、备份和系统表查询就越慢
  • Mergeout 干扰:Tuple Mover(TM)不会区分表的新旧——即使表已经几年没被查询,TM 仍然在 mergeout 时处理它的 ROS 容器
  • 统计信息负担:每张表的列都参与 ANALYZE_STATISTICSANALYZE_ROW_COUNT 的收集范围

打个比方: 表就像仓库里的货架。即使某个货架上的货物(数据)已经没人需要了,仓库管理员(Tuple Mover)还是要定期整理它,仓库登记簿(Catalog)上也要记着它的位置。货架越多,管理员跑断腿。

1.2 Vertica 如何记录表的访问历史?

Vertica 通过 Data Collector(DC) 组件持续记录系统活动。与表使用分析直接相关的有两个关键视角:

视角 系统表 记录粒度 保留周期(默认) 用途
投影使用 v_monitor.projection_usage 每个查询×每个投影 取决于 QueryExecutions DC 组件 查询到哪些表
查询记录 v_monitor.query_requests 每个查询 取决于 QueryExecutions DC 组件 从 SQL 文本提取表名

推荐优先使用 projection_usage,因为它是结构化的投影→锚定表关系,不需要解析 SQL 文本。query_requests 作为补充——当你需要知道哪些表被 DML(INSERT/COPY/UPDATE/DELETE)写入时,因为 projection_usage 只记录查询中实际读取的投影。

重要前提:DC 组件的保留周期限制了你能回溯的时间范围。QueryExecutions 组件的默认 DISK_SIZE_KB = 20000(约 20MB),INTERVAL_SET = false(不限制时间间隔,但受磁盘配额限制)。在典型生产负载下这通常是 7-30 天。如果 DC 只保留了 7 天的数据,你就只能分析最近 7 天的表使用情况——这是本文所有 SQL 的前提约束,后面会教你怎么检查和调整这个周期。

1.3 表使用分析的三维框架

本文的诊断逻辑按以下三维推进:

存储占用(大不大)──→ 访问频率(用没用)──→ 删除残留(浪费多少)
维度 回答的问题 核心系统表
存储占用 哪些表占空间最大? projection_storage
访问频率 哪些表最近被查询过?哪些从未被查? projection_usage
删除残留 哪些表有大量已删除但未回收的空间? delete_vectors, storage_containers

第 2 节:系统级监控——从宏观入手

在定位具体的问题表之前,先从全局了解存储压力和数据保留范围。

2.1 全局磁盘使用概览

SELECT node_name,
       storage_usage,
       storage_status,
       disk_space_used_mb,
       disk_space_free_mb,
       ROUND(disk_space_free_mb * 100.0 / NULLIF(disk_space_free_mb + disk_space_used_mb, 0), 1) AS free_pct
FROM v_monitor.disk_storage
ORDER BY node_name, storage_usage;

如何解读结果:

  • free_pct < 20%:存储压力较高,清理未使用表可以释放空间
  • storage_usage = 'CATALOG' vs 'DATA,TEMP':Catalog 路径通常较小(此处为 4.5GB),DATA/TEMP 路径是主要的数据存储位置
  • 各节点不一致:如果节点 1 的 free_pct 显著低于其他节点(如此处的 node0001 DATA=13% 而 node0002=26%),说明可能存在数据倾斜或该节点承载了更多的投影副本

2.2 检查 DC 组件保留周期

这是最容易跳过但最关键的一步。 如果 QueryExecutions 组件只保留了 3 天的数据,那么「从未被查询」的结论只对最近 3 天有效——无法排除表在 4 天前被大量使用。

SELECT component, description, table_name,
       disk_size_kb, interval_set, interval_time,
       memory_buffer_size_kb,
       TO_CHAR(first_time, 'YYYY-MM-DD HH24:MI') AS first_time,
       TO_CHAR(last_time, 'YYYY-MM-DD HH24:MI') AS last_time,
       kb_per_day
FROM v_monitor.data_collector
WHERE component = 'QueryExecutions'
ORDER BY node_name;

如何解读结果:

  • first_time / last_time:直接告诉你 DC 数据的历史范围。如果 first_time 是 3 天前,那你的表使用分析最多只能覆盖 3 天
  • interval_set = false:表示不限制时间间隔,由 disk_size_kb 磁盘配额控制保留量
  • kb_per_day:每天产生的数据量,除以 disk_size_kb 可以估算保留天数。例如 kb_per_day = 5000disk_size_kb = 20000,大概保留 4 天

如果保留周期太短,需要延长:

-- 将 QueryExecutions 的磁盘配额扩大到 100MB(约 4-8 周数据,取决于查询量)
-- 注意:这会消耗更多磁盘空间
SELECT SET_DATA_COLLECTOR_POLICY('QueryExecutions', '100000', false);

⚠️ 注意:增大 disk_size_kb 会立即生效,但之前的数据已经丢失无法恢复。所以建议在开始定期分析之前就先扩大保留周期

2.3 全局表存储排名

-- 注意:used_bytes 含所有投影副本(K-safe buddy + 跨节点),代表实际磁盘占用
-- approx_row_count 区分了分段表(SUM/投影数)和非分段表(每节点全副本,取一节点)
SELECT ps.anchor_table_schema,
       ps.anchor_table_name,
       SUM(ps.used_bytes) AS total_bytes,
       ROUND(SUM(ps.used_bytes) / 1073741824.0, 2) AS total_gb,
       CASE WHEN BOOL_AND(p.is_segmented)
           THEN SUM(ps.row_count) / COUNT(DISTINCT ps.projection_name)
           ELSE MAX(ps.row_count)
       END AS approx_row_count,
       SUM(ps.ros_count) AS total_ros,
       COUNT(DISTINCT ps.projection_name) AS proj_count
FROM v_monitor.projection_storage ps
JOIN (SELECT DISTINCT projection_id, projection_name, is_segmented FROM v_catalog.projections) p
  ON ps.projection_id = p.projection_id
 AND ps.projection_name = p.projection_name
GROUP BY ps.anchor_table_schema, ps.anchor_table_name
ORDER BY total_bytes DESC
LIMIT 20;

如何解读结果:

  • total_bytes 含所有投影副本(K-safe buddy projection + 跨节点分段),代表该表的实际磁盘占用。例如 K-safe=1 + 3 节点 = 6 份副本的字节总和。删表时释放的就是这个量
  • approx_row_count 自动处理两种投影类型:分段表(is_segmented=true)用 SUM / 投影数 消去 buddy 翻倍;非分段表(unsegmented/replicated)每节点存全量,用 MAX 取一节点即可
  • total_ros 很高(如 > 500):该表的 ROS 容器碎片化严重,Tuple Mover 处理负担重,即使表不大也建议关注
  • total_gb 占比过大(单表 > 总存储的 20%):大表,清理前务必确认业务影响
  • proj_count:记录了该表有多少个投影。proj_count = 2 意味着有超投影及其 buddy projection(K-safe=1 集群),正常。如果 proj_count > 4 说明该表有额外的自定义投影

第 3 节:逐步定位根因——从宏观到微观

步骤 1:识别「从未被查询」的完全未使用表

做什么: 列出 v_catalog.tables 中存在,但 v_monitor.projection_usage 中没有任何记录的表。这些表在 DC 保留周期内从未出现在任何查询的投影访问中。

-- 找出 DC 保留周期内从未被查询的表
SELECT t.table_schema,
       t.table_name,
       t.create_time,
       t.owner_name,
       COALESCE(ps_sum.total_bytes, 0) AS storage_bytes,
       ROUND(COALESCE(ps_sum.total_bytes, 0) / 1073741824.0, 2) AS storage_gb,
       COALESCE(ps_sum.total_rows, 0) AS row_count
FROM v_catalog.tables t
LEFT JOIN (
    SELECT ps2.anchor_table_schema, ps2.anchor_table_name,
           SUM(ps2.used_bytes) AS total_bytes,
           CASE WHEN BOOL_AND(p2.is_segmented)
               THEN SUM(ps2.row_count) / COUNT(DISTINCT ps2.projection_name)
               ELSE MAX(ps2.row_count)
           END AS total_rows
    FROM v_monitor.projection_storage ps2
    JOIN (SELECT DISTINCT projection_id, projection_name, is_segmented FROM v_catalog.projections) p2
      ON ps2.projection_id = p2.projection_id
     AND ps2.projection_name = p2.projection_name
    GROUP BY ps2.anchor_table_schema, ps2.anchor_table_name
) ps_sum
  ON t.table_schema = ps_sum.anchor_table_schema
  AND t.table_name = ps_sum.anchor_table_name
WHERE t.table_schema NOT IN ('v_catalog', 'v_monitor', 'v_internal')
  AND NOT t.is_system_table
  AND (t.table_schema, t.table_name) NOT IN (
      SELECT DISTINCT pu.anchor_table_schema, pu.anchor_table_name
      FROM v_monitor.projection_usage pu
  )
ORDER BY storage_bytes DESC;

如何解读:

  • 结果非空且有数 GB 的表:这些是优先清理目标,释放空间效果立竿见影
  • storage_bytes = 0 但表存在:表可能只有元数据(空表或只有投影定义但无数据),删除它们可以减少 Catalog 条目
  • create_time 距今很久(如 > 6 个月):长期未使用的表,清理决策风险较低

如果不是,进入步骤 2: 如果所有表都有访问记录,说明没有完全未使用的表。接下来找「低频使用」的表。

步骤 2:识别「低频使用」表——按最后访问时间排序

做什么: 找出最近访问时间最久远的表,即使它们偶尔被查询。

-- 按最后访问时间排序,找出低频使用表
SELECT pu.anchor_table_schema,
       pu.anchor_table_name,
       MAX(pu.query_start_timestamp) AS last_accessed,
       COUNT(DISTINCT pu.request_id) AS query_count,
       COALESCE(ps_sum.total_bytes, 0) AS storage_bytes,
       ROUND(COALESCE(ps_sum.total_bytes, 0) / 1073741824.0, 2) AS storage_gb
FROM v_monitor.projection_usage pu
LEFT JOIN (
    SELECT anchor_table_schema, anchor_table_name,
           SUM(used_bytes) AS total_bytes
    FROM v_monitor.projection_storage
    GROUP BY anchor_table_schema, anchor_table_name
) ps_sum
  ON pu.anchor_table_schema = ps_sum.anchor_table_schema
  AND pu.anchor_table_name = ps_sum.anchor_table_name
GROUP BY pu.anchor_table_schema, pu.anchor_table_name, ps_sum.total_bytes
ORDER BY last_accessed ASC
LIMIT 20;

如何解读:

  • last_accessed 距今 > 30 天:该表可能有归档价值。结合 storage_bytes 看——如果存储大 + 访问少 = 高优先级清理/归档
  • query_count 很低(如 < 5)但 last_accessed 很近:可能有定时任务偶尔全表扫描。需要确认业务方是否还需要
  • query_count 高但 last_accessed 久远:说明之前曾高频使用但最近停了——可能是已下线的业务

如果不是,进入步骤 3: 如果所有表近期都有访问,说明表利用率良好。接下来检查是否有删除残留浪费。

步骤 3:识别「删除残留」浪费空间

做什么: Vertica 使用 delete vector(DV)标记已删除行,物理空间在 mergeout 完成后才回收。大量 DV 意味着存储空间被「占着不用」。

-- 检查 delete vectors 的空间占用
SELECT schema_name,
       projection_name,
       SUM(deleted_row_count) AS total_deleted_rows,
       SUM(used_bytes) AS dv_bytes,
       ROUND(SUM(used_bytes) / 1073741824.0, 2) AS dv_gb,
       COUNT(*) AS dv_count
FROM v_monitor.delete_vectors
GROUP BY schema_name, projection_name
ORDER BY dv_bytes DESC
LIMIT 20;

如何解读:

  • dv_bytes 很大(如 > 1GB),但表本身很小:说明该表执行了大量 DELETE 但 mergeout 尚未完成——SELECT PURGE_TABLE('schema.table_name'); 可以强制回收
  • dv_count 很高(如 > 100):碎片化的 delete vector,可能需要调整 Tuple Mover 的 mergeout 策略
  • 结果为空:测试环境或少量删除的正常状态

步骤 4:检查 ROS 容器碎片化

-- 识别 ROS 容器过多的表
-- 注意:approx_row_count 区分分段/非分段,与 projection_storage 同理
SELECT sc.schema_name,
       sc.projection_name,
       CASE WHEN BOOL_AND(p.is_segmented)
           THEN SUM(sc.total_row_count)
           ELSE SUM(sc.total_row_count) / COUNT(DISTINCT sc.node_name)
       END AS approx_row_count,
       SUM(sc.deleted_row_count) AS deleted_rows,
       SUM(sc.used_bytes) AS total_used_bytes,
       ROUND(SUM(sc.used_bytes) / 1073741824.0, 2) AS total_gb,
       COUNT(*) AS container_count
FROM v_monitor.storage_containers sc
JOIN (SELECT DISTINCT projection_id, projection_name, is_segmented FROM v_catalog.projections) p
  ON sc.projection_id = p.projection_id
 AND sc.projection_name = p.projection_name
GROUP BY sc.schema_name, sc.projection_name
HAVING COUNT(*) > 10
ORDER BY container_count DESC
LIMIT 20;

如何解读:

  • container_count > 50:ROS 容器碎片化严重。每个容器在 mergeout 时都需要被读取和重写,增加 I/O 开销
  • deleted_rows / approx_row_count > 0.5:一半以上的行已被删除但未回收——这是最严重的空间浪费
  • approx_row_count = 0used_bytes > 0:空表但占空间(全被 delete vector 占据),执行 PURGE_TABLE

第 4 节:解决方案——从快速见效到根本治理

4.1 立即措施(当天可执行)

方案 A:DROP 完全未使用的表

适用条件: 步骤 1 的结果,且已与业务方确认该表可删除。

-- 注意:DROP TABLE 是不可逆操作。执行前建议先备份或导出 DDL
-- 获取表 DDL(用于恢复)
SELECT EXPORT_OBJECTS('', 'schema.table_name');

-- 检查是否有外键依赖
SELECT * FROM v_catalog.foreign_keys
WHERE table_schema = 'schema_name' AND table_name = 'table_name';

-- 删除表及其所有投影(包括 buddy projection)
DROP TABLE schema.table_name CASCADE;

为什么用 CASCADE DROP TABLE 默认已删除表及其所有投影。CASCADE 额外删除依赖该表的视图(View)和外键约束,避免残留依赖对象报错。

方案 B:PURGE 有大量删除残留的表

适用条件: 步骤 3 的结果显示 dv_bytes 很高。

-- 强制回收指定表的所有 delete vectors
SELECT PURGE_TABLE('schema.table_name');

为什么不是自动的: Tuple Mover 的 mergeout 在达到一定阈值后才触发。如果删除量不够大但表很多,delete vector 可能长期「挂」在那里。手动 PURGE_TABLE 是快速回收的捷径。

方案 C:移除不必要的数据加载

适用条件: 步骤 1/2 中发现的低频表,如果对应着持续运行的 ETL 任务。

与其每天加载数据到一张没人查的表,不如停掉 ETL。Vertica 中每一条 COPY 语句、每一次 INSERT 都会生成新的 ROS 容器并增加 Tuple Mover 负担。

4.2 短期优化(当周执行)

方案 D:管理冷表的 Depot 缓存(Eon 模式)

Eon 模式下,Depot(本地 SSD 缓存)默认按 LRU 自动淘汰冷数据。如果表曾通过 SET_DEPOT_PIN_POLICY_TABLE 被 pin 住,需要解除以释放 Depot 空间:

-- 查看当前 Depot pin 策略(Eon 模式)
SELECT * FROM v_monitor.depot_pin_policies;

-- 默认情况下所有表都没有 pin,LRU 自动淘汰冷数据即可
-- 如果表曾被 pin 过,解除 pin 使其可被正常淘汰:
SELECT CLEAR_DEPOT_PIN_POLICY_TABLE('schema.cold_table');

-- 如果只需淘汰部分历史分区,标记为优先驱逐:
SELECT SET_DEPOT_ANTI_PIN_POLICY_PARTITION('schema.cold_table', '', '2024-12-31');

方案 E:按分区清理历史数据(替代全表删除)

如果表仍在使用但历史分区不需要,使用 DROP_PARTITIONS 比 DELETE + PURGE 高效得多:

-- 删除 2024 年及以前的所有分区(比 DELETE 快很多,且不产生 delete vectors)
SELECT DROP_PARTITIONS(
    'schema.table_name',
    '',                                      -- 最小分区键值(空 = 从头开始)
    '2025-01-01'                             -- 最大分区键值(不包含这一天)
);

为什么推荐 DROP_PARTITIONS 而不是 DELETE: DELETE 会产生 delete vectors,需要等 mergeout 才能回收空间。DROP_PARTITIONS 直接删除 ROS 容器,物理空间立即释放。

4.3 根本治理——建立表生命周期管理机制

定期监控脚本:全量表生命周期报告

将以下 SQL 配置为每周定时任务,输出所有用户表的完整生命周期报告——覆盖存储占用、访问频率、删除残留、使用状态分级四个维度,按存储从大到小排列,大表未使用或低频使用会排在最前面。

-- 全量表生命周期报告(建议每周执行)
WITH
-- CTE 1:所有用户表的基本信息
-- v_catalog.tables 包含系统表,需要用 is_system_table 过滤
all_tables AS (
    SELECT t.table_schema, t.table_name, t.create_time, t.owner_name, t.table_id
    FROM v_catalog.tables t
    WHERE t.table_schema NOT IN ('v_catalog', 'v_monitor', 'v_internal')
      AND NOT t.is_system_table
),

-- CTE 2:每张表的存储统计
-- SUM(used_bytes) 含 buddy projection + 跨节点副本 = 实际磁盘占用(删表释放量)
-- approx_row_count 自动区分分段/非分段表,消除 row_count 翻倍
table_storage AS (
    SELECT ps.anchor_table_schema, ps.anchor_table_name,
           SUM(ps.used_bytes) AS total_bytes,
           CASE WHEN BOOL_AND(p.is_segmented)
               THEN SUM(ps.row_count) / COUNT(DISTINCT ps.projection_name)
               ELSE MAX(ps.row_count)
           END AS approx_row_count,
           SUM(ps.ros_count) AS total_ros,
           COUNT(DISTINCT ps.projection_name) AS proj_count
    FROM v_monitor.projection_storage ps
    JOIN (SELECT DISTINCT projection_id, projection_name, is_segmented
          FROM v_catalog.projections) p
      ON ps.projection_id = p.projection_id
     AND ps.projection_name = p.projection_name
    GROUP BY ps.anchor_table_schema, ps.anchor_table_name
),

-- CTE 3:每张表的访问统计(基于 DC 保留周期内的数据)
-- last_accessed = NULL 表示该表在 DC 周期内从未被查询
-- query_count 是 DISTINCT request_id 计数,不是行数
table_access AS (
    SELECT pu.anchor_table_schema, pu.anchor_table_name,
           MAX(pu.query_start_timestamp) AS last_accessed,
           COUNT(DISTINCT pu.request_id) AS query_count
    FROM v_monitor.projection_usage pu
    GROUP BY pu.anchor_table_schema, pu.anchor_table_name
),

-- CTE 4:每张表的 delete vector 残留(已删除行占用的未回收空间)
-- 注意:delete_vectors 用 projection_name 标识投影(如 product_dimension_DBD_4_rep_first),
-- 不能直接 JOIN table_name。需通过 v_catalog.projections 桥接回 anchor table
table_dv AS (
    SELECT p.anchor_table_name, p.projection_schema AS anchor_table_schema,
           SUM(dv.deleted_row_count) AS deleted_rows,
           SUM(dv.used_bytes) AS dv_bytes
    FROM v_monitor.delete_vectors dv
    JOIN (SELECT DISTINCT projection_schema, projection_name, anchor_table_name
          FROM v_catalog.projections) p
      ON dv.schema_name = p.projection_schema
     AND dv.projection_name = p.projection_name
    GROUP BY p.projection_schema, p.anchor_table_name
)

-- 主查询:串联以上四个维度,输出全量表报告
SELECT a.table_schema,
       a.table_name,
       a.create_time,
       a.owner_name,
       -- 存储维度
       COALESCE(s.total_bytes, 0) AS storage_bytes,
       ROUND(COALESCE(s.total_bytes, 0) / 1073741824.0, 2) AS storage_gb,
       COALESCE(s.approx_row_count, 0) AS row_count,
       COALESCE(s.total_ros, 0) AS total_ros,
       COALESCE(s.proj_count, 0) AS proj_count,
       -- 访问维度
       acl.last_accessed,
       acl.query_count,
       -- 删除残留维度
       COALESCE(dv.deleted_rows, 0) AS deleted_rows,
       ROUND(COALESCE(dv.dv_bytes, 0) / 1073741824.0, 2) AS dv_gb,
       -- 使用状态分级:基于 DC 保留周期内的最后访问时间
       CASE
           WHEN acl.last_accessed IS NULL
               THEN '未使用'
           WHEN acl.last_accessed < CURRENT_TIMESTAMP - INTERVAL '30 days'
               THEN '低频(>30天)'
           WHEN acl.last_accessed < CURRENT_TIMESTAMP - INTERVAL '7 days'
               THEN '低频(>7天)'
           ELSE '活跃'
       END AS usage_status
FROM all_tables a
LEFT JOIN table_storage s
  ON a.table_schema = s.anchor_table_schema
 AND a.table_name = s.anchor_table_name
LEFT JOIN table_access acl
  ON a.table_schema = acl.anchor_table_schema
 AND a.table_name = acl.anchor_table_name
LEFT JOIN table_dv dv
  ON a.table_schema = dv.anchor_table_schema
 AND a.table_name = dv.anchor_table_name
ORDER BY COALESCE(s.total_bytes, 0) DESC;

输出:

 table_schema |            table_name             |          create_time          | owner_name | storage_bytes |    storage_gb     |         row_count          | total_ros | proj_count |         last_accessed         | query_count | deleted_rows |       dv_gb       | usage_status
--------------+-----------------------------------+-------------------------------+------------+---------------+-------------------+----------------------------+-----------+------------+-------------------------------+-------------+--------------+-------------------+--------------
 online_sales | online_sales_fact                 | 2025-08-30 14:13:16.669305+08 | dbadmin    |     193795920 | 0.180000000000000 | 5000000.000000000000000000 |         6 |          2 | 2026-06-06 08:29:55.041027+08 |          12 |            0 | 0.000000000000000 | 活跃
 store        | store_sales_fact                  | 2025-08-30 14:13:16.627045+08 | dbadmin    |     167064330 | 0.160000000000000 | 5000000.000000000000000000 |         6 |          2 | 2026-06-01 15:00:19.205483+08 |          12 |            0 | 0.000000000000000 | 活跃
 store        | store_orders_fact                 | 2025-08-30 14:13:16.64386+08  | dbadmin    |      12261744 | 0.010000000000000 |  300000.000000000000000000 |         6 |          2 | 2026-06-01 15:00:19.603346+08 |          12 |            0 | 0.000000000000000 | 活跃
 public       | product_dimension                 | 2025-08-30 14:13:14.498106+08 | dbadmin    |       4947552 | 0.000000000000000 |   60000.000000000000000000 |         3 |          1 | 2026-06-06 12:14:08.168764+08 |           9 |            0 | 0.000000000000000 | 活跃
 public       | customer_dimension                | 2025-08-30 14:13:14.434014+08 | dbadmin    |       4356879 | 0.000000000000000 |   50000.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.281505+08 |           8 |            0 | 0.000000000000000 | 活跃
 public       | inventory_fact                    | 2025-08-30 14:13:16.607218+08 | dbadmin    |       2252116 | 0.000000000000000 |  300000.000000000000000000 |         6 |          2 | 2026-06-01 15:00:18.683354+08 |          11 |            0 | 0.000000000000000 | 活跃
 public       | employee_dimension                | 2025-08-30 14:13:14.881694+08 | dbadmin    |        871566 | 0.000000000000000 |   10000.000000000000000000 |        12 |          1 | 2026-06-06 13:41:01.015406+08 |          11 |            0 | 0.000000000000000 | 活跃
 public       | promotion_dimension               | 2025-08-30 14:13:14.507036+08 | dbadmin    |         55260 | 0.000000000000000 |    1000.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.483738+08 |           8 |            0 | 0.000000000000000 | 活跃
 public       | date_dimension                    | 2025-08-30 14:13:14.513635+08 | dbadmin    |         49458 | 0.000000000000000 |    1826.000000000000000000 |         3 |          1 | 2026-06-06 12:18:09.904429+08 |           8 |            0 | 0.000000000000000 | 活跃
 store        | store_dimension                   | 2025-08-30 14:13:16.621421+08 | dbadmin    |         26301 | 0.000000000000000 |     250.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.811249+08 |           8 |            0 | 0.000000000000000 | 活跃
 online_sales | call_center_dimension             | 2025-08-30 14:13:16.66313+08  | dbadmin    |         17193 | 0.000000000000000 |     200.000000000000000000 |         3 |          1 | 2026-06-01 15:00:19.745743+08 |           8 |            0 | 0.000000000000000 | 活跃
 online_sales | online_page_dimension             | 2025-08-30 14:13:16.657524+08 | dbadmin    |         14898 | 0.000000000000000 |    1000.000000000000000000 |         3 |          1 | 2026-06-01 15:00:19.725607+08 |           8 |            0 | 0.000000000000000 | 活跃
 public       | warehouse_dimension               | 2025-08-30 14:13:16.523159+08 | dbadmin    |          6762 | 0.000000000000000 |     100.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.642482+08 |           8 |            0 | 0.000000000000000 | 活跃
 public       | vendor_dimension                  | 2025-08-30 14:13:14.876262+08 | dbadmin    |          5445 | 0.000000000000000 |      50.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.548064+08 |           8 |            0 | 0.000000000000000 | 活跃
 public       | shipping_dimension                | 2025-08-30 14:13:16.517765+08 | dbadmin    |          2613 | 0.000000000000000 |     100.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.6275+08   |           8 |            0 | 0.000000000000000 | 活跃
 public       | t                                 | 2025-08-30 13:03:45.480904+08 | dbadmin    |          1926 | 0.000000000000000 |       3.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.017922+08 |           8 |            0 | 0.000000000000000 | 活跃
 public       | rep                               | 2025-08-30 13:04:03.856729+08 | dbadmin    |          1926 | 0.000000000000000 |       3.000000000000000000 |         3 |          1 | 2026-06-01 15:00:18.126652+08 |           9 |            0 | 0.000000000000000 | 活跃
 public       | a                                 | 2026-06-04 16:20:12.137149+08 | dbadmin    |           216 | 0.000000000000000 |       2.000000000000000000 |         8 |          2 | 2026-06-04 16:30:34.736375+08 |          14 |            0 | 0.000000000000000 | 活跃
 vsbak        | cpu_usage                         | 2026-05-11 13:21:31.088616+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | dc_resource_pool_status_by_hour   | 2026-05-11 13:21:31.359414+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | dc_resource_pool_status_by_minute | 2026-05-11 13:21:31.450017+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | memory_usage                      | 2026-05-11 13:21:31.130998+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | network_usage                     | 2026-05-11 13:21:31.143478+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | query_consumption                 | 2026-05-11 13:21:31.161448+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 | 2026-05-21 17:59:29.098583+08 |           2 |            0 | 0.000000000000000 | 低频(>7)
 vsbak        | resource_acquisitions             | 2026-05-11 13:21:31.310233+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 | 2026-05-21 17:59:41.305821+08 |           4 |            0 | 0.000000000000000 | 低频(>7)
 vsbak        | query_requests                    | 2026-05-11 13:21:31.524956+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 | 2026-05-21 17:59:41.260062+08 |           5 |            0 | 0.000000000000000 | 低频(>7)
 vsbak        | query_profiles                    | 2026-05-11 13:21:31.540551+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | projection_usage                  | 2026-05-11 13:21:31.555842+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 | 2026-05-21 17:59:25.704393+08 |           3 |            0 | 0.000000000000000 | 低频(>7)
 vsbak        | dc_depot_fetches                  | 2026-05-11 13:21:31.56865+08  | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         0 |          0 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | dc_depot_uploads                  | 2026-05-11 13:21:31.59988+08  | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         0 |          0 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | projection_storage                | 2026-05-11 13:21:31.619916+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 | 2026-05-21 17:59:19.322252+08 |           6 |            0 | 0.000000000000000 | 低频(>7)
 vsbak        | system_resource_usage             | 2026-05-11 13:21:31.509564+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 | 2026-05-21 17:59:24.904859+08 |           3 |            0 | 0.000000000000000 | 低频(>7)
 vsbak        | user_sessions                     | 2026-05-11 13:21:31.183189+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | dc_lock_attempts                  | 2026-05-11 13:21:31.497515+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
 vsbak        | tables                            | 2026-05-11 13:21:31.637046+08 | dbadmin    |             0 | 0.000000000000000 |       0.000000000000000000 |         6 |          2 |                               |             |            0 | 0.000000000000000 | 未使用
(35 rows)

如何解读这份报告:

关注点 解读方法
高优先级清理目标 usage_status = '未使用' + storage_gb > 1 → 联系业务确认后直接 DROP
候选归档表 usage_status = '低频(>30天)' + storage_gb > 0.5 → 按分区归档或 DROP_PARTITIONS
删除残留堆积 dv_gb > 0.5 + storage_gb 不大 → 执行 PURGE_TABLE 立即回收
ROS 碎片化 total_ros > 50 + storage_gb < 1 → 小表但容器碎片化严重,Tuple Mover 负担
Catalog 膨胀 storage_gb = 0 + create_time < 30 天前 → 空表占 Catalog 条目,可清理
报告覆盖范围 所有判断基于 DC 保留周期(默认 7-30 天)。如果 query_count = 0,只说明该周期内未被查——需结合业务确认

⚠️ 注意事项:

  • delete_vectors 的 JOIN 已通过 v_catalog.projections 桥接,将投影名映射回锚定表名,确保精确匹配
  • usage_status 的 7 天/30 天阈值是可调参数,根据业务节奏调整

建立表生命周期文档

为每个表记录以下信息(可集成到 Vertica 的 comment 或外部治理系统):

  • 创建日期和业务负责人
  • 预期保留期(如「保留 13 个月」)
  • 归档策略(如「过期后归档到 S3 存储桶」)

在 Vertica 中为表添加注释:

COMMENT ON TABLE schema.table_name IS '业务负责人:张三 | 预期保留:13个月 | 归档:HDFS';

第 5 节:深入案例

📝 虚构案例 1:历史归档表无人清理

场景: 某电商平台,3 节点集群,Vertica Enterprise 模式。DBA 发现磁盘使用率已达 87%,收到存储告警。

诊断过程:

-- 先看全局
SELECT node_name, storage_usage,
       disk_space_free_mb, disk_space_used_mb,
       ROUND(disk_space_free_mb * 100.0 / NULLIF(disk_space_free_mb + disk_space_used_mb, 0), 1) AS free_pct
FROM v_monitor.disk_storage;

输出:

node001 / DATA   2880 / 25000 = 11.5%
node002 / DATA   3150 / 24730 = 12.7%
node003 / DATA   3010 / 24870 = 12.1%

全部节点只剩 12% 左右,紧急。

-- 找最大的表(虚构案例,数据为模拟)
SELECT ps.anchor_table_schema, ps.anchor_table_name,
       SUM(ps.used_bytes) AS total_bytes,
       ROUND(SUM(ps.used_bytes) / 1073741824.0, 2) AS total_gb,
       CASE WHEN BOOL_AND(p.is_segmented)
           THEN SUM(ps.row_count) / COUNT(DISTINCT ps.projection_name)
           ELSE MAX(ps.row_count)
       END AS approx_row_count
FROM v_monitor.projection_storage ps
JOIN (SELECT DISTINCT projection_id, projection_name, is_segmented FROM v_catalog.projections) p
  ON ps.projection_id = p.projection_id AND ps.projection_name = p.projection_name
GROUP BY anchor_table_schema, anchor_table_name
ORDER BY total_bytes DESC
LIMIT 10;

输出(虚构数据):

ods.order_history_2019_2022   98GB   520亿行
ods.order_history_2023_2024   52GB   260亿行
dwd.user_behavior_log         34GB   180亿行
...
-- 检查 ods.order_history_2019_2022 的最后访问时间
SELECT MAX(query_start_timestamp) AS last_query
FROM v_monitor.projection_usage
WHERE anchor_table_name = 'order_history_2019_2022';

输出:2024-01-15距今超过 2 年,近 2 年没有任何查询!

根因分析: 2019-2022 的订单历史数据在 2023 年迁移到新表结构后,旧表被遗忘。ODS 层仍然每天 COPY 新数据到该表,但因为下游报表早已切换到新表,没有任何查询访问它。表持续增长到 98GB,占了集群总存储的 40%。

修复方案:

  1. 联系业务负责人确认该表已废弃
  2. 导出表 DDL(EXPORT_OBJECTS)以备不时之需
  3. DROP TABLE ods.order_history_2019_2022 CASCADE

效果: 磁盘使用率从 87% 降至 54%。释放 98GB 空间。


📝 虚构案例 2:ETL 中间表堆积

场景: 某运营商经分系统,5 节点集群。系统慢得异常——不是查询慢,而是系统表查询和数据库日常运维操作(如备份)变得很慢。

诊断过程:

-- 先看有多少非系统表
SELECT COUNT(*) AS total_user_tables
FROM v_catalog.tables
WHERE NOT is_system_table;

输出:8347 张用户表。对于一个 5 节点分析集群来说异常高。

-- 找「没有数据但有投影」的表(占 Catalog 条目但不占存储)
SELECT t.table_schema, t.table_name, t.create_time
FROM v_catalog.tables t
WHERE t.table_schema NOT IN ('v_catalog', 'v_monitor', 'v_internal')
  AND NOT t.is_system_table
  AND t.table_id NOT IN (
      SELECT DISTINCT anchor_table_id
      FROM v_monitor.projection_storage
      WHERE row_count > 0
  )
ORDER BY t.create_time DESC
LIMIT 20;

输出:4700+ 张空表,表名模式为 tmp_20250401_*tmp_20250402_*……显然是无 ETL 清理机制留下的临时表。

-- 再检查最近被查询过的表有多少
SELECT COUNT(DISTINCT anchor_table_schema || '.' || anchor_table_name) AS tables_used_recently
FROM v_monitor.projection_usage;

输出:162。也就是说 8347 张用户表中,只有 162 张在 DC 保留周期内被查询过

根因分析: ETL 脚本使用 CREATE TABLE AS SELECT(CTAS)生成每日中间表,格式为 tmp_YYYYMMDD_xxx,但从未清理旧表。日积月累 8000+ 张表的 Catalog 元数据导致 v_catalog.tables 扫描变慢、数据库启动变慢、备份耗时增加。

修复方案:

  1. 修改 ETL 脚本,增加每日清理逻辑(保留最近 3 天即可)
  2. 批量删除历史临时表。由于表太多,用一个生成脚本批量执行:
-- 生成 DROP 语句(先审查再执行!)
SELECT 'DROP TABLE IF EXISTS ' || table_schema || '."' || table_name || '" CASCADE;' as drop_table_sql
FROM v_catalog.tables
WHERE table_schema = 'temp_schema'
  AND table_name LIKE 'tmp_2024%'
  AND NOT is_system_table
ORDER BY table_name;

效果: Catalog 元数据条目减少 60%,系统表查询速度恢复,备份时间从 35 分钟降至 6 分钟。


📋 真实案例 1:某运营商系统表快照堆积

来源: vault 中 vsbak schema 下的表——在本次验证环境(v26.1.0-2)中,vsbak schema 包含 15 张表(如 cpu_usage, query_requests, projection_usage, system_resource_usage 等),它们是通过某种监控脚本将系统表数据定期快照保存的备份表,但所有表的 storage_bytes = 0(空表)。

诊断发现: 这 15 张备份表中,11 张在 projection_usage没有任何访问记录——它们被创建后从未被查询过。

-- 已验证的发现:vsbak schema 的表完全没有查询记录
SELECT t.table_schema, t.table_name, t.create_time
FROM v_catalog.tables t
WHERE t.table_schema = 'vsbak'
  AND NOT t.is_system_table
  AND (t.table_schema, t.table_name) NOT IN (
      SELECT DISTINCT anchor_table_schema, anchor_table_name
      FROM v_monitor.projection_usage
  );

输出确认了 11 张完全未被使用但仍在占用 Catalog 元数据的表。

启示: 监控/备份脚本创建的表,如果脚本后来被弃用,这些表就成为「僵尸表」。它们不占数据空间,但占 Catalog 条目并可能在未来数据加载后被意外查询。


第 6 节:完整诊断流程实战

📝 虚构场景 · 完整演练

背景: 某金融机构的数据仓库 DBA 收到告警——节点 node01 磁盘使用率超过 90%。集群为 4 节点 Enterprise 模式,K-safe=1。

时间线:

09:15 — 确认全局状态

SELECT node_name, storage_usage,
       disk_space_used_mb, disk_space_free_mb,
       ROUND(disk_space_free_mb * 100.0 / NULLIF(disk_space_free_mb + disk_space_used_mb, 0), 1) AS free_pct
FROM v_monitor.disk_storage
WHERE storage_usage = 'DATA,TEMP';

输出:

node01 DATA   32500 / 3601 = 9.9%   ← 告警源
node02 DATA   18500 / 17600 = 48.7%
node03 DATA   19200 / 16900 = 46.8%
node04 DATA   19000 / 17100 = 47.3%

判断: node01 异常高(比其他节点多占了 14GB),说明数据分布不均或该节点有额外负担。

09:20 — 定位 node01 上的大表

SELECT anchor_table_schema, anchor_table_name, projection_name,
       SUM(used_bytes) AS total_bytes,
       ROUND(SUM(used_bytes) / 1073741824.0, 2) AS total_gb
FROM v_monitor.projection_storage
WHERE node_name = 'node01'
GROUP BY anchor_table_schema, anchor_table_name, projection_name
ORDER BY total_bytes DESC
LIMIT 5;

输出(虚构):

risk.var_calculation_history   8.2GB
risk.var_calculation_history   8.2GB   ← buddy projection
dwd.trade_detail_2023          3.1GB
dwd.trade_detail_2024          2.8GB
...

判断: risk.var_calculation_history 一张表就占了 node01 的 8.2GB。K-safe=1 时,该表有两个投影(super + buddy),每个投影在 node01 上都有一个数据分段,所以 node01 上显示了两条各 8.2GB 的记录。这张表在 node01 上的两个分段合计约 16.4GB。

09:25 — 检查这张表的使用频率

SELECT MAX(query_start_timestamp) AS last_query,
       COUNT(DISTINCT request_id) AS query_count
FROM v_monitor.projection_usage
WHERE anchor_table_schema = 'risk'
  AND anchor_table_name = 'var_calculation_history';

输出:

last_query: 2025-11-03  ← 距今 7 个月
query_count: 3           ← 整个 DC 周期只被查了 3 次

09:30 — 联系风险管理部门确认

发现:VaR(Value at Risk)计算在 2025 年 11 月已迁移到新模型,不再使用这张历史计算表。该表保留仅因「忘了删」。

09:35 — 检查是否有 delete vectors

SELECT schema_name, SUM(deleted_row_count) AS total_deleted,
       SUM(used_bytes) AS dv_bytes
FROM v_monitor.delete_vectors
WHERE schema_name = 'risk'
  AND projection_name LIKE '%var_calculation_history%'
GROUP BY schema_name;

输出:0 rows(无 delete vector,单纯就是数据大)

09:40 — 确认可删除后执行

SELECT EXPORT_OBJECTS('', 'risk.var_calculation_history');
-- 保存 DDL 到运维文档

DROP TABLE risk.var_calculation_history CASCADE;

10:00 — 验证效果

SELECT node_name, disk_space_free_mb,
       ROUND(disk_space_free_mb * 100.0 / NULLIF(disk_space_free_mb + disk_space_used_mb, 0), 1) AS free_pct
FROM v_monitor.disk_storage
WHERE storage_usage = 'DATA,TEMP';

输出:

node01 DATA   32500 / 11801 = 36.3%  ← 从 9.9% 恢复到 36.3%

结论: 删除一张 8.2GB 的未使用表(含 buddy projection = 16.4GB),node01 磁盘使用率从 90.1% 降至 63.7%。后续建议每周运行一次未使用表扫描脚本。


第 7 节:快速诊断 SQL 工具箱

诊断目标 SQL 说明
全局磁盘使用 v_monitor.disk_storage 查询 free_pct 先看全局压力
DC 保留周期 v_monitor.data_collector WHERE component='QueryExecutions' 确认分析时间范围
表存储排名 TOP 20 v_monitor.projection_storage GROUP BY 表 → ORDER BY total_bytes DESC 找存储大户
全量表生命周期报告 4.3 节完整 CTE(all_tables + table_storage + table_access + table_dv) 一键输出存储/访问/残留/状态四维报告
完全未使用的表 v_catalog.tables LEFT JOIN v_monitor.projection_usage → WHERE last_accessed IS NULL 快速版,找僵尸表
低频使用表 v_monitor.projection_usage GROUP BY 表 → ORDER BY last_accessed ASC 找候选归档表
Delete Vector 堆积 v_monitor.delete_vectors GROUP BY 表 → ORDER BY dv_bytes DESC 找删除残留
ROS 容器碎片化 v_monitor.storage_containers GROUP BY 投影 → HAVING COUNT(*) > 10 找碎片化表
表总数统计 SELECT COUNT(*) FROM v_catalog.tables WHERE NOT is_system_table Catalog 膨胀检查
无数据空表 v_catalog.tables WHERE table_id NOT IN (SELECT anchor_table_id FROM projection_storage WHERE row_count > 0) 占 Catalog 不占空间
导出表 DDL SELECT EXPORT_OBJECTS('', 'schema.table_name') 删除前备份
强制回收删除空间 SELECT PURGE_TABLE('schema.table_name') 无需等 mergeout
按分区清理 SELECT DROP_PARTITIONS('schema.table', 'min', 'max') 比 DELETE 更高效
解除 Depot pin SELECT CLEAR_DEPOT_PIN_POLICY_TABLE('schema.table') Eon 模式不再缓存
添加表注释 COMMENT ON TABLE schema.table IS '...' 建立表生命周期文档

第 8 节:最佳实践清单

按投入产出比排序:

  1. 先扩大 DC 保留周期再分析(投入:1 分钟,产出:准确的分析基础) 如果 QueryExecutions 只保留了 3 天的数据,「从未使用」的结论就毫无意义。建议至少保留 30 天。执行 SET_DATA_COLLECTOR_POLICY 扩大 disk_size_kb
  2. 每月跑一次「完全未使用表」扫描(投入:5 分钟,产出:避免存储浪费累积) 使用步骤 1 的 SQL,发现僵尸表及时清理。越是大的集群,越容易有被遗忘的表
  3. 删除前先 EXPORT_OBJECTS 备份 DDL(投入:10 秒/表,产出:可恢复性保障) DROP 是不可逆的。保留 DDL 让恢复只需要重新加载数据,而无需从零建表
  4. 优先清理有大量 delete vector 的表(投入:5 分钟,产出:立即释放空间) PURGE_TABLE 是纯 I/O 操作,不需要改业务代码或停服务。比 DROP 风险低得多
  5. DROP_PARTITIONS 替代 DELETE + PURGE(投入:改 ETL 代码,产出:无 delete vector,空间即时回收) 如果你的清理逻辑是「删除 30 天前的数据」,而表恰好有分区,用 DROP_PARTITIONS 效率高几个数量级
  6. projection_usage 分析与 ETL 上线流程集成(投入:1 天开发,产出:防止新僵尸表) 在新表上线 checklist 中加入一项:「确认旧表是否有下游依赖,如有则在 N 天后清理旧表」
  7. Eon 模式检查 Depot pin 状态(投入:1 分钟,产出:避免冷表 pin 浪费 Depot 空间) 默认 LRU 会自动淘汰冷数据。但如果表曾被 SET_DEPOT_PIN_POLICY_TABLE pin 住,用 CLEAR_DEPOT_PIN_POLICY_TABLE 解除,释放被占用的 Depot 缓存
  8. 为所有用户表添加 COMMENT(投入:2 分钟/表,产出:告别「这张表是谁建的」之问) COMMENT ON TABLE 记录业务负责人、预期保留期和归档策略。半年后你不用猜
  9. 关注 ROS 容器数 > 50 的表(投入:每月 5 分钟,产出:减少 mergeout I/O 竞争) 容器数过高说明该表碎片化严重或加载模式有问题。及早发现可以避免演变成性能问题
  10. 区分「不查询」和「不写入」(投入:理解本文的概念,产出:精准的操作决策) 一张表可能 projection_usage 中没有记录(没人查),但 ETL 仍然每天写入——这说明下游报表已下线但上游加载未停。清理时不仅要删表,还要停掉对应的 ETL,否则下次又会重新加载数据

扩展阅读