跳转至

MPP JOIN 策略全解析与优化器决策逻辑 —— 优化器如何在 Local / Broadcast / Resegment 之间做 cost-based 决策

作者:JiangChong | 发布时间:2026-06-09

适用场景框:当你需要理解为什么一个 JOIN 查询有时飞快有时极慢、EXPLAIN 输出中 RESEGMENT / BROADCAST 的含义、以及如何通过投影设计和统计信息来引导优化器选择正确的 JOIN 策略时,这篇文章适合你。

关联文章

理解全文脉络

本文从「分布式 JOIN 为什么难」开始,先建立三种 JOIN 策略(Local / Broadcast / Resegment)的直觉理解(第 1 节),然后深入优化器内部——Join Ranking、Cost Model、SIPS(第 2 节),再分析关键设计决策的 trade-off(第 3 节)。如果你关注的是「我的 JOIN 为什么慢」这种实操问题,第 4 节和第 5 节提供了从统计信息到投影设计的完整诊断闭环;第 6 节将原理浓缩为 8 条可执行的设计原则。


1. 问题背景 — MPP 中的 JOIN 为什么是一个难题

1.1 单机 JOIN 很简单,MPP JOIN 很复杂

在单机数据库中,JOIN 是一个纯本地操作——两张表的数据都在同一台机器的内存和磁盘上,优化器只需要选择算法(Hash Join 还是 Merge Join)和驱动表(哪张表建哈希表)。但在 MPP 数据库中,数据分布在多台机器的本地磁盘上,JOIN 的瓶颈从「CPU / 内存」变成了「网络」

考虑一个最简场景:orders 表按 order_id 哈希分布在 4 个节点上,customers 表按 customer_id 哈希分布在 4 个节点上。执行 orders JOIN customers ON orders.customer_id = customers.customer_id 时:

  • order_id = 100 的订单在 node1,但对应的 customer_id = 50 的客户信息在 node3
  • node1 无法在本地完成 JOIN——它需要从 node3 获取客户数据

这就是 MPP JOIN 的核心矛盾:数据按一种键分布,但 JOIN 按另一种键关联。优化器必须在三种策略之间做出选择。

1.2 三种 JOIN 策略的直观理解

策略 原理 网络开销 何时适用
Local Join 两张表的 JOIN 键数据已在同一节点上(分段键对齐),JOIN 纯本地完成 分段键 = JOIN 键,或小表 UNSEGMENTED
Broadcast Join 将较小的表完整复制到所有节点,每节点用本地副本与本地的大表数据 JOIN 中等(= 小表大小 × 节点数) 一张表远小于另一张表
Resegment Join 将一张或两张表按 JOIN 键重新哈希分布到所有节点 (= 被重分段的表的数据量) 两张表都很大,广播不现实

比喻:假设你在 4 个城市的 4 个仓库中各存了一批订单,客户资料分散在 4 个仓库中。现在要统计每个客户的订单金额:

  • Local Join:如果订单和客户资料已经按客户 ID 分配在同一仓库,直接本地统计就完了——零运输成本
  • Broadcast Join:如果客户资料只有 50 页,复印 3 份快递到另外 3 个仓库——运输成本是 150 页
  • Resegment Join:如果订单和客户资料都很大,每个仓库把属于「北京客户」的订单和客户资料都发到仓库 1,属于「上海客户」的发到仓库 2……——运输成本是全部数据量

1.3 Vertica 的分段机制与 JOIN 的关系

Vertica 在 projection 级别定义分段(SEGMENTED BY HASH(cols) ALL NODES),这意味着:

  • 同一张表的不同 projection 可以用不同的键分段。优化器可以针对不同 JOIN 选择不同 projection——按 customer_id 分段的 projection 用于客户维度的 JOIN,按 product_id 分段的用于商品维度的 JOIN
  • 分段键可以独立于排序键。数据按 customer_id 分到各节点,但在节点内按 order_date 排序——分段(决定数据在哪台节点)和排序(决定节点内数据如何组织)是解耦的

这个设计比传统 MPP 的 table-level 分布更灵活,但也意味着优化器的搜索空间更大——它不仅要选择 JOIN 算法和顺序,还要为每张表选择最合适的 projection。来源:C-Store 7 Years §3.6,Vertica Query Optimizer 论文 §II-A。


2. 核心概念与机制 — 优化器如何做 JOIN 决策

2.1 优化器的流水线

