跳转至

MPP 物化视图与投影的设计哲学 —— 从三元 trade-off 理解为什么 Vertica 把 projection 做成了唯一的物理存储结构

作者:JiangChong | 撰写时间:2026年06月

适用场景框:当你需要在查询加速、加载吞吐和存储成本之间做权衡决策时——无论是设计表结构、审核 projection 方案,还是排查「为什么加了 projection 反而慢了」的问题。

开篇声明:本文以 Vertica projection 为主要剖析对象,但在所有环节对比其他 MPP 系统的不同设计选择——包括 Greenplum(页内标记 + VACUUM)、ClickHouse(MergeTree + 触发器式物化视图)、Doris/StarRocks(Rollup/同步物化视图)、Snowflake(微分区 + 自动聚类)。「MPP 共性」与「Vertica 专属」将在文中明确区分。

关联文章MPP 数据分布策略(projection 的分段机制详解)、Projection 优化最佳实践(实操指南)、MPP JOIN 策略全解析与优化器决策逻辑(JOIN 中 projection 如何影响优化器选择)

理解全文脉络

本文的叙事逻辑是从「物化视图的通解」到「Vertica projection 的特解」:第 1-3 节建立三元 trade-off 框架,第 4 节分析这个框架在实际生产中的表现形态,第 5 节的案例提供具体的数据锚点,第 6 节将分析结果提炼为可操作的设计原则。

  • 如果你只想知道怎么做:直接跳到第 6 节设计原则 + 第 5 节案例
  • 如果你想理解为什么 Vertica 选了这种极端设计:从第 2 节读到第 3 节
  • 如果你有 MPP 基础但想快速理解 projection 和物化视图的区别:先读第 2 节末的对比表,再回头看第 2 节

1. 问题背景 — 物化视图要解决什么

在传统的 OLAP 数据仓库中,分析师面对一个基本矛盾:原始数据按业务实体组织(如订单表、客户表),而查询按分析维度组织(如按地区、按时间)。 这两种组织方式的差异,导致每次查询都需要扫描大量原始数据并做 JOIN、聚合、排序。

数据库行业的标准解法是物化视图(Materialized View, MV)——把查询结果预先算好存下来,查询时直接读取而非重新计算。这是一个古老而有效的想法:用空间换时间

但不同 MPP 系统对「物化什么、何时刷新、如何保证一致性」这三个子问题给出的答案截然不同。下表概括了主流 MPP 系统的策略分歧:

系统 物化策略 刷新机制 一致性保证 存储独立性
Vertica Projection 即唯一物理存储 加载时同步写入(epoch 机制) 严格一致(所有投影共享 epoch) 无独立基表,投影互为主存储
Greenplum AO 表 + 物化视图 手动 REFRESH / 触发器 取决于刷新策略 基表 + MV 两份独立存储
ClickHouse MergeTree 投影 / 触发器式 MV 写入时增量计算(MV 本质是 INSERT 触发器) 最终一致(无事务保证) 基表独立,MV 另存
Doris / StarRocks Rollup / 同步物化视图 同步:写入时 Base 表 → Rollup 级联刷新 强一致(同步模式) Base 表 + Rollup 独立存储
Snowflake 物化视图 + 自动聚类 后台自动刷新(serverless) 取决于刷新配置 基表独立,MV 另算存储费用

本文将以 Vertica 为深度剖析对象,在关键决策点对比 Greenplum、ClickHouse 和 Snowflake 的不同选择,揭示每种设计背后的架构约束。

但物化视图引入了一个经典的三元矛盾:

           查询性能
             /\
            /  \
           /    \
          /      \
         /  最优?  \
        /__________\
   加载性能 -------- 存储空间
  • 查询性能:物化的副本越多、越贴近查询模式,查询越快
  • 加载性能:每多一个物化副本,每次数据加载就要多更新一份,加载变慢
  • 存储空间:每多一个副本就多占一份磁盘空间

这三者不可能同时最优。传统数据库的做法是「按需选择」——DBA 分析慢查询,手动创建索引或物化视图,每个都是一次性的权衡。 这在数据量 TB 级、查询模式相对固定的 OLAP 场景下勉强可工作。

但当数据进入 PB 级,查询模式随业务快速变化时,传统做法暴露出三个致命问题:

  1. 手动设计跟不上:成百上千张表,每张表可能有几十种查询模式,不可能靠 DBA 逐个分析
  2. 维护成本失控:物化视图的增量刷新在分布式环境下极其复杂——C-Store 7 Years 论文明确指出「维护带聚合和过滤的物化视图的代价,在真实分布式系统中不切实际」(来源:The Vertica Analytic Database: C-Store 7 Years Later §3.1)
  3. 存储成本爆炸:每个物化视图都是一份完整的数据副本,在 PB 级数据量下成本不可接受

因此我们需要一种新的设计思路:不是「在表上加索引/物化视图」这种辅助结构的思路,而是让「物理存储本身就是查询优化的载体」。


2. 核心概念与机制

本节按 「MPP 共性模式 → 各系统实现差异」 两层结构组织。每个子节先描述所有列存 MPP 面临的共同问题,再展示 Vertica 的解法和不同系统的对比选择。

所有列存 MPP 数据库在设计物理存储时,必须回答同一个架构问题:存储单元是否同时承担查询优化的职责? 传统数据库的回答是「分离」——基表文件负责存储,索引/物化视图负责优化。Vertica 的回答是「统一」——projection 既是存储也是优化载体。ClickHouse 和 Snowflake 则各取了中间路线。下表概括四种范式:

范式 代表系统 存储独立性 查询优化载体 设计复杂度
存储=优化 Vertica 无独立基表,投影互为主存储 Projection 的排序/分段/编码 DBA 需理解投影设计
存储+插件式索引 Greenplum 基表独立,AO/Heap 文件 物化视图、索引(B-tree/Bitmap) 需手动为不同查询创建不同对象
存储+触发器式 MV ClickHouse MergeTree 独立存储 物化视图(INSERT 触发器)、Projection(v23+) MV 是触发器流,理解门槛高
存储=自动优化 Snowflake 微分区独立存储 自动聚类 + Search Optimization Service 几乎零设计,但有费用

