MPP JOIN 策略全解析与优化器决策逻辑 —— 优化器如何在 Local / Broadcast / Resegment 之间做 cost-based 决策¶
作者:JiangChong | 发布时间:2026-06-09
适用场景框:当你需要理解为什么一个 JOIN 查询有时飞快有时极慢、EXPLAIN 输出中
RESEGMENT/BROADCAST的含义、以及如何通过投影设计和统计信息来引导优化器选择正确的 JOIN 策略时,这篇文章适合你。
关联文章:
- Vertica Query Optimizer 论文 — 优化器架构、Join Ranking、Cost Model、物理属性枚举
- SIPS 论文 — SIPS 的设计动机、与 Early/Late Materialization 的 trade-off
- Vertica Join 重分段倾斜诊断与修复 — 实操层面的 JOIN 倾斜诊断与修复方法论
- Vertica 统计信息管理与查询性能 — 统计信息如何影响优化器的 RESEGMENT/BROADCAST 决策
- Projection 优化最佳实践 — 投影分段设计与排序选择
理解全文脉络¶
本文从「分布式 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 决策最相关的是三个阶段:
- Projection Set Chooser:为查询中的每张表选择一组候选 projection。选择标准包括:是否覆盖所需列、排序键是否匹配
ORDER BY/GROUP BY、分段键是否与 JOIN 键对齐 - Join Order Enumerator:通过 Join Ranking 探索 JOIN 顺序,每次探索时决定 JOIN 算法(Hash/Merge)、内外表顺序、是否需要
Resegment/Broadcast - 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_id,customers 的分段键也是 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 为例):
- JOIN 先加载 inner 表(较小的表),构建哈希表(键集)
- 哈希表的键信息被传递给 outer 表的 Scan 操作
- Scan 在读取 outer 表的列数据时,逐行检查该行的 JOIN 键是否存在于哈希表中——如果不存在,直接跳过这行
- 只有通过 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;
优化器的决策过程:
- Projection 选择:
orders的 super projection 按order_id分段 → 与 JOIN 键customer_id不匹配。customers按customer_id分段 → 与 JOIN 键匹配。优化器给出中等 co-location 分数 - Join Ranking:
customers小(100 万行)、orders大(10 亿行)→customers作为 inner 表 - 策略决策:
- Broadcast:
customers100 万行 → 广播成本 = 100 万行 × 4 个节点 = 400 万行的网络传输 - Resegment:
orders10 亿行 → 重分段成本 = 10 亿行的网络传输 - → 选择 Broadcast(400 万行 << 10 亿行)
- Broadcast:
- SIPS:
customers的customer_id哈希表传给orders的 Scan → 筛选出只属于 2026-06-09 且有对应客户的订单 - 执行计划定型: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 多个维度表(如 customers 按 customer_id、products 按 product_id、dates 按 date_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。
三个关键结论:
- SIPS 在低选择性时效果最好(23 ms vs EM 的 1206 ms,快 52 倍)
- SIPS 从不显著有害——在 100% 选择性时 SIPS 的 1222 ms 接近 EM 的 1204 ms(SIPS 自检到无效后关闭)
- 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_id,customer_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 小时。
诊断:
- 检查执行计划,发现关联关系选择不合理
- 检查表数据量——驱动表
ofr_subs_his_d有统计信息,但其余 5 张 JOIN 表均无统计信息 - 优化器在缺失 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。
诊断过程:
- 执行计划显示绝大部分步骤在单节点上执行(
Execute on: v_edw_node0039) - 执行计划中出现
Outer (RESEGMENT)+Inner (RESEGMENT)的双向重分段 - 两个表的统计信息均已过期(
NO STATISTICS) - 表 1 按
statis_date分段——与 JOIN 键(user_id_zk, user_id_fk)不匹配 - 表 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 内笔记(按推荐阅读顺序)¶
- Vertica Query Optimizer 论文 — 优化器架构的完整描述。§III(Plan Search Space)对理解优化器需要选择什么至关重要;§IV-E(Join Order Enumerator + Join Ranking)是本文 §2.2 的详细展开
- SIPS 论文 — SIPS 的完整设计动机和实验验证。§V 的 SIPS 与其他技术(Bloomjoin、Semijoin、Magic Sets)的关系值得深读
- Vertica Join 重分段倾斜诊断与修复 — 从实操角度覆盖 JOIN 倾斜的完整诊断流程(5 步诊断法 + 4 类解决方案),包含丰富的 SQL 工具箱
- Vertica 统计信息管理与查询性能 — 统计信息的生命周期管理:何时收集、如何验证、自动化策略
- 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 问题时绕不开的课题。
扩展阅读¶
- Vertica Join 重分段倾斜诊断与修复 — JOIN 倾斜的完整诊断流程与修复方案
- Vertica 统计信息管理与查询性能 — 统计信息如何决定 Broadcast vs RESEGMENT 决策
- Projection 优化最佳实践 — 投影分段键选择与排序设计
- Vertica CPU 持续高负载诊断与优化 — RESEGMENT 是 CPU 高负载的常见根因