Vertica 大表统计信息维护最佳实践¶
作者:JiangChong | 发布时间:2026-04-14
适用场景:你管理着数十亿行到千亿行级别的大表(运营商场景下常见),需要决定统计信息的收集时机、频率和策略——全量统计还是分区级统计?每天收集还是每小时收集?如何避免统计收集本身拖慢业务?
关联文章¶
- Vertica 统计信息管理与查询性能 — 统计信息基础概念、全局监控、根因定位和快速修复(本文的上游文章)
- Vertica 性能调优 - 1 如何阅读执行计划 — 理解 EXPLAIN 输出,识别统计信息对执行计划的影响
- Vertica 查询 Spill 到磁盘的原因与优化 — 统计信息缺失是 Spill 的关键根因
- Vertica 内存压力诊断与调优 — 统计信息与内存预算估算的关系
- Vertica CPU 持续高负载诊断与优化 — 第 5.5 节:统计信息对 CPU 的影响
- Vertica 表行数与存储大小估算关系 — 行数 ↔ 存储换算参考(497 张生产表实测数据,中位数 48 bytes/行)
理解全文脉络¶
本文假设你已经读过 Vertica 统计信息管理与查询性能,了解统计信息的基本概念(FULL/ROWCOUNT/NONE 三层结构、EXPLAIN 诊断方法)。本文聚焦于大表特有的挑战:
- 如果你在纠结「这个 200 亿行的分区表应该每天收集还是每周收集」,从第 1 节开始理解决策框架
- 如果你想知道「分区级统计和全量统计到底该怎么选」,直接看第 3 节
- 如果你担心「ANALYZE_STATISTICS 太慢会拖慢业务」,请重点阅读第 4 节资源配置和第 8 节最佳实践清单
第 1 节:大表统计信息的核心挑战¶
1.1 大表与普通表的本质区别¶
对于大表(数十亿行到千亿行),统计信息维护面临三个普通表不会遇到的问题。
行数 ↔ 存储换算:根据 Vertica 表行数与存储大小估算关系(497 张生产表实测),中位数 48 bytes/行(10 亿行 ≈ 45 GB),CDR 话单类宽表 ~120 bytes/行(10 亿行 ≈ 112 GB)。以下阈值同时标注行数和参考存储量,存储量以中位数 ~50 bytes/行估算。
| 挑战 | 普通表(百万-千万行 / <500MB) | 大表(十亿行以上 / >50GB) |
|---|---|---|
| 收集耗时 | 秒级 | 分钟级到小时级 |
| 过期风险 | 低——全量收集快,随时可补 | 高——收集耗时长,不能频繁执行 |
| 资源争抢 | 可忽略 | 可能挤占业务查询的 CPU 和内存 |
| 优化器影响 | 估算偏差影响有限 | 估算偏差可能导致 TB 级数据被错误广播 |
| 分区复杂性 | 通常不分区或简单分区 | 多层分区,需考虑分区级统计 |
核心认知:大表的统计信息维护不是一个「要不要做」的问题,而是一个「怎么做才能兼顾准确性和成本」的问题。错误的策略要么让统计信息长期过期(查询变慢),要么让统计收集本身成为性能瓶颈。
1.2 统计信息缺失对大表的放大效应¶
统计信息缺失对大表的破坏性远大于小表。考虑一个典型的 JOIN 场景:
- 有准确统计:优化器知道 A 很大、B 很小 → 选 B 做 Inner 表(构建哈希表)→ 广播 100MB → 正常执行
- 无统计或过期:优化器不知道 A 有多大 → 可能选 A 做 Inner 表 → 尝试将 TB 级数据加载到各节点内存中建立哈希表 → 内存爆炸、Spill、甚至 OOM
这就是为什么 某运营商真实案例中,14.5 亿行的 user_dtal 表因统计信息过期被错误选为 Inner 表,单条查询消耗 28GB 内存、执行 1 小时,而修复后只需几十秒。
1.3 大表统计信息的决策因素¶
决定大表统计信息策略时,你需要考虑以下因素:
| 决策因素 | 问题 | 影响 |
|---|---|---|
| 数据变化率 | 每天新增/修改多少数据? | 决定收集频率 |
| 表大小 | 表的总数据量多大? | 决定收集成本(耗时) |
| 查询模式 | 哪些列参与 JOIN/WHERE/GROUP BY? | 决定需要哪些列的 FULL 统计 |
| 分区策略 | 表是否分区?按什么键分区? | 决定是否可以用分区级统计替代全量统计 |
| 业务窗口期 | 什么时间可以执行统计收集? | 决定调度策略 |
| 加载模式 | 全量替换还是增量追加? | 决定收集时机 |
第 2 节:大表统计信息监控(量化评估)¶
在决定策略之前,需要先量化评估当前的大表统计信息状态。
2.1 识别需要重点关注的大表¶
做什么:找出数据量大但统计信息不完整的表。在运营商等场景下,重点关注 10 亿行以上的大表——这些表对查询性能的影响最大,需要优先处理。
-- 先从 >100 万行的表开始筛查,按行数降序排列
SELECT
ps.anchor_table_schema AS table_schema,
ps.anchor_table_name AS table_name,
SUM(ps.row_count) AS total_rows,
ROUND(SUM(ps.disk_size) / (1024*1024*1024), 1) AS total_size_gb,
MAX(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) AS has_full_stats,
MAX(pc.statistics_updated_timestamp) AS last_full_stats
FROM v_monitor.projection_storage ps
LEFT JOIN v_catalog.projection_columns pc
ON ps.anchor_table_id = pc.table_id
AND pc.statistics_type = 'FULL'
WHERE ps.anchor_table_schema NOT IN ('v_catalog', 'v_monitor', 'v_internal')
GROUP BY ps.anchor_table_schema, ps.anchor_table_name
HAVING SUM(ps.row_count) > 1000000 -- >100 万行
AND MAX(CASE WHEN pc.statistics_type = 'FULL' THEN 1 ELSE 0 END) = 0
ORDER BY total_rows DESC;
如何解读结果:
has_full_stats = 0:该大表完全没有 FULL 统计——紧急处理total_rows越大越靠前:行数多的表优先处理——一条错误 JOIN 可能消耗几百 GB 内存- 如果结果为空:所有 >100 万行的表都有 FULL 统计——但你仍需检查统计是否过期(2.2 节)
2.2 评估统计信息新鲜度¶
做什么:有 FULL 统计 ≠ 统计信息准确。如果统计收集之后数据发生了显著变化,统计信息就过期了。
-- 两个注意点:
-- 1. projection_columns(每列一行)不能直接 JOIN projection_storage → 用 CTE 分离聚合
-- 2. 每个表的 K-safety buddy 投影会导致 SUM(row_count) 重复 → 只取一个 super projection
WITH anchor_proj AS (
SELECT
anchor_table_name,
projection_schema,
MIN(projection_id) AS projection_id
FROM v_catalog.projections
WHERE is_super_projection = true
GROUP BY anchor_table_name, projection_schema
),
actual_rows AS (
SELECT
ps.anchor_table_schema AS table_schema,
ps.anchor_table_name AS table_name,
ps.anchor_table_id AS table_id,
SUM(ps.row_count) AS current_rows
FROM v_monitor.projection_storage ps
JOIN anchor_proj ap ON ps.projection_id = ap.projection_id
WHERE ps.anchor_table_schema NOT IN ('v_catalog', 'v_monitor', 'v_internal')
GROUP BY ps.anchor_table_schema, ps.anchor_table_name, ps.anchor_table_id
),
stats_info AS (
SELECT
table_schema,
table_name,
table_id,
MAX(statistics_updated_timestamp) AS last_stats_update
FROM v_catalog.projection_columns
WHERE statistics_type = 'FULL'
GROUP BY table_schema, table_name, table_id
)
SELECT
ar.table_schema,
ar.table_name,
ar.current_rows,
ts.row_count AS stats_row_count,
si.last_stats_update,
ts.stat_collection_time AS last_rowcount_update,
CASE
WHEN ar.current_rows > 0
THEN ROUND(ABS(ar.current_rows - COALESCE(ts.row_count, 0)) * 100.0 / ar.current_rows, 1)
ELSE 0
END AS row_count_deviation_pct
FROM actual_rows ar
JOIN stats_info si ON ar.table_id = si.table_id
LEFT JOIN v_catalog.tables t ON ar.table_id = t.table_id
LEFT JOIN v_monitor.TABLE_STATISTICS ts ON t.table_id = ts.table_oid
WHERE ar.current_rows > 1000000 -- >100 万行
ORDER BY row_count_deviation_pct DESC
LIMIT 30;
如何解读结果:
row_count_deviation_pct > 10%:实际行数与统计信息记录的行数偏差超过 10%——统计信息已过期,优化器的 JOIN 估算可能已经出错row_count_deviation_pct > 30%:严重过期——优化器严重低估或高估表大小,执行计划很可能已经变差stats_row_count IS NULL:TABLE_STATISTICS 中没有记录——该表从未被执行过ANALYZE_STATISTICS或 Tuple Mover 尚未覆盖。ANALYZE_STATISTICS会同时更新projection_columns的列级统计和TABLE_STATISTICS的行数记录last_stats_update早于最近数据加载时间:即使行数偏差不大,MIN/MAX 和直方图也可能过期(参考 2.4 节的谓词范围检查)
2.3 按分区粒度检查统计覆盖¶
做什么:对于分区表,检查哪些分区缺少统计信息,哪些分区统计信息过期。
-- 同样:选一个 super projection + CTE 分离,避免 K-safety 和 column JOIN 双重膨胀
WITH anchor_proj AS (
SELECT
anchor_table_name,
projection_schema,
MIN(projection_id) AS projection_id
FROM v_catalog.projections
WHERE is_super_projection = true
GROUP BY anchor_table_name, projection_schema
),
actual_rows AS (
SELECT
ps.anchor_table_schema AS table_schema,
ps.anchor_table_name AS table_name,
ps.anchor_table_id AS table_id,
SUM(ps.row_count) AS current_rows
FROM v_monitor.projection_storage ps
JOIN anchor_proj ap ON ps.projection_id = ap.projection_id
WHERE ps.anchor_table_schema NOT IN ('v_catalog', 'v_monitor', 'v_internal')
GROUP BY ps.anchor_table_schema, ps.anchor_table_name, ps.anchor_table_id
),
stats_info AS (
SELECT
table_id,
MAX(statistics_updated_timestamp) AS newest_stats,
MIN(statistics_updated_timestamp) AS oldest_stats
FROM v_catalog.projection_columns
WHERE statistics_type = 'FULL'
GROUP BY table_id
)
SELECT
ar.table_schema,
ar.table_name,
ar.current_rows AS total_rows,
si.newest_stats,
si.oldest_stats
FROM actual_rows ar
JOIN stats_info si ON ar.table_id = si.table_id
WHERE ar.current_rows > 1000000
ORDER BY ar.current_rows DESC
LIMIT 20;
如何解读结果:
- 比较
newest_stats和oldest_stats的时间差:如果差距很大(如几天),说明不同分区的统计信息收集时间不统一——某些分区的统计可能已经过期 - 将
newest_stats与最近一次数据加载时间对比:如果早于加载时间,需要重新收集 - 与 2.2 节的结果交叉验证:如果行数偏差大且统计时间跨度大,确认是增量加载的表——需要更频繁的分区级统计收集
2.4 检查优化器事件中的统计信息告警¶
做什么:dc_optimizer_events 记录优化器在执行计划生成过程中遇到的统计信息相关问题。这是判断统计信息是否影响实际查询的最直接证据。
SELECT
event_type,
COUNT(*) AS event_count,
MIN("time") AS first_seen,
MAX("time") AS last_seen
FROM dc_optimizer_events
WHERE event_type IN ('NO HISTOGRAM', 'PREDICATE OUTSIDE HISTOGRAM')
AND "time" > CURRENT_TIMESTAMP - INTERVAL '7 days'
GROUP BY event_type
ORDER BY event_count DESC;
如何解读结果:
| 事件类型 | 含义 | 行动 |
|---|---|---|
NO HISTOGRAM |
优化器尝试使用直方图估算谓词选择性,但该列没有直方图 | 对该列的 JOIN/WHERE 条件列执行 ANALYZE_HISTOGRAM |
PREDICATE OUTSIDE HISTOGRAM |
查询的过滤条件值落在直方图记录的范围之外 | 需重新收集统计——直方图的 MIN/MAX 已过期 |
| 两种事件同时高频出现 | 统计信息全面过期 | 紧急:立即对相关表执行全量 ANALYZE_STATISTICS |
第 3 节:全量统计 vs 分区级统计(策略选择框架)¶
这是大表统计信息维护最核心的决策。本节提供一个可操作的决策框架。
3.1 Vertica 统计信息的四级体系¶
从 v_catalog.projection_columns.statistics_type 的取值来看,Vertica 实际上支持四级统计:
statistics_type 优先级(从高到低):
┌─────────────────────────────────────────────┐
│ FULL — 全表级列统计(最完整) │
│ 直方图 + NDV + MIN/MAX + 磁盘占用信息 │
│ 来源:ANALYZE_STATISTICS() │
├─────────────────────────────────────────────┤
│ PARTITION — 分区级列统计(局部准确) │
│ 某个分区的直方图 + NDV + MIN/MAX │
│ 来源:ANALYZE_STATISTICS_PARTITION() │
├─────────────────────────────────────────────┤
│ ROWCOUNT — 表级行数统计(基础) │
│ 来源:Tuple Mover 自动维护 │
├─────────────────────────────────────────────┤
│ NONE — 无统计信息(盲操作) │
└─────────────────────────────────────────────┘
关于 PARTITION 类型:
statistics_type = 'PARTITION'表示该列使用的是分区级统计而非全表统计。优化器在查询时优先使用 FULL,如果 FULL 不存在则回退到 PARTITION。详见v_catalog.projection_columns.statistics_type的字段说明和 TABLE_STATISTICS 参考。
3.2 全量统计(FULL)的适用场景¶
全量统计通过 ANALYZE_STATISTICS('schema.table') 收集,扫描所有分区的所有数据,生成直方图、NDV、MIN/MAX。
适用条件:
- 表行数在可控范围内(一般 < 10 亿行,收集耗时 < 10 分钟)
- 查询涉及跨分区范围扫描(如
WHERE date BETWEEN '2024-01-01' AND '2024-12-31') - 数据是全量替换式加载(整个表 DROP + 重新 COPY)
- 有足够的维护窗口期(如每日凌晨 2:00-6:00)
优点:统计信息最完整,优化器估算最准确。
缺点:大表收集耗时长(数百亿行的表可能需要数十分钟到数小时),在收集期间消耗大量 CPU 和内存。
3.3 分区级统计(PARTITION)的适用场景¶
分区级统计通过 ANALYZE_STATISTICS_PARTITION() 收集,只扫描指定的一个或多个分区,生成该分区的列级统计。
适用条件:
- 表非常大(>100 亿行),全量收集耗时远超业务窗口
- 数据是增量追加式加载(每天只加载一个新分区,历史分区不变)
- 查询模式以「最近 N 天」为主(只查最新分区)
- 分区键与数据加载的时间维度对齐
优点:收集速度快(只扫描新增分区),对业务影响最小。
缺点:跨分区查询时优化器需要合并多个分区的统计信息(PARTITION 统计的优先级低于 FULL);如果查询频繁跨大范围分区扫描,分区级统计的估算精度不如全量统计。
历史教训:Vertica 在某些版本中存在 bug(VER-95196/VER-95198/VER-95200),分区统计信息可能代替全表统计信息被不当使用导致次优计划。这些问题已在 12.0.x、23.4.x、24.2.x 等版本中修复。如果你的版本较老,建议升级或优先使用 FULL 统计。
3.4 决策流程图¶
你可以按以下逻辑选择策略:
大表统计信息策略决策:
表行数 < 10 亿?
├── 是 → 全量统计(FULL),每日收集
│ 条件:收集耗时在维护窗口内
│
└── 否 → 表是分区表吗?
├── 是 → 数据加载模式?
│ ├── 增量追加(每天加载新分区)
│ │ └── 分区级统计(PARTITION),每个新分区加载后立即收集
│ │ + 每周/每月做一次全量统计作为兜底
│ │
│ └── 全量替换(整个表重新加载)
│ └── 全量统计(FULL),加载完成后立即收集
│
└── 否 → 评估是否可以添加分区
├── 可以 → 按时间键分区 → 采用分区级统计策略
└── 不行 → 全量统计 + 列选择优化(只收集 JOIN/WHERE 列)
3.5 混合策略:增量收集 + 定期全量¶
对于超大型表(>100 亿行),最佳实践是混合策略:
- 增量收集(日常):每次数据加载后,对新增分区执行
ANALYZE_STATISTICS_PARTITION() - 全量兜底(周末/月末):选择一个业务完全空闲的时段,执行全量
ANALYZE_STATISTICS()作为质量兜底 - 列级精准收集(按需):对 JOIN 列和高选择性 WHERE 列,执行
ANALYZE_HISTOGRAM()确保直方图精度
这种策略在某运营商 138 节点集群的实际运维中验证有效(详见第 5 节真实案例)。
第 4 节:如何避免统计信息收集影响业务¶
这是本文的第二个核心主题。ANALYZE_STATISTICS 本身也是一个查询——它会扫描数据、消耗 CPU 和内存、持有锁。如果管理不当,它可能从「性能优化工具」变成「性能杀手」。
4.1 专用资源池隔离¶
核心思路:为 ANALYZE_STATISTICS 操作建立专用资源池,限制它能使用的内存和并发度,确保即使统计收集变慢,也不会挤占业务查询的资源。
创建专门的统计收集资源池:
-- 创建低优先级的统计收集专用资源池
CREATE RESOURCE POOL analyzestatistics
MEMORYSIZE '0%' -- 不使用固定内存配额
MAXMEMORYSIZE '20%' -- 最多使用 20% 总内存——防止统计收集吞噬全部内存
PLANNEDCONCURRENCY 2 -- 最多同时执行 2 条统计收集——控制并发度
MAXCONCURRENCY 4
RUNTIMEPRIORITY LOW -- 低优先级——业务查询优先执行
QUEUETIMEOUT 0 -- 不限制排队时间
EXECUTIONPARALLELISM 4; -- 限制并行度——大表收集不要用完所有 CPU 核心
-- 将统计操作路由到此资源池
-- 在运行 ANALYZE_STATISTICS 之前执行:
SET SESSION RESOURCE_POOL = analyzestatistics;
为什么每个参数取这个值:
| 参数 | 取值 | 原因 |
|---|---|---|
MAXMEMORYSIZE '20%' |
最多占用总内存的 20% | 防止大表统计收集吞噬全部内存。参考某运营商 OOM 案例——高频收集导致内存耗尽,触发 OOM Killer |
PLANNEDCONCURRENCY 2 |
最多 2 条并发 | 限制同时收集的表数,避免多表并发收集导致 IO 风暴 |
RUNTIMEPRIORITY LOW |
低优先级 | 业务查询优先——统计收集慢一点没关系,但不能拖慢用户查询 |
EXECUTIONPARALLELISM 4 |
限制并行度 | 大表收集的并行度太高会导致短时间内 CPU 飙升,影响其他查询 |
关于
analyzestatistics资源池:这一实践来自 数据库状态监控脚本,其中监控查询开头就执行set resource_pool=analyzestatistics;来确保自己的查询在专用资源池中运行。同理,所有ANALYZE_STATISTICS操作也应该使用专用资源池。
4.2 调度策略:在正确的时间做正确的事¶
原则:统计信息收集必须安排在数据加载之后、业务查询高峰之前。
# ETL 脚本中的统计收集示例
# 每个 INSERT/COPY 步骤之后立即收集,而非等到批处理末尾
vsql -c "
INSERT INTO fact_table
SELECT * FROM staging_table;
COMMIT;
-- 立即收集新分区的统计
SET SESSION RESOURCE_POOL = analyzestatistics;
SELECT ANALYZE_STATISTICS_PARTITION('fact_table', 'p_$(date +%Y%m%d)', '');
"
调度时间线示例(典型的日批处理场景):
| 时间 | 操作 | 说明 |
|---|---|---|
| 02:00 | 数据加载开始 | COPY/INSERT 批量写入 |
| 03:30 | 数据加载完成 | |
| 03:31 | 统计信息收集 | 对变动的表立即收集,在 analyze statistics 资源池中执行 |
| 04:00 | 统计收集完成 | |
| 06:00 | 业务查询开始 | 优化器使用最新统计信息 |
关键原则:
- 数据加载和统计收集必须紧耦合——中间不要隔数小时。某运营商 2021 年 4 月案例中,统计在 21:55 收集,数据在 04:08 加载,中间 6 小时的间隔导致 9% 的新数据未被统计覆盖,查询内存从 16GB 飙升到 83GB。
- 大表的统计收集应分批串行执行——不要同时收集多张 TB 级大表。
- 利用
MultiplanAnalyzeStats=1并行加速单表收集——将一个大表的统计收集拆成多个子任务并行执行,减少单表收集的墙钟时间。
4.3 锁定风险管控¶
ANALYZE_STATISTICS 在完成统计计算后,需要将结果写入 Catalog。这一步需要获取 Global Catalog (GCL) X 锁——这是一个数据库级别的排他锁,会阻塞其他所有 DDL 和 Tuple Mover 操作。
监控 GCL X 锁的持有时间:
-- 检查 Analyze Statistics 操作持有的 GCL X 锁时长
SELECT
node_name,
TRUNC(grant_time, 'HH24') AS hour,
COUNT(*) AS lock_count,
ROUND(AVG(EXTRACT(EPOCH FROM ("time" - grant_time))), 2) AS avg_hold_seconds
FROM v_internal.dc_lock_releases
WHERE object_name = 'Global Catalog'
AND transaction_id IN (
SELECT transaction_id FROM v_monitor.tuple_mover_operations
WHERE operation_name = 'Analyze Statistics'
AND operation_start_timestamp > CURRENT_TIMESTAMP - INTERVAL '24 hours'
)
GROUP BY node_name, TRUNC(grant_time, 'HH24')
ORDER BY hour, node_name;
注意:
dc_lock_releases在v_internalschema 下,其中time是锁释放时间,grant_time是锁获取时间。锁持有时间 =time - grant_time。此 SQL 来源于 集群性能问题处理检查要点 中的 GCL X 锁分析脚本。
如何解读结果:
avg_hold_seconds < 1:正常——统计写入 Catalog 很快avg_hold_seconds > 5:需要关注——GCL X 锁持有时间过长可能阻塞 Tuple Mover 的 mergeout 操作avg_hold_seconds > 30:严重——可能存在性能问题。检查:- 是否在 mergeout 高峰期执行了 ANALYZE_STATISTICS?
- Catalog 是否过大?(检查
v_catalog.tables总表数) - 是否同时收集了过多表导致 Catalog 写入积压?
缓解措施:
- 避开 mergeout 高峰:不要在执行期间执行大量
ANALYZE_STATISTICS,尤其是大表 - 分批执行:将统计收集分散到不同时段,避免短时间内持有大量 GCL X 锁
- 使用
MultiplanAnalyzeStats=1:加速单表统计写入
4.4 收集频率的最优区间¶
基于真实运维经验总结的频率建议:
| 表类别 | 数据变化频率 | 推荐收集策略 | 最小间隔 |
|---|---|---|---|
| 超大型事实表(>100 亿行) | 每天增量加载 | 分区级统计:加载后立即收集 + 周末全量兜底 | 分区统计 <5 分钟,全量统计 ≥1 周 |
| 大型事实表(10 亿-100 亿行) | 每天全量替换 | 全量统计:加载后立即收集 | ≥24 小时 |
| 中型表(千万-10 亿行) | 不定期更新 | 全量统计:更新后收集 | 按需 |
| 小型表(<千万行) | 几乎不变 | 全量统计:一次性收集,除非结构变更 | 一次性即可 |
绝对底线:
📋 真实案例教训 — 来自 某运营商 OOM 案例:
ANALYZE_STATISTICS以 每 5 秒的频率执行,持续消耗内存,最终触发 OOM Killer 导致节点宕机。
- 最小间隔绝对不低于 5 分钟
- 大表(>10 亿行)建议间隔 ≥ 1 小时
- 超大型表(>100 亿行)建议间隔 ≥ 24 小时(全量统计)
第 5 节:深入案例¶
5.1 案例一:大表混合策略——增量分区统计 + 周末全量兜底¶
📝 虚构案例
场景:某金融机构,16 节点集群。核心事实表 fact_trade 按日期分区,每天凌晨 2:00 加载前一天的分区数据,日均新增 800 万行(约 1GB 压缩后,按宽表 ~120 bytes/行估算)。表总数据量 18 个月积累:约 45 亿行 / 500 GB。
问题:原有的「每天凌晨全量 ANALYZE_STATISTICS」策略,随着数据量增长,统计收集时间从最初的 3 分钟延长到 35 分钟——已经侵占到 6:00 开始的业务查询时间。运维团队在考虑是否降低收集频率。
诊断过程:
1. 确认收集耗时趋势
-- 检查最近 30 天 ANALYZE_STATISTICS 的耗时变化
-- 注意:fact_trade 需替换为实际表名
SELECT
DATE(start_timestamp) AS run_date,
ROUND(AVG(request_duration_ms) / 1000, 1) AS avg_duration_sec
FROM v_monitor.query_requests
WHERE request ILIKE '%analyze_statistics%fact_trade%'
AND start_timestamp > CURRENT_TIMESTAMP - INTERVAL '30 days'
GROUP BY DATE(start_timestamp)
ORDER BY run_date;
输出:
run_date | avg_duration_sec
-----------+-----------------
2026-05-01 | 180.0
2026-05-06 | 380.5
2026-05-11 | 605.2
2026-05-16 | 850.8
2026-05-21 | 1105.3
2026-05-26 | 1600.7
2026-05-30 | 2100.0
收集时间从 30 天前的 180 秒线性增长到 2100 秒(35 分钟)——平均每 5 天增加约 6 分钟。按此趋势,再过 30 天收集耗时将超过 1 小时。
2. 分析数据变化模式
-- 检查 fact_trade 各分区的最近数据写入时间,区分活跃分区和静态历史分区
SELECT
pu.projection_name,
MAX(pu.query_start_timestamp) AS last_write_time,
CURRENT_TIMESTAMP - MAX(pu.query_start_timestamp) AS time_since_last_write
FROM v_monitor.projection_usage pu
WHERE pu.anchor_table_name = 'fact_trade'
AND pu.io_type = 'output'
GROUP BY pu.projection_name
ORDER BY last_write_time DESC;
如何解读:projection_usage 的 io_type = 'output' 记录每次数据写入。对于按日期分区的表,每个投影对应一个或多个分区——最近有写入的投影就是仍在变化的「活跃分区」;很久没有写入的投影对应的是「静态历史分区」。
输出:
projection_name | last_write_time | time_since_last_write
-----------------------------------+-------------------------+----------------------
fact_trade_DBD_18_seg_p20260530_b0| 2026-05-30 03:15:22 | 02:45:00
fact_trade_DBD_18_seg_p20260529_b0| 2026-05-29 03:18:05 | 1 day 02:42:17
fact_trade_DBD_18_seg_p20260528_b0| 2026-05-28 03:12:44 | 2 days 02:47:38
fact_trade_DBD_18_seg_p20260527_b0| 2026-05-27 03:15:30 | 3 days 02:44:52
fact_trade_DBD_18_seg_p20260526_b0| 2026-05-26 03:20:11 | 4 days 02:40:11
fact_trade_DBD_18_seg_p20260525_b0| 2026-05-25 03:14:55 | 5 days 02:45:27
fact_trade_DBD_18_seg_p20260524_b0| 2026-05-24 03:18:33 | 6 days 02:41:49
fact_trade_DBD_18_seg_p20260523_b0| 2026-05-23 03:16:40 | 7 days 02:43:42
fact_trade_DBD_18_seg_p20260522_b0| 2026-05-22 03:17:18 | 8 days 02:43:04
fact_trade_DBD_18_seg_p20260515_b0| 2026-05-15 03:19:52 | 15 days 02:40:30
...
可见只有最近 7 天(p20260524 ~ p20260530)的投影有写入记录——这些分区每天凌晨接收增量数据;7 天前的分区(p20260522 及更早)last_write_time 均已超过 7 天,说明数据已稳定。
关键发现:fact_trade 只有最近 7 天的分区数据会有少量 UPDATE(修正数据),7 天前的分区已经是静态历史数据——不再变化。
3. 评估分区级统计的精度
对比实验:对 fact_trade 分别执行分区统计和全量统计后,运行 5 条典型业务查询,对比 EXPLAIN 的 Cost 估算。
结果:
| 查询类型 | 全量统计 Cost 估算 | 分区统计 Cost 估算 | 偏差 |
|---|---|---|---|
当日交易查询(WHERE trade_date = today) |
45K | 45K | 0% |
| 近 7 天汇总 | 280K | 275K | -1.8% |
| 近 30 天趋势 | 950K | 930K | -2.1% |
| 跨季度对比(2025 Q1 vs 2025 Q4) | 2.1M | 1.5M | -28.6% |
关键发现:分区级统计对「最近 N 天」的查询估算精度几乎与全量统计一致,但对跨季度大范围查询的估算偏差较大(28.6%)。而实际业务中,92% 的查询只涉及最近 30 天。
根因分析:全量统计每天重复扫描 18 个月的历史数据——其中 17 个月的数据是完全静态的。这不是策略问题,而是资源浪费——每天花 35 分钟做 95% 无意义的工作。
修复方案:
- 日常策略(每日):只对最新分区执行
ANALYZE_STATISTICS_PARTITION() - 兜底策略(每周日凌晨):执行全量
ANALYZE_STATISTICS()确保跨年度查询的估算精度 - 对
ANALYZE_STATISTICS使用专用资源池analyzestatistics,限制EXECUTIONPARALLELISM=4
-- 日常:分区级统计(加载后立即执行)
-- 注意:p_20260530 需替换为实际分区名
SET SESSION RESOURCE_POOL = analyzestatistics;
SELECT ANALYZE_STATISTICS_PARTITION('fact_trade', 'p_20260530', '');
-- 周末:全量统计(周日凌晨 3:00 执行)
SET SESSION RESOURCE_POOL = analyzestatistics;
SELECT ANALYZE_STATISTICS('fact_trade');
效果对比:
| 指标 | 优化前(每日全量) | 优化后(增量+兜底) |
|---|---|---|
| 每日统计收集耗时 | 35 分钟 | 2 分钟 |
| 每日查询性能 | 正常 | 正常(无退化) |
| 跨年度查询精度 | 完美 | 周末全量恢复后完美 |
| 统计收集期间的 CPU 峰值 | 85% | 25% |
| 业务查询受影响时间段 | 5:30-6:00 | 无影响 |
5.2 案例二:大表无限增长导致统计收集耗时失控¶
📝 虚构案例
场景:某电商平台,8 节点集群。订单表 orders 不做分区,以每月约 5 亿行的速度增长,当前总量 80 亿行(按中等表 ~50 bytes/行估算,约 400 GB)。统计收集策略是「每次数据加载后全量 ANALYZE_STATISTICS」。
问题:统计收集时间从 3 个月前的 3 分钟增长到现在的 12 分钟。虽然单次 12 分钟不算长,但问题在于:(1) 它每月增长约 3 分钟,按此趋势 6 个月后将超过 30 分钟,(2) 凌晨 4:00 启动时恰好与 mergeout 高峰期重叠,GCL X 锁竞争导致实际墙钟时间拖到 25 分钟以上,(3) 统计收集期间的 CPU 使用率高达 90%+,导致同一时段运行的备份任务和 Tuple Mover 操作明显变慢。ETL 总窗口只有 2 小时,统计收集吃掉的比例从 2.5% 增长到现在的 10%,趋势堪忧。
诊断过程:
- 时间线分析:统计收集在 4:00 启动——恰好与 mergeout 高峰期重叠。
ANALYZE_STATISTICS结束时需要获取 GCL X 锁写入 Catalog,而 mergeout 也在竞争同一把锁,互相阻塞导致实际完成时间延长到 4:25。 - 趋势分析:查询
projection_storage确认该表以每月 5 亿行的速度增长。当前 80 亿行,统计 12 分钟;按此趋势 6 个月后 110 亿行,统计将超过 18 分钟——必然超出 ETL 窗口。 - CPU 分析:
EXECUTIONPARALLELISM使用默认值,8 节点 × 默认并行度 = 统计收集在 12 分钟内占用 90%+ CPU。虽短但密集,恰好卡在 mergeout/备份的同一时段。
根因分析:这是一个典型的「用全量统计处理无限增长的不分区表」问题。表不做分区,每次统计收集都必须扫描全部数据(当前 80 亿行,且以每月 5 亿行增长)。随着表增长,收集时间线性增加,加上与 mergeout 的锁竞争——在可预见的未来(3-6 个月)将必然超出 ETL 维护窗口。
修复方案:
- 立即(本周):为
orders表添加按月份的分区(PARTITION BY order_month),将统计策略改为「当月分区全量 + 历史分区不收集」 -
过渡期(当天):
-
设置
SET SESSION RESOURCE_POOL = analyzestatistics;将统计收集隔离 - 限制
EXECUTIONPARALLELISM=4降低 CPU 占用 - 将收集频率从每天改为每 3 天一次(临时降低频率,等待分区改造完成)
效果对比(分区改造后):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 统计收集耗时 | 25 分钟(纯收集 12 分钟 + 锁排队 13 分钟) | <1 分钟(仅当月分区) |
| CPU 峰值 | 90% | 15% |
| mergeout 延迟 | 频繁排队 | 正常 |
| ETL 超时次数 | 3-4 次/周 | 0 |
关键教训:对于持续增长的大表,分区是解决统计信息维护成本的根本方案。不分区的大表,全量统计收集的时间只会越来越长,最终必然超过维护窗口。
5.3 案例三:统计收集与业务查询的资源争抢¶
📋 真实案例 · 来源:20210430某运营商 Vertica 数据库性能问题处理报告
场景:某运营商,138 节点集群。每天上午 9:00-12:00 app_pool 出现大量查询排队,而统计信息在凌晨 3:00 收集。
诊断过程:
- 定位到问题 SQL——一个多表关联 INSERT 语句
- EXPLAIN 显示:
TB_DW_CONTRACT_PROD_ITEMS → PREDICATE VALUES OUT-OF-RANGE - 时间线分析:
- 全量统计收集:28 日 21:55
- 数据装载完成:29 日 04:08
- 数据在统计之后 6 小时加载——统计信息从加载完成那一刻就已过期
- 数据变化率:1,157 / 12,711 = 9%——新数据不在直方图的 MIN/MAX 范围内
根因分析:统计收集时间(21:55)和数据加载时间(04:08)之间存在6 小时的窗口期。数据加载后,统计信息立即过期。优化器基于过期统计选择了错误的大表做驱动表,导致查询消耗 83GB 内存。
修复方案:
- 立即:对问题表重新收集统计 + 调整
force_outer - 长期:部署四层自动收集策略,将统计收集与数据加载紧密耦合:
| 层级 | 策略 | 触发时机 | 适用对象 |
|---|---|---|---|
| 第 1 层 | 批处理脚本内嵌 | INSERT/COPY 后立即执行 | 所有有数据变更的表 |
| 第 2 层 | query_profiles 增量识别 | 每小时检查哪些表被写入过 | 未被第 1 层覆盖的表 |
| 第 3 层 | HDFS 同步后收集 | 外部数据同步完成后 | 通过 HDFS 加载的表 |
| 第 4 层 | TOP SQL 汇总收集 | 每天一次 | 所有被高频查询的表(兜底) |
效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 问题查询内存消耗 | 83 GB | 16 GB |
| 问题查询执行时间 | 非常慢 | 11 秒 |
核心教训:统计收集的时间窗口比统计收集的频率更重要。统计信息必须在数据加载之后立即收集,而不是在一天中的固定时间——哪怕你每天都收集,如果收集在加载之前,等于白收集。
5.4 案例四:临时表——统计信息维护的最大盲区¶
📋 真实案例 · 来源:20210624某运营商 Vertica 数据库性能问题处理报告
场景:同一运营商集群(138 节点),ETL 批处理脚本中使用大量中间临时表(如 mid_xxx、tmp_xxx)。这些表在单个批处理脚本中创建 → INSERT 数据 → 参与后续 JOIN → 脚本结束后 DROP。
问题:user_dtal 表因统计过期被选为错误的 Inner 表,导致单条查询消耗 28GB 内存、执行 1 小时。同时,批处理脚本中的中间临时表完全没有统计信息,因为它们在定时统计收集运行时已经不存在了。
根因分析:
user_dtal表有 14.5 亿行,统计信息过期导致优化器完全错估其大小- 中间临时表在脚本执行期间存在,但定时统计收集脚本运行时它们已被 DROP——形成了统计信息的永久盲区
- 临时表参与后续 JOIN 时,优化器对其数据量一无所知——只能用默认值(10K)估算
修复方案:
在 ETL 脚本的每个 INSERT 步骤之后,立即对目标表(包括临时表)收集统计信息:
-- ETL 批处理脚本中的模式(Perl/shell 封装)
INSERT INTO mid_order_summary
SELECT ... FROM raw_orders WHERE ...;
COMMIT;
-- 临时表也必须收集统计——否则后续 JOIN 会盲操作
SET SESSION RESOURCE_POOL = analyzestatistics;
SELECT ANALYZE_STATISTICS('mid_order_summary');
-- 后续 JOIN 现在有准确统计了
INSERT INTO final_report
SELECT ... FROM mid_order_summary JOIN dim_product ...;
效果:
user_dtal的 JOIN 计划恢复正常- 中间临时表的 JOIN 估算从 (NO STATISTICS) → Row 准确估算
- 批处理整体执行时间显著缩短
核心教训:临时表的统计信息维护是最容易被忽视的问题。解决方案不是定时收集,而是在每个 INSERT 后立即内联收集——即便这会让脚本多一两秒的执行时间,但比起后续 JOIN 因无统计而跑数小时,这个代价微乎其微。
第 6 节:完整诊断流程实战¶
📝 虚构场景 · 完整演练
场景设定:周二上午 10:00,业务团队反馈「今天的日报跑了一个小时还没出来,平时 5 分钟就好了」。你登录数据库,开始系统化排查。
时间线¶
10:05 — 确认问题 SQL
SELECT
transaction_id, statement_id,
user_name,
start_timestamp,
request_duration_ms / 1000 AS duration_sec,
memory_acquired_mb,
substr(request, 1, 150) AS request_preview
FROM v_monitor.query_requests
WHERE start_timestamp > CURRENT_TIMESTAMP - INTERVAL '2 hours'
AND is_executing = true
AND request_duration_ms > 600000 -- 超过 10 分钟
ORDER BY request_duration_ms DESC;
输出:
transaction_id | statement_id | user_name | start_timestamp | duration_sec | memory_acquired_mb | request_preview
---------------+--------------+-----------+----------------------+-------------+--------------------+-------------------------------------
800234 | 1 | report_etl| 2026-05-26 09:02:15 | 3360 | 18432 | INSERT INTO report.daily_summary SELECT ...
transaction_id = 800234,日报汇总 SQL,已执行 56 分钟,消耗 18GB 内存——远超正常范围(该报表平时 5 分钟、<2GB 内存即可完成)。
10:07 — 查看执行计划
关键发现:
(ROW COUNT ONLY)出现——关键表只有行数统计,没有 FULL 列级统计- Cost 仅 35K、Rows 仅 50K——明显低估。日报表应该有数百万行
- Inner 表做 BROADCAST——优化器认为它很小(但实际上不是)
10:10 — 定位缺少 FULL 统计的大表
-- 只取一个 super projection 避免 K-safety 重复;projection_columns 单独查避免 JOIN 膨胀
WITH anchor_proj AS (
SELECT anchor_table_name, MIN(projection_id) AS projection_id
FROM v_catalog.projections WHERE is_super_projection = true
GROUP BY anchor_table_name
),
actual_rows AS (
SELECT
ps.anchor_table_name AS table_name,
SUM(ps.row_count) AS total_rows,
ROUND(SUM(ps.disk_size) / (1024*1024*1024), 1) AS total_size_gb
FROM v_monitor.projection_storage ps
JOIN anchor_proj ap ON ps.projection_id = ap.projection_id
WHERE ps.anchor_table_name IN ('daily_summary', 'transaction_fact')
GROUP BY ps.anchor_table_name
)
SELECT
ar.table_name,
ar.total_rows,
ar.total_size_gb,
pc.best_stat_type,
pc.last_update
FROM actual_rows ar
JOIN (
SELECT table_name,
CASE WHEN COUNT(CASE WHEN statistics_type = 'FULL' THEN 1 END) > 0 THEN 'FULL'
WHEN COUNT(CASE WHEN statistics_type = 'PARTITION' THEN 1 END) > 0 THEN 'PARTITION'
ELSE 'ROWCOUNT' END AS best_stat_type,
MAX(statistics_updated_timestamp) AS last_update
FROM v_catalog.projection_columns
GROUP BY table_name
) pc ON ar.table_name = pc.table_name;
输出:
table_name | total_rows | total_size_gb | best_stat_type | last_update
-----------------+-------------+---------------+----------------+---------------------------
daily_summary | 5,200,000 | 0.3 | FULL | 2026-05-26 03:15:22
transaction_fact | 300,000,000 | 18.0 | ROWCOUNT | 2026-05-23 03:12:05
关键发现:transaction_fact——3 亿行 / 约 18 GB(按 ~60 bytes/行估算)——只有 ROWCOUNT 统计。最近一次更新是 5 月 23 日,距今已 3 天。而该表每天凌晨有增量加载,意味着已经经历了 3 次数据加载而统计信息一直未更新。
10:12 — 检查统计偏差
WITH anchor_proj AS (
SELECT anchor_table_name, MIN(projection_id) AS projection_id
FROM v_catalog.projections WHERE is_super_projection = true
GROUP BY anchor_table_name
)
SELECT
ts.table_name,
ts.row_count AS stats_row_count,
SUM(ps.row_count) AS actual_row_count,
ROUND((SUM(ps.row_count) - ts.row_count) * 100.0 / SUM(ps.row_count), 1) AS deviation_pct
FROM v_monitor.projection_storage ps
JOIN anchor_proj ap ON ps.projection_id = ap.projection_id
JOIN v_monitor.TABLE_STATISTICS ts ON ps.anchor_table_id = ts.table_oid
WHERE ps.anchor_table_name = 'transaction_fact'
GROUP BY ts.table_name, ts.row_count;
输出:
table_name | stats_row_count | actual_row_count | deviation_pct
-----------------+-----------------+------------------+--------------
transaction_fact | 240,000,000 | 300,000,000 | 20.0%
统计信息记录的行数比实际少 20%——优化器基于 2.4 亿行做 JOIN 决策,而实际有 3 亿行。对于一个大表 JOIN,20% 的偏差足以让优化器选择错误的驱动表和 JOIN 策略。
10:15 — 修复:收集统计
SET SESSION RESOURCE_POOL = analyzestatistics;
SELECT ANALYZE_STATISTICS('transaction_fact');
-- 返回: 0(成功)
3 亿行 / 18GB 的表,ANALYZE_STATISTICS 耗时约 45 秒。
10:18 — 验证修复
修复后的 EXPLAIN 输出:
+-JOIN HASH [Cost: 450K, Rows: 4.5M]
| Outer (LOCAL ROUND ROBIN)
| Inner (RESEGMENT) ← 不再做 BROADCAST!
对比:
| 指标 | 修复前 | 修复后 | 说明 |
|---|---|---|---|
| 统计标记 | (ROW COUNT ONLY) |
(无标记) | FULL 统计已收集 |
| Cost | 35K | 450K | 优化器正确评估了真实代价 |
| Rows 估算 | 50K | 4.5M | 从严重低估恢复到接近真实值 |
| Inner 表策略 | BROADCAST |
RESEGMENT |
大表不再被广播到所有节点 |
10:20 — 确认查询恢复
重新运行日报 SQL:4 分 30 秒完成。对比之前的 56 分钟+,性能恢复。
事后根因:3 天前(5 月 23 日)是周五,统计收集脚本在周五正常运行。但周六和周日的数据加载没有触发统计收集(因为周末脚本只加载数据,不收集统计)。周一凌晨的收集脚本因一个配置错误跳过了 transaction_fact 表。连续 3 次数据加载后统计信息严重过期,优化器在周二上午的日报查询中基于 3 天前的数据做了错误的 JOIN 决策。
改进措施:
- 在 ETL 脚本的每个数据加载步骤后添加
ANALYZE_STATISTICS(而非依赖单独的收集脚本) - 添加监控告警:对 >10 亿行的表,如果统计更新时间超过 24 小时,触发告警
- 周末的加载脚本增加统计收集步骤——周末数据也在增长,统计不能停
第 7 节:快速诊断 SQL 工具箱¶
| 诊断目标 | SQL |
|---|---|
| 找出缺少 FULL 统计的大表 | 见第 2.1 节完整 SQL(含 anchor_proj 去重 + LEFT JOIN 检测无 FULL 的表) |
| 统计信息行数偏差检查 | 见第 6 节行数偏差 SQL——比较 TABLE_STATISTICS.row_count 与 projection_storage.row_count 的实际汇总 |
| 统计最新更新时间 | SELECT table_schema, table_name, statistics_type, MAX(statistics_updated_timestamp) FROM v_catalog.projection_columns WHERE statistics_type IN ('FULL','PARTITION') GROUP BY 1,2,3 ORDER BY 4; |
| 优化器事件统计告警 | SELECT event_type, COUNT(*) FROM dc_optimizer_events WHERE event_type IN ('NO HISTOGRAM','PREDICATE OUTSIDE HISTOGRAM') AND "time" > CURRENT_TIMESTAMP - INTERVAL '7 days' GROUP BY 1; |
| EXPLAIN 检查统计状态 | EXPLAIN SELECT ...; — 查找 (NO STATISTICS) / (ROW COUNT ONLY) / PREDICATE VALUES OUT-OF-RANGE |
| 收集全量统计 | SELECT ANALYZE_STATISTICS('schema.table'); |
| 收集分区级统计 | SELECT ANALYZE_STATISTICS_PARTITION('schema.table', 'partition_name', ''); |
| 收集列直方图 | SELECT ANALYZE_HISTOGRAM('schema.table', 'column_name'); |
| 统计收集耗时趋势 | SELECT DATE(start_timestamp), AVG(request_duration_ms)/1000 FROM v_monitor.query_requests WHERE request ILIKE '%analyze_statistics%' AND start_timestamp > CURRENT_TIMESTAMP - INTERVAL '30 days' GROUP BY 1 ORDER BY 1; |
| ANALYZE STATISTICS 的 GCL X 锁监控 | 见第 4.3 节完整 SQL——检查 dc_lock_releases 中 object_name='Global Catalog' 且 transaction_id 在 tuple_mover_operations 中 operation_name='Analyze Statistics' 的记录 |
| 统计相关配置参数 | SELECT parameter_name, current_value, default_value FROM configuration_parameters WHERE parameter_name ILIKE '%analy%' OR parameter_name ILIKE '%stat%' ORDER BY 1; |
| 生成大表批量收集命令 | 见第 2.1 节「识别需要重点关注的大表」SQL,将其中 has_full_stats=0 的表名代入 SELECT ANALYZE_STATISTICS('schema.table');。大表建议先 SET SESSION RESOURCE_POOL = analyzestatistics; 再执行收集 |
第 8 节:最佳实践清单¶
| # | 实践 | 为什么 | 投入产出比 |
|---|---|---|---|
| 1 | 数据加载后立即收集统计 | 统计信息的最佳时机是数据刚加载完成。迟到的统计 = 过期的统计 = 错误的执行计划。详见案例 5.3——6 小时窗口导致 83GB 内存消耗 | ⭐⭐⭐⭐⭐ |
| 2 | 为 ANALYZE_STATISTICS 建立专用资源池 | 限制内存上限(≤20%)、低优先级、限制并发度——确保统计收集即使变慢也不会挤占业务查询。RUNTIMEPRIORITY LOW 是核心 |
⭐⭐⭐⭐⭐ |
| 3 | 超大表(>100 亿行)使用分区级统计 + 定期全量兜底 | 每天重复扫描历史静态数据是最大的资源浪费。分区级统计在「最近 N 天」查询上的精度几乎等同全量——用几分钟的代价替代数十分钟 | ⭐⭐⭐⭐⭐ |
| 4 | 持续增长的大表必须分区 | 不分区的大表,全量统计收集时间随数据量线性增长,最终必然超过维护窗口。分区是解决这个问题的根本方案 | ⭐⭐⭐⭐⭐ |
| 5 | 临时表也需要统计——在每个 INSERT 后立即收集 | 临时表是统计信息的永久盲区——它们在定时收集脚本运行时已经不存在。内联收集让脚本多花 2 秒,但可能让后续 JOIN 少跑 2 小时 | ⭐⭐⭐⭐ |
| 6 | 监控统计信息的行数偏差(阈值 >10%) | 行数偏差是统计过期的最直接信号。10% 的偏差足以让优化器选错驱动表。按 2.2 节的 SQL 建立巡检 | ⭐⭐⭐⭐ |
| 7 | 收集大表统计时要限制 EXECUTIONPARALLELISM | 默认并行度可能让统计收集占用全部 CPU 核心。设为 4-8 可以显著降低 CPU 峰值,同时收集时间只增加 20-30% | ⭐⭐⭐⭐ |
| 8 | 避开 mergeout 高峰期执行大表 ANALYZE_STATISTICS | 统计收集结束时需要获取 GCL X 锁写入 Catalog——如果与 mergeout 高峰期重叠,会互相阻塞。按时段错开安排 | ⭐⭐⭐ |
| 9 | 高频统计收集的绝对底线:≥5 分钟间隔 | 某运营商真实案例——每 5 秒收集一次导致 OOM Killer 触发节点宕机。这是生产事故级别的教训 | ⭐⭐⭐ |
| 10 | 对 JOIN 列和高选择性 WHERE 列单独执行 ANALYZE_HISTOGRAM | 如果大表无法频繁做全量统计,至少保证 JOIN 列的直方图是最新的——这些列对查询计划的影响最大 | ⭐⭐⭐ |
| 11 | 建立统计收集的监控告警 | 对 >10 亿行的表,如果统计更新时间超过 24 小时,自动告警。不要等问题出现了才发现统计已过期数天 | ⭐⭐⭐ |
| 12 | 统计收集脚本中加入去重保护 | 如果一张表在 30 分钟内已被收集过,跳过本次收集。避免多个 ETL 脚本重复收集同一张表(某运营商 5 秒间隔的教训) | ⭐⭐ |
扩展阅读¶
- Vertica 统计信息管理与查询性能 — 统计信息基础概念、全局监控、根因定位和快速修复
- Vertica 查询 Spill 到磁盘的原因与优化 — 统计信息缺失是 Spill 的关键根因
- Vertica 内存压力诊断与调优 — 统计信息与内存预算估算的关系
- TABLE_STATISTICS 参考 — 系统表字段参考