2.1 物化视图:从通用概念到 Vertica 的极端

物化视图在传统数据库中的定位是「辅助索引」——表是主体,物化视图是附属。查询优化器在运行时决定是读基表还是读物化视图。

Vertica projection 将这个逻辑推到了极端:projection 不是辅助结构,它就是唯一的物理存储。 在 Vertica 中,不存在「基表文件」这种概念——你创建一张表,数据并不以表为单位存储,而是以 projection 为单位存储。逻辑表只是一个视图,其物理实体是对应的 projection 集合。

对比:

特性 传统物化视图 Vertica Projection
定位 基表的辅助索引 表的唯一物理存储
是否必须 可选 至少 1 个 super projection(含所有列)
数据冗余 基表 + MV 两份数据 每个 projection 独立存储,super projection 即「基表数据」
聚合支持 支持 不直接支持(LAP 是特例,见下文)
JOIN 预计算 支持 支持 prejoin projection(但实践中少用)
自动刷新 需配置 自动同步(通过 epoch 机制)
物理设计 通常手动 DBDesigner 自动生成

比喻:传统物化视图像「图书馆里额外印刷的专题目录」——馆藏原书(基表)不变,目录(MV)帮你更快找到想要的书。Vertica projection 则像「图书馆根本就没有按原书排列的书架」——每本书从一开始就被拆成章节,按不同主题重新排列上架。你要读一本书,就是从这些已经按查询模式组织好的书架上取章节。

2.2 Projection 的四个设计维度

MPP 共性:无论哪个系统,描述一个「物理数据副本」都需要回答四个问题——存哪些列、按什么顺序、数据怎么分布到节点、用什么压缩编码。只是各系统的术语和实现粒度不同:ClickHouse 的 ORDER BY + PARTITION BY 对应排序维度的两个层级,Greenplum 的 DISTRIBUTED BY + PARTITION BY 覆盖分布+排序,Snowflake 则将这些完全自动化。

Vertica 的每个 projection 由四个属性定义:

(1)列集(Column Set)

  • Super projection:包含表的所有列,必须至少 1 个
  • Non-super projection:只包含列的子集,用于窄查询加速
  • 实践中「大多数客户有 1 个 super projection 和 0-3 个窄投影」(来源:The Vertica Analytic Database: C-Store 7 Years Later §3.1)
  • 对比Greenplum 的 AO 表不区分「全列/子列」——每张表固定存全部列,靠分区裁剪跳过数据文件而非列级裁剪。Doris/StarRocks 的 Rollup 支持列子集(类似 narrow projection),但 Rollup 是独立物理表,不共享 super projection 的「全量兜底」。ClickHouse v23+ 也引入了一个同名「Projection」特性——隐藏的轻量排序副本,支持列子集和独立排序,但编码和分布跟随基表、不改变存储拓扑。有趣的是,两个系统的 Projection 用了相同的名字和相似的优化器自动选择机制,但一个是系统的唯一物理存储基石,另一个只是加速插件。Snowflake 微分区自动存全部列,列级裁剪在扫描时自动完成,无需手动设计列子集。

(2)排序(Sort Order)

  • 数据在磁盘上按排序列全局有序
  • 排序决定了:RLE 压缩效率(低基数列排前面 → 更好的 RLE)、Merge Join 可用性、GROUP BY PIPELINED 可用性
  • 排序列的第一个是最关键的——它决定了数据在磁盘上的物理布局
  • 对比ClickHouseORDER BY 在概念上最接近 projection 排序——数据同样按主键全局有序,RLE 压缩、谓词下推等优化类似,但 ClickHouse 只有一份主排序(除非手动建 Projection)。Greenplum 的 Heap 表不保证物理顺序,AO 表按 INSERT 顺序而非显式排序存储——排序只能通过分区裁剪 + 索引弥补。Snowflake 的自动聚类维护的是「近似排序」而非全局有序,牺牲精度换零运维。
-- 按日期排序的 super projection:适合按日期范围查询
CREATE PROJECTION sales_by_date
AS SELECT * FROM sales
ORDER BY sale_date
SEGMENTED BY HASH(sale_id) ALL NODES;

(3)分段(Segmentation)

  • 决定数据如何分布到集群各节点
  • SEGMENTED BY HASH(col):按哈希分布到各节点(适合大数据量)
  • UNSEGMENTED:在每个节点存完整副本(适合小维度表)
  • 投影级分段:同一张表的不同 projection 可以有不同分段键。这是 Vertica 区别于 Greenplum/Teradata 表级分发的关键差异(详见第 3 节)
  • 对比GreenplumDISTRIBUTED BY 是表级唯一分布键——要不同分布策略就得建物化视图。Doris/StarRocks 同样是表级 DISTRIBUTED BY HASH,Colocate JOIN 要求关联表同分桶键。ClickHouse 不走 hash 分布——每个 shard 存独立数据子集,没有「同值同节点」的概念。Snowflake 分布对用户透明,无法干预。

(4)编码(Encoding)

  • RLE、Delta Value、Block Dictionary、Compressed Delta Range 等
  • 排序列前面的低基数列自动使用 RLE → 查询时可在压缩态直接求值,跳过解压
  • 对比ClickHouse 有类似的列级编码体系(Delta、DoubleDelta、Gorilla、T64 等),加上通用压缩(ZSTD/LZ4),且用户可指定 CODEC 链。Greenplum AO 表在列级用 ZLIB/ZSTD 通用压缩,没有专门的编码算法。Doris 在 v1.2+ 引入了 RLE/Delta/字典编码。Snowflake 的编码全自动、不暴露——用户不知道也无需知道数据用什么编码存储。

这四个维度叠加,构成了 projection 的搜索空间。DBDesigner 论文中描述了如何在这个空间中枚举候选 projection 并按 cost/benefit 模型筛选(来源:DBDesigner: A Customizable Physical Design Tool for Vertica Analytic Database §V)。