Vertica 优化器将 SQL 转换为执行计划的过程分为多步。来源:Vertica Query Optimizer 论文 §IV。

SQL → Parser → Rewriter → Join Graph → Projection Set Chooser → Join Order Enumerator → Plan Constructor → Execution Engine

与 JOIN 决策最相关的是三个阶段:

  1. Projection Set Chooser:为查询中的每张表选择一组候选 projection。选择标准包括:是否覆盖所需列、排序键是否匹配 ORDER BY/GROUP BY、分段键是否与 JOIN 键对齐
  2. Join Order Enumerator:通过 Join Ranking 探索 JOIN 顺序,每次探索时决定 JOIN 算法(Hash/Merge)、内外表顺序、是否需要 Resegment/Broadcast
  3. Cost Model:对每个候选计划计算 Disk/CPU/Network/Memory 四个维度的 cost aspect,选择总成本最低的计划

2.2 Join Ranking:优化器的搜索策略

Vertica 优化器不穷举所有可能的 JOIN 顺序,而是通过 Join Ranking 启发式地引导搜索。来源:Vertica Query Optimizer 论文 §IV-E。

Join Ranker 对每对剩余待 JOIN 的表计算一个积分排名,排名越低的 JOIN 越先执行。排名基于:

因素 说明 对 JOIN 策略的影响
Selectivity JOIN 能淘汰多少行?越筛选性的 JOIN 越优先 高选择性 → 优先执行 → 减少后续步骤的数据量
Cardinality JOIN 两边的相对大小 小表作为 inner(构建哈希表),大表作为 outer(探测)
Co-location 数据是否已按 JOIN 键分布在同一节点? 已对齐 → Local Join,无需网络传输
Sortedness 输入数据是否已按 JOIN 键排序? 已排序 → 可用 Merge Join(更快、更省内存)
Constraints 是否有主键/外键约束可用? FK-PK → 优化器可利用约束简化 JOIN 估算

Co-location 排名的影响:如果 orders 的分段键是 customer_idcustomers 的分段键也是 customer_id,那么 Co-location 排名会非常高——优化器可以确定这个 JOIN 完全不需要网络传输,几乎总会优先执行。

2.3 Cost Model:四维成本估算

Vertica 优化器使用「数据流」模型而非「执行时间」模型来比较计划。来源:Vertica Query Optimizer 论文 §IV-F。

每个计划节点的数据流被分为四个 cost aspect

Aspect 含义 JOIN 场景示例
Disk 需要从磁盘读取的数据量 扫描两张表各需要多少列
CPU 需要 CPU 处理的数据量 哈希计算、编码解码、表达式求值
Network 需要在节点间传输的数据量 Resegment 重分发、Broadcast 广播
Memory 需要的内存(哈希表大小等) Inner 表大小 = 哈希表大小;必要时 spill 到磁盘
Parallelism 计划在多少个节点上执行 高并行度 = 更多节点分担负载

Parallelism(计划在多少个节点上并行执行)作为额外的执行维度被单独描述——高并行度意味着更多节点分担负载。论文中将 Disk / CPU / Network / Memory 明确归为「cost aspect」这个术语,而 Parallelism 是与它们并列但不属于 cost aspect 的独立考量。

Network aspect 的权重很大——因为网络带宽通常是 MPP 集群的瓶颈。当一个 JOIN 需要 Resegment 时,Network cost 会显著增加,优化器会倾向于寻找 Co-location 更好的替代计划(例如:翻转内外表顺序、选择不同 projection、或者接受 Broadcast 如果被广播的表足够小)。

2.4 SIPS(Sideways Information Passing):列存 JOIN 的加速器

SIPS 是 Vertica 对列存 JOIN 最重要的优化之一。它的核心思想是:在 JOIN 执行之前,把过滤信息「侧向传递」给扫描操作,让扫描在读取时就跳过多余的行。来源:SIPS 论文 §I,Design Choices §3.1。

SIPS 的工作流程(以 Hash Join 为例):

  1. JOIN 先加载 inner 表(较小的表),构建哈希表(键集)
  2. 哈希表的键信息被传递给 outer 表的 Scan 操作
  3. Scan 在读取 outer 表的列数据时,逐行检查该行的 JOIN 键是否存在于哈希表中——如果不存在,直接跳过这行
  4. 只有通过 SIPS 过滤的行才会继续参与 JOIN

