MPP 统计信息与 Cost-Based Optimization —— 优化器如何在黑暗中摸索,又如何在光明中精确导航¶
作者:JiangChong | 撰写时间:2026年06月
适用场景框:查询突然变慢、执行计划出现
(NO STATISTICS)或PREDICATE VALUES OUT-OF-RANGE、大表被错误选为驱动表、JOIN 策略从 RESEGMENT 变成 BROADCAST 导致网络洪水——这些都指向同一个根因:优化器缺少准确的统计信息。本文声明:本文以 Vertica 为主要剖析对象,深度展开其统计信息模型(超立方体、Join Ranker、数据流代价模型)。但全文以 MPP 优化器的共性问题 为主轴——统计信息缺失在任何 CBO-MPP 中都是性能杀手,只是不同系统用不同的方式应对。本文将在所有关键环节对比 Greenplum、ClickHouse、Snowflake、Doris 等系统的不同设计选择,「MPP 共性」与「Vertica 专属」将在文中明确区分。
关联文章¶
- Vertica 统计信息管理与查询性能 — 统计信息监控、诊断与修复的操作手册
- Vertica 大表统计信息维护最佳实践 — 大表统计策略选择(全量 vs 分区级、频率与代价权衡)
- Vertica 性能调优:如何阅读执行计划 — EXPLAIN 输出的完整解读指南
- MPP JOIN 策略全解析与优化器决策逻辑 — 优化器如何选择 Local/Broadcast/Resegment JOIN,统计信息如何影响这个决策
理解全文脉络¶
本文聚焦于 MPP 数据库优化器的「眼睛」——统计信息。全文按「什么痛点驱动了这个设计 → 核心机制如何运作 → 关键设计决策的 trade-off → 设计对生产环境的影响 → 真实案例验证 → 设计原则总结」组织。
- 如果你想知道「为什么 MPP 对统计信息的依赖比单机数据库更深」,请从第 1 节开始
- 如果你关心「Vertica 优化器的统计信息模型具体怎么工作(超立方体、Join Ranker)」,直接跳到第 2 节
- 如果你想了解「统计信息缺失到底会导致什么后果,有过哪些真实案例」,重点阅读第 4-5 节
- 如果你要建立统计信息管理的设计原则和评判标准,第 6 节是核心输出
第 1 节:问题背景 — 为什么 MPP 优化器比单机数据库更需要统计信息¶
1.1 从单机到 MPP:决策维度的爆炸式增长¶
在单机数据库中,优化器的核心任务是选择一个好的 JOIN 顺序和访问路径(用哪个索引)。决策变量相对有限:
- 表 A JOIN 表 B,谁做 Inner(驱动表/构建哈希表的一方)?
- 用嵌套循环还是哈希 JOIN?
- 用哪个索引扫描?
单机数据库即使统计信息不完美,代价通常可控——最坏情况是查询变慢,但不会出现数据在网络上被错误搬运的问题,因为根本没有网络这一步。
MPP 数据库完全不同。以 Vertica 为例,优化器在生成执行计划时需要同时做出以下决策(来源:The Vertica Query Optimizer: The Case for Specialized Query Optimizers §III):
- 投影选择(Projection Set Chooser):同一张表可能有多个投影(不同排序键、不同分段键),选哪一个?
- JOIN 顺序:4 张表的查询,有 4! × 多种 JOIN 算法组合的可能计划
- JOIN 算法:Merge Join 还是 Hash Join?前者要求数据预排序,后者灵活但消耗更多内存
- 数据分布策略:数据在不同节点间如何搬运?BROADCAST(全量广播)、RESEGMENT(按哈希键重分布)、还是 LOCAL(数据恰好在同一节点)?
- 列物化时机(Late Materialization):列存特有的优化——哪些列在哪个阶段从磁盘读取?
这些决策构成一个组合爆炸级的搜索空间。如果每个决策的正确概率是 80%,5 个决策都对的概率只有 0.8^5 ≈ 33%。而在 MPP 中,任何一个决策出错,都可能导致数据被错误地在网络上搬运——这是单机数据库不会遇到的代价维度。
1.2 统计信息缺失在 MPP 中的放大效应¶
考虑一个典型场景:
有准确统计:优化器知道 fact 大、dim 小 → 选 dim 做 Inner 表(构建哈希表)→ 广播 100MB 到所有节点 → 毫秒级哈希探测 → 正常完成。
无统计或统计过期:优化器不知道两张表的相对大小 → 可能选 fact 做 Inner 表 → 尝试将 TB 级数据加载到各节点内存中建立哈希表 → 内存爆炸、Spill 到磁盘、甚至 OOM。
这不是假设。某运营商真实案例中,14.5 亿行的 user_dtal 表因统计信息过期被选为 Inner 表,单条查询消耗 28GB 内存、执行 1 小时;修复统计后执行计划恢复正常。
MPP 环境中统计信息缺失的破坏性被三个因素叠加放大:
| 放大因素 | 单机数据库 | MPP 数据库 |
|---|---|---|
| 决策维度 | JOIN 顺序 + 访问路径(2-3 个) | + 投影选择 + 分布策略 + 列物化时机(5+ 个) |
| 出错代价 | 查询慢 | 查询慢 + 网络洪水 + 其他查询被挤占 |
| 资源影响 | 单机 CPU/内存 | 所有节点 CPU/内存/网络同时受压 |
1.3 传统方法 vs 现代 MPP 优化器¶
在 CBO(Cost-Based Optimizer)普及之前,数据库优化器主要依赖启发式规则(Rule-Based Optimizer,RBO)。RBO 的典型逻辑是:「如果有索引就用索引」「总是先做选择率高的过滤」——这些规则不考虑数据实际分布。
System R(1979 年,IBM)首次引入了基于成本的优化思想:为每个候选计划估算代价,选代价最低的(来源:Selinger et al., Access Path Selection in a Relational Database Management System, SIGMOD 1979)。这个范式被所有现代 MPP 数据库继承,但 MPP 对成本估算的精度要求高了一个数量级——因为在分布式环境中,代价估算的误差会被网络搬运放大。
因此,我们需要一个专门为 MPP 架构设计的统计信息体系:不仅要告诉优化器「这张表有多少行」「这个列有哪些不同值」,更要让优化器能够在不实际执行查询的情况下,精确预判数据在网络上的流动模式。
1.4 各 MPP 系统的统计信息策略一览¶
虽然所有 CBO-MPP 都面临同样的统计信息精度问题,但不同系统在「谁负责收集」「何时收集」「收集什么」三个维度上做出了不同的选择:
| 系统 | 收集触发 | 优化器类型 | 统计粒度 | 多列谓词处理 | 可调性 |
|---|---|---|---|---|---|
| Vertica | 用户主动 ANALYZE_STATISTICS + Tuple Mover 自动维护 ROWCOUNT |
数据流代价模型(CBO) | Projection 级(同一表可有多个 projection,每个需单独统计) | 超立方体 + ExprAn(表达式范围分析) | 封闭模型,通过 Syntactic Optimizer(hints)直接控制 |
| Greenplum | ANALYZE + autoanalyze 守护进程(更激进) |
GPORCA / PostgreSQL Planner(CBO) | Table 级(每表一个物理结构) | 多列 MCV(Most Common Values)+ 列独立性假设 | 开放 GUC 参数(如 optimizer_enable_*) |
| ClickHouse | 后台自动抽样(statistics_columns 配置) |
启发式规则为主,逐步引入 CBO | Part 级(按分区粒度) | 列独立性假设为主,部分版本支持多列统计 | 通过 join_algorithm / max_bytes_in_join 等参数间接控制 |
| Snowflake | 零运维自动维护(数据写入时自动更新) | 自动 CBO(用户不可见) | Micro-partition 级元数据 | 自动列相关性检测(未公开具体算法) | 几乎不可调——设计哲学是「优化器比用户更懂」 |
| Doris / StarRocks | 自动 + 手动 ANALYZE TABLE |
CBO(基于 Cascades 框架) | 表/分区级 | 直方图 + 列独立性假设 | 通过 Session 变量调整(如 enable_cbo) |
说明:上表中非 Vertica 系统的信息基于公开文档和社区知识。具体参数名和实现细节请以各产品最新 GA 版本文档为准。标注「推测」的部分表示基于 MPP 共性架构的合理推断,尚未逐条在对应产品文档中验证。
从上表可以读出几个关键差异:
- 统计维护负担:Vertica > Greenplum > Doris/StarRocks > ClickHouse > Snowflake。原因在于 Vertica 的 projection 级物理设计使得同一张表可能需要维护多份统计;Snowflake 的完全自动化则消除了维护负担,但代价是用户丧失了干预能力。
- 优化器可干预程度:Greenplum > Doris/StarRocks > Vertica > ClickHouse > Snowflake。Greenplum 继承 PostgreSQL 的 GUC 体系,提供了最多的调优旋钮;Snowflake 几乎不提供任何调节手段。
- 多列谓词精度:Vertica(超立方体)≈ Snowflake(自动相关性检测)> Greenplum(多列 MCV)> Doris/StarRocks(直方图 + 独立性假设)≈ ClickHouse。Vertica 的超立方体 + ExprAn 组合在多列表达式估算上有独特优势,但列相关性仍是所有系统的共同短板。
本文选取 Vertica(深度剖析)、Greenplum(架构相似但设计选择不同的对比参照)、Snowflake(完全相反的设计哲学——零运维自动化)三个系统做重点对比,ClickHouse 和 Doris 在特定维度穿插提及。
第 2 节:核心概念与机制 —— 优化器的「眼睛」如何工作¶
2.1 MPP 统计信息体系的三个层次¶
所有 CBO-MPP 的统计信息体系都可以分解为三个独立的设计层次。每一层,不同系统做出了不同的选择——理解这些选择的差异,是理解优化器行为差异的关键。
| 层次 | 解决的问题 | Vertica | Greenplum | Snowflake | ClickHouse |
|---|---|---|---|---|---|
| L1: 收集方式 | 统计信息从哪来?谁来触发? | 用户主动 + Tuple Mover 自动 ROWCOUNT | ANALYZE + autoanalyze 守护进程 | 零运维自动维护 | 后台自动抽样 |
| L2: 存储粒度 | 统计信息与物理存储单元的关系? | Projection 级(同一表可有多个独立物理结构,每个需单独统计) | Table 级(每表一个物理结构) | Micro-partition 级(元数据与存储一体) | Part 级(与 MergeTree 分区对齐) |
| L3: 代价模型 | 收集到的统计信息如何转化为计划选择? | 数据流模型(CPU/Memory/Disk/Network 四种 Aspect) | GPORCA:执行时间模型;Planner:基于代价估算 | 自动 CBO(算法未公开) | 启发式为主,逐步引入 CBO |
本节阅读提示:以下 §2.2-§2.4 主要覆盖 L1/L2(以 Vertica 为例深度展开,并标注其他系统的差异),§2.5 覆盖 L3(代价模型对比),§2.6 以 Join Ranker 为例展示统计信息如何驱动计划搜索。如果你只关心跨系统对比,直接看各小节末尾的「对比小结」。
2.2 统计信息:优化器的三种视觉模式¶
Vertica 的统计信息体系可以类比为三种视觉模式(来源:Vertica 统计信息管理与查询性能 §1.2):
视觉模式 统计层级 优化器能"看到"什么 决策精度
──────────────────────────────────────────────────────────────────────────────────────
全盲 NONE 什么都不知道 完全随机
近视(只看轮廓) ROWCOUNT 表有多少行,但不知道列分布 JOIN方向可能对,JOIN策略可能错
明视 FULL 行数 + 列值分布 + NDV 所有决策都有数据支撑
- NONE:该表对优化器完全不可见。EXPLAIN 显示
(NO STATISTICS),优化器使用内置默认值(如行数 = 10K)猜测——这种猜测在大数据场景下几乎必定出错。 - ROWCOUNT:Tuple Mover 自动维护(
AnalyzeRowCountInterval控制频率,默认 24 小时),提供表级行数和分区 MIN/MAX。EXPLAIN 显示(ROW COUNT ONLY)。优化器知道表的大小,但不知道列值如何分布——估算谓词选择性时只能凭经验。 - FULL:由
ANALYZE_STATISTICS()收集,包含每列的直方图、NDV(Number of Distinct Values,近似不同值计数)、MIN/MAX、磁盘占用信息。优化器获得最完整的输入。
通俗类比:ROWCOUNT 就像你知道一座城市有 100 万人口,但不知道他们的年龄分布。如果你要规划「所有 18-25 岁人群的活动」,你只能假设年龄均匀分布——实际可能是年轻人占 60%,也可能只占 10%。FULL 统计相当于给你一份人口普查的年龄直方图,你可以精确计算目标人群的数量。
对比小结:NONE → ROWCOUNT → FULL 的三级递进是 Vertica 特有的显式分层设计——用户可以用
statistics_type列精确查看每列的统计级别。其他系统的处理方式不同:
- Greenplum:没有显式的统计等级概念。
ANALYZE总是收集 FULL 级统计(直方图 + MCV + NDV),要么有要么没有。行数由pg_class.reltuples维护,类似 Vertica 的 ROWCOUNT 但粒度更粗。- Snowflake:不存在「没有统计」的状态——统计信息在数据写入时自动维护。用户不需要也不被允许手动触发统计收集,因此也没有 NONE/ROWCOUNT/FULL 这些概念。
- ClickHouse:通过
statistics_columns配置指定哪些列需要统计,未配置的列视为 NONE。统计粒度更像「指定哪些列」而非 Vertica 的「指定哪个等级」。- Doris/StarRocks:
ANALYZE TABLE支持全量和采样两种模式,但没有 Vertica 这样明确的中间 ROWCOUNT 状态。
2.3 直方图:从行数到分布¶
行数告诉你「表有多大」,但选择率(selectivity)——「WHERE 过滤后还剩多少行」——才是优化器最关键的输入。直方图是回答选择率问题的核心工具。
Vertica 使用等高直方图(Equi-Height Histogram),采用 Smoothed Jackknife 算法估算不同值数量(NDV),最多 100 个桶(来源:Optimizer 论文 §IV-A 脚注 4)。等高意味着每个桶包含的行数大致相等,而不是值域范围相等。
等宽直方图(不好的选择):
值域:[0-100] 分 5 个桶 → [0-20], [20-40], [40-60], [60-80], [80-100]
问题:如果 90% 的数据集中在 [0-20],其他 4 个桶几乎是空的 → 选择率估算极不准确
等高直方图(Vertica 的选择):
行数均分 5 个桶 → 桶 1 覆盖 [0-5](数据密集),桶 2 覆盖 [5-8],...
优势:每个桶行数相等 → 对偏斜数据的选择率估算仍然准确
为什么选择等高而非等宽:MPP 分析场景中最常见的数据分布就是偏斜的——少数热门值占据大量行(如大客户的交易记录、热门商品)。等高直方图每个桶包含的行数大致相等,无论值域范围如何,对偏斜数据的选择率估算都能保持稳定——这是等宽直方图做不到的(来源:Optimizer 论文 §IV-F2:直方图基于 Smoothed Jackknife 算法,对偏斜数据容忍度高)。
2.4 超立方体:多列谓词的选择率估算¶
单列谓词(WHERE col = 5)的选择率可以直接查直方图得出。但实际查询中,谓词经常涉及多列:
这就不再是独立查三个直方图那么简单——因为 a + b 的分布不能直接从 a 和 b 的直方图推导。
Vertica 的做法是构建多维超立方体(Hypercube)(来源:Optimizer 论文 §IV-F2,图 9)。具体流程:
- 对表达式中的每个列引用,取其直方图的桶边界,构建一个 N 维网格
- 每个网格单元对应一个 N 维矩形区域(如「a ∈ [100, 500], b ∈ [2000, 7000]」)
- 使用 ExprAn(表达式范围分析器)对每个区域评估:给定列值范围,表达式在这个区域内恒真、恒假、还是不确定?
- 对确定区域直接累加行数,对不确定区域继续细分(divide and conquer)
- 最终输出:表达式选择率的估算值
ExprAn 不只是优化器用——执行引擎也用它来做存储裁剪(partition pruning):如果某个磁盘块的数据范围被 ExprAn 判定为「表达式恒假」,则跳过不读。这是同一套机制在不同层面的复用。
关键局限:列独立性假设。超立方体方法假定了列之间相互独立——现实中列通常有相关性(如 age 和 salary 正相关)。论文明确指出:「列相关性是我们在客户案例中看到的选择率估算误差的最大来源」(Optimizer 论文 §IV-F2)。这是统计信息体系的一个已知短板,所有基于直方图的 CBO 都面临这个问题。
2.5 数据流成本模型:为什么不直接估算执行时间?¶
多数数据库优化器试图直接估算执行时间作为代价。Vertica 选择了一条不同的路:估算数据流(data flow),即查询计划中流经每个算子的数据量(来源:Optimizer 论文 §IV-F)。
代价分为四种 Cost Aspect:
| Aspect | 建模的内容 | 示例:Hash Join 算子 |
|---|---|---|
| CPU | 需要 CPU 处理的数据量 | 内外表的大小——都需要对 JOIN 键做哈希 |
| Memory | 需要分配的内存大小 | 哈希表的大小(内表大小) |
| Disk | 溢出到磁盘的数据量 | 哈希表超出内存时溢出部分 |
| Network | 网络传输的数据量 | 如果需要 RESEGMENT 或 BROADCAST,传输的数据量 |
每个算子的代价 = 四种 Aspect 的加权和 ÷ 并行度(节点数)。
为什么要用数据流模型而非直接估时?论文给出了三个理由:
- 简单性(Simplicity):生产环境中,可调试性和可预测性往往比精确度更重要。数据流模型让 DBA 可以直观理解「为什么优化器选这个计划」——Cost 大意味着数据搬运多,而不需要反推「优化器认为 CPU 会花多少微秒」。
- 鲁棒性(Robustness):现代硬件的 NUMA、流水线、超线程等特性让执行时间极难预测。同一查询在不同硬件上的执行时间可能差数倍,但数据搬运量是稳定的。数据流模型对硬件环境变化的敏感度更低。
- 集群规模不变性:代价除以节点数后,4 节点跑 4TB 数据和 8 节点跑 8TB 数据的计划代价相同——集群扩容不会无故改变执行计划。
代价的代价:论文实验中(图 10),优化器选择的计划(Cost 最低的)执行时间并非绝对最短,但非常接近最优(在 TPCH Q8 的快计划区间内)。这是数据流模型的一个已知局限:它擅长区分「快计划」和「慢计划」,但在「快计划」之间的排序可能不够精确。
对比小结:数据流模型 vs 执行时间模型:
维度 Vertica(数据流) Greenplum GPORCA(执行时间) Snowflake(自动 CBO) 代价含义 数据搬运量(行数 × 网络跳数) 估算的执行时间(ms) 用户不可见 对硬件敏感度 低——扩容只改变并行度分母 高——依赖 CPU/IO 速度校准 云环境统一硬件,无需校准 可调试性 高——Cost 大说明数据搬运多 中——需要理解代价因子含义 低——用户看不到代价 快计划区分度 中——快计划间排序不够精确 高——时间模型可以区分微秒差异 未公开 Vertica 选择数据流模型的一个深层原因是:它面向的是异构硬件的本地部署环境(客户自建集群,硬件配置千差万别),数据流模型不依赖硬件校准,对部署环境的变化具有天然鲁棒性。Snowflake 则因为运行在同构云环境上,可以使用完全自动化的执行时间模型而不必担心硬件差异。
2.6 Join Ranker:统计信息如何驱动 JOIN 顺序搜索¶
有了统计信息(直方图 → 基数估算 → Cost aspects),优化器如何搜索 JOIN 顺序?Vertica 使用 Join Order Enumerator + Join Ranker 的组合(来源:Optimizer 论文 §IV-E)。
这是一个从底向上的工作列表算法:
- 初始状态:一个 IPJO(In Progress Join Order),包含所有待 JOIN 的表,还没有任何部分计划
- 对当前 IPJO 中所有未完成的 JOIN,用 Join Ranker 打分
- 选 Rank 最低的 JOIN 推进,生成新的 IPJO
- 用 Join Pruning 去掉次优的 IPJO(相同部分结果只保留代价最低的)
- 重复直到所有 JOIN 完成 → 从完成的计划中选总代价最低的
Join Ranker 的评分依据包括(原文 §IV-E1):
| 评分维度 | 依赖的统计信息 | 含义 |
|---|---|---|
| Selectivity | 直方图 + 超立方体 | 这个 JOIN 能筛掉多少比例的数据?越高越好 |
| Cardinality | 行数 + NDV | JOIN 输入的相对大小——优先 JOIN 小表 |
| Co-location | 分段键信息 | 两个表的数据是否已在同一节点?免去网络搬运 |
| Sortedness | 排序键信息 | 输入是否已排序?能否用更快的 Merge Join? |
| Constraints | Schema 约束(主键等) | 是否有主键-外键关系可以利用? |
Join Ranker 的评分不是静态的——同一个 JOIN 在不同 IPJO 中可能得到不同的得分,因为部分计划的中间结果会影响后续 JOIN 的估算。Ranker 和 Pruning 一起压缩了搜索空间,使优化器在绝大多数查询上能以毫秒级完成计划选择(论文表 I 显示,97M+ 真实客户查询的平均优化时间为 53ms)。
Vertica 专属:Join Ranker 是 Vertica 优化器特有的设计——它不是通用的 JOIN 枚举算法(如动态规划、GEQO),而是在 Vertica 的「先选 projection 再枚举 JOIN」这一特殊流程中起衔接作用的组件。其他 MPP 系统的对应机制:
- Greenplum GPORCA:使用 Cascades 框架的自顶向下搜索,不需要独立的 Ranker 组件——代价评估内嵌在搜索过程中
- PostgreSQL Planner:使用动态规划(FROM 子句 ≤12 表)或 GEQO 遗传算法(>12 表),也不需 Ranker
- Doris/StarRocks:基于 Cascades 框架,与 GPORCA 类似,代价评估与搜索过程一体化
- Snowflake:优化器内部机制未公开,但推测使用了类似的代价驱动的搜索策略
第 3 节:设计决策与 Trade-off¶
3.1 核心设计权衡总览¶
| 设计决策 | 可选方案 | Vertica 的选择 | 收益 | 代价 | 最佳场景 | 短板场景 |
|---|---|---|---|---|---|---|
| 代价模型 | 估算执行时间 vs 估算数据流 | 数据流模型 | 简单可调试,硬件无关 | 快计划之间排序不够精确 | 大部分生产场景 | 需要精确区分微秒级差异的场景 |
| 直方图类型 | 等宽 vs 等高 vs 混合 | 等高直方图 | 偏斜数据估算准确 | 桶边界不对齐列值边界 | 分析型负载(数据偏斜常见) | 均匀分布数据(等宽也无差异) |
| 多列选择率 | 独立假设 vs 多列直方图 vs 超立方体 | 超立方体 + ExprAn | 可处理任意表达式 | 仍假定列独立,相关性是最大误差源 | 多列谓词场景 | 强相关列(如 age↔salary) |
| 统计收集时机 | 自动触发 vs 定时触发 vs 用户触发 | 用户触发为主 + Tuple Mover 自动补充 | 用户完全掌控 | 容易遗忘或频繁收集 | 有 ETL 流程的场景 | 高频实时加载场景 |
| 优化器可调性 | 开放内部权重 vs 封闭模型 + 直接控制 | 封闭模型 + Syntactic Optimizer(hints) | 计划可预测、可固定 | 无法微调权重适配极端场景 | 99% 生产场景 | 需要微调成本模型的特殊场景 |
| 统计收集粒度 | 仅全量 vs 全量 + 分区级 | 全量 + 分区级 | 大表增量维护成本低 | 跨分区查询可能用分区级统计做次优估算 | 按日期分区的超大表 | 跨大范围分区的复杂查询 |
3.2 为什么不用多维直方图(Multi-Dimensional Histogram)?¶
超立方体方法在 N 个列上构建 N 维网格,随着列数增加,网格单元数指数增长——N 维网格在每维 100 个桶时产生 100^N 个单元。Vertica 优化器论文未公开具体的列数阈值,但工程上通常会限制参与超立方体的列数,超出的列退回到列独立性假设。
为什么不做多维直方图?这是一个工程权衡:
- 多维直方图(如 Oracle 12c+ 支持的):用聚类或密度估计来捕获列间相关性,精度更高但构建成本大,且存储开销随列数快速增长
- Vertica 的取舍:接受列独立性假设的误差,用等高直方图 + 超立方体的组合覆盖最常见场景(≤3 列的谓词),保持收集和存储成本可控
论文坦诚指出列相关性是「选择率估算误差的最大来源」。这不是 Vertica 独有的问题——任何不建多维直方图的 CBO 都面临同样的取舍。
3.3 Syntactic Optimizer:当统计信息失效时的安全阀¶
Vertica 优化器的代价模型权重是不可调的(Optimizer 论文 §IV-F3)。对比 Oracle 允许 DBA 调整优化器内部参数(如 optimizer_index_cost_adj),Vertica 选择了完全不同的哲学:
「我们相信,用户覆写优化器计划选择的最简单、最有效的方式,不是通过间接的旋钮,而是通过直接控制期望的计划特征。」
这就是 Syntactic Optimizer 模式:用户通过查询 hints 或 session 属性直接指定 JOIN 顺序和投影选择,而不是调整内部代价权重。
为什么这样设计?
- 调权重是「间接控制」——你改了某个数字,但不知道最终计划会变成什么样,需要反复试
- Hints 是「直接控制」——你说「用这个投影、按这个顺序 JOIN」,优化器照做
- 在大多数场景下,用户不需要调任何东西——优化器的默认选择已经足够好
- 当统计信息确实无法修复(如临时表、外部表),直接指定比调权重更可靠
3.4 跨系统对比:不同 MPP 对统计信息-优化器关系的设计哲学¶
3.4.1 综合对比¶
下表覆盖本文重点对比的五个系统,从六个维度揭示它们对「统计信息如何影响优化器」这一问题的不同回答:
| 维度 | Vertica | Greenplum | ClickHouse | Snowflake | Doris/StarRocks |
|---|---|---|---|---|---|
| 统计收集触发 | 用户主动 + TM 自动 ROWCOUNT | ANALYZE + autoanalyze 守护进程 | 后台自动抽样 | 零运维自动维护 | 自动 + 手动 ANALYZE |
| 维护负担 | 高——projection 级,多副本多份统计 | 中——table 级,每列 ANALYZE | 低——自动抽样,用户几乎无感 | 零——完全透明 | 中低——自动 + 手动补充 |
| 代价模型 | 数据流(四种 Aspect 加权) | 执行时间(GPORCA)+ 传统 CBO | 启发式为主,CBO 逐步引入 | 自动 CBO(不公开) | Cascades 框架 CBO |
| 优化器可调性 | Hints(Syntactic Optimizer) | GUC 参数(最多旋钮) | join_algorithm / 内存限制 |
几乎不可调 | Session 变量 |
| 多列谓词精度 | 超立方体 + ExprAn(高) | 多列 MCV(中高) | 列独立性假设(中) | 自动相关性检测(推测高) | 直方图 + 独立性假设(中) |
| 统计过期检测 | statistics_updated_timestamp + PREDICATE VALUES OUT-OF-RANGE |
pg_stat_user_tables.last_analyze + autoanalyze 阈值 |
自动维护,按 part 粒度更新 | 自动维护,无过期概念 | 自动 + last_analyze_time 监控 |
3.4.2 设计哲学分歧:三派之争¶
上表不仅是功能罗列——它揭示了三种根本不同的设计哲学:
派别一:专家手工派(Vertica、Greenplum)
核心理念:DBA 比优化器更了解自己的数据和业务。提供完整的统计信息收集工具和优化器调优手段,让 DBA 有最终控制权。
- Vertica 偏「直接控制」——Syntactic Optimizer(hints)直接指定计划,不绕弯
- Greenplum 偏「间接调优」——GUC 参数调整优化器行为,但最终计划仍由优化器决定
派别二:零运维自动化派(Snowflake)
核心理念:优化器应该比用户更懂。统计信息、查询优化、资源分配全部自动化,用户只写 SQL——连 ANALYZE 命令都不存在。代价是用户失去了所有干预能力:如果优化器选错了计划(虽然 Snowflake 宣称极少发生),用户除了改写 SQL 别无他法。
派别三:渐进式自动化派(ClickHouse、Doris/StarRocks)
核心理念:从简单规则出发,逐步引入 CBO。这些系统起步时以 OLAP 实时查询为核心场景,优化器最初以启发式规则为主。随着分析场景复杂化,它们逐步引入 CBO 组件——ClickHouse 从 v23.x 开始强化 CBO,Doris 从 2.0 开始引入 Cascades 框架。它们目前处于「规则 + 代价」的混合状态,既不像 Vertica 那样依赖 DBA 也不像 Snowflake 那样完全封闭。
3.4.3 物理设计粒度对统计维护成本的影响¶
在所有对比维度中,物理存储单元的粒度是最根本的差异——它直接决定了统计信息维护的复杂度:
Vertica: 1 张表 × N 个 projection × K 列 = N × K 个需要统计的列
Greenplum: 1 张表 × 1 个物理结构 × K 列 = K 个需要统计的列
Snowflake: 1 张表 × M 个 micro-partition = 统计与存储元数据一体,无需额外维护
ClickHouse: 1 张表 × P 个 part × C 个统计列 = P × C 个统计单元(但自动抽样大幅降低负担)
Doris: 1 张表 × K 列 = K 个统计单元(分区级可选)
Vertica 的 projection 级物理设计提供了最大的优化空间(同一张表可以用不同排序键、不同分段键存储多份副本),但代价是统计信息维护复杂度高了 N 倍——N 是 projection 数量。这是一个灵活性 vs 维护成本的典型 trade-off:在 DBA 资源充足的大型企业(Vertica 的目标客户),这个代价是可接受的;在追求零运维的 SaaS 场景(Snowflake 的目标客户),这个代价是不可接受的。
第 4 节:设计对实际使用的影响¶
4.1 查询维度:EXPLAIN 中的统计信息 SOS 信号¶
当你检查执行计划时,以下标记是统计信息问题的直接证据(来源:Vertica 统计信息管理与查询性能 §3.4):
| EXPLAIN 标记 | 含义 | 严重度 |
|---|---|---|
(NO STATISTICS) |
该表完全没有任何统计信息——连行数都不知道 | 🔴 严重 |
Rows: 10K (NO STATISTICS) |
优化器用默认值 10K 猜测行数 | 🔴 严重 |
(ROW COUNT ONLY) |
只有 ROWCOUNT,缺 FULL 列级统计 | 🟡 警告 |
PREDICATE VALUES OUT-OF-RANGE |
谓词值超出直方图记录范围 | 🟡 警告 |
此外,Cost 异常偏低(如仅 1K)通常也伴随
(NO STATISTICS)——优化器因缺少统计信息严重低估了查询代价。
一个核心认知:当你看到 (NO STATISTICS) 时,不要继续分析其他优化方向——先收集统计信息再重新 EXPLAIN。很多时候执行计划会自动恢复正常。这是排版优先级最高的修复动作。
4.2 加载维度:统计收集的时机决定统计的质量¶
这是生产环境中最常见的认知误区:「我每天定时收集统计信息,所以统计信息是准确的。」
错。统计信息的准确性不由收集频率决定,而由收集时间与数据加载时间的先后顺序决定。
错误的时序(最常见):
21:55 统计收集 → 04:00 数据加载 → 业务查询使用过期统计
↑
统计收集在数据加载之前!收集到的是旧数据的信息
正确的时序:
04:00 数据加载 → 04:05 统计收集 → 06:00 业务查询使用最新统计
↑
统计紧随数据加载之后
某运营商 2021 年 4 月案例(来源:某运营商 Vertica 数据库性能问题处理报告)精准地验证了这一点:统计在 21:55 收集,数据在 04:08 加载——6 小时的窗口期导致 9% 的新数据未被统计覆盖,查询内存从 16GB 飙升到 83GB。
4.3 运维维度:临时表是统计信息的最大盲区¶
ETL 批处理脚本创建的中间临时表(mid_xxx、tmp_xxx)有一个致命特征:它们只在脚本执行期间存在,脚本结束后就被 DROP。这意味着任何定时统计收集脚本都无法覆盖它们——它们形成了统计信息的永久盲区。
某运营商 2021 年 6 月案例(来源:某运营商 Vertica 数据库性能问题处理报告)揭示了这个问题:user_dtal 表有 14.5 亿行,统计信息过期导致被错误选为驱动表;同时批处理中大量中间临时表完全没有统计信息。修复方案是:在每个 INSERT 后立即对目标表(包括临时表)收集统计——让脚本多花 2 秒,但可能让后续 JOIN 少跑 2 小时。
4.4 常见误解与澄清¶
误解 1:「统计信息收集一次就够了,数据不变统计就不会过期。」
澄清:统计信息本身不会「变质」,但数据在变化。一张表即使只有增量追加(没有 DELETE/UPDATE),MIN/MAX 也可能过期——新数据的值范围可能超出旧统计的 MIN/MAX,导致 PREDICATE VALUES OUT-OF-RANGE。
误解 2:「has_statistics = true 说明统计信息是好的。」
澄清:v_catalog.projections.has_statistics 只在所有非 epoch 列均拥有 FULL 统计时才为 true(来源:v26.2 官方文档 03-system-tables.md:HAS_STATISTICS returns true only when all non-epoch columns for a table or table partition have full statistics)。它不检查:(1) 统计是否过期——昨天的全列 FULL 统计今天可能已经过时,(2) 统计精度是否足够——100% 采样和 1% 采样都满足 FULL 条件但估算质量天差地别,(3) 分区级统计与全量统计的覆盖关系。需要用 projection_columns.statistics_type 和 statistics_updated_timestamp 做更细粒度的检查。
误解 3:「ANALYZE_STATISTICS 太慢了,能不跑就不跑。」
澄清:大表(>100 亿行)的 ANALYZE_STATISTICS 确实需要数十分钟,但不跑的代价远比跑的代价大。一次错误的 JOIN 计划可能让查询消耗 10 倍于正常的内存、执行 100 倍于正常的时间。且可以用分区级统计(ANALYZE_STATISTICS_PARTITION)替代全量统计大幅降低维护成本。
误解 4:「EnableAutoDMLStats=1 可以替代 ANALYZE_STATISTICS。」
澄清:EnableAutoDMLStats 只在 DML 后更新 ROWCOUNT 和 MIN/MAX,不产生直方图,不计算 NDV。它是在 ANALYZE_STATISTICS 之间的兜底,不是替代。
第 5 节:案例验证¶
5.1 虚构案例:当统计信息说「这张表很小」¶
📝 虚构案例
场景:某电商平台,12 节点集群。每天凌晨 2:00 ETL 批处理将前一天的订单数据加载到 fact_orders(日均新增 2000 万行),然后执行一系列关联计算生成报表。
某天上午 8:00,运维收到告警:报表 SQL 执行超过 1 小时仍未完成(平时 3 分钟)。
诊断过程:
EXPLAIN 输出:
关键发现:
(ROW COUNT ONLY)——fact_orders只有行数统计,没有 FULL 列级统计- Cost 仅 15K,Rows 仅 30K —— 严重低估(实际应为 2000 万行)
- Inner 表做 BROADCAST —— 优化器认为它只有 3 万行,所以广播到所有节点
检查统计信息状态:
SELECT table_schema, table_name, statistics_type, MAX(statistics_updated_timestamp)
FROM v_catalog.projection_columns
WHERE table_name = 'fact_orders'
GROUP BY 1, 2, 3;
结果:
table_schema | table_name | statistics_type | max
-------------+-------------+-----------------+---------------------------
etl | fact_orders | ROWCOUNT | 2026-06-15 03:12:00
fact_orders 只有 ROWCOUNT 统计,且最近更新是 6 月 15 日——距今 5 天。该表每天凌晨加载数据,已经错过了 5 次统计更新。优化器使用的是 5 天前的数据量做 JOIN 决策。
根因:ETL 脚本中数据加载和统计收集是解耦的——加载在 2:00,统计收集在 5:00。但 5:00 的统计收集脚本因周五晚上的网络抖动失败,此后连续 5 天未成功执行,没有人注意到。直到数据量增长到临界点,优化器的错误决策才暴露出来。
修复:
耗时约 1 分钟(2000 万行)。修复后 EXPLAIN:+-JOIN HASH [Cost: 380K, Rows: 22M]
| Outer (LOCAL ROUND ROBIN)
| Inner (RESEGMENT) ← 不再做 BROADCAST!
| 指标 | 修复前 | 修复后 |
|---|---|---|
| Cost | 15K | 380K |
| Rows 估算 | 30K | 22M |
| Inner 策略 | BROADCAST | RESEGMENT |
| 执行时间 | >1 小时(超时) | 3 分 15 秒 |
回溯原理:优化器的 Join Ranker 依赖 Cardinality(输入大小)来决定 JOIN 方法。当 ROWCOUNT 统计远低于实际行数时,Join Ranker 错误地认为内表很小,选择了 BROADCAST——这本该是最优策略(小表广播),但因为统计信息过期变成了灾难。
5.2 跨系统模拟:同一场景下,不同 MPP 的「代价付在哪里」¶
📝 虚构案例(跨系统模拟)
场景:某企业有一张 500 亿行的事实表 fact_sales 和一张 500 万行的维度表 dim_customer,每天凌晨 3:00 ETL 加载约 5 亿行新数据(增量 1%)。某天 ETL 加载完成后,统计信息因故未更新,上午 8:00 业务高峰期执行以下查询:
SELECT c.region, SUM(f.amount)
FROM fact_sales f
JOIN dim_customer c ON f.customer_id = c.customer_id
WHERE f.sale_date = CURRENT_DATE - 1
GROUP BY c.region;
下表模拟同一场景在五个 MPP 系统中的预估行为——统计过期后,系统如何应对,代价付在哪个环节:
| 维度 | Vertica | Greenplum | ClickHouse | Snowflake | Doris/StarRocks |
|---|---|---|---|---|---|
| 统计过期概率 | 高——用户未在 ETL 脚本中内嵌 ANALYZE,Tuple Mover 的 ROWCOUNT 可能已自动更新但不保证覆盖 | 中——autoanalyze 守护进程可能在业务高峰期之前触发,但不保证(默认 10% 变更阈值,1% 增量不触发) | 低——后台自动抽样按 part 粒度维护,新 part 写入时自动触发统计更新 | 极低——统计在数据写入时自动维护,不存在「过期」概念 | 低——自动统计收集在数据变更后触发,但可能有短暂窗口 |
| 优化器行为 | (ROW COUNT ONLY) → Join Ranker 低估 fact_sales 行数 → 错误选为 Inner 表 |
统计过期但 autoanalyze 未触发 → Planner 用旧统计估算 → 可能选错 JOIN 方向 | 新 part 统计准确,但跨 part 查询时优化器用启发式规则 → JOIN 策略可能次优 | 统计始终准确 → 优化器做出正确决策 | 自动统计更新 → 正常情况决策正确;极端情况下短暂过期 |
| 查询表现 | 大表被 BROADCAST → 所有节点内存同时飙升 → 触发 Spill → 执行时间从 30 秒恶化到 >60 分钟 | JOIN 方向错误 → 单节点内存溢出 → 查询失败或降级为嵌套循环 → 执行时间 >1 小时 | JOIN 策略次优(如未使用合适的 JOIN 算法) → 执行时间可能 2-3× 于正常 | 正常完成,约 30 秒——不受影响 | 正常完成,或短暂过期导致次优计划 → 执行时间可能 1.5-2× 正常 |
| 恢复方式 | 手动 SELECT ANALYZE_STATISTICS('fact_sales') → 约 5-10 分钟(500 亿行),期间不阻塞查询 |
autoanalyze 最终触发 → 被动等待;或手动 ANALYZE fact_sales → 约 10-15 分钟,期间可能阻塞查询 |
自动恢复——下次后台抽样覆盖新 part | 无需恢复 | 自动恢复——下次自动收集覆盖 |
| 运维介入 | 必须——DBA 收到告警后手动执行 ANALYZE_STATISTICS | 可选——autoanalyze 最终会修复,但等待期间业务持续受影响 | 通常不需要 | 不需要 | 通常不需要 |
| 代价付在哪里 | DBA 的时间 + 业务受损窗口——手动修复前查询持续变慢 | 查询失败 + 自动恢复延迟——autoanalyze 的阈值设计意味着小批量增量可能长期不被触发 | 查询性能的次优波动——启发式规则在复杂 JOIN 场景下不如 CBO 精确 | 用户失去干预能力——如果 Snowflake 优化器在极端场景选错计划(极少发生),用户无法强制修正 | 极端场景下的短暂性能波动——自动收集的窗口期 |
注:本表为基于各系统公开架构文档的模拟估算,非实测数据。实际表现受集群规模、硬件配置、数据分布、并发负载等多因素影响。所有非 Vertica 系统的具体参数(如 Greenplum 的 autoanalyze 阈值、ClickHouse 的后台抽样频率)以各自最新 GA 版本文档为准。
这个模拟案例揭示的核心洞察:
统计信息过期带来的代价不是均匀分布的。五个系统形成了从"全靠 DBA"到"完全自动化"的谱系:
统计维护负担: Vertica >>> Greenplum >> Doris/StarRocks > ClickHouse >>> Snowflake
优化器自主恢复能力:Snowflake >>> ClickHouse > Doris/StarRocks >> Greenplum > Vertica
关键推论:选择 MPP 系统时,统计信息维护负担与优化器自主恢复能力是同一枚硬币的两面——Snowflake 把负担降到零,但也剥夺了用户干预计划选择的能力;Vertica 给了 DBA 完全的控制权,但要求 DBA 承担相应的维护责任。没有哪个选择绝对优于另一个——它取决于团队是否有专职 DBA、业务对查询延迟的容忍度、以及数据变更的频率与规模。
5.3 真实案例:分段键与过滤键重合 + 统计信息缺失 → 6.5 小时不死查询¶
📋 真实案例 · 来源:某运营商 Vertica 数据仓库性能问题分析报告
场景:某运营商数据仓库,93 节点集群。凌晨 3 点发现接口装载和脚本运行极度缓慢。
诊断过程:工程师发现系统资源正常,但有一个从 20 日 22:00 开始运行的 SQL 一直未结束,已持续超过 6.5 小时。检查执行计划:
+-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)
双重根因:
- 结构根因:表按
statis_date分段,查询WHERE statis_date = '20200819'—— 分段键与过滤键重合,导致某一天的所有数据落在一台节点上。表在查询中被引用 3 次,三份中间结果全部汇聚在同一台节点,并行度降为 1。 - 统计信息缺失(放大因素):
(NO STATISTICS)意味着优化器对数据量一无所知——Cost 仅 1K、Rows 仅 10K。如果优化器有准确的统计信息,它至少会尝试寻找替代的分布式执行方案(如主动 RESEGMENT 到不同节点),而不是按默认的分段键默默地把所有数据堆到一个节点上。
效果对比:
| 状态 | 执行时间 |
|---|---|
| 故障时(无统计 + 分段键重合 → 单节点) | 6.5 小时+(被杀掉) |
| 修复统计但投影未优化 | 11 分 48 秒 |
| 统计修复 + 投影按 JOIN 列重分段 | 5 分 37 秒 |
关键洞察:统计信息缺失不会单独导致 6.5 小时——它需要与结构问题(分段键/过滤键重合)叠加。但统计信息缺失掩盖了结构问题:优化器因为不知道数据量,没有尝试任何补救策略。有统计的情况下,即使投影设计不佳,优化器也会评估 RESEGMENT 的成本并可能做出更优选择。
5.4 真实案例:数据加载后统计过期 → 内存从 16GB 飙升到 83GB¶
📋 真实案例 · 来源:某运营商 Vertica 数据库性能问题处理报告
场景:同一运营商,138 节点集群。每天上午 9:00-12:00 资源池出现大量排队。
根因链路:
统计数据收集(21:55,28日)
↓
数据装载(04:08,29日)—— 比统计收集晚了 6 小时
↓
1,157 / 12,711 = 9% 新数据不在直方图范围内
↓
EXPLAIN: PREDICATE VALUES OUT-OF-RANGE
↓
优化器基于过期统计选择了错误的大表做 Inner 表
↓
内存消耗: 83 GB(修复后: 16 GB)
执行时间: 非常慢(修复后: 11 秒)
时间线的教训:统计收集在 21:55,数据在 04:08 加载。不是统计收集不够频繁,而是统计收集在数据加载之前。即使每天都收集,如果收集在加载前面,等于每天都白收集。
这个案例直接催生了某运营商四层自动统计收集策略:批处理脚本内嵌 → query_profiles 增量识别 → HDFS 同步后收集 → TOP SQL 兜底。详见 Vertica 大表统计信息维护最佳实践 §5.3。
5.5 真实案例:临时表无统计 → 批处理中多表 JOIN 频繁选错驱动表¶
📋 真实案例 · 来源:某运营商 Vertica 数据库性能问题处理报告
场景:同一集群,6 月 23 日性能恶化——12 点平均响应时间从 13 秒飙升到 94 秒(恶化 7 倍)。
根因:
user_dtal表(14.5 亿行)因统计过期被错误选为 Inner 表 → 单条查询 28GB 内存、执行 1 小时- 批处理脚本中的中间临时表(
mid_xxx、tmp_xxx)完全没有统计信息——它们在定时统计收集脚本运行时已经不存在了 - 临时表参与后续 JOIN 时,优化器用默认值(10K)估算数据量——严重低估
修复:在每个 INSERT 后立即内联 ANALYZE_STATISTICS,覆盖所有临时表。详见 Vertica 大表统计信息维护最佳实践 §5.4。
第 6 节:设计原则总结¶
1. 【通用】统计信息的时效性优先于完整性¶
为什么:当关键决策依赖行数比例时(如 JOIN 方向选择、BROADCAST vs RESEGMENT),一个今天数据加载后立即收集的 ROWCOUNT 统计,其价值远高于一个昨天收集的 FULL 统计。过期的精确统计仍是错误信息——它精确地描述了过去的数据分布,而优化器需要的是当前数据的分布。但如果关键决策依赖选择率估算(如多列 WHERE 谓词),ROWCOUNT 完全无法替代 FULL 统计——此时过期 FULL 仍优于新鲜 ROWCOUNT。本原则强调的是「不要把完整性当作推迟收集的借口」——做完数据加载就应该立即收集统计,哪怕先从轻量级统计开始。
反例:某运营商 2021 年 4 月案例——3 天前的 FULL 统计(21:55 收集)在 04:08 数据加载后立即过期,导致查询内存从 16GB 飙升到 83GB。如果那天凌晨加载后只跑了 ROWCOUNT 级别的统计,也不会有如此严重的偏差。在 Greenplum 中,同样的问题表现为 autoanalyze 阈值未触发导致统计过期——pg_stat_user_tables.n_mod_since_analyze 持续增长但 last_analyze 不变,优化器用过期统计做出错误计划。
2. 【通用】统计信息维护必须与数据加载紧耦合,而非依赖定时任务¶
为什么:定时任务的时间窗口是固定的,而数据加载的完成时间可能因数据量波动而变化。解耦的设计必然产生「统计收集在加载之前」或「加载完等了数小时才收集」的窗口期。紧耦合消除窗口期。这一原理与具体 MPP 系统无关——Greenplum 的 ETL 脚本中也需要在批量 INSERT 后显式调用 ANALYZE,仅靠 autoanalyze 守护进程无法保证时效性。
反例:某运营商四层策略的建立正是因为定时收集的窗口期导致了多次性能事故。事后改进的核心就是将统计收集从定时任务移到 ETL 脚本内部。在 Greenplum 部署中,依赖 autoanalyze 而不同步在 ETL 中嵌入 ANALYZE 的案例同样常见——autoanalyze 的阈值(默认 10% 变更)可能在数据量波动时延迟触发数小时。
3. 【通用】JOIN 列和 GROUP BY 列的统计精度优先级最高¶
为什么:优化器对 JOIN 列的 NDV 依赖最深(用于估算 JOIN 结果集大小),对高选择性 WHERE 列的直方图依赖最深(用于估算过滤后的剩余行数)。其他列(如 SELECT 列表中的展示列)的统计缺失几乎不影响执行计划质量。这个原则在所有 CBO 系统中通用——无论是 Vertica 的 Join Ranker 还是 Greenplum 的 GPORCA,基数估算是 JOIN 策略选择的核心输入。
反例:如果一张 50 列的表只有 3 个 JOIN 列有 FULL 统计,其余 47 列是 ROWCOUNT——执行计划质量与全列 FULL 统计几乎没有差别。但反过来——展示列全有 FULL 统计而 JOIN 列只有 ROWCOUNT——执行计划几乎必定出错。在 ClickHouse 中同样适用:statistics_columns 应优先配置 JOIN 和 WHERE 中的列,而非 SELECT 中的所有列。
4. 【通用】大表的统计信息缺失破坏性远大于小表,应优先保障¶
为什么:优化器的代价估算是相互比较的——如果小表的估算偏了 50%,绝对误差可能只有几千行;但如果大表(数百亿行)偏了 10%,绝对误差就是数十亿行。优化器基于相对大小做决策,但误差的绝对量级决定实际影响。
反例:本文虚构案例 5.1——fact_orders 实际有 2000 万行,优化器认为只有 3 万行,被 BROADCAST 到所有节点导致网络和内存灾难。在 Snowflake 中这个问题理论上不存在(统计自动维护),但在 Greenplum 和 ClickHouse 中,大表统计过期的破坏模式与 Vertica 完全一致——大表被错误认为小表 → 被选为 Inner 表做 BROADCAST → 资源爆炸。
5. 【通用】临时表是统计信息的最大盲区,必须在创建后立即收集¶
为什么:临时表的生命周期决定了定时统计收集永远无法覆盖它们。而临时表又往往是批处理中后续 JOIN 的输入——相当于优化器在批处理的后半段完全「失明」。所有需要手动触发统计收集的 MPP 系统(Vertica、Greenplum、Doris)都面临这个盲区。Snowflake 是个例外——自动统计维护也覆盖临时表。
反例:某运营商 2021 年 6 月案例——批处理中间临时表无统计,加上 user_dtal 表统计过期,导致批处理中后半段所有 JOIN 估算都是错误的。修复方案在每个 INSERT 后立即 ANALYZE_STATISTICS。Greenplum 场景下同样的问题表现为 CTAS 或 SELECT INTO 创建的中间表触发 autovacuum 但不会触发 autoanalyze(autovacuum_analyze_scale_factor 只影响已有表),导致中间表统计永久缺失。
6. 【通用】不要用统计收集的频率来补偿收集时机的错误¶
为什么:每天收集 24 次(每小时一次)但每次都在数据加载之前 = 24 次都是在收集旧数据。每天收集 1 次但在数据加载后立即执行 = 1 次就足够准确。时机 >> 频率。这是一个工程调度原则,与数据库系统无关。
反例:某运营商最初的做法是每天凌晨定时收集(频率 OK),但数据加载因业务调整推迟到早上,导致统计在加载之前就收集了。改成加载后立即收集,一天一次就够了。Greenplum 中同样的反模式:cron 定时 ANALYZE 在凌晨 2:00,但 ETL 在 3:00 加载数据——每 24 小时中 23 小时用的是过期统计。
7. 【通用】统计信息缺失时,不要继续分析其他优化方向¶
为什么:(NO STATISTICS) 意味着优化器对数据量一无所知,它在 EXPLAIN 中展示的 Cost、Rows、JOIN 策略都是基于默认值的猜测。在这个基础上分析「为什么 JOIN 策略不对」是无意义的——先给优化器装上眼睛,让它重新做决策。这是所有 CBO 系统的通用诊断原则:统计信息是第一依赖项,任何基于执行计划的优化分析必须以统计信息准确为前提。
反例:常见错误——看到 BROADCAST 认为需要加 hint 强制 RESEGMENT,看到 Hash Join 认为需要改用 Merge Join。但这些决策本身是优化器在缺少统计的情况下做出的——修复统计后,这些决策可能自动变成正确的。在 Greenplum 中同样常见:看到 Nested Loop 认为需要 SET enable_nestloop = off,但实际上是因为统计缺失导致优化器低估了内表行数。
第 7 节:延伸阅读¶
按阅读顺序排列:
- The Vertica Query Optimizer: The Case for Specialized Query Optimizers [Vertica 专属] — 本文的学术基础。§IV(Statistics、Cardinality Estimation、Histogram、Cost Model)是本文核心机制的学术来源,§IV-E(Join Ranker)解释了统计信息如何驱动 JOIN 顺序搜索。
- Vertica 统计信息管理与查询性能 [Vertica 专属] — 本文的操作伙伴。当你想把理论落地为监控、诊断、修复的操作时,从这篇开始。
- Vertica 大表统计信息维护最佳实践 [Vertica 专属] — 第 3 节的决策框架和真实案例的扩展版。当你的表从百万行增长到百亿行时,这篇告诉你统计策略如何随之演化。
- The Vertica Analytic Database: C-Store 7 Years Later [MPP 通用(论文结论) + Vertica 专属(实现细节)] — §3(Segmentation)解释了为什么 projection 级的物理设计使得统计信息维护复杂度高于 table 级的 MPP 系统。§5(Execution Engine)说明了执行引擎如何与优化器协作。
- DBDesigner: A Customizable Physical Design Tool for Vertica Analytic Database [Vertica 专属] — 介绍 DBDesigner 如何基于 cost-based 方法选择 distribution key 和 sort key。与本文互补:DBDesigner 用 cost-based 做物理设计,Optimizer 用 cost-based 做查询优化——同一套统计信息体系支撑两个子系统。
- Materialization Strategies in the Vertica Analytic Database: Lessons Learned [Vertica 专属] — §III 讨论了 SIP(Sideways Information Passing)如何利用统计信息在 JOIN 之间传递过滤条件,与本文 §2.4 的超立方体机制互补——SIP 是运行时优化,超立方体是计划时优化。