Vertica 统计信息管理与查询性能¶
作者:JiangChong | 发布时间:2026-04-10
适用场景:查询突然变慢、执行计划异常(如大表做驱动表)、EXPLAIN 显示 NO STATISTICS、或数据加载后性能退化——这些都可能与统计信息缺失或过期有关。
关联文章¶
- Vertica 性能调优 - 1 如何阅读执行计划 — 理解 EXPLAIN 输出,识别统计信息对执行计划的影响
- Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 — 结合系统表诊断查询性能问题
- Vertica 查询 Spill 到磁盘的原因与优化 — 统计信息缺失是 Spill 的重要根因之一
- Vertica 内存压力诊断与调优 — 统计信息如何影响查询内存预算估算
- Vertica 监控最佳实践 — 第 8 节「对象统计」涵盖统计信息健康度监控
理解全文脉络¶
本文按「为什么重要 → 怎么监控 → 怎么定位 → 怎么解决 → 案例验证 → 工具箱」的路径组织。如果你已经知道某个查询变慢且怀疑是统计信息问题,可以直接跳到第 3 节对照 EXPLAIN 诊断;如果你需要建立统计信息的自动化管理机制,请从第 4 节开始;如果你刚开始接触统计信息概念,建议从头阅读。
第 1 节:原理理解¶
1.1 Vertica 优化器如何工作¶
Vertica 使用基于成本的查询优化器(CBO, Cost-Based Optimizer)。当你提交一条 SQL 时,优化器会评估多种可能的执行路径——不同的 JOIN 顺序、不同的投影选择、不同的数据分布策略——并为每条路径计算一个成本估算值(Cost),最终选择成本最低的计划执行。
成本估算的核心输入就是统计信息。 优化器需要回答以下问题:
| 问题 | 依赖的统计信息 | 缺失后果 |
|---|---|---|
| 这个表有多少行? | 行数统计(ROW_COUNT) | 无法判断表大小,JOIN 顺序可能颠倒 |
| 过滤条件能筛掉多少行? | 列级最小值/最大值(MIN/MAX) | 无法估算谓词选择性,可能选错驱动表 |
| JOIN 列有多少不同值? | 近似不同值计数(NDV) | 无法估算 JOIN 结果集大小,内存预算错误 |
| 列值如何分布? | 直方图(Histogram) | LIKE/BETWEEN 等范围查询选择性估算不准 |
| 每列占用多少磁盘? | 磁盘空间统计 | 内存和 IO 成本估算偏差 |
通俗类比:统计信息就像优化器的「地图和交通报告」。没有地图,优化器只能在黑暗中摸索,凭经验猜测每条路(执行路径)的耗时,很容易选择一条看似平坦但实际堵车的路。
1.2 Vertica 统计信息的三层结构¶
Vertica 的统计信息不是单一维度的「有」或「没有」,而是分为三个层次:
┌─────────────────────────────┐
│ FULL:完整列级统计 │
│ - 直方图 (Histogram) │
│ - NDV (不同值数量) │
│ - MIN/MAX │
│ - 磁盘占用 │
├─────────────────────────────┤
│ ROWCOUNT:行数统计 │
│ - 表级行数 │
│ - 分区 MIN/MAX key │
├─────────────────────────────┤
│ NONE:无统计信息 │
│ 优化器完全盲猜 │
└─────────────────────────────┘
- FULL(完整统计):由
ANALYZE_STATISTICS()或ANALYZE_HISTOGRAM()生成,提供每列的详细分布信息。这是优化器最理想的输入。 - ROWCOUNT(行数统计):由 Tuple Mover 自动维护(
AnalyzeRowCountInterval控制频率,默认 24 小时),只提供表级行数和分区范围。比没有好,但缺少列级选择性信息。 - NONE(无统计):优化器对该表没有任何数据量信息,只能使用内置的启发式规则(heuristics)猜测,这是绝大多数性能问题的根源。
1.3 为什么 Vertica 特别依赖统计信息¶
与传统单机数据库(如 PostgreSQL、MySQL)相比,Vertica 对统计信息的需求更高,原因有三:
1. 分布式执行的复杂性。Vertica 需要决定数据是否在节点间重分布(Resegment),选择 BROADCAST 还是 RESEGMENT 策略。没有准确的行数估算,优化器可能:
- 把大表 BROADCAST 到所有节点 → 网络洪水
- 把应该 BROADCAST 的小表做了 RESEGMENT → 不必要的网络开销
2. 投影(Projection)的自动选择。同一张表可能有多个投影(不同排序键、不同分段键),优化器必须根据查询的 WHERE 条件和 JOIN 列选择最优投影。没有列级统计,这个选择就是随机的。
3. 内存预算的精确分配。Vertica 的 Resource Manager 根据优化器的成本估算来分配 query_budget。如果优化器低估了内存需求(因为缺少统计信息),查询执行中会发现内存不够,导致:
- 溢写到磁盘(Spill)
- 重新申请内存(重新排队等待)
- 最严重场景:执行计划完全错误,查询消耗远超预期的内存,挤占其他查询资源
1.4 统计信息的生命周期¶
统计信息不会自动保持同步。以下场景会导致统计信息过期或缺失:
| 场景 | 影响 | 发生频次 |
|---|---|---|
| 数据加载后(INSERT/COPY) | 新数据未体现在统计信息中 | 每次数据加载 |
| 大量 DELETE 后 | 统计信息中的行数偏高 | 按业务频率 |
| 表结构变更(ADD/DROP COLUMN) | 新列无统计信息 | 偶尔 |
| 投影重建(REFRESH) | 统计信息需要重新收集 | 偶尔 |
| 分区数据被替换(DROP_PARTITION + COPY) | 分区级统计过期 | 按业务频率 |
| Tuple Mover 自动更新延迟 | ROWCOUNT 可能滞后最多 24 小时 | 持续 |
核心认知:统计信息的最佳时机是在数据变更之后立即收集,而不是等到查询变慢再来补救。
第 2 节:系统级监控(从宏观入手)¶
2.1 全局统计信息覆盖率¶
第一步是了解整个数据库的统计信息健康状况。使用 v_catalog.projection_columns 查看所有列按统计类型的分布:
SELECT
statistics_type,
count(*) AS column_count,
ROUND(count(*) * 100.0 / SUM(count(*)) OVER(), 1) AS pct
FROM v_catalog.projection_columns
GROUP BY statistics_type
ORDER BY statistics_type;
如何解读结果:
| 输出模式 | 含义 | 行动 |
|---|---|---|
FULL > 80% |
统计信息健康 | 维持现状 |
ROWCOUNT 占主导 |
行数统计有但缺少列级统计 | 对关键表执行 ANALYZE_STATISTICS() |
NONE > 10% |
大量列无统计 | 紧急:立即排查哪些表缺少统计 |
| 同时存在 FULL 和 ROWCOUNT | 正常现象——不同表可能有不同收集策略 | 检查是否大表缺 FULL 统计 |
FULL 类型占比不足 80% 是巡检系统中的告警阈值(table_full_stats_ratio_pct: 80),低于此值应触发统计信息收集。
2.2 按表的统计信息覆盖详情¶
全局数字可能掩盖问题——假如 90% 的列有 FULL 统计,但那 10% 恰好是最大的事实表,问题就严重了。需要按表粒度检查:
SELECT
pc.table_schema,
pc.table_name,
MAX(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) AS has_full,
MAX(CASE WHEN pc.statistics_type = 'ROWCOUNT' THEN 1 ELSE 0 END) AS has_rowcount,
COUNT(*) AS total_columns,
MAX(pc.statistics_updated_timestamp) AS last_stats_update
FROM v_catalog.projection_columns pc
GROUP BY pc.table_schema, pc.table_name
ORDER BY has_full ASC, has_rowcount ASC, total_columns DESC;
如何解读结果:
has_full=0, has_rowcount=0:该表完全没有任何统计信息——最高优先级处理has_full=0, has_rowcount=1:只有行数统计——优化器知道表大小但无法估算列选择性,JOIN 估算可能不准has_full=1, total_columns > 某值:有完整统计——确认last_stats_update是否在数据变更之后
2.3 投影级别的统计标记¶
v_catalog.projections 有一个 has_statistics 布尔列,提供快速的投影级统计状态检查。注意:该列在至少有一个列拥有 FULL 统计时才为 true,仅 ROWCOUNT 不足以将其置为 true。
SELECT
projection_schema,
anchor_table_name,
has_statistics,
is_segmented
FROM v_catalog.projections
WHERE is_super_projection = true
ORDER BY has_statistics ASC, anchor_table_name;
如何解读结果:
has_statistics = false的表需要重点关注——这些表要么完全没有列级统计,要么仅有 ROWCOUNT- 结合
is_segmented判断:分段的大表如果has_statistics = false,JOIN 优化几乎肯定会出错
2.4 表级行数统计与收集时间¶
v_monitor.TABLE_STATISTICS 记录了 Tuple Mover 自动维护的表级行数信息。通过检查 stat_collection_time 可以判断统计信息是否过期:
SELECT
t.table_schema,
t.table_name,
ts.row_count,
ts.stat_collection_time,
CURRENT_TIMESTAMP - ts.stat_collection_time AS age
FROM v_catalog.tables t
JOIN v_monitor.TABLE_STATISTICS ts ON t.table_id = ts.table_oid
ORDER BY ts.stat_collection_time ASC
LIMIT 20;
如何解读结果:
stat_collection_time远早于最近数据加载时间:统计信息已过期——优化器使用的是旧的行数估算age超过 7 天:即使数据量没有大变化,也应该重新收集以确保 Tuple Mover 的自动更新正常- 某些表不在结果中:这些表完全没有任何统计信息记录——最高优先级处理
关于
schema_name的说明:v_monitor.TABLE_STATISTICS不直接包含 schema 名称列,它通过table_oid和schema_oid关联表和 schema。上述 SQL 通过table_oid与v_catalog.tables建立关联。
第 3 节:逐步定位根因(从宏观到微观)¶
当你确定统计信息覆盖率有问题后,需要进一步定位具体是哪些查询受影响,以及影响程度如何。以下按排查优先级展开。
3.1 步骤 1:找到完全缺失统计信息的表¶
做什么:定位 projection_columns 中所有列 statistics_type 均为 NONE 的表。这些表对优化器完全不可见。
SELECT
table_schema,
table_name,
COUNT(*) AS column_count
FROM v_catalog.projection_columns
GROUP BY table_schema, table_name
HAVING MAX(statistics_type) = 'NONE'
ORDER BY column_count DESC;
如何解读:如果结果为空——则所有表至少都有 ROWCOUNT 级别的统计。如果有结果——立即对这些表执行 SELECT ANALYZE_STATISTICS('schema.table');。
如果不是则进入步骤 2。
3.2 步骤 2:找到只有 ROWCOUNT 没有 FULL 统计的大表¶
做什么:ROWCOUNT 统计让优化器知道表有多少行,但不知道列值的分布。对于大表(尤其是常做 JOIN 的表),只有 ROWCOUNT 统计意味着 JOIN 顺序和 JOIN 策略可能出错。
SELECT
pc.table_schema,
pc.table_name,
COUNT(*) AS column_count,
MAX(ts.row_count) AS estimated_rows
FROM v_catalog.projection_columns pc
LEFT JOIN v_catalog.tables t
ON pc.table_schema = t.table_schema AND pc.table_name = t.table_name
LEFT JOIN v_monitor.TABLE_STATISTICS ts
ON t.table_id = ts.table_oid
WHERE pc.statistics_type = 'ROWCOUNT'
AND NOT EXISTS (
SELECT 1 FROM v_catalog.projection_columns pc2
WHERE pc2.table_schema = pc.table_schema
AND pc2.table_name = pc.table_name
AND pc2.statistics_type = 'FULL'
)
GROUP BY pc.table_schema, pc.table_name, ts.row_count
ORDER BY COALESCE(ts.row_count, 0) DESC;
如何解读:
estimated_rows > 1000 万:大表缺少 FULL 统计——严重。优化器无法估算 JOIN 列的不同值数量,可能严重低估或高估 JOIN 结果集estimated_rows > 100GB 的原始数据量:超大表缺少统计——紧急。- 如果所有大表都有 FULL 统计:进入步骤 3 检查统计信息是否过期
3.3 步骤 3:检查统计信息是否过期¶
做什么:即使表有 FULL 统计,如果在数据加载后没有重新收集,统计信息与真实数据已经不一致。
SELECT
pc.table_schema,
pc.table_name,
MAX(pc.statistics_updated_timestamp) AS last_stats_update,
MAX(ts.stat_collection_time) AS last_rowcount_update,
MAX(ts.row_count) AS current_row_count
FROM v_catalog.projection_columns pc
LEFT JOIN v_catalog.tables t
ON pc.table_schema = t.table_schema AND pc.table_name = t.table_name
LEFT JOIN v_monitor.TABLE_STATISTICS ts
ON t.table_id = ts.table_oid
WHERE pc.statistics_type = 'FULL'
GROUP BY pc.table_schema, pc.table_name
ORDER BY last_stats_update ASC
LIMIT 20;
如何解读:
- 按
last_stats_update升序排列,最早收集的表排在最前面——这些最有可能是过期的 - 结合业务数据加载频率判断:如果表每天凌晨加载数据,而统计信息是 3 天前收集的,则已经过期 2 次加载周期
- 通用阈值:如果
last_stats_update早于最近一次数据加载时间,统计信息就是过期的
如果不是过期问题,进入步骤 4。
3.4 步骤 4:对具体查询检查 EXPLAIN 输出¶
当你锁定了一个慢查询,直接看它的执行计划,统计信息问题会暴露无遗。
在 EXPLAIN 输出中查找以下关键标识:
| EXPLAIN 中的标记 | 含义 | 严重度 |
|---|---|---|
(NO STATISTICS) |
该表完全没有任何统计信息——连行数都不知道 | 🔴 严重 |
Rows: 10K (NO STATISTICS) |
优化器用默认值(10K)猜测行数 | 🔴 严重 |
| Cost 异常低(如 Cost: 1K) | 优化器严重低估了查询成本——通常伴随 (NO STATISTICS) |
🔴 严重 |
(ROW COUNT ONLY) |
优化器仅拥有表级行数统计(如 ANALYZE_STATISTICS 部分收集或系统自动更新),但缺少列级分布统计(如直方图等 FULL 统计) | 🟡 警告 |
PREDICATE VALUES OUT-OF-RANGE |
谓词值超出统计信息记录的范围 | 🟡 警告 |
关于
(ROW COUNT ONLY)的说明:此标记表示该表仅有表级行数统计而缺少 FULL 列级统计。当表中完全没有统计信息时(如新创建未 ANALYZE 的表),EXPLAIN 显示(NO STATISTICS)。判断列级统计是否完整的正确方式是查询v_catalog.projection_columns中的statistics_type字段看是否为FULL,EXPLAIN 中的(ROW COUNT ONLY)可作为快速筛查信号,但最终确认需要查系统表。
实际 EXPLAIN 输出示例(来自真实案例):
这个标记意味着:查询中的过滤条件值落在了统计信息记录的最小值/最大值范围之外,优化器无法评估过滤后的行数,可能导致选择错误驱动表。
另一个真实案例:
Cost 仅 1K、Rows 仅 10K——优化器因为缺少统计信息使用了极低的默认估算值,导致它认为这个 JOIN 成本很低,但实际上该表有数亿行数据。结果这个 JOIN 成了单节点上运行 6.5 小时的瓶颈。
关键认知:当你看到
(NO STATISTICS)时,不要继续分析其他优化方向——先收集统计信息再重新 EXPLAIN,很多时候执行计划会自动恢复正常。
3.5 步骤 5:确认配置参数是否支持自动收集¶
检查 Vertica 的统计信息相关配置参数是否已优化:
SELECT parameter_name, current_value, default_value, allowed_levels, description
FROM configuration_parameters
WHERE parameter_name IN (
'AnalyzeRowCountInterval',
'EnableAutoDMLStats',
'AnalyzeStatsPlanMaxColumns',
'MultiplanAnalyzeStats',
'ComputeApproxNDVsDuringAnalyzeStats'
)
ORDER BY parameter_name;
关键参数解读:
| 参数 | 默认值 | 含义 | 建议 |
|---|---|---|---|
AnalyzeRowCountInterval |
86400(24h) | Tuple Mover 自动更新行数统计的间隔 | 根据数据加载频率调小(如 3600 = 1 小时) |
EnableAutoDMLStats |
0(关闭) | DML 操作后自动更新统计信息 | 建议设为 1——尤其适合频繁小批量 INSERT 的场景 |
AnalyzeStatsPlanMaxColumns |
20 | 单次 ANALYZE_STATISTICS 最多分析的列数 | 对大宽表可调大,但会延长收集时间 |
MultiplanAnalyzeStats |
0(关闭) | 使用多个执行计划并行收集统计 | 大集群可设为 1 以加速收集 |
ComputeApproxNDVsDuringAnalyzeStats |
0(关闭) | 收集统计时计算近似不同值数 | 对高基数列(ID 类)的 JOIN 估算至关重要,建议设为 1 |
修改任何系统参数前,请务必先在测试环境进行测试。
第 4 节:解决方案(从快速见效到根本治理)¶
4.1 立即措施:手动收集统计信息¶
对单个表进行全量统计收集:
执行后返回 0 表示成功。该命令收集表中所有列的 FULL 统计,包括直方图、NDV、MIN/MAX。
对特定列进行直方图收集(当只需要优化涉及这些列的查询时):
批量收集多个表:
-- 生成 ANALYZE_STATISTICS 命令(不直接执行)
SELECT 'SELECT ANALYZE_STATISTICS(''' || table_schema || '.' || table_name || ''');'
FROM v_catalog.projection_columns
GROUP BY table_schema, table_name
HAVING MAX(statistics_type) != 'FULL';
注意:
ANALYZE_STATISTICS是一个耗时操作——它会扫描表的所有列数据来构建统计信息。对于超大表(>100GB),收集过程可能持续数分钟。但比起查询因缺少统计信息而跑数小时甚至跑不出来,这个投入是值得的。
4.2 短期优化:配置自动统计收集策略¶
根据实际生产经验,有效的统计信息自动收集策略分为四层:
策略 1:批处理脚本内嵌收集(推荐度:⭐⭐⭐⭐⭐)
在每个 ETL 步骤的 INSERT/COPY 之后,立即对目标表收集统计:
INSERT INTO target_table SELECT ... FROM source_table;
COMMIT;
SELECT ANALYZE_STATISTICS('target_table');
为什么这是最佳策略:统计信息在数据加载后立即更新,保证后续查询始终使用最新统计。代价最小,效果最好。
策略 2:基于 query_profiles 的增量收集(推荐度:⭐⭐⭐⭐)
定时(如每小时)查询哪些表在过去一小时内被 INSERT/COPY 过,对这些表收集统计:
-- 查找最近 1 小时内有过数据写入请求的表
SELECT DISTINCT
regexp_substr(request, 'INTO\s+(\S+\.\S+)', 1, 1, 'i', 1) AS target_table
FROM v_monitor.query_requests
WHERE request_type = 'LOAD'
AND start_timestamp > CURRENT_TIMESTAMP - INTERVAL '1 hour'
AND success = true;
策略 3:配置 EnableAutoDMLStats(推荐度:⭐⭐⭐)
为什么设为 1:每次 DML(INSERT/UPDATE/DELETE/MERGE)操作后自动更新被修改列的行数统计和 MIN/MAX。注意这只更新 ROWCOUNT 级别的统计,不替代 ANALYZE_STATISTICS 的 FULL 统计。
策略 4:定期全量收集(推荐度:⭐⭐)
通过 cron 或其他调度工具在业务低峰期执行全库或按 schema 的统计收集。适合作为兜底策略,但不应作为主策略——因为它在数据加载后才批量运行,存在时间窗口内的统计过期风险。
4.3 统计信息配置调优¶
调整 AnalyzeRowCountInterval:
-- 将 Tuple Mover 自动行数更新间隔从 24 小时缩短到 1 小时
SELECT SET_CONFIG_PARAMETER('AnalyzeRowCountInterval', 3600);
为什么调小:默认 24 小时的间隔意味着 Tuple Mover 每天只更新一次行数统计。在数据频繁变更的场景下(如白天持续加载),行数统计可能在 23 小时内都是过期的。调小到 1-6 小时可以显著减少过期窗口。
开启近似 NDV 计算:
为什么:默认情况下,ANALYZE_STATISTICS 不计算列的不同值数量(NDV)。但对于 JOIN 列(如 user_id、order_id),NDV 是优化器决定 JOIN 策略的关键输入。开启后收集时间会略有增加,但对 JOIN 估算准确度的提升是显著的。
4.4 统计信息收集的风险防控¶
风险 1:高并发收集导致资源争抢
ANALYZE_STATISTICS 本身也是一个查询,会消耗 CPU 和内存。如果在业务高峰期同时收集多个大表,可能加剧资源紧张。
缓解措施:
- 按表大小排序,分批执行(先小表后大表)
- 避免在业务高峰期执行大表的统计收集
- 大集群可设置
MultiplanAnalyzeStats=1并行收集以缩短单表收集时间
风险 2:高频收集导致 OOM
在生产环境的真实案例中,ANALYZE_STATISTICS 每 5 秒执行一次(通过 Kafka 监控 SQL 自动触发),持续消耗内存,最终触发 OOM Killer 导致节点宕机。
缓解措施:
- 绝对不要以秒级频率执行
ANALYZE_STATISTICS - 最小间隔建议 ≥ 5 分钟
- 对大表(>100GB)建议间隔 ≥ 1 小时
- 在收集脚本中加入去重逻辑:如果该表 30 分钟内已被收集过则跳过
第 5 节:深入案例¶
5.1 案例一:统计信息完全缺失导致 JOIN 拒绝分布式执行¶
📋 真实案例
场景:某运营商数据仓库,93 节点集群。凌晨 3 点发现接口装载和脚本运行极度缓慢。
诊断过程:
工程师到达现场后发现:系统资源正常,没有排队,但有一个从 20 日 22:00 开始运行的 SQL 一直未结束(持续超过 6.5 小时)。通过 query_requests 定位到问题 SQL 后,检查其执行计划:
+-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)
根因分析:
- 两个 JOIN 的表均显示
(NO STATISTICS)——优化器对这两张表的数据量一无所知 - Cost 仅 1K、Rows 仅 10K——优化器使用了极低的默认估算值
- 整个 JOIN 在单节点上执行(
Execute on: v_vmart_node0039)——因为优化器认为数据量很小,不需要分布式执行 - 实际这两个表的数据量远超 10K,导致单节点成为瓶颈
修复方案:
- 杀掉卡住的 SQL——集群立即恢复正常
- 为两张表重新设计投影:按 JOIN 关联列(
user_id_zk, user_id_fk)进行 SEGMENTED BY HASH 分布 - 立即收集统计信息
效果对比:
| 状态 | 执行时间 |
|---|---|
| 故障时(无统计 + 单节点) | 6.5 小时+(被杀掉) |
| 正常时(有统计但投影不佳) | 11 分 48 秒 |
| 优化后(统计 + 新投影) | 5 分 37 秒 |
5.2 案例二:数据加载后统计信息过期导致内存飙升¶
📋 真实案例
场景:某运营商,138 节点集群。每天上午 9 点到 12 点 app_pool 出现大量排队。
诊断过程:
- 定位到问题 SQL——一个涉及多表关联的 INSERT 语句
- EXPLAIN 显示:
table: TB_DW_CONTRACT_PROD_ITEMS → PREDICATE VALUES OUT-OF-RANGE - 检查统计信息收集时间:
| 事件 | 时间 |
|---|---|
| 全量统计信息收集 | 28 日 21:55 |
| 数据装载完成 | 29 日 04:08 |
| 数据变化率 | 1,157 / 12,711 = 9% |
根因分析:统计信息在 21:55 收集,但 04:08 又装载了新数据。9% 的新数据导致统计信息过时,优化器错误估计表的数据量,选择了大表做驱动表(inner table)。大表作为 inner 表意味着它要被加载到内存中的哈希表——消耗远超预期的内存。
修复方案:
- 立即:对问题表重新收集统计信息 + 调整
force_outer设置 - 长期:部署四层自动收集策略(批处理脚本即时收集 + query_profiles 增量识别 + HDFS 同步后收集 + TOP SQL 汇总收集)
效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 使用内存 | 83 GB | 16 GB |
| 执行时间 | 非常慢 | 11 秒 |
5.3 案例三:大表统计缺失导致执行计划选了错误驱动表¶
📋 真实案例
场景:同一集群(138 节点),6 月 23 日性能恶化——12 点平均响应时间从 13 秒飙升到 94 秒(恶化 7 倍)。
诊断过程:
- 发现一个 SQL 使用 28GB 内存,执行 1 小时
- 执行计划显示
user_dtal表因统计信息过期被错误选为 inner 表(驱动表) user_dtal表实际有 14.5 亿数据,即使加了时间条件也还有 1,500 万行- 中间临时表(mid/tmp 等)在脚本执行完后数据被删除,批量收集统计信息时无法覆盖这些临时表
根因分析:大批量数据写入后统计信息没有及时更新,加上临时表完全无统计信息,导致批处理程序中的多表 JOIN 频繁选错驱动表。
修复方案:
- 对
user_dtal重新收集统计信息——执行计划立即恢复正常 - 调整
app_pool的plannedconcurrency和maxconcurrency - 在 Perl 批处理脚本的 INSERT 后加入统计信息收集步骤(覆盖临时表)
- 在资源池上设置
MAXQUERYMEMORYSIZE=50G作为安全阀
关键教训:中间临时表是最容易被忽视的统计信息盲区——它们的统计信息在脚本执行期间不存在,但脚本中的后续 JOIN 又依赖它们。
解决方案:在每个 INSERT 提交后立即对目标表收集统计。
5.4 案例四:统计信息缺失导致查询从 700ms 变成几分钟¶
📋 真实案例
场景:某运营商报表系统。每日数据表在批处理程序后会执行 ANALYZE_STATISTICS,但查询仍然间歇性变慢。
诊断过程:
- 2019 年 7 月 11 日 14:10,关联查询该表再次变慢
- 使用 EXPLAIN 查看执行计划:关联顺序异常
- 执行
SELECT ANALYZE_STATISTICS('app_db.app_user_fraud_info_day')后,同样语句执行效率恢复 - 正常时查询约 700ms,变慢时几分钟都不出结果
根因分析:虽然批处理程序每天会收集一次统计信息,但在某些情况下(如数据加载时间窗口冲突、收集操作被跳过等),统计信息仍然会丢失。一旦统计信息缺失,优化器改变了 JOIN 顺序,从正常的小表驱动大表变成了大表驱动小表,查询时间从毫秒级暴跌到分钟级。
修复方案:
- 每次报表变慢时立即手动收集统计信息(临时止血)
- 在报表 SQL 前面加上统计信息收集步骤,确保每次运行前统计信息都是最新的
关键教训:即使有定时收集机制,也不能保证 100% 覆盖。对于关键报表,最好的策略是:在查询之前收集统计信息,而非依赖定时任务。
5.5 案例五:自动统计收集策略的设计与部署¶
📝 虚构案例
场景:某金融机构,24 节点集群。每天凌晨 2:00-6:00 执行 ETL 批处理,涉及 500+ 张表的增量加载和关联计算。近期发现随着数据量增长,ETL 执行时间从 3 小时延长到 5 小时。
诊断过程:
1. 全局统计覆盖率
SELECT statistics_type, count(*) AS cols,
ROUND(count(*) * 100.0 / SUM(count(*)) OVER(), 1) AS pct
FROM v_catalog.projection_columns
GROUP BY statistics_type;
输出结果:
statistics_type | cols | pct
----------------+--------+-------
FULL | 4,200 | 35.0
ROWCOUNT | 7,500 | 62.5
NONE | 300 | 2.5
FULL 列级统计仅覆盖 35% 的列,远低于 80% 的告警阈值。
2. 按表粒度定位缺口
SELECT pc.table_schema, pc.table_name,
COUNT(*) AS total_cols,
MAX(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) AS has_full,
MAX(ts.row_count) AS row_count
FROM v_catalog.projection_columns pc
JOIN v_catalog.tables t ON pc.table_schema = t.table_schema AND pc.table_name = t.table_name
JOIN v_monitor.TABLE_STATISTICS ts ON t.table_id = ts.table_oid
WHERE pc.table_schema = 'etl'
GROUP BY pc.table_schema, pc.table_name
HAVING MAX(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) = 0
ORDER BY COALESCE(MAX(ts.row_count), 0) DESC
LIMIT 15;
输出结果(节选):
table_schema | table_name | total_cols | has_full | row_count
-------------+--------------------------+------------+----------+------------
etl | fact_transaction_daily | 45 | 0 | 150,000,000
etl | fact_user_behavior | 38 | 0 | 120,000,000
etl | dim_account_snapshot | 52 | 0 | 85,000,000
...(共 15 张,均 >5000 万行)
最大的 20 张表(每张 >50GB 原始数据)中,15 张只有 ROWCOUNT 统计,完全没有 FULL 列级统计。
3. 时间线分析
对照 ETL 时间线:凌晨 2:00 开始数据加载 → 3:30 全量加载完成 → 但统计收集脚本安排在凌晨 5:00 运行。3:30-5:00 之间执行的 90 分钟 ETL 关联计算,全部基于过期统计信息。
4. EXPLAIN 验证
EXPLAIN 输出中的关键行:
多个 JOIN 步骤显示 (ROW COUNT ONLY),确认优化器在缺少 FULL 统计的情况下做 JOIN 决策。
根因分析:统计信息收集时机错误——应该紧随数据加载之后,而不是等到批处理末尾。在 3:30-5:00 期间运行的 SQL(ETL 的关联计算部分),所有的执行计划都是基于过期的统计信息生成的。
修复方案:
- 立即:对最大的 20 张表执行
ANALYZE_STATISTICS() - 重构 ETL 流程:将统计收集从批处理末尾移到每个 INSERT 步骤之后
- 开启
EnableAutoDMLStats=1作为兜底 - 设置
ComputeApproxNDVsDuringAnalyzeStats=1提升 JOIN 估算精度 - 对临时表(ETL 中间步骤创建的),在每个 INSERT 后立即收集统计
效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 统计覆盖率(FULL) | 35% | 95% |
| ETL 总耗时 | 5 小时 | 2.8 小时 |
| JOIN Spill 事件 | 127 次/天 | 8 次/天 |
| RESOURCE_REJECTED | 45 次/天 | 2 次/天 |
第 6 节:完整诊断流程实战¶
📝 虚构场景 · 完整演练
场景设定:周五下午 3 点,业务团队反馈「昨天的报表跑得好好的,今天同样的报表跑了 20 分钟还没出结果」。你登录到数据库开始排查。
时间线¶
15:05 — 确认问题
SELECT transaction_id, statement_id, user_name,
start_timestamp, request_duration_ms,
memory_acquired_mb,
substr(request, 1, 200) AS request_preview
FROM v_monitor.query_requests
WHERE start_timestamp > CURRENT_TIMESTAMP - INTERVAL '1 hour'
AND request_duration_ms > 600000 -- 超过 10 分钟
ORDER BY request_duration_ms DESC
LIMIT 5;
输出结果:
transaction_id | statement_id | user_name | start_timestamp | request_duration_ms | memory_acquired_mb | request_preview
----------------+--------------+------------+------------------------+---------------------+--------------------+---------------------------------------------
500123 | 1 | etl_user | 2026-03-13 14:42:15 | 1080000 | 32768 | INSERT INTO report.daily_summary SELECT ...
50098 | 3 | bi_user | 2026-03-13 14:35:00 | 720000 | 8192 | SELECT ... FROM report.transaction_detail...
关键发现:transaction_id = 500123 正在运行中(is_executing = true),已执行 18 分钟,消耗 32GB 内存,远超正常范围。
15:07 — 查看执行计划
EXPLAIN 输出中的关键发现:
+-JOIN HASH [Cost: 50K, Rows: 500K (ROW COUNT ONLY)]
| Outer (LOCAL ROUND ROBIN)
| Inner (BROADCAST)
关键发现:Rows 估算为 500K,且 Inner 表做了 BROADCAST——说明优化器基于过期的统计信息认为 inner 表很小。但根据业务团队的反馈,该表现在约有 1,200 万行。过期的行数统计导致优化器严重低估了数据量,把应该 RESEGMENT 的大表做了 BROADCAST。
15:10 — 检查两个 JOIN 表的统计信息状态
SELECT
table_schema, table_name,
statistics_type,
statistics_updated_timestamp,
projection_column_name
FROM v_catalog.projection_columns
WHERE (table_schema = 'report' AND table_name = 'daily_summary')
OR (table_schema = 'report' AND table_name = 'transaction_detail')
ORDER BY table_name, statistics_type;
输出结果(节选):
table_schema | table_name | statistics_type | statistics_updated_timestamp | projection_column_name
-------------+-------------------+-----------------+------------------------------+-----------------------
report | daily_summary | FULL | 2026-03-13 03:15:22 | report_date
report | daily_summary | FULL | 2026-03-13 03:15:22 | total_amount
report | transaction_detail| ROWCOUNT | 2026-03-10 03:12:05 | trans_id
report | transaction_detail| ROWCOUNT | 2026-03-10 03:12:05 | user_id
report | transaction_detail| ROWCOUNT | 2026-03-10 03:12:05 | trans_amount
关键发现:
daily_summary的所有列statistics_type = 'FULL',且统计更新时间是今天 03:15(数据加载脚本中立即收集了)——正常transaction_detail的所有列statistics_type = 'ROWCOUNT',统计更新时间是 3 月 10 日,而该表每天凌晨 3 点都有数据加载——自那之后已经历了 3 月 11 日、12 日、13 日共 3 次数据加载,优化器看到的仍是 3 天前的旧数据
15:12 — 确认表的数据变化
SELECT t.table_name, ts.row_count, ts.stat_collection_time
FROM v_catalog.tables t
JOIN v_monitor.TABLE_STATISTICS ts ON t.table_id = ts.table_oid
WHERE t.table_schema = 'report'
AND t.table_name = 'transaction_detail';
输出结果:
table_name | row_count | stat_collection_time
-------------------+-----------+---------------------------
transaction_detail | 8,500,000 | 2026-03-10 03:12:05
统计信息记录的行数为 850 万,但业务团队确认该表自那之后经历了 2 次数据加载,当前应有约 1,200 万行——统计行数比实际少了 29%。优化器基于 850 万的估算做 JOIN 决策,严重低估了数据量。
15:15 — 立即修复
执行约 2 秒后返回 0(成功)。对于 1,200 万行的表,ANALYZE_STATISTICS 通常很快——数据量在亿级以下时一般不会超过 1 分钟。
15:20 — 验证修复效果
修复后的 EXPLAIN 输出:
对比修复前后:
(ROW COUNT ONLY)标签消失——FULL 统计收集后优化器拥有了列级分布信息- Rows 估算从 500K → 12M(与真实行数一致)
- Cost 从 50K → 450K(优化器正确评估了查询代价)
- Inner 表从 BROADCAST → RESEGMENT(大表不再被广播到所有节点,改为按 hash 分布)
15:25 — 确认业务查询恢复
重新运行问题 SQL:2 分 15 秒完成。对比之前的 20 分钟+,性能提升 近 10 倍。
事后根因总结:transaction_detail 表凌晨加载数据后,统计收集脚本因为一个异常跳过(表被锁),导致该表统计信息连续 3 天未更新。随着新数据不断累积,优化器的行数估算偏差越来越大,最终在周五下午的数据量下触发了错误的 JOIN 策略。
第 7 节:快速诊断 SQL 工具箱¶
| 诊断目标 | SQL | 已验证版本 |
|---|---|---|
| 全局统计覆盖率 | SELECT statistics_type, count(*) FROM v_catalog.projection_columns GROUP BY statistics_type; |
v26.1.0-2 |
| 按表统计覆盖详情 | SELECT table_schema, table_name, MAX(...has_full...), MAX(...has_rowcount...) FROM v_catalog.projection_columns GROUP BY table_schema, table_name ORDER BY has_full; |
v26.1.0-2 |
| 投影级统计标记 | SELECT projection_schema, anchor_table_name, has_statistics FROM v_catalog.projections WHERE is_super_projection = true ORDER BY has_statistics; |
v26.1.0-2 |
| 完全无统计的表 | SELECT table_schema, table_name FROM v_catalog.projection_columns GROUP BY 1,2 HAVING MAX(statistics_type)='NONE'; |
v26.1.0-2 |
| 仅有 ROWCOUNT 的大表 | 见第 3.2 节完整 SQL——关联 table_statistics 获取行数 |
v26.1.0-2 |
| 表级行数+收集时间 | SELECT t.table_schema, t.table_name, ts.row_count, ts.stat_collection_time FROM v_catalog.tables t JOIN v_monitor.TABLE_STATISTICS ts ON t.table_id = ts.table_oid; |
v26.1.0-2 |
| 统计最早过期的表 | 见第 3.3 节——按 statistics_updated_timestamp ASC 排序 |
v26.1.0-2 |
| 查看 EXPLAIN 统计标记 | EXPLAIN SELECT ...; — 查找 (NO STATISTICS) / (ROW COUNT ONLY) / PREDICATE VALUES OUT-OF-RANGE |
v26.1.0-2 |
| 收集单表统计 | SELECT ANALYZE_STATISTICS('schema.table'); |
v26.1.0-2 |
| 收集列直方图 | SELECT ANALYZE_HISTOGRAM('schema.table', 'column'); |
v26.1.0-2 |
| 统计相关配置参数 | SELECT parameter_name, current_value, default_value FROM configuration_parameters WHERE parameter_name ILIKE '%stat%' OR parameter_name ILIKE '%analy%'; |
v26.1.0-2 |
| 生成批量收集命令 | SELECT 'SELECT ANALYZE_STATISTICS(''' \|\| table_schema \|\| '.' \|\| table_name \|\| ''');' FROM v_catalog.projection_columns GROUP BY 1,2 HAVING MAX(statistics_type)!='FULL'; |
v26.1.0-2 |
| 查询最近写入的表 | SELECT DISTINCT regexp_substr(request, 'INTO\s+(\S+\.\S+)', 1, 1, 'i', 1) FROM v_monitor.query_requests WHERE request_type='LOAD' AND start_timestamp > CURRENT_TIMESTAMP - INTERVAL '1 hour'; |
v26.1.0-2 |
第 8 节:最佳实践清单¶
| # | 实践 | 为什么 | 投入产出比 |
|---|---|---|---|
| 1 | 数据加载后立即收集统计 | 统计信息的最佳时机是数据刚加载完——此时统计与数据一致。等到查询变慢再收集,已经浪费了 CPU 和用户时间 | ⭐⭐⭐⭐⭐ |
| 2 | EXPLAIN 发现 NO STATISTICS 立即处理 | NO STATISTICS 意味着优化器盲猜,任何进一步优化(调资源池、改 SQL)都是本末倒置——先修统计,再看效果 | ⭐⭐⭐⭐⭐ |
| 3 | 开启 EnableAutoDMLStats | 零成本的兜底方案。DML 后自动更新 ROWCOUNT 统计,减少因行数估算不准导致的计划偏差 | ⭐⭐⭐⭐ |
| 4 | 对 JOIN 列开启 NDV 计算 | JOIN 列的不同值数量是优化器决定 JOIN 策略的核心依据。ComputeApproxNDVsDuringAnalyzeStats=1 的额外开销远小于错误的 JOIN 计划 |
⭐⭐⭐⭐ |
| 5 | 临时表也需要统计 | ETL 中间临时表是统计信息的最大盲区。INSERT 后立即 ANALYZE_STATISTICS,别等后续查询发现慢才补 | ⭐⭐⭐⭐ |
| 6 | 监控 FULL 统计覆盖率(阈值 80%) | 低于 80% 意味着每 5 次 JOIN 优化中至少有 1 次可能出错。定期巡检,发现不足立即补 | ⭐⭐⭐ |
| 7 | 大表(>100GB)优先保障 FULL 统计 | 大表统计缺失的破坏性最大——错误的 JOIN 顺序可能导致上百 GB 的数据被广播到每个节点。先保住大表的统计质量 | ⭐⭐⭐ |
| 8 | AnalyzeRowCountInterval 根据需要调整 | 默认 24 小时对于频繁加载的场景太长。降到 1-6 小时可以减少 Tuple Mover 自动统计的过期窗口 | ⭐⭐⭐ |
| 9 | 避免高频 ANALYZE_STATISTICS(≥5 分钟间隔) | ANALYZE_STATISTICS 本身消耗 CPU 和内存。秒级频率会变成自伤——已知真实案例:每 5 秒收集导致 OOM | ⭐⭐⭐ |
| 10 | 统计收集加入 ETL 流程而非依赖定时任务 | 定时任务的时间窗口固定,无法匹配数据加载的完成时间。嵌入 ETL 流程中可以保证「数据到→统计到」的原子性 | ⭐⭐⭐ |
| 11 | 多表关联时检查驱动表选择 | EXPLAIN 中 Inner 表(驱动表)应该是小表。如果发现大表做 Inner,99% 是因为统计信息过期——重新收集后 EXPLAIN 再检查 | ⭐⭐ |
扩展阅读¶
- MPP JOIN 策略全解析与优化器决策逻辑 — JOIN 优化器如何依赖统计信息做 Broadcast/Resegment 决策
- Vertica Join 重分段倾斜诊断与修复 — 统计信息缺失是 RESEGMENT 错误决策的根因之一
- Vertica 大表统计信息维护最佳实践 — 大表特有的统计策略:全量 vs 分区、频率决策、资源隔离
- Vertica 性能调优 - 1 如何阅读执行计划 — EXPLAIN 输出的完整解读指南
- Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 — 结合系统表诊断查询性能
- Vertica 查询 Spill 到磁盘的原因与优化 — 统计信息缺失是 Spill 的重要根因
- Vertica 内存压力诊断与调优 — 统计信息如何影响查询内存预算
- Vertica 监控最佳实践 — 包含统计信息健康度监控
- Vertica 资源池配置的最佳实践 — 与查询内存预算分配密切相关