这个优化的威力在于:在列存中,Scan 返回的数据不必全部物化。如果 SIPS 过滤掉了 99% 的行,后续只需物化 1% 的列数据——这相当于把 Late Materialization 的收益包装成了一个更简单、更稳健的机制

在真实 EXPLAIN 输出中,SIPS 的痕迹如下:

Access Path:
+-JOIN HASH [Cost: 5K, Rows: 5M] (PATH ID: 2)
|  Join Cond: (s.product_key = p.product_key)
| +-- Outer -> STORAGE ACCESS for s [Cost: 4K, Rows: 5M]
| |      Runtime Filter: (SIP1(HashJoin): s.product_key)  ← SIPS 过滤器
| +-- Inner -> STORAGE ACCESS for p [Cost: 152, Rows: 60K]

Runtime Filter: (SIP1(HashJoin): s.product_key) 表示外表扫描时会用 JOIN 的哈希表过滤 product_key——不在内表中的键不会继续参与后续处理。

SIPS 的优雅之处(来自 Design Choices §3.1 的经验教训):

  • SIPS 从不显著有害。如果检测到过滤效果不佳,SIPS 会自动关闭,避免运行时开销
  • SIPS + Early Materialization 的效果几乎等于 Late Materialization,但实现复杂度低得多
  • Vertica 工程团队后来认为「只做 SIPS + Early Materialization 就够了,如果重来一次,不会费劲实现 Late Materialization」

2.5 完整决策链路:从 SQL 到 JOIN 策略

假设以下查询:

SELECT c.name, SUM(o.amount)
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date = '2026-06-09'
GROUP BY c.name;

优化器的决策过程:

  1. Projection 选择orders 的 super projection 按 order_id 分段 → 与 JOIN 键 customer_id 不匹配。customerscustomer_id 分段 → 与 JOIN 键匹配。优化器给出中等 co-location 分数
  2. Join Rankingcustomers 小(100 万行)、orders 大(10 亿行)→ customers 作为 inner 表
  3. 策略决策
    • Broadcastcustomers 100 万行 → 广播成本 = 100 万行 × 4 个节点 = 400 万行的网络传输
    • Resegmentorders 10 亿行 → 重分段成本 = 10 亿行的网络传输
    • 选择 Broadcast(400 万行 << 10 亿行)
  4. SIPScustomerscustomer_id 哈希表传给 orders 的 Scan → 筛选出只属于 2026-06-09 且有对应客户的订单
  5. 执行计划定型:Hash Join with Broadcast + SIPS

如果在步骤 1 中 orders 有一个按 customer_id 分段的 projection,co-location 分数会变成满分 → 优化器会选择 Local Join,网络传输量为零。这就是投影分段设计与 JOIN 性能之间的直接因果链


3. 设计决策与 Trade-off

3.1 Broadcast vs Resegment:何时哪个更好?

Broadcast 的基础成本 = 被广播表大小 × 节点数,Resegment 的成本 = 被重分段表的大小(与节点数无关)。因此决策的核心是比较这两个乘积

条件 倾向 原因
被广播表「足够小」 Broadcast 广播成本远小于重分段大表
被广播表太大 Resegment 广播到所有节点的总成本超过重分段
边界情况 取决于统计信息准确性 优化器根据行数、列宽、节点数计算两种方案的实际 cost aspect

最危险的错误:统计信息缺失时,优化器不知道 inner 表的真实大小,可能将 10 亿行的大表 Broadcast 到所有节点——导致 10 亿行 × N 节点的数据洪水。这比选择 Resegment 要糟糕得多,因为 Resegment 的总网络传输量等于一张表的大小,而错误广播等于大表 × 节点数

📋 真实案例:甘肃移动某集群(50 节点),5 张表缺失统计信息,优化器对多表关联做出了错误的 JOIN 策略选择,查询执行超过 1 小时。收集统计信息后降至 3.5 秒——提升超过 1000 倍。来源:甘肃移动 Vertica 数据库性能问题报告(2021-05-17)。

3.2 Hash Join vs Merge Join:列存下的特殊考量

Vertica 支持两种 JOIN 算法,选择取决于数据的 sortedness。来源:Design Choices §3.3。

维度 Hash Join Merge Join
前提条件 至少一侧输入按 JOIN 键排序
内存需求 需要构建完整哈希表 只需缓存两侧当前行
CPU 效率 对每行外表数据做一次哈希查找 逐行归并比较,JIT 编译可加速
Spill 风险 高(哈希表 > 内存则 spill) 低(外排序可 spill)
Vertica 倾向 默认首选 输入已排序 + 大表场景