2.3 Buddy Projection 与 K-Safety(Vertica 专属)

⚠️ Vertica 专属机制。其他 MPP 系统的数据冗余策略不同:Greenplum 用 mirror segment(物理复制节点),ClickHouse 用 ReplicatedMergeTree(表级多副本),Snowflake 的数据冗余在云存储层透明处理——它们都不存在「同一张表的不同投影需要独立容错」的概念,因为它们的存储单元是「表」而不是「投影」。

K-safety 是 Vertica 的容错机制:K=1 意味着任意 1 个节点故障时数据库仍可运行。实现方式是为每个 projection 创建 K 个 buddy projection,它们的列集相同、分段相同但节点偏移不同,确保同一份数据不会只落在一个节点上。

一个关键设计选择:从 C-Store 时代起,Vertica 在理论上允许 buddy projection 使用与主投影不同的排序——DBDesigner 论文中有提及「可以调整为 buddy 生成不同排序以优化查询和存储」,但这始终不是默认行为。DBDesigner 的默认策略是生成相同排序的 identical buddy projection,因为这样可以实现节点故障后的直接 ROS container 拷贝恢复,而非走 INSERT...SELECT 全量重建——在 Design Choices 论文报告的案例中,排序不同的 buddy 导致恢复性能退化 42x(来源:Analytic Database Design Choices: Vertica's Experience and Perspectives §3.4)。

实测验证(v26.1.0-2):虽然 CREATE PROJECTION ... /*+basename(...)*/ ... OFFSET 语法允许为 buddy 指定不同的 ORDER BY,但 K-Safety 机制直接拒绝该配置——MARK_DESIGN_KSAFE(1) 返回失败,verified_fault_tolerance 不对称(原始投影报告 0,不同排序的投影报告 1),集群整体无法满足 KSAFE≥1。只有排序完全一致的 buddy 才能通过 K-Safety 检查。

2.4 物化策略的演变:EM → LM → EM+SIP

MPP 共性:所有列存数据库执行查询时,都需要将分散的列数据拼接成行——这个过程叫物化(Materialization)。物化的时机(早 vs 晚)直接影响 I/O 和内存的平衡,是列存共有的架构问题。Vertica 的 SIPS 论文对此做了最系统的实验分析,结论对其他列存 MPP 有参考意义(推测:基于共同架构原理,ClickHouse/Doris 的列存引擎也面临类似的内存 vs I/O 权衡,但各系统的具体物化策略未公开到论文级别的细节)。

Projection 解决了「数据如何存」的问题,但还有一个深层问题:列存数据库中,查询执行时如何把分散的列拼接成行? 这就是物化策略(Materialization Strategy)

SIPS 论文系统比较了三种策略(来源:Materialization Strategies in the Vertica Analytic Database: Lessons Learned):

策略 原理 优势 劣势
早期物化 (EM) 扫描时就拼接各列为行 简单,spilling join 不崩溃 扫描了大量不需要的列
延迟物化 (LM) 只在需要时才取列 过滤性 JOIN 后 I/O 大幅减少 实现极复杂;spilling join 时「灾难性」性能
EM + SIP EM 基础上,JOIN 的 key 信息向下传递到扫描端做过滤 兼具两者优势,spilling join 安全 自适应开销(非选择性 JOIN 时 SIP 自动关闭)

TPC-H 1TB 单节点测试数据(来源:SIPS 论文 §V):

Query EM LM EMSIP
Q5(spilling join) 1.00 0.99 0.31
Q17(相关子查询) 1.00 0.41 0.27
Q18(相关子查询) 1.00 1.00 0.66
Q10(非选择性 JOIN) 1.00 0.58 0.70

这三个策略的取舍直接反映了三元 trade-off 的执行层面:LM 追求查询极致(减少 I/O),但无法处理 spilling(内存不够时两表都无法放入内存的 JOIN);EM 更稳健但 I/O 更多;EM+SIP 通过「在扫描端就过滤」同时获得两者的好处。

⚠️ LM ≠ LMJ:上表测试的是 LM(延迟物化)——将列拼接推迟到必须时才做。LMJ(延迟物化 JOIN)是 LM 的一个子集,特指在延迟物化状态下执行 JOIN 操作的实现。Design Choices 论文后来承认「LMJ 是一个错误。如果今天重新做,不会再花精力实现它」(来源:Analytic Database Design Choices: Vertica's Experience and Perspectives §3.1)——否定的是 LMJ 而非 LM。LM 在过滤性查询(如 Q17)上仍然有效,只是 spilling join 场景下 LMJ 的实现复杂性远超收益。这反过来验证了 SIP 路线的正确性。


3. 设计决策与 Trade-off

3.1 核心决策:为什么 projection 是唯一的物理存储

这是本文要回答的最根本问题。Vertica 做了一个在数据库领域相当激进的选择:不保留「基表」这个概念对应的物理文件,projection 既是存储也是索引。

这个选择背后的推理链:

(a)放弃传统物化视图

C-Store 7 Years 论文明确指出(来源:§3.1):

"Experience has shown that the maintenance cost and additional implementation complexity of maintaining materialized views with aggregation and filtering is not practical in real world distributed systems."

译:在真实分布式系统中,维护带聚合和过滤的物化视图的代价和实现复杂性不切实际。

(b)放弃 Join Index

C-Store 原论文提出了 join index 概念(用多个 partial projection 通过索引拼接还原完整行)。Vertica 放弃了它(来源:C-Store 7 Years §3.2):

"Join indices were complex to implement and the runtime cost of reconstructing full tuples during distributed query execution was very high... The excellent compression achieved by our columnar design helped keep the cost of super projections to a minimum."

(c)放弃 Prejoin Projection 作为主要手段

Prejoin projection(预 JOIN 投影)理论上可以消除查询时的 JOIN 开销,但实践中很少使用(来源:C-Store 7 Years §3.3):

"Most customers are unwilling to slow down bulk loads to optimize such joins."

本质上是加载性能压倒查询性能——在数据新鲜度要求高的场景下,加载速度是硬约束。

推理结论:既然所有「辅助结构」的维护成本都太高,那不如让主存储本身就具备查询优化能力。这就是 projection 作为唯一物理存储的逻辑终点。

3.2 三元 Trade-off 的数学表达

将 §1 的三元框架套用在 projection 设计上,DBDesigner 论文的三个设计策略(来源:DBDesigner: A Customizable Physical Design Tool for Vertica Analytic Database §IV)提供了精确的量化视角:

策略 Projection 数量 查询性能 加载性能 存储占用
Load-optimized 最少(K+1 个 super projection) 基线 最优 最小
Query-optimized 最多(直到所有查询都充分优化) 最优 最差 最大
Balanced(默认) 中庸(75% 查询充分优化即停止) 接近最优 接近最优 接近最小

DBDesigner 论文的实验验证了:Balanced 策略的查询性能非常接近 Query-optimized(仅略差),而存储占用接近 Load-optimized。这个 75% 的阈值是启发式设定的,在生产环境多种客户数据集上表现良好。

这里有一个反直觉的发现:不是 projection 越多查询越快。每增加一个 projection:

  • 加载时需要多写一份数据(Tuple Mover 的工作量线性增长)
  • 优化器的覆盖投影集搜索空间从 P^T 增长(P=每表投影数,T=表数)
  • 存储空间线性增长
  • 但查询收益递减——前 1-2 个窄投影覆盖了大部分优化空间

这就是 DBDesigner 论文把 Balanced 设为默认策略的原因:边际收益递减曲线在 2-3 个投影后快速变平。

3.3 对比其他 MPP 系统:投影级 vs 表级设计

如果把物化视图/投影的设计空间画一条光谱,各系统的位置大致如下:

辅助索引视角 ←————————————————————————————→ 唯存储视角
   (PostgreSQL MV)   (Greenplum AO)  (Vertica Projection)
         │                  │                │
      表是主体            表有分布策略       投影即存储
      MV是附属            MV改变分布        同一表多种分布

GreenplumDISTRIBUTED BY 是表级别的——整张表只有一个分布键。如果你有两种不同 JOIN 键的查询,你必须选一个分布键(让一类 JOIN 本地化,另一类走重分布),或者在表外创建物化视图。

Vertica 的 projection-level segmentation 是这个问题的解法:同一张表可以有一份按 customer_id hash 分段的投影(优化 customer JOIN),同时有一份按 product_id hash 分段的投影(优化 product JOIN)。优化器根据查询选择最合适的投影。

这个设计的代价是:

  • 存储占用翻倍(多份不同分段的副本)
  • 加载耗时增加(每多一份投影就多一次写入)
  • 优化器复杂度增加(覆盖投影集搜索:P^T 种组合)

收益是:

  • JOIN 本地化:不用重分段,网络传输降为 0
  • 无需手动改写查询:优化器自动选择
  • 设计灵活性:可以为不同查询模式做不同优化

3.3.1 跨系统物理设计能力对比

将上述分析扩展到更多系统,得到全文最核心的一张对比表:

能力维度 Vertica Projection Greenplum MV + AO ClickHouse MV + Projection Doris/StarRocks Rollup Snowflake MV
多排序副本 ✅ 同一表多投影,不同排序 ❌ 需手工 MV + 不同 ORDER BY(AO 表) ✅ v23+ Projection(轻量排序) ✅ Rollup 独立排序 ⚠️ 自动聚类(非精确排序)
多分布策略 ✅ 投影级分段:同表多分布键 ❌ 表级 DISTRIBUTED BY ❌ 表级单 sharding key,不支持多分布 ❌ 表级分桶 N/A(存储层透明)
刷新方式 加载时同步(epoch) 手动 REFRESH MATERIALIZED VIEW / 触发器 MV 为 INSERT 触发器写入时增量 同步:写入时级联刷新 后台自动(serverless)
存储冗余 每投影一份完整数据 基表 + MV 独立存储 基表 + MV 独立存储 Base 表 + Rollup 独立存储 MV 另算存储(无独立基表概念)
自动设计 DBDesigner(75% 覆盖率) 无,完全手动 无,完全手动 无,完全手动 全自动(无设计暴露)
优化器自动选择 ✅ 运行时自动选投影 ✅ MV 透明重写(但需匹配) ⚠️ MV 不自动匹配,需显式查 MV 表 ✅ 自动选择最优 Rollup ✅ 自动(透明)
JOIN 本地化 ✅ 多分段键投影消除重分布 ❌ 单分布键,非匹配 JOIN 走 Motion ❌ 分布式 JOIN 走网络 ❌ Colocate JOIN 需要同分桶键 N/A(存储层透明)
运维负担 中(需理解投影设计) 高(手动 MV 维护 + VACUUM) 中高(MV 调试 + 分区管理) 中(Rollup 调试) 低(全自动,但费用不可控)

这张表揭示的核心洞察:Vertica 在多排序和多分布上的「投影级」设计自由,是以存储冗余和加载开销为代价换来的。Snowflake 选择了正好相反的路——放弃所有显式物理设计,把复杂性和成本转移到云账单上。ClickHouse 和 Doris/StarRocks 则处于中间——提供了轻量的多排序/多副本能力,但不支持表内多分布策略。

3.4 Live Aggregate Projection:对三元平衡的另一种解法

LAP(Live Aggregate Projection)是 Vertica 7.1 引入的一个特例——它是 projection 中唯一支持聚合的变体。LAP 的设计哲学与通用物化视图截然不同(来源:Live Aggregate Projections):

  • 通用物化视图:先算后存,刷新时全量或增量重算
  • LAP:加载时即聚合(每批数据在加载流中就完成 partial aggregate),后台 Tuple Mover 合并 partial aggregate,查询时再合并剩余部分

这本质上是把「查询时的聚合开销」转移到了「加载时」——再次体现了三元 trade-off 的约束:你用加载性能换查询性能。但 LAP 巧妙之处在于聚合大幅压缩了数据量(存储空间甚至可能小于原始投影),所以它在三元中做到了查询改善、存储减少,代价是加载性能下降——且只适用于可分解聚合(SUM/COUNT/MIN/MAX)场景。

性能数据(来源:Live Aggregate Projections,3 节点 DL320 集群,~$10K):

  • 智能电表场景(1000 万电表,15 分钟读数,1 个月):Oracle 20 小时 → Vertica 18 分钟 → Vertica + LAP 24 秒(3000x vs Oracle, 45x vs 无 LAP)
  • Top Consumers 查询:486x 加速(1 分钟读数频率下)
  • 关键特性:读数频率越高,LAP 相对优势越大——因为 LAP 查询时间恒定,而普通查询随数据量线性增长

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

4.1 查询维度:什么情况下 projection 自动帮到你

🏷️ 以下场景中,排序优化(GROUP BY PIPELINED)和压缩态求值是所有排序存储的列存 MPP 通用的优势,而「覆盖投影集自动选择」是 Vertica 专属(Greenplum 的 MV 重写需匹配完整查询,ClickHouse 的 MV 不自动匹配,需显式查 MV 表)。

Projection 的排序和分段不是显式指定的——你不需要在查询中写 USE PROJECTION sales_by_customer。优化器自动评估可用的覆盖投影集,选择成本最低的组合。

以下几个场景 projection 自动生效:

  • 排序与 GROUP BY 对齐 → GROUP BY PIPELINED(免 hash 表,内存省 80%+)
  • 分段的表都在 JOIN 键上对齐 → 本地 JOIN(网络传输为 0)
  • 排序列的 RLE 编码 → 谓词在压缩态直接求值(I/O 大幅减少)
  • 窄投影覆盖查询列 → 不需要读 super projection 的全部列(I/O 减少)

4.2 加载维度:projection 数量是加载速度的敌人

🏷️ 「物化副本数量 = 加载开销倍数」是所有 MPP 通用的约束——无论是 projection、物化视图还是 Rollup,每多一份物理副本就要多一次写入。但 Tuple Mover mergeout 的额外开销是 Vertica 专属(其他系统的合并/压缩机制不同,见 §3.3.1 对比表)。

每增加一个 projection,每次 COPY/INSERT 都要多写一份。具体来说:

-- 如果有 3 个 projection(1 super + 2 non-super)
-- 每行数据要写入 3 份,Tuple Mover 也要做 3 份 mergeout
INSERT INTO sales VALUES (...);
-- 加载吞吐约为单 projection 的 1/3

DBDesigner 论文数据:Query-optimized 策略的加载性能比 Load-optimized 策略差,因为每表多了 1-3 个非超投影。

加载性能的影响也解释了为什么 prejoin projection 实践中用得少——prejoin 在加载时就要做 JOIN,数据量大时加载性能急剧下降(来源:C-Store 7 Years §3.3)。

4.3 存储维度:为什么需要 2-3x 的空间预算

🏷️ 「预留操作缓冲空间」是所有 MPP 通用的原则,但具体机制和倍数不同:Vertica 的峰值来自 mergeout 新旧 ROS 共存 + REFRESH 全量写入 + delete vector 延迟 purge;ClickHouse 的峰值来自后台 merge + mutation,倍数类似但路径不同;Snowflake 没有显式的存储峰值(云存储层透明),但写入/聚类费用会飙升。

最佳实践建议(来源:Projection 优化最佳实践):

「磁盘空间为 projection 大小的 2-3 倍可以避免刷新问题」

这个 2-3x 不是一个精确公式,而是覆盖三种操作叠加场景的经验安全系数——留够缓冲空间避免磁盘满导致操作失败(磁盘满时只能 rollback,浪费大量时间)。

因素一:Tuple Mover mergeout 时新旧 ROS 并存

Vertica 数据以 ROS container(Read Optimized Store)为单位存储。Tuple Mover 定期将小 ROS 合并为大 ROS(mergeout)。关键点:合并过程中,旧的若干小 ROS 和新的一个大 ROS 同时存在于磁盘上,直到新 ROS 提交后旧 ROS 才被删除。

Mergeout 前:   [ROS-1 100MB] [ROS-2 120MB] [ROS-3 80MB]     = 300MB
Mergeout 中:   [ROS-1 100MB] [ROS-2 120MB] [ROS-3 80MB]     ← 旧 ROS 还在
               [ROS-merged 300MB]                           ← 新 ROS 正在写入
               峰值占用 = 600MB(2x)
Mergeout 后:   [ROS-merged 300MB]                           ← 旧 ROS 已删除

mergeout 期间单 projection 峰值占用可达实际数据的 接近 2x

因素二:新建 Projection 的 REFRESH 操作

在已有数据的表上新建 projection 时,SELECT REFRESH() 从已有 projection 读取全量数据、写入新 projection。过程中:

  • 源 projection 的数据不能删(查询还在用)
  • 新 projection 的数据正在全量写入
  • 如果新投影是 super projection,写入量等于整张压缩表大小

这也是一个接近 2x 的叠加——原有数据 + 新写入数据同时存在。

因素三:DELETE 的 Purge 延迟

Vertica 的 DELETE 不立即回收物理空间,而是写 delete vector 标记删除。实际空间回收靠 Tuple Mover 的 purge 操作在后台完成。在 purge 之前,被标记删除的行仍然占用磁盘。

叠加效应:一个具体例子

假设一张表在 projection_storage 中显示压缩后 500GB(K=1,已包含 super + buddy 两份投影)。注意:这个 500GB 本身已经是常态占用,峰值在此基础上叠加:

场景 实际占用 说明
正常稳态 500GB 即 projection_storage 查到的值
Mergeout 峰值 750GB~1TB 各投影 mergeout 时新旧 ROS 短暂共存
REFRESH 新建投影 750GB~1TB 新建投影全量写入,叠加在已有投影之上
三者叠加(最坏情况) 接近 1.25~1.5TB mergeout + REFRESH + delete vector 未 purge 同时发生

所以 2-3x 指的是:磁盘总容量至少是 projection_storage 查到的稳态占用的 2-3 倍。 500GB 的表 → 预留 1TB~1.5TB 总磁盘空间。

在大表操作前(尤其是新建投影的 REFRESH),建议先检查 DISK_STORAGE 系统表确认剩余空间。

DBDesigner 论文报告(来源:§VI):Query-optimized 策略的存储占用在各表上均高于 Load-optimized,但差异因表而异——小维度表差异可忽略,大事实表差异显著。

4.4 常见误解

误解 1:「projection 越多查询越快」

事实:超出了 2-3 个窄投影后,边际收益急剧递减。每多一个投影都增加了加载开销和优化器搜索空间。DBDesigner 论文的 Balanced 策略之所以默认设为 75% 覆盖率,正是因为 100% 覆盖的代价远超收益。

误解 2:「分段键必须和 JOIN 键一样」

事实:如果 JOIN 键低基数或数据倾斜严重,分段在 JOIN 键上反而会导致单节点热点。分段键应该是「高基数 + 分布均匀」的列,不一定是 JOIN 键本身。

误解 3:「UNIQUE 约束不会影响 projection 设计」

事实:添加 ENABLED UNIQUE 约束时,Vertica 会自动创建一个额外的 constraint projection(排序和分段仅包含约束列)。如果约束列的排序/分段未在已有投影的前缀中出现,就会多出一个投影——增加加载和存储开销。可以在加约束前手动创建满足条件的投影来避免(来源:表约束产生的 projection 问题)。


5. 案例验证

📝 虚构案例 1:过度投影化的代价

场景:某零售企业的数据团队在 Vertica 上维护一张 50 亿行的销售事实表(sales_fact,原始数据约 2TB 压缩后 ~400GB)。为了覆盖所有查询模式,他们创建了:

  • 1 个 super projection(按 sale_date 排序,按 sale_id 分段)
  • 4 个 non-super projection(分别按 customer_idproduct_idstore_idregion_id 排序和分段)
  • K=1 安全级别 → 总共 10 个投影(5 个主投影 × 2 buddy)

后果

指标 优化前(1 super + 1 non-super) 优化后(1 super + 4 non-super)
每日加载耗时 45 分钟 150 分钟(+233%)
存储空间 800 GB 2.1 TB(+163%)
95% 查询延迟 3.2 秒 2.8 秒(-12.5%)
慢查询(>30s) 12 个/天 8 个/天(-33%)

分析:4 个窄投影带来了 12.5% 的中位数查询改善和 33% 的慢查询减少,但代价是加载时间翻了 3.3 倍、存储翻了 2.6 倍。ROI 极低——如果只保留 product_idstore_id 两个高频 JOIN 键的投影,加载只增加 80%,而查询收益几乎没有损失(因为 customer_idregion_id 的查询频率很低)。

回溯到原理:这正好印证了 DBDesigner Balanced 策略 75% 阈值的逻辑——后 25% 的查询优化付出的代价远超前 75%。


📝 虚构案例 2:排序键选择失误

场景:某金融机构的 trades 表(30 亿行),super projection 按 (trade_date, trade_id) 排序。

最常见的查询模式是:

SELECT symbol, SUM(volume), AVG(price)
FROM trades
WHERE trade_date BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY symbol;

这个查询在现有排序下表现良好——trade_date 排第一,谓词可以高效过滤。但当分析师问:

SELECT trade_date, SUM(volume)
FROM trades
WHERE symbol = 'AAPL'
GROUP BY trade_date;

时,symbol 不是排序列,需要扫描全部 30 亿行的 symbol 列,再过滤。

优化:创建一个按 (symbol, trade_date) 排序的窄投影:

CREATE PROJECTION trades_by_symbol
AS SELECT symbol, trade_date, volume
FROM trades
ORDER BY symbol, trade_date
SEGMENTED BY HASH(symbol) ALL NODES;

优化后,AAPL 查询从 45 秒降到 0.3 秒(过滤效率大幅提升,且 symbol 列 RLE 后只有几千个 distinct value,压缩态直接求值)。代价是加载多写一份投影、存储增加约 15%。

回溯到原理:排序键决定了「什么查询最快」,本质上是把查询时的过滤/聚合开销提前到数据写入时做了排序。三元框架下,这是用加载和存储换查询性能的标准 trade-off。


📋 真实案例 · 来源:某省级运营商 Vertica 数据仓库性能问题分析报告

脱敏处理:客户为某运营商,原始工单号已移除。集群规模和故障特征保留真实值。

集群规模:93 节点

现象:一张大表上执行按日期过滤 + JOIN 的查询,执行 6.5 小时仍未完成。而同样的查询在其他日期上只需数分钟。

关键发现:该表的 super projection 按 statis_date 做了 HASH 分段。查询的过滤条件是 statis_date = '20200819'——恰好等于分段键的值。

根因:按 statis_date 分段意味着某一天的所有数据都落在同一个节点上(Hash 函数对相同值产生相同结果)。当查询过滤到特定日期时,93 个节点中只有 1 个在工作——其他 92 个节点空转。这就是分段键选择错误导致的单节点热点。

修复:将分段键从 statis_date 改为 HASH(user_id_zk, user_id_fk)——高基数的用户 ID 组合,确保数据均匀分布。

效果:查询从 6.5 小时(未完成)降到 5 分 37 秒

回溯到原理

  1. 分段键的首要标准是高基数 + 分布均匀,不是「过滤频率最高的列」。statis_date 作为过滤键很合理(放排序前面),但作为分段键则是灾难
  2. 这也说明了 projection-level segmentation 的灵活性——你可以在一个投影上按 statis_date 排序(优化日期过滤),同时在另一个投影上按 user_id 分段(优化 JOIN),避免单一分段键的困境
  3. 三元框架的视角:原有设计在存储和加载上很「省」(只有一个分段键),但查询性能在特定日期彻底崩溃——这是过分偏向「加载/存储」而牺牲「查询」的典型

跨系统启示:分段键选择错误导致单节点热点的问题,在 Greenplum 中同样会发生(表级 DISTRIBUTED BY 选错 → 数据倾斜 → 查询集中在少数 segment)。Greenplum 的解法是通过 gp_toolkit.gp_skew_coefficient 检测倾斜后改分布键重建表——比 Vertica 更重(需锁表重建 vs 新建一个不同分段投影即可)。在 ClickHouse 中不存在此问题,因为 ClickHouse 不做 hash 分布——每个 shard 存独立数据子集,查询在所有 shard 并行执行后汇总,本质是多副本而非 MPP 分布,不存在「同一键值数据挤在同一节点」的问题。这个对比说明了 Vertica projection-level segmentation 的独特价值:你可以保留原有设计的同时,添加一个不同分段键的投影来补救,而不需要停业务重建表。


📝 跨系统模拟案例:同一场景,五种解法,代价付在哪里

📝 虚构案例

场景:一张 200 亿行的零售销售事实表,需要同时优化以下 5 种查询模式:

  • Q1:按日期范围汇总(WHERE dt BETWEEN ... GROUP BY dt
  • Q2:按客户维度 JOIN(JOIN customer_dim ON cust_id
  • Q3:按产品维度 JOIN(JOIN product_dim ON prod_id
  • Q4:按门店维度聚合(GROUP BY store_id
  • Q5:Top-N 排名(ORDER BY amount DESC LIMIT 100

下面是各系统的物理设计方案和预估代价对比(数据量基于前述案例的合理外推,执行时间基于各系统文档描述的典型性能特征):

维度 Vertica Greenplum ClickHouse Doris Snowflake
设计方案 1 super + 3 narrow(按 cust/prod/store 分段) 1 AO 表 DISTRIBUTED BY cust_id + 2 MV JOIN 物化 1 MergeTree ORDER BY (dt, cust_id) + 1 触发器式 MV 聚合(Q4)
⚠️ Q5(Top-N)触发器式 MV 不可行——INSERT 只看到增量,无法维护全局 Top-N
1 Base DISTRIBUTED BY HASH(cust_id) + 2 Rollup 1 表 + 自动聚类(无显式设计)
Q1 耗时 0.5s(排序对齐 + RLE) 2-5s(分区裁剪 + 全扫描) 0.1s(ORDER BY dt 主键裁剪) 1-3s(分区裁剪 + 扫描) 1-3s(微分区裁剪)
Q2 耗时 0.3s(cust 分段投影 → 本地 JOIN) 0.5-1s(分布键对齐 → 本地 JOIN) 2-5s(分布式 JOIN → 网络传输) 0.5-1s(分桶对齐 → Colocate JOIN) 1-3s(自动优化)
Q3 耗时 0.3s(prod 分段投影 → 本地 JOIN) 10-30s(需重分布 Motion) 2-5s(分布式 JOIN) 10-30s(需 shuffle) 1-5s(自动优化)
Q4 耗时 0.3s(store 分段投影 → GROUP BY PIPELINED) 3-8s(全扫描 + Hash GROUP BY) 0.05s(MV 预聚合,查 MV 表) 0.2s(Rollup 预聚合) 1-3s(自动优化)
Q5 耗时 2-5s(全扫描 + 排序,amount 不在排序列) 3-8s(全扫描 + 排序 + Motion 合并) 1-3s(Top-N per part + 合并)⚠️ 触发器式 MV 不适用 2-5s(Top-N 下推存储层) 1-3s(自动优化)
存储 ~3.5x 原始数据(4 投影 × K=1) ~2.5x(1 表 + 2 MV) ~1.6x(1 表 + 1 MV) ~2.5x(1 Base + 2 Rollup) ~1x + 聚类费用
加载延迟 基准 ×4(4 投影同步写) 基准 + MV 刷新耗时 基准 ×1.5(1 MV 异步写入) 基准(Rollup 同步级联) 基准(无本地加载概念)
设计工作 DBDesigner 默认 + 1-2 次手动迭代 DBA 分析慢查询 → 设计 MV → 测试 → 部署 开发写 MV 触发器 SQL → 测试 → 部署 DBA 设计 Rollup → 测试 → 部署 0 设计工作(全自动)

核心洞察

  • Vertica 的 Q3 是亮点——product 维度 JOIN 通过独立分段投影实现本地化,而 Greenplum 和 Doris 因表级单分布键限制,Q3 必须走跨节点重分布,慢了 30-100x。这就是 projection-level segmentation 的「杀手场景」。
  • 代价付在不同的地方:Vertica 付在存储和加载上(4 份数据),Greenplum 付在运维和特定查询延迟上,ClickHouse 付在查询时的网络传输上(分布式 JOIN 走网络),Snowflake 付在云账单上(自动聚类 + 查询费用)。
  • ClickHouse 的 MV 是触发器而不是查询重写,且受限于 INSERT 可见性——Q4(GROUP BY)可用触发器式 MV 覆盖(查询时需显式指定 MV 表名),但 Q5(Top-N)因 INSERT 只能看到增量数据、无法维护全局排名,必须走定时全量重算或其他方案。不像 Vertica 优化器自动选择投影。
  • Q5(Top-N)是物理设计的盲区——所有系统都走全扫描 + 排序,没有物理设计策略能绕过(除非把 amount 放进排序键,但那会牺牲其他查询的优化)。这验证了 §3.2 的结论:没有任何单一 projection 方案能覆盖所有查询模式。

6. 设计原则总结

【通用】原则 1:物理副本数量是三元平衡的调节器,不是越多越好

  • 为什么:每增一个投影,加载写一份、TM mergeout 做一份、存储占一份。DBDesigner 论文证明,2-3 个窄投影后边际收益递减到可忽略
  • 反例:虚构案例 1——4 个窄投影换来 12.5% 查询改善,加载翻了 3.3 倍

【通用】原则 2:分段键 = 最高频的 JOIN 键(或高基数均匀分布的列),不是最高频的过滤键

  • 为什么:分段决定数据分布,过滤键放排序前面即可优化。按过滤键分段导致单节点热点
  • 反例:真实案例——按 statis_date 分段,查询特定日期全压在 1/93 节点上,6.5 小时未完成

【通用】原则 3:排序键的第一个列决定一切

  • 为什么:数据全局按排序列物理排序。第一列决定 RLE 压缩效率、谓词下推效果、GROUP BY PIPELINED 可用性
  • 反例:虚构案例 2——trade_date 排第一时 symbol 查询要全表扫描

【通用】原则 4:全节点副本(UNSEGMENTED)只适用于 < 10 万行的小维度表

  • 为什么:UNSEGMENTED 在每个节点存完整副本。表大了存储和加载都不可接受
  • 反例:如果 1 亿行的表 UNSEGMENTED,每个节点都存 1 亿行,存储是分段方案的 N 倍

【Vertica】原则 5:Buddy projection 必须用相同排序

  • 为什么:语法上允许不同排序,但 K-Safety 机制直接拒绝——MARK_DESIGN_KSAFE(1) 失败,verified_fault_tolerance=0。即使绕过 K-Safety 检查,排序不同的 buddy 在节点故障后无法直接拷贝 ROS container,必须走 INSERT...SELECT 重建——42x 更慢
  • 实测:v26.1.0-2 验证,不同排序的 buddy 创建成功但 KSAFE 不认可
  • 反例:Design Choices 论文 §3.4 报告的生产事故

【通用】原则 6:统计信息是多副本设计生效的前提

  • 为什么:优化器基于统计信息做 cost-based 投影选择。统计信息过期 → 优化器选错投影 → 查询变慢
  • 反例:Vertica:某运营商案例——5 张 JOIN 表缺统计信息,优化器误判导致查询 1 小时,修复后 3.5 秒(1000x 改善);Greenplum:缺统计信息的表会被优化器低估行数 → 选错 JOIN 顺序 → 中间结果爆炸 → 查询超时——同样是 CBO 的共性问题。详见 某运营商 Vertica 数据库性能问题报告

【Vertica】原则 7:LAP 是聚合查询的最优解,但不是通用解

  • 为什么:LAP 在加载时做聚合,查询时直接读结果——查询常数时间、不随数据增长。但只适用于可分解聚合,且自动改写尚未完全实现
  • 反例:用 LAP 做 JOIN 加速——无效,LAP 不支持 JOIN 预计算

【Vertica】原则 8:Prejoin projection 几乎总是不值得的

  • 为什么:加载时做 JOIN 开销远大于查询时做 JOIN(查询时可利用 hash/merge join 优化,加载时数据未知无优化空间)。生产环境几乎不用
  • 反例:如果事实表和维度表都在 TB 级且必须 JOIN 过滤——这种极端情况才考虑 prejoin,但仍需评估加载性能代价

7. 延伸阅读

按推荐阅读顺序

  1. The Vertica Analytic Database: C-Store 7 Years Later必读 · Vertica 专属。本文大量引用的核心架构论文,涵盖 projection 定义、为什么不实现传统物化视图和 join index、super projection 要求。读 §3「Data Model」即可获得投影设计的全部理论依据。
  2. DBDesigner: A Customizable Physical Design Tool for Vertica Analytic DatabaseVertica 专属。理解 DBDesigner 如何在你创建表时自动生成 projection 方案。Load-optimized / Query-optimized / Balanced 三种策略的量化分析,以及 SortExt/RLEExt 算法的细节。读完你会明白为什么 CREATE TABLE 的默认行为已经足够好。
  3. Materialization Strategies in the Vertica Analytic Database: Lessons LearnedMPP 通用。深入理解查询执行时 EM / LM / SIP 三种物化策略的 trade-off。虽然实验基于 Vertica,但物化时机(早 vs 晚)是所有列存数据库共有的架构问题。如果你想理解「为什么有时候 SIP filter 让查询快 100x」,这篇是答案。
  4. Live Aggregate ProjectionsVertica 专属。LAP 的设计原理和性能数据。如果你有大量聚合查询且数据持续增长,这篇会告诉你 LAP 为什么能做到查询常数时间。
  5. Analytic Database Design Choices: Vertica's Experience and PerspectivesMPP 通用。Vertica 团队的自省文章,包含「LMJ 是一个错误」「buddy 排序不同的代价」等坦诚讨论。其中关于「过度工程化」的反思对所有 MPP 系统设计都有参考价值。适合读完前 4 篇后作为「设计评审」来读。
  6. Projection 优化最佳实践Vertica 专属。实操指南,涵盖排序、分段、编码、K-Safety 的具体建议。
  7. MPP 数据分布策略MPP 通用。深入分段机制的 hash ring 实现和 rebalance 原理。虽然以 Vertica 为载体,但 hash 分布、数据倾斜检测等概念是所有分布式 MPP 的通用知识。读完本文第 3 节后如果对分段细节感兴趣,这篇是补充。
  8. 表约束产生的 projection 问题 — Vertica 专属。UNIQUE 约束自动创建 projection 的机制详解。如果你在表上加了约束但不知道会影响投影数量,看这篇。
  9. Vertica 性能调优:重新设计 PROJECTIONVertica 专属。projection 设计的实操调整指南,包含 Merge Join vs Hash Join、GROUP BY PIPELINED vs HASH 的对比表。

Vertica 把 projection 做成了唯一的物理存储结构——这个看似激进的选择背后,是一条清晰的逻辑链:放弃传统物化视图(维护代价太高)、放弃 Join Index(运行时拼接成本太高)、放弃 Prejoin(加载性能不可接受),最后只剩下一条路:让主存储本身就具备优化的能力。其他 MPP 系统在今天仍在不同程度上坚持「存储与优化分离」——不是因为分离更好,而是因为统一设计的代价(存储冗余 × 加载开销)在它们的架构约束下太高。理解了这个选择背后的推力和阻力,你就不只是在学 Vertica,而是在理解整个 MPP 数据库行业在物理设计上的根本张力。