跳转至

Vertica 统计信息管理与查询性能

作者:JiangChong | 发布时间:2026-04-10

适用场景:查询突然变慢、执行计划异常(如大表做驱动表)、EXPLAIN 显示 NO STATISTICS、或数据加载后性能退化——这些都可能与统计信息缺失或过期有关。

关联文章

理解全文脉络

本文按「为什么重要 → 怎么监控 → 怎么定位 → 怎么解决 → 案例验证 → 工具箱」的路径组织。如果你已经知道某个查询变慢且怀疑是统计信息问题,可以直接跳到第 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_oidschema_oid 关联表和 schema。上述 SQL 通过 table_oidv_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 SELECT ... FROM your_table WHERE ...;

在 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 输出示例(来自真实案例):

table: TB_DW_CONTRACT_PROD_ITEMS → PREDICATE VALUES OUT-OF-RANGE

这个标记意味着:查询中的过滤条件值落在了统计信息记录的最小值/最大值范围之外,优化器无法评估过滤后的行数,可能导致选择错误驱动表。

另一个真实案例

+-JOIN HASH [LeftOuter] [Cost: 1K, Rows: 10K (NO STATISTICS)] (PATH ID: 1)

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 立即措施:手动收集统计信息

对单个表进行全量统计收集

SELECT ANALYZE_STATISTICS('schema_name.table_name');

执行后返回 0 表示成功。该命令收集表中所有列的 FULL 统计,包括直方图、NDV、MIN/MAX。

对特定列进行直方图收集(当只需要优化涉及这些列的查询时):

SELECT ANALYZE_HISTOGRAM('schema_name.table_name', 'column_name');

批量收集多个表

-- 生成 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(推荐度:⭐⭐⭐)

-- 已通过参数查询验证参数存在
SELECT SET_CONFIG_PARAMETER('EnableAutoDMLStats', 1);

为什么设为 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 计算

SELECT SET_CONFIG_PARAMETER('ComputeApproxNDVsDuringAnalyzeStats', 1);

为什么:默认情况下,ANALYZE_STATISTICS 不计算列的不同值数量(NDV)。但对于 JOIN 列(如 user_id、order_id),NDV 是优化器决定 JOIN 策略的关键输入。开启后收集时间会略有增加,但对 JOIN 估算准确度的提升是显著的。

4.4 统计信息收集的风险防控

风险 1:高并发收集导致资源争抢

ANALYZE_STATISTICS 本身也是一个查询,会消耗 CPU 和内存。如果在业务高峰期同时收集多个大表,可能加剧资源紧张。

缓解措施

  • 按表大小排序,分批执行(先小表后大表)
  • 避免在业务高峰期执行大表的统计收集
  • 大集群可设置 MultiplanAnalyzeStats=1 并行收集以缩短单表收集时间

风险 2:高频收集导致 OOM

在生产环境的真实案例中,ANALYZE_STATISTICS5 秒执行一次(通过 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,导致单节点成为瓶颈

修复方案

  1. 杀掉卡住的 SQL——集群立即恢复正常
  2. 为两张表重新设计投影:按 JOIN 关联列(user_id_zk, user_id_fk)进行 SEGMENTED BY HASH 分布
  3. 立即收集统计信息

效果对比

状态 执行时间
故障时(无统计 + 单节点) 6.5 小时+(被杀掉)
正常时(有统计但投影不佳) 11 分 48 秒
优化后(统计 + 新投影) 5 分 37 秒

5.2 案例二:数据加载后统计信息过期导致内存飙升

📋 真实案例

场景:某运营商,138 节点集群。每天上午 9 点到 12 点 app_pool 出现大量排队。

诊断过程

  1. 定位到问题 SQL——一个涉及多表关联的 INSERT 语句
  2. EXPLAIN 显示:table: TB_DW_CONTRACT_PROD_ITEMS → PREDICATE VALUES OUT-OF-RANGE
  3. 检查统计信息收集时间:
事件 时间
全量统计信息收集 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 倍)。