Vertica 早期的外存 JOIN 算法是 Partitioned Hash Join(PHJ),但在 v4.0 改为了 Sort Merge Join(SMJ)。原因:SMJ 更快、更可扩展、且能利用 Vertica 已有的排序——如果输入已经按 JOIN 键排序,SMJ 几乎零额外成本。来源:Design Choices §3.3。

3.3 投影级分段 vs 表级分段:Vertica 的独特设计

传统 MPP(如 Greenplum)在表级定义分布键——一张表的所有数据按同一键分布。Vertica 在 projection 级定义分段——同一张表可以有多个 projection,每个用不同的分段键。

设计 优势 代价
表级分段(Greenplum 等) 简单,优化器不需要在 projection 之间选择 只有一种分布方式,无法针对不同 JOIN 优化
投影级分段(Vertica) 每种 JOIN 模式可用不同分段键的 projection 存储冗余、加载多次写入、优化器搜索空间更大

投影级分段的核心价值:事实表(如 orders)通常需要同时 JOIN 多个维度表(如 customerscustomer_idproductsproduct_iddatesdate_id)。表级分段只能优化其中一个 JOIN,投影级分段可以创建三个 projection——每个针对一个维度 JOIN 优化,优化器自动选择最合适的。

但在实践中,大多数客户只有 0-3 个额外的窄 projection 外加 1 个 super projection(来源:C-Store 7 Years §3.1)——因为投影的存储和加载成本是真实的,只在「高频 JOIN 成本远高于额外投影成本」时才值得创建。

3.4 Early vs Late Materialization + SIPS:一个被重新评估的 trade-off

SIPS 论文的核心观点是:SIPS + Early Materialization 的效果几乎等价于 Late Materialization,但复杂度低得多

策略 JOIN 选择性 0% JOIN 选择性 50% JOIN 选择性 100% 实现复杂度
EM only 1206 ms 1202 ms 1204 ms
LM only 39 ms 1050 ms 1720 ms 极高
SIPS only 23 ms 1086 ms 1222 ms
SIPS + LM 27 ms 1047 ms 1724 ms 极高

数据来源:Design Choices §3.1 Table 1。

三个关键结论:

  1. SIPS 在低选择性时效果最好(23 ms vs EM 的 1206 ms,快 52 倍)
  2. SIPS 从不显著有害——在 100% 选择性时 SIPS 的 1222 ms 接近 EM 的 1204 ms(SIPS 自检到无效后关闭)
  3. LM 在高选择性时有性能悬崖(1720 ms vs EM 的 1204 ms)——因为大量行通过 JOIN 后需要物化列,物化成本超过预过滤收益

Vertica 的选择:用 SIPS + EM 作为默认,LM 仅在特定场景(极低选择性 + 非溢写 JOIN)被保留。来源:Design Choices §3.1。


4. 设计对实际使用的影响

4.1 查询维度

自动生效的收益

  • Co-located Local Join:如果两张表的投影分段键相同且等于 JOIN 键,优化器自动选择纯本地 JOIN——这是「零干预」的最高性能
  • SIPS 自动激活:对于 Hash Join,SIPS 自动生成 Runtime Filter,无需任何手动配置
  • 小表 Broadcast:优化器自动判断被广播的表是否足够小

需要手动干预的场景

  • 分段设计不当导致 RESEGMENT:在 EXPLAIN 输出中搜索 RESEGMENT——如果出现且涉及大表,说明投影分段需要调整。详见 Vertica Join 重分段倾斜诊断与修复 的完整诊断流程
  • 统计信息缺失导致错误 Broadcast:EXPLAIN 中显示 (NO STATISTICS) 时,优化器可能把大表 Broadcast。立即运行 ANALYZE_STATISTICS()
  • 单节点执行:如果 EXPLAIN 显示 Execute on: v_xxx_node0001(单节点),通常是某张表采用了 UNSEGMENTED 且优化器无法并行化

4.2 加载维度

分段设计对数据加载有直接影响:

  • 每个 SEGMENTED projection 在加载时都需要将数据按哈希键分发到各节点 → 加载时会消耗网络带宽
  • Prejoin projection 在加载时执行 JOIN → 如果维度表很大,加载速度会显著下降
  • 多 projection = 多次写入 → 每个额外 projection 都增加加载时间

