跳转至

Vertica Join 重分段倾斜诊断与修复

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

适用场景: 当你的 Vertica 集群在执行多表 JOIN 查询时,发现个别节点 CPU/网络负载远高于其他节点,或者在 EXPLAIN 输出中看到 RESEGMENTBROADCAST 关键字,本文提供从诊断到修复的完整方法论。

关联文章:

理解全文脉络:

本文按照「发现 → 定位 → 修复 → 预防」的闭环组织。第 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)。 源表使用了非均匀分布的分段键(如 genderstatus 等低基数列),导致数据在写入时就已经倾斜。当这些数据被重分段时,倾斜会被放大——原因在于重分段时每个节点都要把自己持有的数据按 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 都会发生。以下条件同时满足时才会触发:

  1. 两张表做 JOIN(无论是 Hash Join 还是 Merge Join)
  2. 两张表的分段键不一致(或者至少有一张表的分段键与 JOIN 键不匹配)
  3. 没有其他更好的执行策略(如优化器无法通过翻转 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_idstatement_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_idstatement_id,后续步骤会反复使用。

如果不是: 如果没有 RESEGMENTED_MANY_ROWS 事件但怀疑存在分段问题,直接跳到 3.2 步,通过 EXPLAIN 手动检查执行计划。

3.2 第二步:分析执行计划中的 RESEGMENT/BROADCAST

做什么: 使用上一步获取的 transaction_idstatement_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 = truesegment_expression 包含 JOIN 键列:设计正确。JOIN 可以在本地完成。
  • is_segmented = truesegment_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 确认执行计划中不再出现 RESEGMENTBROADCAST

EXPLAIN SELECT ... FROM schema.table_a JOIN schema.table_b ON table_a.join_key = table_b.join_key;

如果执行计划显示 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 ...;

确认执行计划中不再出现 RESEGMENTBROADCAST 即表示优化器已切换到新投影。

3. 删除旧投影,释放存储

-- 确认新投影稳定运行后再删除旧投影
DROP PROJECTION schema.table_a_old_projection;

⚠️ 删除旧投影前务必确认:① 新投影已 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)

根因分析: 两个问题叠加致命——

  1. 内存层面accounts 是 UNSEGMENTED 投影,3 亿行数据在每个节点上都有全量副本。Hash Join 时每个节点把 3 亿行数据加载到自己的内存中构建哈希表。3 亿行的哈希表远超资源池 query_budget 容量,进而 spill 到磁盘。
  2. 网络层面transactionstrans_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 小时。

诊断过程:

  1. 检查执行计划,发现关联关系选择不合理
  2. 检查表数据量后发现驱动表 ofr_subs_his_d 有统计信息,但其余 5 张表均无统计信息
  3. 对这 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 小时。

诊断过程:

  1. 通过 query_requests 定位到问题 SQL:网络传输总量达 11GB,绝大部分执行步骤在单节点上进行(Execute on: v_edw_node0039

  2. 检查执行计划:

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. 检查投影设计:

  2. 表 1:按 statis_date 分段,未按关联字段分段

  3. 表 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_factsale_id 分段
  • customer_dimcustomer_name 分段
  • JOIN 键是 s.customer_id = c.customer_id

三个键各不相同。结果是:sales_factcustomer_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 节:最佳实践清单

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

  1. 建表时让分段键 = 最高频 JOIN 键。 这是投入产出比最高的优化,因为它在根源上消除了重分段。在表设计阶段就应该明确「这张表最常和哪张表做 JOIN、用什么键」,然后让两张表共用相同的分段键。如果一张表参与多种 JOIN(如事实表 JOIN 多个维度表),优先选择数据量最大的那个 JOIN 作为分段键依据。

  2. 小维度表(< 10 万行)使用 UNSEGMENTED 投影。 每个节点保存一份完整副本,任何 JOIN 都可以本地完成。但必须严格控制大小——如果一张表从 10 万行增长到 1000 万行,UNSEGMENTED 会从优化变成瓶颈。

  3. 数据加载后立即收集统计信息。 统计信息是优化器正确选择 Broadcast vs Resegment 的前提。缺失统计信息时,优化器可能将大表 Broadcast,这是比 Resegment 更严重的错误——因为 Broadcast 让单节点承担全部数据。建议在 ETL 流程中每个 INSERT / COPY 后加一行 SELECT ANALYZE_STATISTICS('')

  4. 上线前用 EXPLAIN 验证执行计划。 在 SQL 上线到生产环境之前,在 EXPLAIN 输出中搜索 RESEGMENTBROADCAST(NO STATISTICS)。如果出现这些关键字且涉及大表,说明投影设计需要优化。

  5. 监控 RESEGMENTED_MANY_ROWS 事件。 将它加入日常监控 Dashboard。如果这个事件从偶尔出现变成每天出现,说明数据量增长导致原本可接受的重分段变成了瓶颈。

  6. 避免对 JOIN 键使用函数变换。 JOIN ON UPPER(a.name) = b.name 这类写法会强制重分段,因为哈希计算基于变换后的值。如果需要在 JOIN 中使用函数,考虑在 ETL 阶段预计算一个标准化列,JOIN 时直接用标准化列。

  7. 当数据倾斜无法通过分段键解决时,考虑分区。 如果 JOIN 键本身的基数分布就严重不均(如少数超级客户贡献了 80% 的数据),即使哈希分段也会倾斜。此时可以结合分区表设计,将热点数据和不热点数据分开处理。但这属于进阶优化,一般场景中用对齐分段键即可解决。

  8. 创建新投影后验证优化器是否真的用了它。 使用 SELECT * FROM query_profiles WHERE table_name = ':table' 检查优化器实际选择了哪个投影。如果新投影没有被使用,可能是因为它的排序键不如旧投影匹配查询条件,或者优化器认为旧投影的 I/O 成本更低。

  9. 控制每张表的投影数量在 3 个以内。 每个投影都需要独立的存储空间和数据加载时间。过多的投影不仅增加维护成本,还会让优化器需要评估更多选择,增加查询规划时间。当不需要的旧投影成为优化的障碍时,用 DROP PROJECTION 清理。

  10. 硬件配置必须一致。 即使分段设计完美,如果某个节点的网卡是 1Gbps 而其他是 10Gbps,重分段或广播数据到该节点时它就会成为瓶颈。扩容时新节点配置不得低于现有节点。


扩展阅读