诊断过程

  1. 发现一个 SQL 使用 28GB 内存,执行 1 小时
  2. 执行计划显示 user_dtal 表因统计信息过期被错误选为 inner 表(驱动表)
  3. user_dtal 表实际有 14.5 亿数据,即使加了时间条件也还有 1,500 万
  4. 中间临时表(mid/tmp 等)在脚本执行完后数据被删除,批量收集统计信息时无法覆盖这些临时表

根因分析:大批量数据写入后统计信息没有及时更新,加上临时表完全无统计信息,导致批处理程序中的多表 JOIN 频繁选错驱动表。

修复方案

  1. user_dtal 重新收集统计信息——执行计划立即恢复正常
  2. 调整 app_poolplannedconcurrencymaxconcurrency
  3. 在 Perl 批处理脚本的 INSERT 后加入统计信息收集步骤(覆盖临时表)
  4. 在资源池上设置 MAXQUERYMEMORYSIZE=50G 作为安全阀

关键教训中间临时表是最容易被忽视的统计信息盲区——它们的统计信息在脚本执行期间不存在,但脚本中的后续 JOIN 又依赖它们。

解决方案:在每个 INSERT 提交后立即对目标表收集统计。


5.4 案例四:统计信息缺失导致查询从 700ms 变成几分钟

📋 真实案例

场景:某运营商报表系统。每日数据表在批处理程序后会执行 ANALYZE_STATISTICS,但查询仍然间歇性变慢。

诊断过程

  1. 2019 年 7 月 11 日 14:10,关联查询该表再次变慢
  2. 使用 EXPLAIN 查看执行计划:关联顺序异常
  3. 执行 SELECT ANALYZE_STATISTICS('app_db.app_user_fraud_info_day') 后,同样语句执行效率恢复
  4. 正常时查询约 700ms,变慢时几分钟都不出结果

根因分析:虽然批处理程序每天会收集一次统计信息,但在某些情况下(如数据加载时间窗口冲突、收集操作被跳过等),统计信息仍然会丢失。一旦统计信息缺失,优化器改变了 JOIN 顺序,从正常的小表驱动大表变成了大表驱动小表,查询时间从毫秒级暴跌到分钟级。

修复方案

  1. 每次报表变慢时立即手动收集统计信息(临时止血
  2. 在报表 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 SELECT ... -- 取 3:30-5:00 期间运行的关联 SQL

EXPLAIN 输出中的关键行:

JOIN HASH [Cost: 8K, Rows: 200K (ROW COUNT ONLY)]

多个 JOIN 步骤显示 (ROW COUNT ONLY),确认优化器在缺少 FULL 统计的情况下做 JOIN 决策。

根因分析:统计信息收集时机错误——应该紧随数据加载之后,而不是等到批处理末尾。在 3:30-5:00 期间运行的 SQL(ETL 的关联计算部分),所有的执行计划都是基于过期的统计信息生成的。

修复方案

  1. 立即:对最大的 20 张表执行 ANALYZE_STATISTICS()
  2. 重构 ETL 流程:将统计收集从批处理末尾移到每个 INSERT 步骤之后
  3. 开启 EnableAutoDMLStats=1 作为兜底
  4. 设置 ComputeApproxNDVsDuringAnalyzeStats=1 提升 JOIN 估算精度
  5. 对临时表(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 SELECT ... -- 复制问题 SQL

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 — 立即修复

SELECT ANALYZE_STATISTICS('report.transaction_detail');

执行约 2 秒后返回 0(成功)。对于 1,200 万行的表,ANALYZE_STATISTICS 通常很快——数据量在亿级以下时一般不会超过 1 分钟。

15:20 — 验证修复效果

EXPLAIN SELECT ... -- 同样的 SQL

修复后的 EXPLAIN 输出:

+-JOIN HASH [Cost: 450K, Rows: 12M]
|  Outer (LOCAL ROUND ROBIN)
|  Inner (RESEGMENT)

对比修复前后:

  • (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 再检查 ⭐⭐

扩展阅读