4.3 常见误解与澄清

误解 事实
「RESEGMENT 总是坏的」 不一定。如果两张表都很大且分段键不同,RESEGMENT 可能是唯一选择。问题不在于 RESEGMENT 本身,而在于是否需要它——如果 JOIN 是高频操作且分段键可以对齐,避免 RESEGMENT 才有意义
「统计信息不重要,优化器会自己判断」 这是最危险的误解。缺失统计信息时优化器可能把 10 亿行的表 Broadcast 到所有节点——这比正确的 Resegment 糟糕一个数量级。详见 §3.1
「所有小表都应该 UNSEGMENTED」 UNSEGMENTED 只在表很小(< 10 万行)时适用。UNSEGMENTED 的大表会导致 JOIN 时所有节点都有全量副本 → 每节点都需要构建完整哈希表 → 内存压力巨大
「Prejoin Projection 是万能的」 Prejoin 只在 N:1 关系(事实表 JOIN 维度表)下可用,且会增加加载成本。C-Store 7 Years §3.3 明确指出 prejoin 的使用频率低于预期

5. 案例验证

5.1 虚构案例:分段键不对齐造成双向 RESEGMENT

📝 虚构案例

场景:某零售企业 Vertica 集群(4 节点),事实表 sales_fact(按 sale_id 分段,2.8 亿行/天),维度表 customer_dim(按 customer_name 分段,8000 万行)。日报查询:

SELECT c.customer_segment, SUM(s.quantity * s.unit_price) AS revenue
FROM sales_fact s
JOIN customer_dim c ON s.customer_id = c.customer_id
WHERE s.sale_date = CURRENT_DATE - 1
GROUP BY c.customer_segment;

EXPLAIN 关键输出

+-JOIN HASH [Cost: 35M, Rows: 280M] (PATH ID: 2)
|  Join Cond: (s.customer_id = c.customer_id)
| +-- Outer -> STORAGE ACCESS for s [Rows: 280M]  ← 按 sale_id 分段
| +-- Inner -> STORAGE ACCESS for c [Rows: 80M](RESEGMENT)  ← 需要按 customer_id 重分段!

根因sales_fact 分段键 = sale_idcustomer_dim 分段键 = customer_name,JOIN 键 = customer_id——三个键各不相同。优化器选择以 customer_dim 为 inner 表并对其做 RESEGMENT。8000 万行重分段 = ~8 GB × 4 节点的网络传输。

此外,customer_dim 重分段后某些 customer_id 热点值(大客户)集中到 node3,导致 node3 接收的网络数据量是其他节点的 3 倍。

修复

-- 将 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');

效果

指标 优化前 优化后
执行时间 18 分钟 2.5 分钟
RESEGMENT 网络量 ~32 GB 0(本地 JOIN)
节点间 CPU 差异 3×(node3 96%) < 20%
RESEGMENTED_MANY_ROWS 每次触发 0

5.2 虚构案例:SIPS 在低选择性 JOIN 中的效果

📝 虚构案例

场景:某电商平台需要对所有 VIP 用户的订单做分析。VIP 用户只有 5 万人(占全部 2000 万用户的 0.25%),订单表 5 亿行。查询:

SELECT v.user_id, SUM(o.amount)
FROM orders o
JOIN vip_users v ON o.user_id = v.user_id
GROUP BY v.user_id;

三种执行策略的对比

策略 扫描行数 I/O (列数据) 哈希表大小 执行时间
EM(无 SIPS) 5 亿行全扫描 全量 3 列 5 万行 1206 ms
LM 先扫 user_id 列,只物化匹配的行 匹配行 3 列 5 万行 39 ms
SIPS + EM 5 亿行扫描但 SIPS 过滤 匹配行 3 列 5 万行 23 ms

SIPS 的策略比 LM 还快一点(23 vs 39 ms),因为 SIPS + EM 避免了 LM 的簿记开销。这是 SIPS 论文(Table 1)中 0% 选择性场景的实际数据。

但反过来:如果 VIP 用户占 80%(选择性 80%),三种策略趋同——SIPS 自动检测到过滤效果不佳,关闭额外开销,性能退回到与 EM 相同的水平。这就是 SIPS「从不显著有害」的体现。

5.3 真实案例:统计信息缺失导致 JOIN 策略选择灾难

📋 真实案例

背景:某运营商,50 节点集群,Vertica v7.2.3,每节点 256GB 内存。

