Vertica Join 重分段倾斜诊断与修复¶
作者:JiangChong | 发布时间:2026-04-22
适用场景: 当你的 Vertica 集群在执行多表 JOIN 查询时,发现个别节点 CPU/网络负载远高于其他节点,或者在
EXPLAIN输出中看到RESEGMENT或BROADCAST关键字,本文提供从诊断到修复的完整方法论。
关联文章:
- Vertica CPU 持续高负载诊断与优化 — RESEGMENT 作为 CPU 高负载根因之一
- Vertica 监控最佳实践 — 数据倾斜的日常监控方法
- Vertica 统计信息管理与查询性能 — 统计信息如何影响 BROADCAST vs RESEGMENT 决策
理解全文脉络:
本文按照「发现 → 定位 → 修复 → 预防」的闭环组织。第 1 节解释 Join Re-Segmentation 的底层原理,帮助你理解「为什么慢」。第 2-3 节从系统级监控逐步缩小到具体查询的根因定位。如果你已经有可疑的目标查询,可以直接跳到第 3 节。第 4 节按「立即止血 → 短期修复 → 根本治理」的优先级给出方案。第 5-6 节通过真实和虚构案例串联所有知识点。第 7 节是速查工具箱,第 8 节是投入产出比排序的实践清单。
第 1 节:原理理解¶
1.1 Vertica 的数据分段机制¶
Vertica 是一个无共享(shared-nothing) 架构的 MPP 数据库。集群中的每个节点独立存储数据、独立计算。为了让查询能在所有节点上并行执行,数据必须在节点间合理分布——这就是「分段」(Segmentation)。
Vertica 使用一致性哈希来分段数据。当你创建投影时指定 SEGMENTED BY HASH(column_list) ALL NODES:
- Vertica 对每一行的分段键计算哈希值
- 将哈希值空间均匀划分给集群中的每个节点
- 每行数据根据其哈希值被分配到对应的节点
通俗理解: 分段就像把一副扑克牌按花色分给 4 个人。每个人拿到的牌(数据)是确定的、互不重叠的。查询时每个人独立处理自己手里的牌,最后汇总结果。
1.2 什么是 Join Re-Segmentation¶
当两张表做 JOIN 时,Vertica 需要确保关联键相同的数据在同一个节点上,这样才能在本地完成 JOIN 而无需跨节点传输数据。
理想情况: 两张表的分段键与 JOIN 键一致。
表 A: SEGMENTED BY HASH(customer_id) ← 与 JOIN 键一致
表 B: SEGMENTED BY HASH(customer_id) ← 与 JOIN 键一致
JOIN: A.customer_id = B.customer_id ← 本地 JOIN,无网络传输
在这个理想情况下,customer_id = 100 的数据在表 A 和表 B 中都落在同一个节点上。Vertica 只需要在每个节点内完成本地 JOIN,不需要任何网络传输。
问题场景: 两张表的分段键与 JOIN 键不一致。
表 A: SEGMENTED BY HASH(order_id) ← 与 JOIN 键不同!
表 B: SEGMENTED BY HASH(customer_id) ← 与 JOIN 键一致
JOIN: A.customer_id = B.customer_id ← 需要重分段或广播
此时,表 A 的 customer_id = 100 的数据分散在不同节点上(因为表 A 是按 order_id 分段的),而表 B 的 customer_id = 100 的数据集中在某个节点上。Vertica 无法直接找到匹配行,必须通过以下两种方式之一来解决:
| 策略 | 原理 | 网络开销 | 适用场景 |
|---|---|---|---|
| Broadcast(广播) | 将一张表的完整副本发送到所有节点 | 中等(取决于被广播表的大小) | 一张表远小于另一张表(通常是 Inner 表) |
| Resegmentation(重分段) | 将一张或两张表的数据按 JOIN 键重新哈希分布到所有节点 | 高(取决于数据量) | 两张表都很大,广播不可接受 |
Resegmentation 的过程:源节点读取数据 → 对 JOIN 键计算哈希 → 根据哈希值将每行数据发送到目标节点 → 目标节点接收数据并构建本地 JOIN。每一步都涉及序列化、网络传输、反序列化,消耗大量 CPU、内存和网络带宽。
1.3 为什么会出现倾斜¶
倾斜(Skew) 是指数据在节点间分布不均匀。Join Re-Segmentation 过程中倾斜有两个来源:
来源一:分段键基数不均(Value Skew)。 如果 JOIN 键的某些值出现的频率远高于其他值,即使使用哈希分段,这些"热点"值对应的数据行也会集中到少数节点上。比如 customer_id = 100 占了 30% 的数据行,那么哈希后这 30% 的数据会全部落在一个节点上,导致该节点的 NetworkRecv 远超其他节点。
来源二:分段设计缺陷(Design Skew)。 源表使用了非均匀分布的分段键(如 gender、status 等低基数列),导致数据在写入时就已经倾斜。当这些数据被重分段时,倾斜会被放大——原因在于重分段时每个节点都要把自己持有的数据按 JOIN 键重新哈希并发送到目标节点,倾斜节点持有的数据量远超其他节点(如 70% vs 15%),它的哈希计算和网络发送工作量是其他节点的数倍。而 JOIN 是全局屏障操作,所有节点必须完成才能继续,因此即使重分段后的目标分布是均匀的,倾斜节点也会成为整个集群的瓶颈,其他节点空等。
来源三:统计信息缺失/过期。 优化器在决定使用 Broadcast 还是 Resegment 时,依赖表的行数估算。如果统计信息缺失(显示 NO STATISTICS),优化器可能错误地将大表 Broadcast 到所有节点,或者将应该 Broadcast 的小表做了 Resegment。
来源四:节点配置不一致。 如果集群中某些节点的 CPU、内存或网络带宽低于其他节点,即使数据分布均匀,性能也会出现明显倾斜——慢节点成为整个 JOIN 操作的瓶颈。
| 倾斜来源 | 原因 | 在系统表中的表现 | 解决方向 |
|---|---|---|---|
| Value Skew | JOIN 键值分布严重不均 | NetworkRecv 各节点差异 > 100% |
选择更高基数的分段键;拆分热点值 |
| Design Skew | 源表分段键选择不当 | projection_storage 各节点 row_count 差异大 |
重新设计投影分段键 |
| 统计信息缺失 | 优化器做出错误的 Broadcast/Resegment 决策 | EXPLAIN 中显示 (NO STATISTICS) |
定期 ANALYZE_STATISTICS |
| 节点不一致 | 硬件配置不统一 | 单节点磁盘 I/O 或 CPU 利用率持续高于其他节点 | 统一硬件配置 |
1.4 触发条件总结¶
Join Re-segmentation 不是每次 JOIN 都会发生。以下条件同时满足时才会触发:
- 两张表做 JOIN(无论是 Hash Join 还是 Merge Join)
- 两张表的分段键不一致(或者至少有一张表的分段键与 JOIN 键不匹配)
- 没有其他更好的执行策略(如优化器无法通过翻转 JOIN 顺序来避免重分段)
第 2 节:系统级监控(从宏观入手)¶
在定位具体查询之前,先从系统层面确认是否存在 Join Re-segmentation 导致的问题。
2.1 检查全局 RESEGMENT 事件¶
QUERY_EVENTS 系统表记录了查询执行期间发生的事件。RESEGMENTED_MANY_ROWS 事件表示查询执行期间大量数据行被跨节点重新分段——这是 Join Re-segmentation 的直接证据。
SELECT
DATE_TRUNC('hour', event_timestamp) AS event_hour,
event_type,
COUNT(*) AS event_count,
COUNT(DISTINCT transaction_id || ':' || statement_id) AS distinct_queries
FROM v_monitor.query_events
WHERE event_type = 'RESEGMENTED_MANY_ROWS'
AND event_timestamp > SYSDATE - INTERVAL '7 days'
GROUP BY 1, 2
ORDER BY 1 DESC;
如何解读结果:
RESEGMENTED_MANY_ROWS每天都在高频出现:说明集群中长期存在分段设计问题。这个问题不会自愈,需要从投影设计层面根治。RESEGMENTED_MANY_ROWS集中在某个时间段:可能是特定的 ETL 或报表 SQL 触发的,找到那个时间段的查询即可定位根因。- 没有
RESEGMENTED_MANY_ROWS事件:不代表没有问题。某些旧版本可能不记录此事件,需要从执行计划层面手动排查。也有可能是统计信息缺失导致优化器做了 BROADCAST 而不是 RESEGMENT——Broadcast 同样有网络开销,只是事件类型不同。
2.2 检查各节点网络收发差异¶
Join Re-segmentation 最直接的影响是网络流量。EXECUTION_ENGINE_PROFILES 系统表记录了每个算子的运行时指标,包括网络收发字节数。
-- 检查最近 1 天内各节点 NetworkRecv 的差异
SELECT
node_name,
SUM(counter_value) AS total_bytes_received,
COUNT(DISTINCT transaction_id || ':' || statement_id) AS query_count
FROM v_monitor.execution_engine_profiles
WHERE counter_name ILIKE '%bytes received%'
AND operator_name = 'NetworkRecv'
AND is_executing = false
AND transaction_id IN (
SELECT transaction_id
FROM v_monitor.query_requests
WHERE start_timestamp > SYSDATE - INTERVAL '1 day'
)
GROUP BY 1
ORDER BY 2 DESC;
如何解读结果:
- 各节点
total_bytes_received差异在 20% 以内:正常范围,数据分布基本均匀。 - 某个节点的
total_bytes_received是其他节点的 2 倍以上:说明存在 Join Re-segmentation 倾斜。该节点承担了不成比例的网络接收负载。 - 所有节点的
total_bytes_received都很高(如每节点 > 10GB/天):说明集群中存在大量跨节点数据传输。即使分布均匀,网络带宽也可能是瓶颈。
2.3 检查查询级别的网络倾斜¶
以下 SQL 通过比较同一查询中各节点的 bytes_received 差异来检测 Join Re-segmentation 倾斜。当 skew_percentage > 100 时表示存在严重倾斜。
SELECT
transaction_id,
statement_id,
max_received,
min_received,
skew_percentage,
CASE WHEN skew_percentage > 100
THEN 'ACTION: 检测到 Join Re-segmentation 数据严重倾斜,请检查查询中 JOIN 键的数据分布'
END AS action
FROM (
SELECT
transaction_id,
statement_id,
MAX(bytes_received) AS max_received,
MIN(bytes_received) AS min_received,
((MAX(bytes_received) * 100 / NULLIF(MIN(bytes_received), 0)) - 100)::INT AS skew_percentage
FROM (
SELECT
node_name,
transaction_id,
statement_id,
SUM(counter_value) AS bytes_received
FROM v_monitor.execution_engine_profiles
WHERE counter_name ILIKE '%bytes received%'
AND operator_name = 'NetworkRecv'
AND (transaction_id, statement_id, path_id) IN (
SELECT transaction_id, statement_id, path_id
FROM v_monitor.execution_engine_profiles
WHERE operator_name = 'Join'
)
GROUP BY 1, 2, 3
HAVING SUM(counter_value) > 0
) AS f1
GROUP BY 1, 2
HAVING MAX(bytes_received) > 500000000 -- 至少 500MB 网络接收
AND ((MAX(bytes_received) * 100 / NULLIF(MIN(bytes_received), 0)) - 100)::INT > 10
) f2
ORDER BY 5 DESC
LIMIT 100;
如何解读结果:
skew_percentage > 100:倾斜率超过 100%——即最高节点的接收量是最低节点的 2 倍以上。这几乎肯定意味着分段设计问题。skew_percentage = 10~50:轻微倾斜,可能来自数据本身的自然分布不均。先检查 JOIN 键的基数分布。max_received> 500MB 且skew_percentage很高:优先级最高的问题。大的网络传输量 + 严重倾斜 = 显著的单节点瓶颈。
第 3 节:逐步定位根因(从宏观到微观)¶
当你确认集群存在 Join Re-segmentation 问题后,按以下步骤定位具体根因。
3.1 第一步:找到触发 RESEGMENT 的目标查询¶
做什么: 从 query_events 中找到触发了 RESEGMENTED_MANY_ROWS 事件的具体查询,获取其 transaction_id 和 statement_id。
SELECT
transaction_id,
statement_id,
event_timestamp,
event_description,
suggested_action,
operator_name
FROM v_monitor.query_events
WHERE event_type = 'RESEGMENTED_MANY_ROWS'
AND event_timestamp > SYSDATE - INTERVAL '1 day'
ORDER BY event_timestamp DESC
LIMIT 20;
如何解读: event_description 会给出行的数量和涉及的表。suggested_action 会给出 Vertica 的建议(如 "Consider different projection segmentation")。记下 transaction_id 和 statement_id,后续步骤会反复使用。
如果不是: 如果没有 RESEGMENTED_MANY_ROWS 事件但怀疑存在分段问题,直接跳到 3.2 步,通过 EXPLAIN 手动检查执行计划。
3.2 第二步:分析执行计划中的 RESEGMENT/BROADCAST¶
做什么: 使用上一步获取的 transaction_id 和 statement_id,查看该查询的执行计划。QUERY_PLAN_PROFILES 系统表存储了已执行查询的计划信息。
-- 将 :t_id 和 :s_id 替换为实际值
SELECT
path_id,
path_line_index,
path_line
FROM v_monitor.query_plan_profiles
WHERE transaction_id = :t_id
AND statement_id = :s_id
ORDER BY path_id, path_line_index;
如何解读执行计划中的关键模式:
| 执行计划模式 | 含义 | 问题严重度 |
|---|---|---|
Outer (RESEGMENT) + Inner (RESEGMENT) |
两张表都需要按 JOIN 键重分段 | 高 — 双向网络传输 |
Outer (RESEGMENT) + Inner (BROADCAST) |
一张表(outer)重分段,另一张(inner)广播 | 中 — 取决于广播表大小 |
Outer (BROADCAST) + Inner (RESEGMENT) |
同上,方向相反 | 高 — 通常outer表数据量更大,所以更严重 |
(GLOBAL RESEGMENT GROUPS) (LOCAL RESEGMENT GROUPS) |
GROUP BY 也需要跨节点重分段 | 中 — 分段键与 GROUP BY 键不匹配 |
Execute on: v_xxx_node0001 |
只在单节点执行 | 高 — 完全放弃并行 |
特别注意: 如果执行计划显示 (NO STATISTICS),说明优化器在做决策时缺少行数估算。这可能导致优化器错误地用 Broadcast 代替 Resegment,或反之。此时需要先收集统计信息(见 4.4 节),再重新分析。
3.3 第三步:检查投影的分段设计¶
做什么: 查看 JOIN 涉及的所有表,确认它们的投影分段键是否与 JOIN 键对齐。
-- 查看指定表的所有投影及其分段设计
SELECT
p.projection_name,
p.is_segmented,
p.segment_expression,
p.is_super_projection,
p.is_prejoin,
p.has_statistics
FROM v_catalog.projections p
WHERE p.projection_schema = 'your_schema' -- 替换为实际 schema
AND p.anchor_table_name = 'your_table_name' -- 替换为实际表名
ORDER BY p.is_super_projection DESC;
如何解读:
is_segmented = true且segment_expression包含 JOIN 键列:设计正确。JOIN 可以在本地完成。is_segmented = true但segment_expression不包含 JOIN 键列:这是分段不匹配的根因。数据按另一个键分布,JOIN 时需要重分段。is_segmented = false(即 UNSEGMENTED):每个节点都有全量副本。对于小表这是合理设计;对于大表,这会导致所有 JOIN 操作都在单节点完成——非常严重的性能瓶颈。is_prejoin = true:预连接投影,将 JOIN 结果预先物化。如果存在此类投影,优化器可能直接使用而无需运行时 JOIN。
如果不是: 如果发现投影设计正确但执行计划仍然显示 RESEGMENT,进入下一步检查数据倾斜。
3.4 第四步:检查数据倾斜(投影级别)¶
做什么: 确认各节点上投影的数据量是否均匀。即使分段设计正确,如果分段键的值分布严重不均,数据依然会倾斜。
SELECT
ps.node_name,
ps.projection_name,
ps.row_count,
ps.used_bytes,
ps.ros_count
FROM v_monitor.projection_storage ps
JOIN v_catalog.projections p
ON ps.projection_id = p.projection_id
WHERE p.projection_schema = 'your_schema' -- 替换为实际 schema
AND p.anchor_table_name = 'your_table_name' -- 替换为实际表名
AND p.is_segmented = true
ORDER BY ps.row_count DESC;
如何解读:
- 各节点
row_count差异在 20% 以内:正常。数据分布基本均匀。 - 某节点的
row_count占总量 50% 以上:严重数据倾斜。根本原因是分段键的基数不均(Value Skew)。需要重新选择分段键。 - 部分节点的
row_count = 0:数据完全没有分布到某些节点上。这通常意味着分段键的基数太低(如只有几个值的status列)。
精确计算倾斜率的 SQL:
SELECT
projection_name,
MAX(row_count) AS max_rows,
MIN(row_count) AS min_rows,
ROUND(MAX(row_count)::NUMERIC / NULLIF(MIN(row_count), 0), 2) AS skew_ratio,
CASE
WHEN MAX(row_count)::NUMERIC / NULLIF(MIN(row_count), 0) > 2.0
THEN 'WARNING: 数据倾斜 > 2x'
WHEN MAX(row_count)::NUMERIC / NULLIF(MIN(row_count), 0) > 1.5
THEN 'INFO: 数据倾斜 > 1.5x'
ELSE 'OK'
END AS skew_status
FROM v_monitor.projection_storage
WHERE projection_name IN (
SELECT projection_name FROM v_catalog.projections
WHERE is_segmented = true
AND projection_schema = 'your_schema'
AND anchor_table_name = 'your_table_name'
)
GROUP BY projection_name
ORDER BY skew_ratio DESC;
3.5 第五步:检查统计信息健康状况¶
做什么: 确认 JOIN 涉及的表是否有最新的统计信息。缺失统计信息会导致优化器做出错误的 Broadcast/Resegment 决策。
-- 按表聚合,避免 projection_columns 每列一行导致的重复
SELECT
p.anchor_table_name,
p.has_statistics,
LISTAGG(DISTINCT pc.statistics_type) AS stats_types,
MIN(pc.statistics_updated_timestamp) AS oldest_stats,
MAX(pc.statistics_updated_timestamp) AS newest_stats,
DATEDIFF('day', MAX(pc.statistics_updated_timestamp), NOW()) AS days_since_last_stats
FROM v_catalog.projections p
JOIN v_catalog.projection_columns pc
ON p.projection_id = pc.projection_id
WHERE p.projection_schema = 'your_schema'
AND p.anchor_table_name IN ('table1', 'table2') -- 替换为 JOIN 涉及的表
AND pc.statistics_type != 'NONE'
AND p.is_super_projection = true
GROUP BY p.anchor_table_name, p.has_statistics
ORDER BY days_since_last_stats DESC;
如何解读:
has_statistics = false:优化器完全不知道表的行数,不得不使用启发式规则猜测。在涉及大 JOIN 时,错误的猜测可能导致灾难性的性能。days_since_last_stats > 7且表有大量数据变更:统计信息过期。优化器基于过时的行数做决策。stats_types全为FULL:最佳状态。如果没有统计信息,此表不会出现在结果中——说明需要执行ANALYZE_STATISTICS。
如果不是: 如果分段设计正确、数据分布均匀、统计信息完整,但 RESEGMENT 仍然发生,可能的原因是:优化器在特定 JOIN 条件下确实需要重分段(如三表 JOIN 中无法同时满足所有分段要求);或者 JOIN 条件涉及函数变换(如 JOIN ON UPPER(a.name) = b.name);或者是 Vertica 优化器版本的已知局限性。
第 4 节:解决方案(从快速见效到根本治理)¶
4.1 立即措施:对齐分段键(当天可执行)¶
这是解决 Join Re-segmentation 最直接有效的方法。 将参与 JOIN 的表的分段键统一为 JOIN 键。对于最常见的两表 JOIN 场景:
-- 为表 A 创建按 JOIN 键分段的新投影
CREATE PROJECTION schema.table_a_join_opt
AS SELECT * FROM schema.table_a
ORDER BY join_column1, join_column2
SEGMENTED BY HASH(join_column1, join_column2) ALL NODES KSAFE 1;
-- 为表 B 创建相同分段键的投影
CREATE PROJECTION schema.table_b_join_opt
AS SELECT * FROM schema.table_b
ORDER BY join_column1, join_column2
SEGMENTED BY HASH(join_column1, join_column2) ALL NODES KSAFE 1;
-- 刷新投影使其可用
SELECT REFRESH('schema.table_a_join_opt');
SELECT REFRESH('schema.table_b_join_opt');
为什么 ORDER BY 中也包含 JOIN 键? 因为如果数据同时按 JOIN 键排序,Vertica 可以使用更高效的 Merge Join 代替 Hash Join。Merge Join 不需要在内存中构建哈希表,内存消耗更少,尤其适合大表 JOIN。
验证效果: 创建投影后,再次运行 EXPLAIN 确认执行计划中不再出现 RESEGMENT 或 BROADCAST。
如果执行计划显示 JOIN HASH [...] 且没有任何 RESEGMENT / BROADCAST 标注,说明优化器已经可以在本地完成 JOIN。
4.2 短期优化一:使用 UNSEGMENTED 投影(当周执行)¶
对于小维度表(行数 < 100,000 或不到最大事实表的 1%),将其投影设为 UNSEGMENTED。这样小表在所有节点上都有完整副本,任何 JOIN 都可以本地完成,无需重分段。
-- 为小维度表创建 unsegmented 投影
CREATE PROJECTION schema.dim_table_unseg
AS SELECT * FROM schema.dim_table
ORDER BY dim_key
UNSEGMENTED ALL NODES KSAFE 1;
SELECT REFRESH('schema.dim_table_unseg');
为什么「ALL NODES」? UNSEGMENTED ALL NODES 表示在每个节点上存储一份完整副本。这意味着表的数据量乘以节点数的存储开销。因此 UNSEGMENTED 只适用于小表。对于大表使用 UNSEGMENTED,不仅浪费存储空间,还会导致所有 JOIN 和 GROUP BY 在单节点执行(因为数据没有分段,优化器无法并行化)。
如何选择? 使用 Database Designer 的阈值规则:
| 条件 | 建议 |
|---|---|
| 行数 ≤ 100,000 | UNSEGMENTED |
| 行数 ≤ 最大表行数的 1% 且 最大表 ≥ 10M | UNSEGMENTED |
| 行数 ≤ 最大表行数的 10% 且 最大表 < 1M | UNSEGMENTED |
| 以上都不满足 | SEGMENTED BY HASH(join_key) |
4.3 短期优化二:创建预连接投影(Pre-join Projection)¶
对于高频执行的固定 JOIN 模式,可以创建预连接投影(Pre-join Projection),将 JOIN 结果预先物化。查询时优化器直接使用预连接投影,完全跳过运行时 JOIN。
-- 创建预连接投影:事实表 LEFT JOIN 维度表
CREATE PROJECTION schema.fact_dim_prejoin
AS SELECT
f.*,
d.dim_attr1,
d.dim_attr2
FROM schema.fact_table f
LEFT JOIN schema.dim_table d ON f.dim_key = d.dim_key
ORDER BY f.date_key
SEGMENTED BY HASH(f.fact_id) ALL NODES KSAFE 1;
SELECT REFRESH('schema.fact_dim_prejoin');
局限性:
- 预连接投影只能处理事实表(N 端)JOIN 维度表(1 端)的 N:1 关系,不支持 M:N JOIN。(这里的 N:1 指的是行数比例——同一维度键在事实表中出现 N 行、在维度表中只有 1 行,不是指表的数量。正因为每行事实数据只匹配一行维度数据,预连接投影的行数才等于锚表行数,不会膨胀)
- 每个预连接投影增加存储开销
- 如果维度表数据变更频繁,需要定期刷新
- 适用于查询模式非常稳定的场景
4.4 辅助措施:确保统计信息准确¶
正确的统计信息是优化器做出正确决策的前提。统计信息缺失可能导致优化器错误地将大表 Broadcast,或将应该 Broadcast 的小表做了 Resegment。
-- 对 JOIN 涉及的表收集统计信息
SELECT ANALYZE_STATISTICS('schema.table_a');
SELECT ANALYZE_STATISTICS('schema.table_b');
-- 检查统计信息覆盖率(FULL 占比)和最后更新时间
SELECT
p.anchor_table_name,
p.has_statistics,
COUNT(*) AS total_columns,
SUM(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) AS full_stats_columns,
ROUND(100.0 * SUM(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) / COUNT(*), 1) AS full_pct,
MAX(pc.statistics_updated_timestamp) AS last_stats_update,
DATEDIFF('day', MAX(pc.statistics_updated_timestamp), NOW()) AS days_since_update
FROM v_catalog.projections p
JOIN v_catalog.projection_columns pc
ON p.projection_id = pc.projection_id
WHERE p.projection_schema||'.'||p.anchor_table_name IN ('schema.table_a', 'schema.table_b')
AND p.is_super_projection = true
GROUP BY p.anchor_table_name, p.has_statistics
ORDER BY full_pct;
关键认知: 来自 Vertica 统计信息管理与查询性能 的真实案例显示,缺乏统计信息时,优化器将 2.5B 行的大表做了 Broadcast,导致单节点内存爆满。收集统计信息后,优化器正确选择了 Resegment 策略,执行时间从 1 小时降至 6 秒。这说明统计信息的准确性比 Resegment 本身更重要——因为错误的 Broadcast 比正确的 Resegment 危害更大。
4.5 根本治理:重建投影 + 删除旧投影¶
前面 4.1-4.4 的方案核心都是创建新投影。新投影通过 REFRESH 从源表读取数据后,按新的 SEGMENTED BY 键重新哈希分布,数据从写入那一刻起就是均匀的。
完整流程只有三步:
1. 创建按 JOIN 键分段的新投影
CREATE PROJECTION schema.table_a_join_opt
AS SELECT * FROM schema.table_a
ORDER BY join_column1, join_column2
SEGMENTED BY HASH(join_column1, join_column2) ALL NODES KSAFE 1;
SELECT REFRESH('schema.table_a_join_opt');
SELECT ANALYZE_STATISTICS('schema.table_a_join_opt');
2. 验证新投影被优化器选用
-- 检查 EXPLAIN 是否使用了新投影、是否还有 RESEGMENT
EXPLAIN SELECT ... FROM schema.table_a JOIN schema.table_b ON ...;
确认执行计划中不再出现 RESEGMENT 或 BROADCAST 即表示优化器已切换到新投影。
3. 删除旧投影,释放存储
⚠️ 删除旧投影前务必确认:① 新投影已
REFRESH完成;②EXPLAIN确认优化器选用了新投影;③ 新投影已经过至少一个业务周期的验证。
4.6 方案对比总结¶
| 方案 | 投入 | 效果 | 适用场景 |
|---|---|---|---|
| 对齐分段键(建新投影) | 低(创建新投影) | 最直接,JOIN 本地完成 | 两表 JOIN 键明确 |
| 删除旧投影 | 低(DROP PROJECTION) | 释放存储,消除优化器干扰 | 新投影已验证稳定后 |
| UNSEGMENTED 投影 | 低 | 小表 JOIN 本地完成 | 小维度表(< 10 万行) |
| Pre-join 投影 | 中(需定义 JOIN 条件) | 跳过运行时 JOIN | 固定查询模式 |
| 统计信息收集 | 低 | 避免优化器决策错误 | 所有场景 |
第 5 节:深入案例¶
5.1 案例一:分段键与 JOIN 键不一致¶
📝 虚构案例
场景: 某电商平台的 Vertica 集群(6 节点,每节点 48 核 / 256GB 内存),每天凌晨执行 orders 表(按 order_id 分段,5 亿行)与 customers 表(按 customer_id 分段,2000 万行)的 JOIN 来生成日报。JOIN 条件是 orders.customer_id = customers.customer_id。该日报 SQL 执行时间从最初的 3 分钟逐步增长到 25 分钟,且 node1 的 CPU 始终 100%,其他节点只有 40%。
诊断过程:
-- 1. 检查是否有 RESEGMENT 事件
SELECT event_type, event_description, operator_name
FROM v_monitor.query_events
WHERE event_timestamp > SYSDATE - INTERVAL '7 days'
AND event_type = 'RESEGMENTED_MANY_ROWS'
LIMIT 5;
-- 输出:
RESEGMENTED_MANY_ROWS | Many rows were resegmented during plan execution | Join
-- 2. 检查执行计划
EXPLAIN SELECT ... FROM sales.orders o JOIN sales.customers c ON o.customer_id = c.customer_id;
-- 关键输出:
+-JOIN HASH [Cost: 5M, Rows: 500M] (PATH ID: 1)
Outer (RESEGMENT)
Join Cond: (o.customer_id = c.customer_id)
Outer (RESEGMENT) 说明 orders 需要按 customer_id 重分段——因为 orders 是按 order_id 分段的,JOIN 键却是 customer_id。而 customers 已经按 customer_id 分段,作为 Inner 表不做重分段,直接在本地参与 JOIN。
-- 3. 检查投影设计
SELECT projection_name, is_segmented, segment_expression
FROM v_catalog.projections
WHERE projection_schema = 'sales'
AND anchor_table_name IN ('orders', 'customers')
AND is_super_projection = true;
orders_super: is_segmented=true, segment_expression=hash(order_id) ← 与 JOIN 键不同!
customers_super: is_segmented=true, segment_expression=hash(customer_id)
根因分析: orders 的分段键是 order_id,但 JOIN 键是 customer_id。优化器以 orders 为 Outer 表,需要将 5 亿行数据按 customer_id 重分段并发送到对应节点;customers 已经按 customer_id 分段,作为 Inner 表在本地直接参与 JOIN。由于 orders 表数据量大且每天都在增长,单侧重分段的网络开销也越来越大(每节点 ~80GB 发送),执行时间从 3 分钟逐步恶化到 25 分钟。
修复方案:
-- 为 orders 创建按 customer_id 分段的新投影
CREATE PROJECTION orders_segmented_by_cust
AS SELECT * FROM sales.orders
ORDER BY customer_id, order_id
SEGMENTED BY HASH(customer_id) ALL NODES KSAFE 1;
SELECT REFRESH('orders_segmented_by_cust');
效果对比:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 执行时间 | 25 分钟 | 3.5 分钟 |
| NetworkSend 总量 | ~480 GB | < 1 GB(本地 JOIN,无重分段) |
| node1 CPU | 100% | 60%(均匀分布) |
RESEGMENTED_MANY_ROWS 事件 |
每次查询触发 | 0 |
这个案例说明了一个关键原则:如果 JOIN 是最频繁的查询模式,那么分段设计必须围绕 JOIN 键展开,即使这意味着要改变表的主分段键。
5.2 案例二:UNSEGMENTED 大表导致内存暴涨 + 重分段网络开销¶
📝 虚构案例
场景: 某金融机构的 Vertica 集群(8 节点,每节点 256GB 内存),两张大表 fin.transactions(按 trans_id 分段,12 亿行)和 fin.accounts(UNSEGMENTED,3 亿行)通过联合主键 (user_id, account_id) 做 JOIN。单条查询执行时间超过 3 小时,且频繁触发 JOIN_SPILLED 事件。
诊断过程:
-- 执行计划
EXPLAIN VERBOSE SELECT ... FROM fin.transactions t JOIN fin.accounts a
ON t.user_id = a.user_id AND t.account_id = a.account_id;
-- 关键输出:
+-JOIN HASH [Cost: 18M, Rows: 1.2B]
Outer (RESEGMENT) ← transactions 需要按 join 键重分段
Join Cond: (t.user_id = a.user_id) AND (t.account_id = a.account_id)
根因分析: 两个问题叠加致命——
- 内存层面:
accounts是 UNSEGMENTED 投影,3 亿行数据在每个节点上都有全量副本。Hash Join 时每个节点把 3 亿行数据加载到自己的内存中构建哈希表。3 亿行的哈希表远超资源池 query_budget 容量,进而 spill 到磁盘。 - 网络层面:
transactions按trans_id分段,与 JOIN 键(user_id, account_id)不匹配,12 亿行数据需要跨节点重分段,每个节点 ~150GB 发送量。
两个问题叠加:重分段把 transactions 按 JOIN 键打散到各节点后,每个节点还要把 accounts 的 3 亿行全量加载进内存做哈希探测——内存和网络同时成为瓶颈。
修复方案:
-- 1. 将 accounts 从 UNSEGMENTED 改为按 JOIN 键分段
CREATE PROJECTION accounts_segmented_by_keys
AS SELECT * FROM fin.accounts
ORDER BY user_id, account_id
SEGMENTED BY HASH(user_id, account_id) ALL NODES KSAFE 1;
-- 2. 同步修改 transactions 的分段键
CREATE PROJECTION transactions_segmented_by_keys
AS SELECT * FROM fin.transactions
ORDER BY user_id, account_id
SEGMENTED BY HASH(user_id, account_id) ALL NODES KSAFE 1;
SELECT REFRESH('accounts_segmented_by_keys');
SELECT REFRESH('transactions_segmented_by_keys');
效果对比:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 各节点哈希表大小 | 3 亿行(全量) | ~3750 万行(1/8) |
| 执行时间 | 3 小时+ | 8 分钟 |
| JOIN_SPILLED 事件 | 每次触发 | 0 |
| 网络传输 | 12 亿行重分段 | 0(本地 JOIN) |
| 各节点内存使用 | 220GB+ | 45GB |
5.3 案例三:统计信息缺失导致错误 Broadcast¶
📋 真实案例
场景: 某运营商(50 节点集群,SUSE 11,256GB 内存/节点,Vertica v7.2.3),业务侧反馈多表关联查询执行超过 1 小时。
诊断过程:
- 检查执行计划,发现关联关系选择不合理
- 检查表数据量后发现驱动表
ofr_subs_his_d有统计信息,但其余 5 张表均无统计信息 - 对这 5 张表收集统计信息:
SELECT ANALYZE_STATISTICS('schema.table1');
SELECT ANALYZE_STATISTICS('schema.table2');
-- ... 共 5 张表
效果: 收集统计信息后,执行时间从 1 小时降至 3.5 秒,提升超过 1000 倍。
关键启示: 这 5 张表缺失统计信息,优化器不知道它们的真实大小,因此无法判断该用 Broadcast 还是 Resegment。在缺失统计信息时,优化器可能做出严重错误的选择——比如将大表 Broadcast、将小表 Resegment。这是统计信息影响 RESEGMENT 决策的典型案例。更多细节见 Vertica 统计信息管理与查询性能。
5.4 案例四:UNSEGMENTED 大表导致单节点瓶颈¶
📋 真实案例
场景: 某运营商(93 节点集群),凌晨 3 点后数据库性能严重下降,接口装载及脚本运行缓慢。排查发现一个从 20 日 22:00 开始运行的 SQL 一直未结束,持续超过 6.5 小时。
诊断过程:
-
通过
query_requests定位到问题 SQL:网络传输总量达 11GB,绝大部分执行步骤在单节点上进行(Execute on: v_edw_node0039) -
检查执行计划:
Access Path:
+-JOIN HASH [LeftOuter] [Cost: 1K, Rows: 10K (NO STATISTICS)] (PATH ID: 1)
Outer (RESEGMENT)(LOCAL ROUND ROBIN)
Inner (RESEGMENT)
Join Cond: (a.JR_USER_ID = d.user_id_fk) AND (a.KD_USER_ID = d.user_id_zk)
-
检查投影设计:
-
表 1:按
statis_date分段,未按关联字段分段 - 表 2:采用 UNSEGMENTED 方式分布,未使用关联字段排序
根因分析: 表 2 的 UNSEGMENTED 设计 + 表 1 的分段键与 JOIN 键不匹配,两个因素叠加导致:
- 查询在单节点上执行(因为 UNSEGMENTED 投影可以被任意节点读取,但优化器选择了单节点)
- 双向 RESEGMENT 仍然发生(因为表 1 的分段键与 JOIN 键不同)
修复方案:
-- 表 1:按关联字段 (user_id_zk, user_id_fk) 分段
CREATE PROJECTION MASAMK.TB_MK_SC_USER_ZFK_DTAL_tuning (...)
AS SELECT ...
ORDER BY statis_date, zfk_type, user_id_zk, user_id_fk, ...
SEGMENTED BY HASH(user_id_zk, user_id_fk) ALL NODES;
-- 表 2:从 UNSEGMENTED 改为按关联字段分段
避免单节点执行问题
效果: 执行时间从 11 分 48 秒(优化前正常情况) 降至 5 分 37 秒,性能提升一倍。故障时单节点执行 6.5 小时的问题不复存在。
第 6 节:完整诊断流程实战¶
📝 虚构场景 · 完整演练
背景: 某零售企业 Vertica 集群(4 节点,每节点 32 核 / 128GB,10Gbps 网络,Vertica v26.1.0)。DBA 发现每天上午 9:00 的销售日报 SQL 执行时间从 2 分钟增长到 18 分钟,且 node3 的 CPU 使用率一直在 95%+,其他节点只有 30-40%。
时间线:
09:15 — 发现异常¶
DBA 收到监控告警:node3 CPU 持续 > 95%。登录数据库检查系统资源:
-- 检查各节点 CPU 使用率
SELECT node_name,
average_cpu_usage_percent,
average_memory_usage_percent,
net_rx_kbytes_per_second,
net_tx_kbytes_per_second
FROM v_monitor.system_resource_usage
ORDER BY average_cpu_usage_percent DESC;
输出:
| node_name | average_cpu_usage_percent | average_memory_usage_percent | net_rx_kbytes_per_second | net_tx_kbytes_per_second |
|---|---|---|---|---|
| node3 | 96.2 | 78.5 | 870,400 | 122,880 |
| node1 | 38.1 | 45.2 | 97,280 | 92,160 |
| node2 | 35.8 | 42.1 | 90,112 | 87,040 |
| node4 | 32.5 | 40.8 | 83,968 | 81,920 |
判断: node3 的 CPU 和网络接收量是其他节点的 10 倍左右。这是典型的数据倾斜或 Join Re-segmentation 倾斜特征。
09:20 — 定位问题查询¶
-- 找到当前正在执行且网络接收量大的查询
SELECT
qr.transaction_id,
qr.statement_id,
qr.request_duration_ms,
qr.memory_acquired_mb,
LEFT(qr.request, 200) AS query_preview
FROM v_monitor.query_requests qr
WHERE qr.is_executing = true
AND qr.request_type = 'QUERY'
ORDER BY qr.request_duration_ms DESC;
找到 transaction_id = 45035996274879950, statement_id = 1,是一个三表 JOIN 的日报查询。
09:25 — 分析执行计划¶
-- EXPLAIN 分析
EXPLAIN
SELECT
p.product_category,
SUM(s.quantity * s.unit_price) AS revenue,
COUNT(DISTINCT s.customer_id) AS unique_customers
FROM sales_fact s
JOIN product_dim p ON s.product_id = p.product_id
JOIN customer_dim c ON s.customer_id = c.customer_id
WHERE s.sale_date = CURRENT_DATE - 1
GROUP BY 1;
关键输出:
+- JOIN HASH [Cost: 35M, Rows: 280M] (PATH ID: 2)
| Join Cond: (s.customer_id = c.customer_id)
| +-- Outer -> JOIN HASH [Cost: 22M, Rows: 280M] (PATH ID: 3)
| | Join Cond: (s.product_id = p.product_id)
| | +-- Outer -> STORAGE ACCESS for s [Rows: 280M] ← sales_fact,按 sale_id 分段
| | +-- Inner -> STORAGE ACCESS for p [Rows: 50K] ← product_dim,按 product_id 分段
| +-- Inner -> STORAGE ACCESS for c [Rows: 80M](RESEGMENT) ← customer_dim 需要重分段!
09:30 — 诊断投影设计¶
SELECT projection_name, is_segmented, segment_expression
FROM v_catalog.projections
WHERE anchor_table_name IN ('sales_fact', 'product_dim', 'customer_dim')
AND is_super_projection = true;
输出:
| projection_name | is_segmented | segment_expression |
|---|---|---|
| sales_fact_super | true | hash(sale_id) |
| product_dim_super | true | hash(product_id) |
| customer_dim_super | true | hash(customer_name) |
09:35 — 根因判断¶
sales_fact按sale_id分段customer_dim按customer_name分段- JOIN 键是
s.customer_id = c.customer_id
三个键各不相同。结果是:sales_fact 与 customer_dim 做 JOIN 时,customer_dim 的数据需要按 customer_id 重分段并发送到各节点。由于 customer_dim 8000 万行,重分段产生的网络传输量约 8GB × 节点数 = 32GB。
为什么 node3 最慢? 因为 customer_dim 重分段后,customer_id 的某些值(如大企业客户,占 30% 的数据)哈希到 node3,导致 node3 接收的数据量远超其他节点。
09:45 — 执行修复¶
-- 方案:将 customer_dim 的分段键改为 customer_id,与 JOIN 键对齐
CREATE PROJECTION customer_dim_seg_by_id
AS SELECT * FROM customer_dim
ORDER BY customer_id, customer_name
SEGMENTED BY HASH(customer_id) ALL NODES KSAFE 1;
SELECT REFRESH('customer_dim_seg_by_id');
-- 同样优化 sales_fact:如果 customer_id 是最常用的 JOIN 键
-- 可以考虑将 sales_fact 的分段键也对齐
-- (本案例中 product_dim JOIN 也需要 product_id,需要权衡)
CREATE PROJECTION sales_fact_seg_by_cust
AS SELECT * FROM sales_fact
ORDER BY sale_date, customer_id, product_id
SEGMENTED BY HASH(customer_id) ALL NODES KSAFE 1;
SELECT REFRESH('sales_fact_seg_by_cust');
10:30 — 验证效果¶
重新运行日报 SQL,检查执行计划:
+- JOIN HASH [Cost: 28M, Rows: 280M] (PATH ID: 2)
| +-- Outer -> JOIN HASH [Cost: 18M, Rows: 280M]
| | +-- Outer -> STORAGE ACCESS for s [Rows: 280M] (使用 sales_fact_seg_by_cust)
| | +-- Inner -> STORAGE ACCESS for p [Rows: 50K]
| +-- Inner -> STORAGE ACCESS for c [Rows: 80M] (使用 customer_dim_seg_by_id)
| ← 无 RESEGMENT!本地 JOIN!
最终效果:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 执行时间 | 18 分钟 | 2.5 分钟 |
| node3 CPU | 96% | 52% |
| 各节点 CPU 差异 | 3x | < 20% |
| 网络传输总量 | ~32 GB | ~0.5 GB |
RESEGMENTED_MANY_ROWS |
每次触发 | 0 |
第 7 节:快速诊断 SQL 工具箱¶
| 诊断目标 | SQL | 说明 |
|---|---|---|
| 查看 RESEGMENT 事件趋势 | SELECT DATE_TRUNC('hour', event_timestamp), COUNT(*) FROM query_events WHERE event_type='RESEGMENTED_MANY_ROWS' GROUP BY 1; |
确认问题是否存在及频率 |
| 查看最近执行计划 | SELECT * FROM query_plan_profiles WHERE transaction_id=:t_id AND statement_id=:s_id ORDER BY path_id, path_line_index; |
定位 RESEGMENT/BROADCAST 发生位置 |
| 检查表的投影分段设计 | SELECT projection_name, is_segmented, segment_expression FROM projections WHERE anchor_table_name=':table'; |
查看分段键是否与 JOIN 键匹配 |
| 检查数据倾斜(按节点) | SELECT node_name, row_count FROM projection_storage ps JOIN projections p ON ps.projection_id=p.projection_id WHERE p.anchor_table_name=':table' AND p.is_segmented; |
确认数据在节点间分布是否均匀 |
| 精确计算倾斜率 | SELECT projection_name, MAX(row_count)/NULLIF(MIN(row_count),0) AS skew_ratio FROM projection_storage WHERE projection_name IN (SELECT projection_name FROM projections WHERE anchor_table_name=':table') GROUP BY 1; |
skew_ratio > 2.0 表示需要关注 |
| 检查统计信息状态 | SELECT anchor_table_name, has_statistics FROM projections WHERE anchor_table_name=':table'; |
确认优化器是否有足够的统计信息 |
| 检查 JOIN Re-seg 倾斜(NetworkRecv) | 查看 2.3 节完整 SQL | 通过各节点 NetworkRecv 差异检测倾斜 |
| 检查是否有 UNSEGMENTED 大表 | SELECT p.anchor_table_name, SUM(ps.row_count) AS total_rows FROM projection_storage ps JOIN projections p ON ps.projection_id=p.projection_id WHERE p.is_segmented=false GROUP BY 1 HAVING SUM(ps.row_count) > 1000000 ORDER BY 2 DESC; |
UNSEGMENTED 大表导致单节点执行 |
| 同一 JOIN 键的表是否分段一致 | SELECT anchor_table_name, segment_expression FROM projections WHERE is_segmented AND anchor_table_name IN (':t1',':t2'); |
确认两张表的分段键是否对齐 |
第 8 节:最佳实践清单¶
按投入产出比从高到低排列:
-
建表时让分段键 = 最高频 JOIN 键。 这是投入产出比最高的优化,因为它在根源上消除了重分段。在表设计阶段就应该明确「这张表最常和哪张表做 JOIN、用什么键」,然后让两张表共用相同的分段键。如果一张表参与多种 JOIN(如事实表 JOIN 多个维度表),优先选择数据量最大的那个 JOIN 作为分段键依据。
-
小维度表(< 10 万行)使用 UNSEGMENTED 投影。 每个节点保存一份完整副本,任何 JOIN 都可以本地完成。但必须严格控制大小——如果一张表从 10 万行增长到 1000 万行,UNSEGMENTED 会从优化变成瓶颈。
-
数据加载后立即收集统计信息。 统计信息是优化器正确选择 Broadcast vs Resegment 的前提。缺失统计信息时,优化器可能将大表 Broadcast,这是比 Resegment 更严重的错误——因为 Broadcast 让单节点承担全部数据。建议在 ETL 流程中每个
INSERT/COPY后加一行SELECT ANALYZE_STATISTICS('')。 -
上线前用 EXPLAIN 验证执行计划。 在 SQL 上线到生产环境之前,在 EXPLAIN 输出中搜索
RESEGMENT、BROADCAST和(NO STATISTICS)。如果出现这些关键字且涉及大表,说明投影设计需要优化。 -
监控
RESEGMENTED_MANY_ROWS事件。 将它加入日常监控 Dashboard。如果这个事件从偶尔出现变成每天出现,说明数据量增长导致原本可接受的重分段变成了瓶颈。 -
避免对 JOIN 键使用函数变换。
JOIN ON UPPER(a.name) = b.name这类写法会强制重分段,因为哈希计算基于变换后的值。如果需要在 JOIN 中使用函数,考虑在 ETL 阶段预计算一个标准化列,JOIN 时直接用标准化列。 -
当数据倾斜无法通过分段键解决时,考虑分区。 如果 JOIN 键本身的基数分布就严重不均(如少数超级客户贡献了 80% 的数据),即使哈希分段也会倾斜。此时可以结合分区表设计,将热点数据和不热点数据分开处理。但这属于进阶优化,一般场景中用对齐分段键即可解决。
-
创建新投影后验证优化器是否真的用了它。 使用
SELECT * FROM query_profiles WHERE table_name = ':table'检查优化器实际选择了哪个投影。如果新投影没有被使用,可能是因为它的排序键不如旧投影匹配查询条件,或者优化器认为旧投影的 I/O 成本更低。 -
控制每张表的投影数量在 3 个以内。 每个投影都需要独立的存储空间和数据加载时间。过多的投影不仅增加维护成本,还会让优化器需要评估更多选择,增加查询规划时间。当不需要的旧投影成为优化的障碍时,用
DROP PROJECTION清理。 -
硬件配置必须一致。 即使分段设计完美,如果某个节点的网卡是 1Gbps 而其他是 10Gbps,重分段或广播数据到该节点时它就会成为瓶颈。扩容时新节点配置不得低于现有节点。
扩展阅读¶
- MPP JOIN 策略全解析与优化器决策逻辑 — JOIN 策略(Local/Broadcast/Resegment)的理论基础与优化器决策原理
- Vertica CPU 持续高负载诊断与优化 — RESEGMENT 是 CPU 高负载的常见根因
- Vertica 统计信息管理与查询性能 — 统计信息如何决定 Broadcast vs RESEGMENT
- Vertica 错误日志解读与常见错误处理 — RESEGMENT 相关的事件监控方法
- Vertica 监控最佳实践 — 数据倾斜的日常监控方案