Vertica 宽表(多列表)的存储与查询优化¶
作者:JiangChong | 撰写时间:2026年06月
适用场景框: 你的表包含上百甚至数百列,查询越来越慢、Catalog 越来越大、备份恢复时间越来越长,同时你怀疑问题出在「列太多」但不知道如何定量分析和解决。
关联文章:
- Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 — 查询性能分析的完整方法论
- 理解 Vertica 的分区 — 分区对 ROS 容器数量的影响
- Vertica 节点与集群规模规划指南 — 硬件规划中的列数考量
- Vertica 大表统计信息维护最佳实践 — 统计信息与查询计划的关系
- Vertica CPU 持续高负载诊断与优化 — CPU 维度补充分析
- Vertica 内存压力诊断与调优 — 内存维度补充分析
理解全文脉络: 文章从 Vertica 列存储的底层原理出发,解释宽表为什么天生与列存「不对付」;然后从系统级监控到逐列诊断,教会你量化宽表的影响;接着给出从快速见效到根本治理的分层解决方案;最后通过虚构案例、真实案例和完整演练串联所有知识点。如果你只是想快速定位问题,可以直接跳转到第 7 节:快速诊断 SQL 工具箱。
第 1 节:原理理解 — 为什么宽表在 Vertica 中是问题¶
1.1 Vertica 列存储的物理真相¶
传统行式数据库(如 MySQL、PostgreSQL)将一行数据完整地存储在一起:一行 = 一个连续的磁盘块。读取一行时,所有列的数据都被一起拉进内存。
Vertica 完全不同。 在 Vertica 的列存架构中,每个列的数据独立存储在各自的文件中。一张 100 列的表,每次数据加载(COPY 语句)会为每个节点上的每个投影创建至少 100 个列数据文件——每列存储在一个独立文件中(v7.2 之前每列分为数据文件和索引文件两个文件,v7.2 起两者合并为一个文件)。这些文件都归属于一个 ROS(Read Optimized Storage)容器。
这意味着,一次 COPY 加载在磁盘上产生的是一组文件,而非一个文件。具体来说:
一次 COPY → 一个 ROS 容器 → 该容器内,表有多少列就有多少个文件(每列一个)。
所以,如果一张表有 200 列,每次 COPY 落盘就是 200 个文件。假设这张表在 3 节点集群上,K-safety=1(投影有 b0 和 b1 两个 buddy,各 buddy 在所有节点上都有 segment——即每个节点上同时存在 b0 和 b1 的数据片段),Tuple Mover 还没来得及把多次加载合并——当前每个投影 segment 在每个节点上累积了 50 个 ROS 容器。那么:
一个投影segment在一个节点上的文件数 = 50 个 ROS 容器 × 200 列/容器 = 10,000 个
整张表的总文件数 = 10,000 × 3 节点 × 2 投影segment = 60,000 个
这只是一张表。 加上分区后,每个分区的数据隔离到独立的 ROS 容器中,文件数可能再翻几倍(详见 分区与 ROS 文件数量的关系)。
1.2 宽表对 Vertica 的三重打击¶
宽表(通常指超过 100 列,尤其是超过 200 列的表)从三个维度影响 Vertica 性能:
| 影响维度 | 机制 | 典型症状 |
|---|---|---|
| Catalog 膨胀 | 每列每 ROS 容器都在 catalog 中有元数据记录;列数×ROS 容器数的乘积决定了 catalog 大小 | v_catalog 查询变慢、数据库启动时间延长、备份/恢复耗时增加 |
| 文件句柄压力 | 查询需要打开它访问的每一列的文件句柄;SELECT * 在宽表上会打开所有列的文件 |
peak file handles 计数器飙升、操作系统 ulimit -n 可能触及上限 |
| 查询启动延迟 | 优化器需要为每列读取元数据(统计信息、编码类型、min/max 值);列越多,Plan 阶段的 catalog 锁持有时间越长 | PreparePlan 阶段耗时异常长、高并发时 catalog 锁争抢 |
通俗比喻: 行式数据库像一本装订好的书——翻到某一页就能看到全部内容。Vertica 列存则像一个图书馆的卡片目录系统——每列是一个独立的卡片柜。读 10 列就像跑 10 个柜台取资料,一个人跑没问题;但 200 列的宽表就像要跑 200 个柜台,光「找到柜台在哪」就消耗大量时间。
1.3 触发条件¶
宽表问题并非列数一多就立即出现,而是在以下条件叠加时才会严重化:
- 列数 > 100 且每列都被查询引用(
SELECT *或大宽表 JOIN) - 频繁的小批量数据加载(导致大量 ROS 容器,列数 × ROS 容器数乘积爆炸)
- 默认投影的 ORDER BY 和 SEGMENTATION 包含过多列(
MaxAutoSegColumns参数控制自动投影的 hash 分段列数:v9.x 之前默认 32 列,v10.x 起默认 8 列。宽表下即使 8 列的 hash 计算仍是可观 CPU 开销) - 没有针对性的编码策略(全部使用默认 AUTO 编码,存储浪费叠加列数放大)
1.4 问题来源总结¶
| 来源 | 机制 | 影响 |
|---|---|---|
| 列数 × ROS 容器 | 元数据量 = 列数 × ROS 容器数;目录大小线性增长 | Catalog 膨胀,所有元数据操作变慢 |
| SELECT * 文件句柄 | 查询打开所有列的文件句柄,每 ROS 容器每列一个 | 文件句柄耗尽,查询失败 |
| 默认分段覆盖全列 | hash(col1, col2, ..., col32) 的 CPU 开销随列数线性增长 |
加载和查询的 CPU 开销增加 |
| 编码策略缺失 | AUTO 编码对低基数列可能浪费空间,且解压多耗 CPU | 存储浪费、查询时 CPU 解码开销大 |
第 2 节:系统级监控 — 从宏观指标入手¶
在定位到具体表之前,先通过系统级指标判断是否存在宽表导致的系统性问题。
2.1 检查 Catalog 大小¶
Catalog 大小是宽表问题最直接的宏观指标。每列在每个 ROS 容器中都有元数据记录,宽表的 catalog 通常比窄表大一个数量级。
-- 查看各节点 catalog 内存峰值(最近 1 小时内每秒采样,取每节点峰值)
-- dc_ 前缀表属于 v_internal schema(数据采集器表)
SELECT nvl(n.subcluster_name, 'Enterprise Mode') AS subcluster_name,
foo.node_name,
NOW() AS check_time,
MAX(catalog_size_in_MB) AS catalog_size_mb
FROM (
SELECT node_name,
SUM((total_memory_max_value - free_memory_min_value)) / (1024*1024) AS catalog_size_in_MB
FROM v_internal.dc_allocation_pool_statistics_by_second
WHERE "time" > CURRENT_TIMESTAMP - INTERVAL '1 hour'
AND total_memory_max_value > 0
GROUP BY node_name, TRUNC("time"::TIMESTAMP, 'SS'::VARCHAR(2))
) foo
LEFT JOIN v_catalog.nodes n ON foo.node_name = n.node_name
GROUP BY 1, 2
ORDER BY 1, 2;
如何解读结果:
catalog_size_mb > 15000 MB(15 GB):属于偏大的 catalog,需要关注。超过 20 GB 则属于严重,数据库启动和备份恢复都会明显变慢。- 各节点
catalog_size_mb差异 > 20%:可能某个节点的 ROS 容器数多于其他节点,存在数据倾斜或 Tuple Mover 合并不均衡。 catalog_size_mb持续增长而不回落:说明 Tuple Mover mergeout 速度跟不上新数据加载速度。
2.2 统计每个投影的列数与 ROS 容器数¶
这个查询帮你找出哪些投影既是宽表又有大量 ROS 容器——这是问题的「热点」组合。
-- 按投影统计列数和 ROS 容器数,定位宽表+多 ROS 的热点投影
-- 先按投影聚合节点级指标,再 JOIN projection_columns 获取列数
-- 注意:① 不能直接 JOIN 后 SUM(row_count),会被列数放大
-- ② 通过 is_segmented 区分分段/复制投影:分段投影行数 SUM(各节点分片求和),
-- 复制投影行数 MAX(每节点存全量,不能加);存储总量统一 SUM(含副本冗余)
WITH proj_stats AS (
SELECT ps.projection_id,
ps.projection_schema,
ps.projection_name,
ps.anchor_table_name,
pi.is_segmented,
MAX(ps.ros_count) AS max_ros_per_node,
CASE WHEN pi.is_segmented
THEN SUM(ps.row_count)
ELSE MAX(ps.row_count)
END AS total_rows,
SUM(ps.used_bytes) AS total_bytes
FROM v_monitor.projection_storage ps
JOIN (SELECT DISTINCT projection_id, is_segmented FROM v_catalog.projections) pi
ON ps.projection_id = pi.projection_id
GROUP BY 1, 2, 3, 4, pi.is_segmented
)
SELECT ps.projection_schema,
ps.projection_name,
ps.anchor_table_name,
COUNT(DISTINCT pc.column_id) AS column_count,
ps.max_ros_per_node,
ps.total_rows,
ROUND(ps.total_bytes / 1024^3::NUMERIC(10,2), 2) AS total_gb
FROM proj_stats ps
JOIN v_catalog.projection_columns pc ON ps.projection_id = pc.projection_id
GROUP BY 1, 2, 3, 5, 6, 7
HAVING COUNT(DISTINCT pc.column_id) > 50 -- 只看超过 50 列的宽表
ORDER BY COUNT(DISTINCT pc.column_id) * ps.max_ros_per_node DESC -- 列数×ROS容器数
LIMIT 20;
如何解读结果:
column_count * max_ros_per_node超过 5000:这个投影的元数据规模开始令人担忧。例如 100 列 × 50 ROS 容器 = 5000。total_rows:分段投影为集群逻辑行数(各节点分片求和);复制投影取单节点行数(避免重复计算)。total_gb:集群总存储占用(含副本冗余),不分段/复制投影均按各节点存储求和。total_gb很大但max_ros_per_node不高:存储优化尚可,但列数多本身可能影响查询。max_ros_per_node> 500:接近 1024 的 ROS pushback 阈值(参见 理解 Vertica 的分区 §3.2),需要立即关注。
2.3 检查文件句柄使用峰值¶
文件句柄是宽表查询最容易触及的操作系统级瓶颈。
-- 查看最近执行中查询的文件句柄峰值
SELECT transaction_id,
statement_id,
operator_name,
path_id,
MAX(CASE WHEN counter_name = 'peak file handles' THEN counter_value END) AS peak_file_handles,
MAX(CASE WHEN counter_name = 'execution time (us)' THEN counter_value/1000000 END) AS exec_seconds
FROM v_monitor.execution_engine_profiles
WHERE counter_name IN ('peak file handles', 'execution time (us)')
AND (transaction_id, statement_id) IN (
SELECT transaction_id, statement_id FROM v_monitor.query_profiles
WHERE query_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 day'
ORDER BY query_duration_us DESC LIMIT 100
)
GROUP BY 1, 2, 3, 4
HAVING MAX(CASE WHEN counter_name = 'peak file handles' THEN counter_value END) > 100
ORDER BY peak_file_handles DESC
LIMIT 20;
如何解读结果:
- peak_file_handles > 1000:查询打开的文件句柄数偏高,宽表
SELECT *的典型特征。 - peak_file_handles > 5000:非常危险,接近 Linux 默认的
ulimit -n上限(通常是 65536),多查询并发时可能触及限制。 - operator_name = 'Scan' 且文件句柄很高:最典型的宽表扫描场景。
通俗解释: 文件句柄就像一个人能同时打开的书本数量。列存数据库中每列都是一个独立的文件,宽表查询就像同时翻 200 本书——操作系统只允许同时打开有限数量的文件,超出就会报错。
2.4 Linux 层面:检查文件系统文件数¶
SSH 到集群节点,统计 Vertica 数据目录下的文件总数,这是最底层的验证手段。
# 统计单个节点上的 Vertica ROS 文件总数
# 注意:/data/vertica/data 需替换为实际的 Vertica 数据目录
find /data/vertica/data -type f | wc -l
# 按投影目录查看文件数分布
for dir in /data/vertica/data/*/; do
count=$(find "$dir" -type f | wc -l)
echo "$count $(basename $dir)"
done | sort -rn | head -20
如何解读结果:
- 文件数持续增长:Tuple Mover mergeout 可能跟不上加载速度,需要检查 Tuple Mover 配置。
第 3 节:逐步定位根因 — 从宏观到微观¶
系统级监控确认存在宽表问题后,需要定位到具体的表和列,找出根因。
3.1 第一步:找到列数最多的表¶
-- 找到列数最多的表(v_catalog.projections 本身不含系统 schema 行,无需额外过滤)
SELECT p.projection_schema,
p.anchor_table_name,
COUNT(DISTINCT pc.table_column_name) AS column_count,
COUNT(DISTINCT p.projection_basename) AS projection_count
FROM v_catalog.projections p
JOIN v_catalog.projection_columns pc
ON p.projection_id = pc.projection_id
WHERE p.is_super_projection
GROUP BY 1, 2
ORDER BY column_count DESC
LIMIT 20;
如何解读:
- column_count > 100:属于宽表范畴,需要进一步分析。
- column_count > 200:严重宽表,查询和存储都可能存在问题。
- projection_count > 2:除了 super projection 和 buddy projection 外,还有额外的自定义投影,这本身也会增加 catalog 开销,如非必要应考虑清理(参见 表约束产生的projection问题)。
如果这张表不是你的目标表,进入下一步。
3.2 第二步:分析宽表的每列存储占用与编码¶
找到目标宽表后,逐列分析存储占用和编码策略,找出浪费空间最大的列。
-- 注意:将 'your_table_name' 替换为实际的表名
-- 分析某张宽表每列的存储占用和编码
-- total_rows:分段投影为集群逻辑行数,复制投影取单节点值
-- total_mb:集群总存储(含所有投影副本),用于评估优化收益
SELECT cs.anchor_table_column_name AS column_name,
cs.encodings,
cs.compressions,
CASE WHEN pi.is_segmented
THEN SUM(cs.row_count)
ELSE MAX(cs.row_count)
END AS total_rows,
SUM(cs.used_bytes) AS total_bytes,
ROUND(SUM(cs.used_bytes) / 1024^2::NUMERIC(10,2), 2) AS total_mb,
MAX(cs.ros_count) AS max_ros_per_node,
pc.data_type,
pc.statistics_type
FROM v_monitor.column_storage cs
JOIN v_catalog.projection_columns pc
ON cs.projection_id = pc.projection_id
AND cs.column_id = pc.column_id
JOIN (SELECT DISTINCT projection_id, is_segmented FROM v_catalog.projections) pi
ON cs.projection_id = pi.projection_id
WHERE cs.anchor_table_schema = 'public'
AND cs.anchor_table_name = 'your_table_name'
GROUP BY 1, 2, 3, pi.is_segmented, pc.data_type, pc.statistics_type
ORDER BY total_bytes DESC;
如何解读:
total_rows:分段投影为集群逻辑行数(各节点分片求和),复制投影取单节点值(避免重复计算)。total_mb> 1 GB 的单列:该列可能是 VARCHAR(65000) 且存储大量数据,或者编码策略不当。total_mb为集群总存储(含所有投影副本),编码优化可同时压缩所有副本的存储。encodings = 'AUTO'但基数列是低基数:例如status字段只有 5 个取值但使用 AUTO(即 Delta Int Pack),换成 RLE 后压缩比可以从 5:1 提升到 100:1 甚至更高。compressions != 'none'(如 lzo)但encodings仍为 LZO:说明列只是被压缩了,但没有选择最适合的编码类型。编码优先于压缩——好的编码让 Vertica 可以直接在编码数据上运算而不必解压。statistics_type为空或ROWCOUNT:该列缺乏直方图统计,优化器无法做出好的代价估算,可能导致查询计划不优。
编码 vs 压缩的区别: 编码(encoding)是对数据值的数学转换,Vertica 可以直接在编码后的数据上执行运算而无需解码。压缩(compression)如 LZO 是对二进制数据的压缩,查询前必须先解压缩。优先选对编码,压缩是最后的手段。
3.3 第三步:分析默认投影的 ORDER BY 和分段设计¶
宽表的默认投影往往把过多列放入 ORDER BY 或 SEGMENTATION 中,这是隐藏的 CPU 开销来源。
-- 注意:将 'your_table_name' 替换为实际的表名
-- 查看 super projection 的排序和分段列
SELECT p.projection_name,
p.is_super_projection,
p.segment_expression,
p.is_segmented,
COUNT(pc.sort_position) AS sort_column_count,
-- 列出排序位置靠前的列
MAX(CASE WHEN pc.sort_position BETWEEN 0 AND 4 THEN pc.projection_column_name END) AS sort_col_0_4
FROM v_catalog.projections p
JOIN v_catalog.projection_columns pc
ON p.projection_id = pc.projection_id
WHERE p.anchor_table_name = 'your_table_name'
AND p.anchor_table_schema = 'public'
AND pc.sort_position >= 0
GROUP BY 1, 2, 3, 4
ORDER BY p.is_super_projection DESC;
如何解读:
segment_expression中包含大量列(如hash(col1, col2, ..., col8)):这是默认行为(MaxAutoSegColumns参数控制自动投影的 hash 分段列数,v9.x 之前为 32 列,v10.x 起默认 8 列)。宽表下对 8 列做 hash 分段仍有 CPU 开销。建议将分段列精简到 2-4 个高基数列。sort_column_count等于列总数:这说明投影 ORDER BY 包含了所有列(默认行为),大部分列在查询中其实不会被用作 GROUP BY 或 JOIN 键,不需要出现在排序里。
3.4 第四步:量化查询中实际使用的列数¶
很多时候 SELECT * 是默认行为,但实际上应用程序只用到其中 10-20 列。通过分析真实查询中引用的列,可以确定哪些列可以拆出去。
-- 注意:将 'your_table_name' 替换为实际的表名
-- 查看最近查询对该宽表的列引用情况
SELECT LEFT(qp.query, 200) AS query_snippet,
qp.query_start,
ROUND(qp.query_duration_us / 1000000::NUMERIC(10,2), 2) AS duration_sec
FROM v_monitor.query_profiles qp
WHERE qp.query ILIKE '%your_table_name%'
AND qp.query_start > CURRENT_TIMESTAMP - INTERVAL '7 days'
ORDER BY qp.query_duration_us DESC
LIMIT 20;
如何解读:
- 手动抽查 SQL 文本,看实际引用了哪些列。通常你会发现大部分查询只用到 20-30% 的列。
- 如果大量查询都是
SELECT *,可以推动应用程序改为只选择需要的列。 duration_sec很高的查询如果只引用少量列但扫描了整个宽表,说明投影设计有改进空间(可以创建只包含常查询列的窄投影)。
3.5 第五步:检查 DELETE_VECTOR 累积¶
宽表如果频繁进行 DELETE 或 UPDATE 操作,delete vector 的累积会进一步放大存储和查询开销。
-- 注意:将 'your_table_name' 替换为实际的表名
-- 检查宽表的 delete vector 累积情况
SELECT sc.node_name,
sc.projection_name,
SUM(sc.deleted_row_count) AS total_deleted_rows,
SUM(sc.total_row_count) AS total_rows,
ROUND(SUM(sc.deleted_row_count) / NULLIF(SUM(sc.total_row_count), 0)::NUMERIC(10,2) * 100, 2) AS deleted_pct,
SUM(sc.disk_size) AS total_disk_bytes,
ROUND(SUM(sc.disk_size) / 1024^3::NUMERIC(10,2), 2) AS total_disk_gb
FROM v_monitor.storage_containers sc
JOIN v_catalog.projections p
ON sc.projection_id = p.projection_id
WHERE p.anchor_table_name = 'your_table_name'
AND p.anchor_table_schema = 'public'
GROUP BY 1, 2
ORDER BY deleted_pct DESC;
如何解读:
deleted_pct > 20%:超过 20% 的已删除行仍在磁盘上占用空间,应考虑执行PURGE()或使用DROP_PARTITIONS代替 DELETE。- 宽表的 DELETE 尤其昂贵:因为每列的 delete vector 都需要被读取和计算。这也是为什么宽表应该优先用分区做数据生命周期管理(分区管理数据的好处)。
第 4 节:解决方案 — 从快速见效到根本治理¶
4.1 立即措施(当天可执行)¶
4.1.1 清洗 ROS 容器:执行 PURGE 和 mergeout¶
如果发现大量 delete vector 或 ROS 容器碎片化,先清理再优化。
-- 注意:将 'your_table_name' 替换为实际的表名
-- 清理 delete vector
SELECT PURGE_TABLE('public.your_table_name');
-- 强制触发 Tuple Mover mergeout(将小 ROS 容器合并)
SELECT DO_TM_TASK('mergeout', 'public.your_table_name');
为什么这样做: 清理 delete vector 后,ROS 容器中的数据更纯粹,减少查询时的行过滤开销。mergeout 将碎片化的小 ROS 合并为大 ROS,减少文件总数。
4.1.2 对低基数列启用 RLE 编码¶
这是投入产出比最高的单步优化。找到低基数(distinct 值 < 1000)且出现在 ORDER BY 子句中的列,将编码改为 RLE。
-- 注意:将 'your_table_name' 和 'low_card_column' 替换为实际值
-- 第一步:查看当前编码类型
SELECT projection_column_name, encoding_type, sort_position
FROM v_catalog.projection_columns
WHERE table_name = 'your_table_name'
AND table_schema = 'public'
AND sort_position >= 0
ORDER BY sort_position;
-- 第二步:通过 Database Designer 重新分析编码,而非逐个手动修改
-- 对单个投影运行 encoding 分析(不实际修改)
SELECT DESIGNER_DESIGN_PROJECTION_ENCODINGS('public.your_table_name', '/tmp/encoding_design', false, true);
为什么这样做: RLE(Run-Length Encoding)对排序的低基数列压缩比极高。在极端场景中,一个有 5 个 distinct 值的 status 列在运行 RLE 后可以从 500 MB 压缩到 5 MB。而且 Vertica 执行引擎可以在 RLE 编码数据上直接做聚合而不需要物化解压(参考 查询引擎中的 RLE 优化)。
为什么用 DBD 而不是手动改: 逐个修改列的编码类型既繁琐又容易犯错。Database Designer 会基于实际数据采样(默认 1%)分析列的数据特征,给出最优编码组合。手动指定编码只能基于你对数据分布的猜测。
4.1.3 精简分段列¶
如果 SEGMENTATION 包含 10+ 列,立即创建新的投影,将分段列精简到 2-4 个高基数列。
-- 注意:将 'your_table_name'、'high_card_key1' 等替换为实际值
-- 为宽表创建一个精简分段的新 super projection
CREATE PROJECTION public.your_table_optimized_super
/*+basename(your_table),createtype(L)*/
(
-- 列出所有列(因为 super projection 必须包含所有列)
col1, col2, ..., colN
)
AS
SELECT col1, col2, ..., colN
FROM public.your_table_name
ORDER BY high_card_key1, high_card_key2 -- 按常用 JOIN/GROUP BY 键排序
SEGMENTED BY hash(high_card_key1, high_card_key2) ALL NODES;
-- 刷新新投影数据
SELECT REFRESH('public.your_table_optimized_super');
-- 确认新投影数据追平后,删除旧的自动投影
DROP PROJECTION public.your_table_name_b0;
DROP PROJECTION public.your_table_name_b1;
为什么这样做: 分段 hash 是对分段列中所有列做 hash 运算。32 列的 hash 计算成本是 4 列的 8 倍以上。而且分段列太多并不提升分布均匀性,2-4 个高基数(> 100K distinct)列已经足够让数据均匀分布到所有节点。
⚠️ 注意: 删除 super projection 后
your_table_name_b0/b1需要确保已有替代的 buddy projection pair。如果使用 OFFSET 0,至少需要创建一对 buddy 投影以维持 K-safety。
4.2 短期优化(当周执行)¶
4.2.1 创建列子集投影¶
宽表的最大矛盾是:表有 200 列,但 80% 的查询只用到其中 20 列。 创建一个只包含这些「热列」的自定义投影,让多数查询走窄投影。
-- 注意:将 'your_table_name'、'hot_col1' 等替换为实际值
-- 创建一个只包含常查询列的「热列投影」
CREATE PROJECTION public.your_table_hot
/*+basename(your_table_hot)*/
(
hot_col1, hot_col2, hot_col3, -- 高频查询列
join_key, -- JOIN 键
date_key, -- 分区/过滤键
hot_col4, hot_col5
)
AS
SELECT hot_col1, hot_col2, hot_col3,
join_key, date_key, hot_col4, hot_col5
FROM public.your_table_name
ORDER BY date_key, join_key
SEGMENTED BY hash(join_key) ALL NODES;
SELECT REFRESH('public.your_table_hot');
为什么这样做: 这个投影只有 7 列而非 200 列。Scan 操作符只需要打开 7 个文件句柄而非 200 个,catalog 锁定周期也相应缩短。Vertica 优化器会自动选择投影——如果查询引用的列全部在热列投影中,优化器会选择它而非 super projection。
如何验证优化器选择了正确的投影: 使用
PROFILE执行查询后,检查v_monitor.projection_usage:
4.2.2 为宽表中的特殊列选择合适的编码¶
并非所有列都适合 AUTO 编码。根据数据特征选择编码,可以在不牺牲查询性能的前提下大幅压缩存储。
编码速查表(基于 vault 已有资源 Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 §3.10.2 的编码选择指南):
| 数据特征 | 推荐编码 | 典型压缩比 | 适用场景 |
|---|---|---|---|
| 低基数(< 1000 distinct)且排序 | RLE |
50:1 ~ 100:1 | 状态码、标志位、类别 |
| 低基数(< 1000 distinct)未排序 | BLOCK_DICT 或 BLOCKDICT_COMP |
20:1 ~ 50:1 | 未排序的枚举列 |
| 自增 ID / 连续时间戳 | COMMONDELTA_COMP |
100:1 ~ 1000:1 | 序列主键、等间隔时间 |
| 窄范围整数 | DELTAVAL |
5:1 ~ 20:1 | 年龄、分数、序号 |
| 公因数倍数 | GCDDELTA |
5:1 ~ 20:1 | 时间戳毫秒(1000 的倍数) |
| 大文本/VARCHAR(>1000) | ZSTD_COMP 或 GZIP_COMP |
5:1 ~ 10:1 | 日志、描述、JSON |
| 高基数数值 | AUTO(默认) |
2:1 ~ 5:1 | 金额、度量值 |
关键原则:
- 排序决定编码效果。 RLE 和 COMMONDELTA_COMP 只有在列按该列排序时才能发挥最大威力。如果列不在 ORDER BY 中,即使改了编码也收益有限。
- 让 DBD 做决策,而不是凭直觉。 运行
DESIGNER_DESIGN_PROJECTION_ENCODINGS()让 Vertica 分析实际数据后给出建议。
4.2.3 用分区代替宽表的部分列¶
宽表中常见的模式是:前 50 列是业务属性、中间 100 列是不同日期的指标值。这种「横向扩展」的宽表在设计上有天然的列式存储劣势。
推荐方案: 将「日期-指标」这一块转置为行存储(即长表),利用 Vertica 的分区和列存优势。
-- 将宽表(每个日期一个列)改为长表(日期+指标值一行)
-- 宽表结构: id, attr1, attr2, metric_20240101, metric_20240102, ...
-- 长表结构: id, attr1, attr2, metric_date, metric_value
-- 长表的优势:
-- 1. 列数固定(5 列),不再是「每加一天就加一列」
-- 2. 按 metric_date 分区,利用存储裁剪
-- 3. 编码只在 1 个 metric_value 列上生效,而非 100 个列上重复配置
CREATE TABLE public.your_table_long (
id INTEGER NOT NULL,
attr1 VARCHAR(100),
attr2 VARCHAR(100),
metric_date DATE NOT NULL,
metric_value NUMERIC(18,2)
)
PARTITION BY EXTRACT(year FROM metric_date);
-- 从宽表转置数据到长表
INSERT INTO public.your_table_long
SELECT id, attr1, attr2,
'2024-01-01'::DATE, metric_20240101 FROM public.your_wide_table WHERE metric_20240101 IS NOT NULL
UNION ALL
SELECT id, attr1, attr2,
'2024-01-02'::DATE, metric_20240102 FROM public.your_wide_table WHERE metric_20240102 IS NOT NULL
-- ... 继续其他日期列
;
为什么这样做: 宽表到长表的转换看似增加了行数,但列数从 100+ 降到 5-10,ROS 文件数大幅减少,且 Vertica 的列存架构天然擅长对少量列做大规模聚合扫描。
4.3 根本治理论 — 宽表拆分¶
如果宽表确实需要保留所有列(所有列在业务上都被频繁查询),那么问题不在投影设计而在数据模型设计。此时应该考虑对表进行拆分。
拆分决策矩阵:
| 场景 | 拆分策略 | 预期效果 |
|---|---|---|
| 表有 200+ 列,其中 50 列是「主实体属性」,150 列是「关联指标」 | 拆为 1 个主表 + 1 个指标表,通过主键 JOIN | 列数减半,每个表可以使用适合的编码和投影 |
| 按业务域可以自然分组(如用户画像 80 列 + 交易行为 120 列合并为 200 列) | 拆为 2 个域表,通过主键 JOIN | 列数大幅下降,且各域的查询互不干扰 |
| 表中大量列为 NULL(稀疏列) | 拆为 1 个稠密表 + 多个「宽表稀疏属性」表 | 减少 NULL 值存储,提升编码效率 |
| 不同列的查询频率差异大(热列 20 个 + 冷列 180 个) | 用列子集投影代替拆分(4.2.1),无需改应用 | 0 应用改造,即刻见效 |
拆分前务必验证 JOIN 性能: 使用 PROFILE 测试拆分后的 JOIN 查询,确保 Vertica 优化器能利用相同的分段键(identical segmentation)避免数据 Resegment。如果两个表的分段键一致,JOIN 可以在本地节点完成,无需网络重分布(见 Identical Segmentation 原理)。
第 5 节:深入案例¶
> 📝 虚构案例 · 运营商经分 300 列表的 Catalog 爆炸¶
场景: 某运营商经分系统,一张「用户日汇总表」包含 300 列(基础属性 50 列 + 日指标 250 列),3 节点 × K-safety=1(2 个 buddy 投影),按天分区,保留 180 天。每天凌晨批量加载约 500 万行。
问题发现: DBA 发现数据库启动时间从 3 分钟逐步增长到 45 分钟,SELECT * FROM v_catalog.projections 耗时从 0.1 秒变成 8 秒。
诊断过程:
-- 检查 catalog 大小
SELECT node_name,
MAX(catalog_size_in_MB) AS catalog_mb
FROM (
SELECT node_name,
SUM((total_memory_max_value - free_memory_min_value)) / (1024*1024) AS catalog_size_in_MB
FROM v_internal.dc_allocation_pool_statistics_by_second
WHERE total_memory_max_value > 0
GROUP BY node_name, TRUNC("time"::TIMESTAMP, 'SS'::VARCHAR(2))
) foo
GROUP BY node_name
ORDER BY node_name;
输出:
node_name | catalog_mb
------------------+------------
v_db_node0001 | 12453
v_db_node0002 | 11982
v_db_node0003 | 12117
catalog 超过 12 GB。
-- 定位热点表
WITH proj_stats AS (
SELECT ps.projection_id, ps.anchor_table_name,
pi.is_segmented,
MAX(ps.ros_count) AS max_ros_count,
CASE WHEN pi.is_segmented
THEN SUM(ps.row_count)
ELSE MAX(ps.row_count)
END AS total_rows,
SUM(ps.used_bytes) AS total_bytes
FROM v_monitor.projection_storage ps
JOIN (SELECT DISTINCT projection_id, is_segmented FROM v_catalog.projections) pi
ON ps.projection_id = pi.projection_id
WHERE ps.anchor_table_schema = 'public'
GROUP BY 1, 2, pi.is_segmented
)
SELECT ps.anchor_table_name,
COUNT(DISTINCT pc.column_id) AS column_count,
ps.max_ros_count,
ps.total_bytes / 1024^3 AS total_gb
FROM proj_stats ps
JOIN v_catalog.projection_columns pc ON ps.projection_id = pc.projection_id
GROUP BY 1, 3, 4
ORDER BY column_count DESC
LIMIT 5;
输出:
anchor_table_name | column_count | max_ros_count | total_gb
--------------------------+--------------+---------------+----------
user_daily_summary | 300 | 180 | 856
call_detail | 85 | 30 | 234
sms_center | 42 | 15 | 87
根因分析:
每个节点上 b0 和 b1 两个投影 segment 各含 180 个分区,单节点列数据文件数 = 300列 × 2投影segment × 180分区 ≈ 108,000 个(3 节点共计 324,000 个)。仅这一张表的 catalog 元数据就达到约 3.5 GB(3 × 108,000 × ~3KB/条)。每天新增一批分区,catalog 持续膨胀。
修复方案:
- 立即:
SELECT DO_TM_TASK('mergeout', 'public.user_daily_summary')减少活动分区中的小 ROS 容器 - 短期:创建「热列投影」(50 列),让 80% 的日常查询走窄投影
- 根本治理:将 250 个日指标列转置为长表
(user_id, attr_1..50, metric_date, metric_name, metric_value),分区按metric_date;列数从 300 降到 55
效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| Catalog 大小 | 12 GB | 2.8 GB |
| 数据库启动时间 | 45 分钟 | 6 分钟 |
| 典型日常查询延迟 | 18 秒 | 2.3 秒 |
| 单节点列数据文件数 | 108,000 | 38,000 |
> 📝 虚构案例 · 金融风控 500 列表的查询超时¶
场景: 某金融机构风控系统,一张「借款人特征宽表」包含 500 列。每个借款人有 500 个维度的特征,用于机器学习模型评分。每天跑批量评分,需要 SELECT * 扫描全表约 2000 万行。
问题: 批量评分 SQL 频繁超时(30 分钟超时阈值),执行计划显示 Scan 操作符耗时占 95%。
诊断过程:
-- 用 PROFILE 执行一条代表性查询
PROFILE SELECT * FROM public.borrower_features WHERE batch_date = '2026-06-05';
-- 查看执行引擎耗时分布
SELECT operator_name, path_id,
SUM(CASE WHEN counter_name = 'execution time (us)' THEN counter_value END) / 1000000 AS exec_sec,
MAX(CASE WHEN counter_name = 'peak file handles' THEN counter_value END) AS peak_fh,
SUM(CASE WHEN counter_name = 'bytes read from disk' THEN counter_value END) / 1024^3 AS read_gb
FROM v_monitor.execution_engine_profiles
WHERE transaction_id = :t_id AND statement_id = :s_id
GROUP BY 1, 2
ORDER BY exec_sec DESC;
输出:
operator_name | path_id | exec_sec | peak_fh | read_gb
---------------+---------+----------+---------+---------
Scan | 3 | 1420 | 9500 | 156
Scan 操作符打开 9500 个文件句柄,从磁盘读取 156 GB 数据(远超实际需要的 2000 万行 × 500 列 × 8 字节 ≈ 80 GB 估算,说明存在数据碎片问题)。
根因分析:
- 500 列 × 每节点 19 个 ROS 容器 = 单节点 Scan 管理约 9500 个文件句柄
- 大量 delete vector(每日更新风控特征导致)叠加,实际扫描数据是有效数据的 2 倍
- 所有列使用 AUTO 编码,500 列中有 150 个低基数列(< 100 distinct),AUTO 对它们浪费了大量存储
修复方案:
- 先
SELECT PURGE_TABLE('public.borrower_features')清理 delete vector -
运行
DESIGNER_DESIGN_PROJECTION_ENCODINGS()让 DBD 重新分析编码- 150 个低基数列从 AUTO 改为 RLE,存储从 180 GB 降到 12 GB
- 实际磁盘读取从 156 GB 降到 42 GB
- 创建「评分用热列投影」:只包含模型实际使用的 80 列而非全部 500 列
- 将每日增量更新改为分区交换(
SWAP_PARTITIONS_BETWEEN_TABLES):先向临时表加载新数据,再与原表做分区交换,避免产生 delete vector
效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 批量评分耗时 | > 30 分钟(超时) | 4.2 分钟 |
| peak file handles | 9500 | 520 |
| 磁盘读取 | 156 GB | 42 GB |
| 表存储大小 | 320 GB | 68 GB |
> 📋 真实案例 · 表约束导致额外 projection 增加宽表开销¶
来源:表约束产生的projection问题
场景: 某金融机构某业务表添加 UNIQUE 约束后,Vertica 自动创建了额外的约束相关 projection,导致该表的 projection 数量增加,在宽表场景下 catalog 开销进一步扩大。
关键发现:
- 启用约束才会产生约束相关 projection
- 如果约束字段已经位于 projection 排序分段字段的前面,则不会另外产生 projection
- 添加约束前,可手动创建一个排序分段仅包含约束字段、或将约束字段放在排序分段前几位的 projection,避免自动产生额外 projection
应对策略: 在宽表上添加约束之前,先确认现有的 super projection 的 ORDER BY 首列是否覆盖了约束字段。如果已覆盖,则不会产生额外 projection。这可以避免宽表因约束而产生更多 projection,进一步放大 catalog 开销。
第 6 节:完整诊断流程实战¶
📝 虚构场景 · 完整演练
背景: 某企业数据仓库的 ETL 开发团队反映「最近所有的表查询都变慢了」。你是 Vertica DBA,需要从零开始排查。
时间线¶
| 时间 | 动作 | 发现 |
|---|---|---|
| 09:00 | 用户反映查询变慢 | 对比上周同时间点,平均查询延迟从 3 秒升到 35 秒 |
| 09:15 | 检查系统级指标 | CPU 40%,内存 60%,I/O 等待 8%——看不出问题 |
| 09:30 | 检查 catalog 大小 | 8.5 GB,比上周的 3.2 GB 增长了 2.6 倍 |
排查步骤¶
Step 1 — 确认 catalog 膨胀:
SELECT node_name,
MAX(catalog_size_in_MB) AS catalog_mb
FROM (
SELECT node_name,
SUM((total_memory_max_value - free_memory_min_value)) / (1024*1024) AS catalog_size_in_MB
FROM v_internal.dc_allocation_pool_statistics_by_second
WHERE total_memory_max_value > 0
GROUP BY node_name, TRUNC("time"::TIMESTAMP, 'SS'::VARCHAR(2))
) foo
GROUP BY node_name
ORDER BY node_name;
node_name | catalog_mb
------------------+------------
v_etl_node0001 | 8720
v_etl_node0002 | 8450
v_etl_node0003 | 8510
判断: catalog 8.5 GB。
Step 2 — 找到最近的变更:
SELECT table_schema, table_name, create_time
FROM v_catalog.tables
WHERE create_time > CURRENT_TIMESTAMP - INTERVAL '7 days'
ORDER BY create_time DESC
LIMIT 10;
发现 3 天前创建了 etl_wide_export 表。
Step 3 — 分析该表的列数和存储:
WITH proj_stats AS (
SELECT ps.projection_id, ps.anchor_table_name,
pi.is_segmented,
MAX(ps.ros_count) AS max_ros,
CASE WHEN pi.is_segmented
THEN SUM(ps.row_count)
ELSE MAX(ps.row_count)
END AS total_rows,
SUM(ps.used_bytes) AS total_bytes
FROM v_monitor.projection_storage ps
JOIN (SELECT DISTINCT projection_id, is_segmented FROM v_catalog.projections) pi
ON ps.projection_id = pi.projection_id
WHERE ps.anchor_table_name = 'etl_wide_export'
GROUP BY 1, 2, pi.is_segmented
)
SELECT ps.anchor_table_name,
COUNT(DISTINCT pc.column_id) AS column_count,
ps.max_ros,
ROUND(ps.total_bytes/1024^3::NUMERIC(10,2), 2) AS total_gb
FROM proj_stats ps
JOIN v_catalog.projection_columns pc ON ps.projection_id = pc.projection_id
GROUP BY 1, 3, 4;
anchor_table_name | column_count | max_ros | total_gb
--------------------+--------------+---------+----------
etl_wide_export | 380 | 320 | 1250
判断: 380列 × 每投影segment 320 个 ROS 容器 × 2 投影segment = 每节点约 243,200 个列数据文件。仅这一张表就贡献了约 3.5 GB 的 catalog 增长。
Step 4 — 根因确认:
ETL 开发人员创建了 CREATE TABLE etl_wide_export AS SELECT * FROM ... 将 30 多个源表的所有列合并到一张宽表中,用于下游数据导出。每天有 48 批小批量加载(每 30 分钟一次),3 天内积累了 320 个 ROS 容器。
Step 5 — 修复:
- 将导出任务改为只 SELECT 下游实际需要的 50 列(而非 380 列)
- 删除
etl_wide_export表,用 VIEW 代替(VIEW 不实际存储数据) - 如果导出确实需要物化中间结果,创建只含 50 列的物化表
- 对物化表使用
DIRECT加载(COPY ... DIRECT)减少 ROS 容器碎片
效果: Catalog 从 8.5 GB 回落到 3.6 GB,查询延迟恢复到 3 秒以内。
第 7 节:快速诊断 SQL 工具箱¶
| 诊断目标 | SQL | 关键阈值 |
|---|---|---|
| 查看 catalog 峰值 | SELECT node_name, MAX(catalog_mb) FROM (SELECT node_name, SUM((total_memory_max_value-free_memory_min_value))/1048576 AS catalog_mb FROM v_internal.dc_allocation_pool_statistics_by_second WHERE total_memory_max_value>0 GROUP BY 1, TRUNC("time"::TIMESTAMP,'SS')) foo GROUP BY 1 ORDER BY 2 DESC |
> 15 GB 偏大,> 20 GB 严重 |
| 找出列数最多的表 | SELECT p.anchor_table_schema, p.anchor_table_name, COUNT(DISTINCT pc.table_column_name) AS col_cnt FROM v_catalog.projections p JOIN v_catalog.projection_columns pc ON p.projection_id = pc.projection_id WHERE p.is_super_projection GROUP BY 1,2 ORDER BY col_cnt DESC LIMIT 20 |
> 100 列关注,> 200 列严重 |
| 列数×ROS容器数热点 | SELECT ps.projection_name, COUNT(DISTINCT pc.column_id) col_cnt, MAX(ps.ros_count) max_ros FROM v_monitor.projection_storage ps JOIN (SELECT DISTINCT projection_id, is_segmented FROM v_catalog.projections) pi ON ps.projection_id=pi.projection_id JOIN v_catalog.projection_columns pc ON ps.projection_id=pc.projection_id GROUP BY 1 HAVING COUNT(DISTINCT pc.column_id) > 50 ORDER BY COUNT(DISTINCT pc.column_id)*MAX(ps.ros_count) DESC LIMIT 20 |
乘积 > 5000 关注 |
| 每列存储占用和编码 | SELECT cs.anchor_table_column_name, cs.encodings, SUM(cs.used_bytes)/1024^2 AS total_mb, MAX(cs.ros_count) max_ros FROM v_monitor.column_storage cs WHERE cs.anchor_table_name = ':t_name' GROUP BY 1,2 ORDER BY total_mb DESC |
单列 > 1 GB 且编码为 AUTO 应检查 |
| 检查分段列数 | SELECT p.projection_name, p.segment_expression, COUNT(pc.sort_position) sort_cols FROM v_catalog.projections p JOIN v_catalog.projection_columns pc ON p.projection_id=pc.projection_id WHERE p.anchor_table_name=':t_name' AND pc.sort_position>=0 GROUP BY 1,2 |
分段列 > 8 列应精简 |
| 查询文件句柄峰值 | SELECT transaction_id, MAX(CASE WHEN counter_name='peak file handles' THEN counter_value END) peak_fh FROM v_monitor.execution_engine_profiles WHERE counter_name IN ('peak file handles', 'execution time (us)') GROUP BY 1 HAVING MAX(CASE WHEN counter_name='peak file handles' THEN counter_value END) > 100 ORDER BY peak_fh DESC LIMIT 20 |
> 1000 偏高,> 5000 危险 |
| 检查 delete vector 占比 | SELECT sc.projection_name, SUM(sc.deleted_row_count)/NULLIF(SUM(sc.total_row_count),0)*100 AS del_pct, SUM(sc.disk_size)/1024^3 disk_gb FROM v_monitor.storage_containers sc JOIN v_catalog.projections p ON sc.projection_id=p.projection_id WHERE p.anchor_table_name=':t_name' GROUP BY 1 ORDER BY del_pct DESC |
> 20% 需要 PURGE |
| 查看查询使用哪些投影 | SELECT projection_name FROM v_monitor.projection_usage WHERE transaction_id = :t_id AND statement_id = :s_id |
确认走的是热列投影还是 super projection |
| 单节点文件数(Linux) | find /data/vertica/data -type f\|wc -l |
> 100 万需要关注 catalog 大小 |
第 8 节:最佳实践清单¶
按投入产出比从高到低排序:
- 永远不要用
SELECT *查询宽表 — 这是投入最小、产出最高的优化。在宽表上SELECT *相当于打开所有列的文件句柄并扫描所有列的磁盘数据。应用层明确列出需要的列名,就能避免 90% 的宽表性能问题。 - 让 Database Designer 选择编码,不要手动猜 — 运行
DESIGNER_DESIGN_PROJECTION_ENCODINGS()基于 1% 实际数据采样自动确定每列最优编码。手动选择编码容易在大量列面前出错,且 DBD 会考虑列之间的编码互作用。 - 精简分段列到 2-4 个高基数列 — 默认的 32 列 hash 分段在宽表上既浪费 CPU 又不提升分布均匀性。选择 DISTINCT 值 > 100K 的列做分段键,2-4 个足够。
- 低基数列 + 在 ORDER BY 中 = RLE 编码 — 这是 Vertica 列存压缩的「杀手锏」。一个只有 10 个 distinct 值的列在 RLE 下可以从 1 GB 压缩到 10 MB,且 Vertica 可以在编码数据上直接做聚合。
- 用列子集投影覆盖高频查询,而不是让所有查询走 super projection — 如果 80% 的查询只用到 20% 的列,创建一个只含这 20% 列的投影。文件句柄从 200 降到 20,Scan 时间可减少 50% 以上。
- 用分区代替宽表的「横向日期列」 — 如果宽表中有
metric_20240101, metric_20240102, ..., metric_20241231这 365 列,立刻转置为长表并按日期分区。列数从 400+ 降到 50+,且分区裁剪自动跳过不需要的日期。 - 定期
PURGE()清理 delete vector — delete vector 随时间累积会显著增加宽表的扫描开销。对于按分区做数据生命周期管理的表,用DROP_PARTITIONS取代 DELETE 可以完全避免 delete vector。 - 监控 ROS 容器数,不要让它超过 500 — 每个节点每投影的 ROS 容器数不应超过 500。超过这个阈值意味着 Tuple Mover 没有及时合并,或者分区粒度过细。使用
DO_TM_TASK('mergeout')主动触发合并。 - 添加约束前确认投影 ORDER BY 已覆盖约束字段 — 宽表上每多一个额外的投影,ROS 文件就翻倍。参照 表约束产生的projection问题 的方式,先确认 ORDER BY 首列是否覆盖约束字段,避免自动产生额外投影。
- Catalog 超过 15 GB 时主动诊断 — 不要等到 Catalog 20 GB+ 才处理。15 GB 是预警线,此时应运行本文第 2 节和第 3 节的诊断 SQL,定位是哪些表在驱动 catalog 膨胀。
扩展阅读¶
- Vertica 列编码策略优化 — 编码类型全景对比与最佳实践
- Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 — 查询性能分析的完整方法论
- 理解 Vertica 的分区 — 分区对 ROS 容器数量的影响
- Vertica 大表统计信息维护最佳实践 — 统计信息与查询计划的关系