故障现象:业务侧反馈多表关联查询执行超过 1 小时。

诊断

  1. 检查执行计划,发现关联关系选择不合理
  2. 检查表数据量——驱动表 ofr_subs_his_d 有统计信息,但其余 5 张 JOIN 表均无统计信息
  3. 优化器在缺失 5 张表统计信息的情况下,无法估算它们的真实大小 → 做出的 JOIN 顺序和 Broadcast/Resegment 决策完全错误

修复

SELECT ANALYZE_STATISTICS('schema.table1');
SELECT ANALYZE_STATISTICS('schema.table2');
-- ... 共 5 张表

效果:执行时间从 1 小时降至 3.5 秒,提升超过 1000 倍。

原理回溯:这个案例体现了 §2.3 Cost Model 的核心逻辑——优化器依赖统计信息(行数、NDV、直方图)来估算每个 cost aspect。5 张表缺失统计信息 = 5 个 cost aspect 估算全部错误 = 优化器在盲猜。这种情况下,错误 Broadcast 一个「优化器以为很小但其实很大」的表,会导致灾难性的网络和内存开销。

5.4 真实案例:UNSEGMENTED 大表 + 分段键不匹配导致单节点瓶颈

📋 真实案例

背景:某运营商,93 节点集群。凌晨 3 点数据库性能严重下降。

故障现象:一条 SQL 从 22:00 持续执行到次日 04:44(超过 6.5 小时仍未完成),网络传输总量达 11 GB

诊断过程

  1. 执行计划显示绝大部分步骤在单节点上执行(Execute on: v_edw_node0039
  2. 执行计划中出现 Outer (RESEGMENT) + Inner (RESEGMENT) 的双向重分段
  3. 两个表的统计信息均已过期(NO STATISTICS
  4. 表 1 按 statis_date 分段——与 JOIN 键 (user_id_zk, user_id_fk) 不匹配
  5. 表 2 采用 UNSEGMENTED 方式——所有数据都在一台节点上

根因:UNSEGMENTED 表 2 的数据集中在一台节点 → 优化器选择在单节点执行 → 同时表 1 的分段键与 JOIN 键不匹配 → 双向 RESEGMENT 仍然发生 → 单节点承担全部网络收发和计算 → 执行时间爆炸。

修复

  • 表 1:投影改为 SEGMENTED BY HASH(user_id_zk, user_id_fk)(对齐 JOIN 键)
  • 表 2:从 UNSEGMENTED 改为 SEGMENTED BY HASH(JR_USER_ID, KD_USER_ID)(对齐 JOIN 键)

效果:单次查询耗时从 11 分 48 秒降至 5 分 37 秒,且不再有单节点瓶颈。


6. 设计原则总结

原则 1:分段键 = 最高频 JOIN 键。这是投入产出比最高的优化

为什么:分段键决定数据在哪个节点。当两张表的分段键相同且等于 JOIN 键时,JOIN 完全本地执行——零网络传输、零 resegment、每节点独立完成。反例:用 order_id 分段但 90% 的 JOIN 用 customer_id——每个 JOIN 都需要 Resegment 或 Broadcast。

原则 2:统计信息是优化器的眼睛。缺失 = 盲目飞行

为什么:优化器依赖行数、不同值数量(NDV)、直方图来估算 Broadcast 的成本。缺失时它可能把 10 亿行的大表 Broadcast——比正确的 Resegment 糟糕一个数量级。反例:大量数据加载后从不运行 ANALYZE_STATISTICS,直到查询从 3 秒变成 1 小时。

原则 3:SIPS 是列存 JOIN 的免费加速器,永远开启

为什么:SIPS 在低选择性时效果显著(最高 52× 加速),高选择性时自动关闭,从不有害。它是 Vertica 执行引擎的内置行为,不需要任何手动配置。反例:无——SIPS 不需要用户干预。但如果 EXPLAIN 中未见 Runtime Filter: (SIP...),检查是否有版本限制或执行计划未选择 Hash Join。

原则 4:Broadcast 的正确决策窗口很小——被广播的表必须确实小

为什么:Broadcast 成本 = 被广播表大小 × 节点数。如果表 100MB × 50 节点 = 5GB 网络传输——可接受。如果表 5GB × 50 节点 = 250GB——灾难。这个决策完全依赖统计信息的准确性。反例:统计信息缺失时把 2.5B 行的表 Broadcast 到 50 节点(来自 Vertica 统计信息管理与查询性能 的真实案例)。

原则 5:UNSEGMENTED 投影是一把双刃剑——只用于真正小的维度表

为什么:UNSEGMENTED 在每个节点存全量副本 → 任意 JOIN 都能本地完成。但大表 UNSEGMENTED = 每节点内存中都有一份完整哈希表 = 内存爆炸 + 单节点执行(数据无分段,优化器无法并行化)。反例:3 亿行的表 UNSEGMENTED → 每节点 3 亿行哈希表 → 200GB+ 内存占用 → JOIN 必然 spill。

原则 6:Merge Join 在排序数据上有天然优势

为什么:如果输入已经按 JOIN 键排序,Merge Join 不需要构建哈希表——内存只用来缓存当前行。Vertica 在 v4.0 从 PHJ 切换到 SMJ 作为主要外存 JOIN 算法,因为 SMJ 更快、更可扩展、且能利用列存的排序。反例:projection 排序键与 JOIN 键无关 → 优化器只能用 Hash Join → 必须构建完整哈希表。

原则 7:投影数量与 JOIN 性能不是线性关系——超过 3 个后收益骤减

为什么:每个额外 projection = 加载时多一次写入 + 存储多一份数据 + Tuple Mover 多一份维护负担。C-Store 7 Years §3.1 指出大多数客户只有 0-3 个窄 projection。反例:为每种 JOIN 模式都建 projection → 10 个 projection → 加载时间是 1 个 projection 的 10 倍。

原则 8:Prejoin Projection 解决的是 JOIN 消除,不是 JOIN 优化

为什么:Prejoin 在加载时把 JOIN 做完,查询时直接读结果——完全跳过运行时 JOIN。但代价是加载成本增加(每次加载都要做 JOIN),且只支持 N:1 关系。适合「固定 JOIN 模式 + 维度表变更极少」的场景。反例:频繁更新的维度表用 Prejoin → 每次更新需要刷新 Prejoin projection → 维护成本超过查询收益。


7. 延伸阅读

Vault 内笔记(按推荐阅读顺序)

  1. Vertica Query Optimizer 论文 — 优化器架构的完整描述。§III(Plan Search Space)对理解优化器需要选择什么至关重要;§IV-E(Join Order Enumerator + Join Ranking)是本文 §2.2 的详细展开
  2. SIPS 论文 — SIPS 的完整设计动机和实验验证。§V 的 SIPS 与其他技术(Bloomjoin、Semijoin、Magic Sets)的关系值得深读
  3. Vertica Join 重分段倾斜诊断与修复 — 从实操角度覆盖 JOIN 倾斜的完整诊断流程(5 步诊断法 + 4 类解决方案),包含丰富的 SQL 工具箱
  4. Vertica 统计信息管理与查询性能 — 统计信息的生命周期管理:何时收集、如何验证、自动化策略
  5. Projection 优化最佳实践 — 投影分段键选择和排序顺序的实操指南

论文章节引用

  • Vertica Query Optimizer 论文 §III-A — 优化器搜索空间:Table Access、Join Order、Algorithms、Column Materialization、Redistribution 五个维度
  • Vertica Query Optimizer 论文 §IV-E — Join Ranking 的排名因素(Selectivity、Cardinality、Co-location、Sortedness、Constraints)
  • Vertica Query Optimizer 论文 §IV-F — Cost Model 的四维 cost aspect(Disk/CPU/Network/Memory)
  • Design Choices §3.1 — EM/LM/SIPS 性能对比数据(Table 1)和 Vertica 工程团队的教训
  • Design Choices §3.3 — PHJ 到 SMJ 的迁移原因与性能对比(Figure 10)
  • SIPS 论文 §I — SIPS 的工作流程和「结合 EM 与 SIP 获得两者最优」的核心论点

Vertica 的 JOIN 优化器设计在 MPP 领域有一个鲜明的特点:它将数据分布(分段)决策从表级下沉到 projection 级,让同一张表可以为不同 JOIN 提供不同分段策略。这种「物理冗余换查询灵活」的思路在其他 MPP 系统中没有直接等价物,但其中的核心原则——统计信息驱动 cost-based 决策、Sideways Information Passing 式的预过滤、分段键与 JOIN 键的对齐——是所有分布式 SQL 引擎在解决 JOIN 问题时绕不开的课题。

扩展阅读