Hive On Spark 统计信息收集深度解析

Spark SQL 统计信息收集深度解析

目录

  1. 统计信息核心概念
  2. 统计信息的类型与内容
  3. 统计信息收集方法详解
  4. 收集时机与策略
  5. 性能优化与开销分析
  6. 统计信息维护与管理
  7. 实际应用场景
  8. 常见问题与解决方案
  9. 自动化方案

统计信息核心概念

什么是统计信息?

统计信息(Statistics) 是 Spark SQL 对表、列、分区数据的元数据描述,用于:

  • 估算查询成本
  • 优化执行计划
  • 选择最优 JOIN 顺序
  • 决定 JOIN 策略(Broadcast vs Shuffle)

统计信息的重要性

复制代码
没有统计信息:
  → CBO 无法计算准确成本
  → 可能选择次优执行计划
  → 性能下降 2-10 倍

有准确的统计信息:
  → CBO 能准确评估成本
  → 选择最优执行计划
  → 性能提升 2-10 倍

统计信息 vs 元数据

复制代码
元数据(Metadata):
  - 表结构(列名、类型)
  - 分区信息
  - 存储格式
  - 自动维护,无需手动收集

统计信息(Statistics):
  - 数据量(行数、大小)
  - 数据分布(最小值、最大值、不同值数量)
  - 数据选择性(直方图)
  - 必须手动收集和更新

统计信息在查询优化中的作用

复制代码
查询优化流程:

1. 解析 SQL
   ↓
2. 生成逻辑计划
   ↓
3. CBO 使用统计信息计算成本
   ├─ 估算每个操作的数据量
   ├─ 估算 JOIN 中间结果大小
   ├─ 估算过滤后的数据量
   └─ 选择最优执行计划
   ↓
4. 生成物理计划
   ↓
5. 执行查询

统计信息的类型与内容

表级统计信息(Table Statistics)

收集命令:

sql 复制代码
ANALYZE TABLE table_name COMPUTE STATISTICS;

包含内容:

复制代码
表级统计信息:
  - rowCount: 总行数
  - sizeInBytes: 总大小(字节)
  - isBroadcastable: 是否可广播(用于 JOIN 优化)

查看方法:

sql 复制代码
-- 方法1: DESCRIBE EXTENDED
DESCRIBE EXTENDED table_name;

-- 方法2: SHOW TABLE EXTENDED
SHOW TABLE EXTENDED LIKE 'table_name';

-- 输出示例:
-- Statistics: 1000000 rows, 1024000000 bytes

列级统计信息(Column Statistics)

收集命令:

sql 复制代码
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2, ...;

包含内容:

复制代码
列级统计信息:
  - distinctCount: 不同值数量(基数)
  - nullCount: 空值数量
  - min: 最小值
  - max: 最大值
  - avgLen: 平均长度(字符串类型)
  - maxLen: 最大长度(字符串类型)
  - histogram: 直方图(数据分布,可选)

查看方法:

sql 复制代码
-- 查看列统计信息
SHOW COLUMN STATS table_name;

-- 输出示例:
-- col_name  data_type  min  max  null_count  distinct_count  avg_col_len  max_col_len
-- id        bigint     1    1000  0           1000           8            8
-- name      string     A    Z     0           500            10           20

分区统计信息(Partition Statistics)

收集命令:

sql 复制代码
-- 单个分区
ANALYZE TABLE table_name PARTITION(dt='2024-01-01') COMPUTE STATISTICS;

-- 多个分区
ANALYZE TABLE table_name PARTITION(dt='2024-01-01') COMPUTE STATISTICS;
ANALYZE TABLE table_name PARTITION(dt='2024-01-02') COMPUTE STATISTICS;

包含内容:

复制代码
分区统计信息:
  - 与表级统计相同,但只针对特定分区
  - 用于分区裁剪优化
  - 支持增量更新

直方图(Histogram)

什么是直方图?

直方图描述数据分布,帮助 CBO 更准确地估算过滤选择性。

复制代码
示例:年龄列的数据分布
  0-20:   10%
  21-30:  30%
  31-40:  35%
  41-50:  20%
  50+:     5%

查询:WHERE age BETWEEN 25 AND 35
  → 没有直方图:估算 50%(不准确)
  → 有直方图:估算 30% + 35% = 65%(更准确)

收集直方图:

sql 复制代码
-- Spark 3.0+ 支持直方图
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS age;

-- 查看直方图
SHOW COLUMN STATS table_name;
-- 输出会包含 histogram 信息

统计信息收集方法详解

基本收集命令

1. 表级统计信息
sql 复制代码
-- 收集表级统计信息(行数、大小)
ANALYZE TABLE table_name COMPUTE STATISTICS;

-- 示例
ANALYZE TABLE sales COMPUTE STATISTICS;

适用场景:

  • 首次收集统计信息
  • 全表数据更新后
  • 定期全量更新

收集内容:

  • 总行数
  • 总大小
  • 是否可广播
2. 列级统计信息
sql 复制代码
-- 收集指定列的统计信息
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2, ...;

-- 示例:收集 JOIN 列和过滤列
ANALYZE TABLE sales COMPUTE STATISTICS FOR COLUMNS 
  product_id,      -- JOIN 列
  customer_id,      -- JOIN 列
  order_date,       -- 过滤列
  amount;           -- 聚合列

适用场景:

  • JOIN 操作的列
  • WHERE 过滤条件的列
  • GROUP BY 的列
  • 需要精确估算选择性的列

收集内容:

  • 不同值数量(distinctCount)
  • 空值数量(nullCount)
  • 最小值/最大值
  • 平均长度
  • 直方图(如果支持)
3. 分区统计信息
sql 复制代码
-- 收集单个分区统计信息
ANALYZE TABLE table_name PARTITION(partition_col='value') COMPUTE STATISTICS;

-- 示例
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;

-- 收集分区列统计信息
ANALYZE TABLE sales PARTITION(dt='2024-01-01') 
COMPUTE STATISTICS FOR COLUMNS product_id, amount;

适用场景:

  • 分区表插入新分区后
  • 分区数据更新后
  • 增量更新统计信息

优势:

  • 只扫描新分区,开销小
  • 支持增量更新
  • 不影响其他分区

高级收集选项

1. 收集所有列的统计信息
sql 复制代码
-- Spark 3.0+ 支持
ANALYZE TABLE table_name COMPUTE STATISTICS FOR ALL COLUMNS;

-- 示例
ANALYZE TABLE sales COMPUTE STATISTICS FOR ALL COLUMNS;

注意:

  • 开销较大(需要扫描所有列)
  • 建议只对关键表使用
  • 或只收集关键列
2. 采样收集(减少开销)
sql 复制代码
-- Spark 3.0+ 支持采样收集
-- 通过配置参数控制采样率
SET spark.sql.statistics.fallBackToHdfs = true;
SET spark.sql.statistics.histogram.enabled = true;
SET spark.sql.statistics.histogram.numBins = 254;

-- 然后收集统计信息
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1;
3. 增量收集(分区表)
sql 复制代码
-- 只收集新分区,不影响已有分区
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;

-- 批量收集多个分区
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;
ANALYZE TABLE sales PARTITION(dt='2024-01-02') COMPUTE STATISTICS;
ANALYZE TABLE sales PARTITION(dt='2024-01-03') COMPUTE STATISTICS;

收集命令的执行过程

复制代码
ANALYZE TABLE 执行流程:

1. 解析命令
   ↓
2. 确定收集范围
   - 全表 or 分区
   - 所有列 or 指定列
   ↓
3. 扫描数据
   - 读取数据文件
   - 计算统计信息
   ↓
4. 计算统计指标
   - 行数、大小
   - 列统计(distinctCount, min, max 等)
   - 直方图(如果启用)
   ↓
5. 存储统计信息
   - 写入 Metastore
   - 更新表元数据
   ↓
6. 完成

收集开销分析

表级统计信息收集开销:

复制代码
开销 = 数据扫描时间 + 计算时间 + 元数据写入时间

影响因素:
  - 表大小(主要因素)
  - 文件数量
  - 存储格式(Parquet < ORC < Text)
  - 网络带宽(HDFS)
  - Metastore 性能

示例:
  10GB Parquet 表:~30 秒
  100GB Parquet 表:~5 分钟
  1TB Parquet 表:~1 小时

列级统计信息收集开销:

复制代码
开销 = 表级统计开销 + 列扫描开销

影响因素:
  - 列数量
  - 列类型(数值 < 字符串)
  - 是否需要计算直方图

示例:
  10 列,10GB 表:~1 分钟
  100 列,10GB 表:~5 分钟

分区统计信息收集开销:

复制代码
开销 = 分区大小相关

优势:
  - 只扫描单个分区
  - 开销远小于全表收集
  - 支持并行收集多个分区

示例:
  1GB 分区:~3 秒
  10GB 分区:~30 秒

收集时机与策略

收集时机决策树

复制代码
是否需要收集统计信息?
  ↓
是分区表?
  ├─ 是 → 插入新分区后立即收集(开销小)
  └─ 否 → 继续判断
      ↓
表大小?
  ├─ < 10GB → 插入后立即收集(开销可接受)
  ├─ 10-100GB → 批量插入后统一收集
  └─ > 100GB → 定时收集(如每天凌晨)
      ↓
数据变化频率?
  ├─ 高频(每小时)→ 定时收集
  ├─ 中频(每天)→ 插入后收集或定时收集
  └─ 低频(每周)→ 变化后收集

场景1: 分区表插入新分区(推荐立即收集)

场景描述:

  • 按日期分区的表
  • 每天插入新分区数据
  • 查询经常按分区过滤

推荐策略:

sql 复制代码
-- 1. 插入数据
INSERT OVERWRITE TABLE sales 
PARTITION(dt='2024-01-01')
SELECT 
  order_id,
  product_id,
  customer_id,
  amount,
  dt
FROM source_table
WHERE dt = '2024-01-01';

-- 2. 立即收集新分区统计信息(推荐!)
ANALYZE TABLE sales 
PARTITION(dt='2024-01-01') 
COMPUTE STATISTICS;

-- 3. 收集关键列统计(如果查询涉及 JOIN 或过滤)
ANALYZE TABLE sales 
PARTITION(dt='2024-01-01') 
COMPUTE STATISTICS FOR COLUMNS 
  product_id,      -- JOIN 列
  customer_id,     -- JOIN 列
  amount;          -- 聚合列

为什么推荐立即收集?

复制代码
✓ 只扫描新分区,开销小(通常几秒到几分钟)
✓ 确保查询时 CBO 有最新统计信息
✓ 避免使用过时统计信息导致次优计划
✓ 不影响其他分区,支持增量更新

性能影响:

复制代码
分区大小:1GB
收集时间:~3 秒
查询性能提升:2-5 倍

分区大小:10GB
收集时间:~30 秒
查询性能提升:3-10 倍

结论:开销小,收益大,强烈推荐!

场景2: 小表全表覆盖(可以立即收集)

场景描述:

  • 维度表或配置表
  • 表大小 < 10GB
  • 全表覆盖更新

推荐策略:

sql 复制代码
-- 1. 覆盖数据
INSERT OVERWRITE TABLE dim_product SELECT ...;

-- 2. 立即收集统计信息
ANALYZE TABLE dim_product COMPUTE STATISTICS;

-- 3. 收集关键列统计
ANALYZE TABLE dim_product 
COMPUTE STATISTICS FOR COLUMNS product_id, category_id;

适用条件:

  • 表大小 < 10GB
  • 更新频率不高(每天或更少)
  • 查询性能要求高

场景3: 大表全表覆盖(批量/定时收集)

场景描述:

  • 事实表或大表
  • 表大小 > 10GB
  • 全表覆盖更新

推荐策略:

sql 复制代码
-- 策略1: 批量插入后统一收集
INSERT OVERWRITE TABLE large_fact_table SELECT ...;
-- ... 其他操作 ...
-- 批量操作完成后统一收集
ANALYZE TABLE large_fact_table COMPUTE STATISTICS;

-- 策略2: 定时收集(推荐用于频繁更新)
-- 每天凌晨定时任务收集
-- 通过调度系统(如 Airflow)执行
ANALYZE TABLE large_fact_table COMPUTE STATISTICS;

为什么不建议立即收集?

复制代码
✗ 全表扫描开销大(可能数小时)
✗ 频繁收集会严重影响性能
✗ 如果数据还在变化,收集的统计信息可能很快过期

最佳实践:

复制代码
1. 批量插入完成后统一收集
2. 或定时收集(如每天凌晨)
3. 监控收集时间,避免影响业务
4. 考虑在低峰期收集

场景4: 增量插入(根据分区大小决定)

场景描述:

  • 分区表增量插入
  • 每天插入多个分区

推荐策略:

sql 复制代码
-- 小分区(< 5GB):立即收集
INSERT INTO TABLE sales PARTITION(dt='2024-01-01') SELECT ...;
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;

-- 大分区(> 5GB):批量收集
INSERT INTO TABLE sales PARTITION(dt='2024-01-01') SELECT ...;
INSERT INTO TABLE sales PARTITION(dt='2024-01-02') SELECT ...;
INSERT INTO TABLE sales PARTITION(dt='2024-01-03') SELECT ...;

-- 批量收集
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;
ANALYZE TABLE sales PARTITION(dt='2024-01-02') COMPUTE STATISTICS;
ANALYZE TABLE sales PARTITION(dt='2024-01-03') COMPUTE STATISTICS;

场景5: 数据更新(UPDATE/MERGE)

场景描述:

  • 使用 UPDATE 或 MERGE 更新数据
  • 数据量可能变化

推荐策略:

sql 复制代码
-- 1. 更新数据
MERGE INTO target_table t
USING source_table s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET ...;

-- 2. 收集统计信息(全表或分区)
-- 如果是分区表,只收集更新的分区
ANALYZE TABLE target_table 
PARTITION(dt='2024-01-01') 
COMPUTE STATISTICS;

-- 如果是非分区表,收集全表
ANALYZE TABLE target_table COMPUTE STATISTICS;

收集策略总结表

场景 表类型 数据量 推荐策略 收集时机 收集范围
新分区插入 分区表 任意 ✅ 立即收集 插入后 新分区
小表覆盖 非分区 < 10GB ✅ 立即收集 插入后 全表
大表覆盖 非分区 > 10GB ⏰ 批量/定时 批量后/定时 全表
增量插入 分区表 < 5GB/分区 ✅ 立即收集 插入后 新分区
增量插入 分区表 > 5GB/分区 ⏰ 批量收集 批量后 多个分区
数据更新 任意 任意 ⏰ 更新后收集 更新后 更新范围

性能优化与开销分析

收集性能优化技巧

1. 只收集必要的列
sql 复制代码
-- ❌ 不推荐:收集所有列(开销大)
ANALYZE TABLE sales COMPUTE STATISTICS FOR ALL COLUMNS;

-- ✅ 推荐:只收集关键列(开销小)
ANALYZE TABLE sales COMPUTE STATISTICS FOR COLUMNS 
  product_id,      -- JOIN 列
  customer_id,     -- JOIN 列
  order_date,       -- 过滤列
  amount;           -- 聚合列

性能对比:

复制代码
100 列的表,收集所有列:~10 分钟
只收集 5 个关键列:~1 分钟
性能提升:10 倍
2. 分区表只收集新分区
sql 复制代码
-- ❌ 不推荐:收集全表(开销大)
ANALYZE TABLE sales COMPUTE STATISTICS;

-- ✅ 推荐:只收集新分区(开销小)
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;

性能对比:

复制代码
365 个分区的表,收集全表:~2 小时
只收集 1 个新分区:~30 秒
性能提升:240 倍
3. 并行收集多个分区
sql 复制代码
-- 串行收集(慢)
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;
ANALYZE TABLE sales PARTITION(dt='2024-01-02') COMPUTE STATISTICS;
ANALYZE TABLE sales PARTITION(dt='2024-01-03') COMPUTE STATISTICS;

-- 并行收集(快)- 使用多个 Spark Session 或脚本
-- Session 1
ANALYZE TABLE sales PARTITION(dt='2024-01-01') COMPUTE STATISTICS;

-- Session 2
ANALYZE TABLE sales PARTITION(dt='2024-01-02') COMPUTE STATISTICS;

-- Session 3
ANALYZE TABLE sales PARTITION(dt='2024-01-03') COMPUTE STATISTICS;
4. 使用采样减少开销
sql 复制代码
-- 配置采样参数(Spark 3.0+)
SET spark.sql.statistics.fallBackToHdfs = true;

-- 收集统计信息(会自动采样)
ANALYZE TABLE large_table COMPUTE STATISTICS;
5. 在低峰期收集
复制代码
高峰期收集:
  - 占用集群资源
  - 可能影响业务查询
  - 收集速度慢

低峰期收集:
  - 资源充足
  - 不影响业务
  - 收集速度快

建议:
  - 大表统计信息收集安排在凌晨
  - 使用调度系统(Airflow)自动执行

开销估算公式

表级统计信息收集时间:

复制代码
时间 ≈ (表大小 / 读取速度) + (计算开销) + (元数据写入时间)

其中:
  读取速度 ≈ 100-500 MB/s(取决于存储和网络)
  计算开销 ≈ 表大小 * 0.01(1% 的开销)
  元数据写入 ≈ 1-5 秒

示例:
  10GB 表:
    读取:10GB / 200MB/s = 50 秒
    计算:10GB * 0.01 = 0.1 秒
    写入:2 秒
    总计:~52 秒

列级统计信息收集时间:

复制代码
时间 ≈ 表级统计时间 * (1 + 列数 * 0.1)

示例:
  10GB 表,10 列:
    表级:52 秒
    列级:52 * (1 + 10 * 0.1) = 104 秒

分区统计信息收集时间:

复制代码
时间 ≈ (分区大小 / 读取速度) + (计算开销) + (元数据写入时间)

示例:
  1GB 分区:
    读取:1GB / 200MB/s = 5 秒
    计算:1GB * 0.01 = 0.01 秒
    写入:2 秒
    总计:~7 秒

性能监控指标

收集性能指标:

复制代码
1. 收集时间
   - 表级统计收集时间
   - 列级统计收集时间
   - 分区统计收集时间

2. 资源使用
   - CPU 使用率
   - 内存使用率
   - 网络带宽
   - 磁盘 I/O

3. 影响评估
   - 是否影响其他查询
   - 集群资源占用情况

监控方法:

sql 复制代码
-- 查看 Spark UI
-- 监控 ANALYZE TABLE 任务的执行情况

-- 查看日志
-- 记录收集开始和结束时间

-- 使用性能分析工具
-- 分析收集过程的瓶颈

统计信息维护与管理

统计信息生命周期

复制代码
统计信息生命周期:

1. 创建表
   → 无统计信息
   ↓
2. 首次收集
   → ANALYZE TABLE ... COMPUTE STATISTICS
   → 生成初始统计信息
   ↓
3. 数据更新
   → INSERT/UPDATE/DELETE
   → 统计信息可能过期
   ↓
4. 重新收集
   → ANALYZE TABLE ... COMPUTE STATISTICS
   → 更新统计信息
   ↓
5. 持续维护
   → 定期检查
   → 及时更新

统计信息过期机制

⚠️ 重要:Spark SQL 统计信息不会自动过期!

复制代码
关键事实:
  ✗ Spark SQL 没有统计信息自动过期机制
  ✗ 没有参数可以控制统计信息过期时间
  ✗ 统计信息会一直保留,直到手动更新或删除
  ✗ 不会因为时间推移而自动失效

这意味着什么?

复制代码
统计信息生命周期:
  1. 收集统计信息 → 永久保存
  2. 数据更新 → 统计信息不会自动更新
  3. 统计信息可能变得不准确
  4. 必须手动重新收集才能更新

影响:
  - 如果数据变化但统计信息未更新,CBO 可能使用过时的统计信息
  - 可能导致选择次优执行计划
  - 需要手动管理统计信息的新鲜度

统计信息过期检测(手动)

由于没有自动过期机制,需要手动检测:

sql 复制代码
-- 方法1: 查看统计信息时间戳
DESCRIBE EXTENDED table_name;
-- 查看 Statistics 中的时间信息(如果有)

-- 方法2: 对比数据修改时间
SHOW TABLE EXTENDED LIKE 'table_name';
-- 对比 lastModified 和统计信息收集时间

-- 方法3: 检查查询性能
-- 如果查询性能下降,可能是统计信息过期

-- 方法4: 对比统计信息和实际数据
SHOW COLUMN STATS table_name;
SELECT COUNT(*) FROM table_name;
-- 对比 rowCount 和实际行数

过期判断标准(需要手动实现):

复制代码
统计信息过期条件(业务判断):
  ✓ 数据变化率 > 10%
  ✓ 上次统计时间 > 7 天(根据业务调整)
  ✓ 查询性能明显下降
  ✓ 执行计划不合理
  ✓ 统计信息中的 rowCount 与实际行数差异大

统计信息过期时间控制方案

由于 Spark SQL 不支持自动过期,需要自己实现:

方案1: 基于时间的过期检测(推荐)
python 复制代码
# Python 脚本:检测统计信息是否过期
from pyspark.sql import SparkSession
from datetime import datetime, timedelta

def check_statistics_age(spark, table_name, max_age_days=7):
    """检查统计信息是否过期"""
    # 获取表的元数据
    result = spark.sql(f"DESCRIBE EXTENDED {table_name}").collect()
    
    # 解析统计信息(需要从 Metastore 获取实际时间戳)
    # 这里简化示例,实际需要访问 Metastore
    stats_info = None
    for row in result:
        if 'Statistics' in str(row):
            stats_info = row
            break
    
    if stats_info is None:
        return True  # 没有统计信息,需要收集
    
    # 检查统计信息时间(需要从 Metastore 获取)
    # 这里需要访问 Hive Metastore 的 TABLE_PARAMS 表
    # 实际实现需要连接 Metastore 数据库
    
    return False  # 未过期

def collect_if_expired(spark, table_name, max_age_days=7):
    """如果过期则收集统计信息"""
    if check_statistics_age(spark, table_name, max_age_days):
        print(f"Statistics for {table_name} expired, collecting...")
        spark.sql(f"ANALYZE TABLE {table_name} COMPUTE STATISTICS")
        return True
    else:
        print(f"Statistics for {table_name} is fresh")
        return False
方案2: 基于数据变化的过期检测
python 复制代码
def check_data_changed(spark, table_name, last_stats_row_count):
    """检查数据是否发生变化"""
    current_row_count = spark.sql(f"SELECT COUNT(*) as cnt FROM {table_name}").collect()[0]['cnt']
    
    if last_stats_row_count is None:
        return True  # 没有统计信息,需要收集
    
    change_rate = abs(current_row_count - last_stats_row_count) / last_stats_row_count
    
    if change_rate > 0.1:  # 变化超过 10%
        return True  # 需要更新统计信息
    
    return False  # 数据变化不大
方案3: 基于查询性能的过期检测
python 复制代码
def check_query_performance(spark, table_name):
    """检查查询性能,如果下降则可能统计信息过期"""
    # 执行测试查询
    start_time = datetime.now()
    spark.sql(f"SELECT COUNT(*) FROM {table_name}").collect()
    query_time = (datetime.now() - start_time).total_seconds()
    
    # 对比历史性能(需要记录历史数据)
    # 如果性能明显下降,可能需要更新统计信息
    pass
方案4: 记录统计信息收集时间(推荐)
sql 复制代码
-- 创建统计信息收集日志表
CREATE TABLE IF NOT EXISTS statistics_collection_log (
    table_name STRING,
    partition_value STRING,
    collection_time TIMESTAMP,
    row_count BIGINT,
    size_bytes BIGINT
) PARTITIONED BY (dt STRING);

-- 收集统计信息时记录
INSERT INTO statistics_collection_log 
PARTITION(dt='2024-01-01')
SELECT 
    'sales' as table_name,
    '2024-01-01' as partition_value,
    CURRENT_TIMESTAMP() as collection_time,
    (SELECT COUNT(*) FROM sales WHERE dt='2024-01-01') as row_count,
    (SELECT SUM(size_bytes) FROM sales WHERE dt='2024-01-01') as size_bytes;

-- 检查统计信息是否过期
SELECT 
    table_name,
    partition_value,
    collection_time,
    DATEDIFF(CURRENT_DATE(), DATE(collection_time)) as age_days
FROM statistics_collection_log
WHERE dt >= '2024-01-01'
  AND DATEDIFF(CURRENT_DATE(), DATE(collection_time)) > 7;  -- 超过 7 天

自动化过期检测和更新

python 复制代码
# 自动化脚本:定期检查并更新过期统计信息
from pyspark.sql import SparkSession
from datetime import datetime, timedelta

def auto_refresh_statistics(spark, max_age_days=7):
    """自动刷新过期统计信息"""
    # 获取所有表
    tables = spark.sql("SHOW TABLES").collect()
    
    for table_row in tables:
        table_name = f"{table_row['database']}.{table_row['tableName']}"
        
        # 检查统计信息是否过期
        # 这里需要实现检查逻辑(访问 Metastore 或日志表)
        
        # 如果过期,重新收集
        try:
            spark.sql(f"ANALYZE TABLE {table_name} COMPUTE STATISTICS")
            print(f"✓ Refreshed statistics for {table_name}")
        except Exception as e:
            print(f"✗ Failed to refresh {table_name}: {e}")

# 使用 Airflow 或其他调度系统定期执行
# 例如:每天凌晨 2 点执行

统计信息过期时间配置建议

由于没有内置参数,建议通过以下方式管理:

复制代码
1. 创建统计信息收集日志表
   → 记录每次收集的时间
   → 便于判断是否过期

2. 实现自动化检测脚本
   → 定期检查统计信息新鲜度
   → 自动触发重新收集

3. 集成到 ETL 流程
   → 数据更新后立即收集
   → 确保统计信息始终最新

4. 设置业务规则
   → 分区表:插入后立即收集
   → 小表:每天收集
   → 大表:每周收集
   → 根据数据变化频率调整

统计信息更新策略

策略1: 按时间更新
sql 复制代码
-- 每天更新
-- 通过调度系统执行
ANALYZE TABLE table_name COMPUTE STATISTICS;

-- 每周更新
ANALYZE TABLE table_name COMPUTE STATISTICS;
策略2: 按数据变化更新
sql 复制代码
-- 检测数据变化率
-- 如果变化 > 10%,则更新
-- 需要自定义脚本实现
策略3: 按查询性能更新
sql 复制代码
-- 监控查询性能
-- 如果性能下降,触发更新
-- 需要监控系统支持

统计信息存储位置

Spark SQL 统计信息存储:

复制代码
存储位置:
  - Hive Metastore(默认)
  - Spark Catalog
  - 表元数据中

存储格式:
  - JSON 格式
  - 存储在 Metastore 的 TABLE_PARAMS 表中

查看存储:
  - DESCRIBE EXTENDED table_name
  - SHOW TABLE EXTENDED LIKE 'table_name'

统计信息备份与恢复

备份方法:

sql 复制代码
-- 导出统计信息(通过 Metastore)
-- 需要访问 Metastore 数据库

-- 或记录统计信息值
DESCRIBE EXTENDED table_name;
-- 保存输出结果

恢复方法:

sql 复制代码
-- 重新收集统计信息
ANALYZE TABLE table_name COMPUTE STATISTICS;

实际应用场景

场景1: ETL 任务脚本模板

sql 复制代码
-- ============================================
-- ETL 任务:数据加载 + 统计信息收集
-- ============================================

-- 1. 插入数据
INSERT OVERWRITE TABLE dwd_order_detail 
PARTITION(dt='${date}')
SELECT 
  order_id,
  product_id,
  customer_id,
  amount,
  order_time,
  dt
FROM source_table
WHERE dt = '${date}';

-- 2. 收集表级统计信息
ANALYZE TABLE dwd_order_detail 
PARTITION(dt='${date}') 
COMPUTE STATISTICS;

-- 3. 收集关键列统计信息
ANALYZE TABLE dwd_order_detail 
PARTITION(dt='${date}') 
COMPUTE STATISTICS FOR COLUMNS 
  order_id,        -- JOIN 列
  product_id,       -- JOIN 列
  customer_id,      -- JOIN 列
  amount,           -- 聚合列
  order_time;       -- 过滤列

-- 4. 验证统计信息
SHOW COLUMN STATS dwd_order_detail PARTITION(dt='${date}');

-- 5. 记录收集结果(可选)
-- INSERT INTO statistics_log VALUES ('dwd_order_detail', '${date}', NOW());

场景2: 批量分区收集脚本

python 复制代码
# Python 脚本:批量收集分区统计信息
from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("Statistics Collection").getOrCreate()

# 配置
table_name = "sales"
partition_column = "dt"
start_date = "2024-01-01"
end_date = "2024-01-31"

# 生成日期列表
from datetime import datetime, timedelta
start = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
dates = [(start + timedelta(days=i)).strftime("%Y-%m-%d") 
         for i in range((end - start).days + 1)]

# 批量收集统计信息
for date in dates:
    print(f"Collecting statistics for partition dt={date}")
    
    # 收集表级统计
    spark.sql(f"""
        ANALYZE TABLE {table_name} 
        PARTITION({partition_column}='{date}') 
        COMPUTE STATISTICS
    """)
    
    # 收集列级统计
    spark.sql(f"""
        ANALYZE TABLE {table_name} 
        PARTITION({partition_column}='{date}') 
        COMPUTE STATISTICS FOR COLUMNS 
        product_id, customer_id, amount
    """)
    
    print(f"Completed: dt={date}")

print("All statistics collected!")

场景3: 定时收集任务(Airflow)

python 复制代码
# Airflow DAG:定时收集统计信息
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta

default_args = {
    'owner': 'data-team',
    'depends_on_past': False,
    'start_date': datetime(2024, 1, 1),
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 1,
    'retry_delay': timedelta(minutes=5),
}

dag = DAG(
    'collect_statistics',
    default_args=default_args,
    description='Daily statistics collection',
    schedule_interval='0 2 * * *',  # 每天凌晨 2 点
    catchup=False,
)

# 收集事实表统计信息
collect_fact_stats = BashOperator(
    task_id='collect_fact_statistics',
    bash_command="""
    spark-sql -e "
    ANALYZE TABLE dwd_order_detail 
    PARTITION(dt='{{ ds }}') 
    COMPUTE STATISTICS;
    
    ANALYZE TABLE dwd_order_detail 
    PARTITION(dt='{{ ds }}') 
    COMPUTE STATISTICS FOR COLUMNS 
    order_id, product_id, customer_id, amount;
    "
    """,
    dag=dag,
)

# 收集维度表统计信息(每周一次)
collect_dim_stats = BashOperator(
    task_id='collect_dim_statistics',
    bash_command="""
    spark-sql -e "
    ANALYZE TABLE dim_product COMPUTE STATISTICS;
    ANALYZE TABLE dim_product 
    COMPUTE STATISTICS FOR COLUMNS product_id, category_id;
    "
    """,
    dag=dag,
)

collect_fact_stats >> collect_dim_stats

场景4: 统计信息收集监控

sql 复制代码
-- 创建统计信息收集日志表
CREATE TABLE IF NOT EXISTS statistics_collection_log (
    table_name STRING,
    partition_value STRING,
    collection_type STRING,  -- 'TABLE' or 'COLUMN'
    collection_time TIMESTAMP,
    duration_seconds INT,
    status STRING  -- 'SUCCESS' or 'FAILED'
) PARTITIONED BY (dt STRING);

-- 收集统计信息并记录日志
INSERT INTO statistics_collection_log 
PARTITION(dt='2024-01-01')
SELECT 
    'dwd_order_detail' as table_name,
    '2024-01-01' as partition_value,
    'TABLE' as collection_type,
    CURRENT_TIMESTAMP() as collection_time,
    30 as duration_seconds,
    'SUCCESS' as status;

-- 查询收集历史
SELECT 
    table_name,
    partition_value,
    collection_type,
    collection_time,
    duration_seconds,
    status
FROM statistics_collection_log
WHERE dt >= '2024-01-01'
ORDER BY collection_time DESC;

常见问题与解决方案

问题1: 统计信息收集失败

症状:

复制代码
ANALYZE TABLE 命令执行失败
错误:权限不足、表不存在、数据损坏等

解决方案:

sql 复制代码
-- 1. 检查表是否存在
SHOW TABLES LIKE 'table_name';

-- 2. 检查权限
-- 确保有表的 SELECT 权限

-- 3. 检查数据完整性
SELECT COUNT(*) FROM table_name;

-- 4. 尝试只收集表级统计
ANALYZE TABLE table_name COMPUTE STATISTICS;

-- 5. 如果失败,检查日志
-- 查看 Spark 日志获取详细错误信息

问题2: 统计信息收集太慢

症状:

复制代码
ANALYZE TABLE 执行时间过长
影响其他任务执行

解决方案:

sql 复制代码
-- 1. 只收集必要的列
ANALYZE TABLE table_name 
COMPUTE STATISTICS FOR COLUMNS col1, col2;  -- 只收集关键列

-- 2. 分区表只收集新分区
ANALYZE TABLE table_name 
PARTITION(dt='2024-01-01') 
COMPUTE STATISTICS;  -- 只收集新分区

-- 3. 增加资源
SET spark.sql.shuffle.partitions = 200;
SET spark.executor.memory = 8g;

-- 4. 在低峰期收集
-- 安排在凌晨执行

-- 5. 使用采样
SET spark.sql.statistics.fallBackToHdfs = true;

问题3: 统计信息不准确

症状:

复制代码
CBO 选择次优执行计划
查询性能没有提升

解决方案:

sql 复制代码
-- 1. 检查统计信息是否过期
DESCRIBE EXTENDED table_name;
-- 查看统计信息时间

-- 2. 重新收集统计信息
ANALYZE TABLE table_name COMPUTE STATISTICS;
ANALYZE TABLE table_name 
COMPUTE STATISTICS FOR COLUMNS col1, col2;

-- 3. 收集列级统计(包含直方图)
ANALYZE TABLE table_name 
COMPUTE STATISTICS FOR COLUMNS col1, col2;

-- 4. 检查数据分布
SELECT 
    COUNT(*) as total_rows,
    COUNT(DISTINCT col1) as distinct_values,
    MIN(col1) as min_value,
    MAX(col1) as max_value
FROM table_name;

-- 5. 对比统计信息和实际数据
SHOW COLUMN STATS table_name;

问题4: 统计信息占用空间过大

症状:

复制代码
Metastore 空间增长过快
统计信息占用大量存储

解决方案:

sql 复制代码
-- 1. 只收集必要的列
-- 不要收集所有列

-- 2. 定期清理过期统计信息
-- 删除不再使用的表的统计信息

-- 3. 使用分区统计(增量更新)
-- 避免全表统计信息过大

-- 4. 限制直方图数量
SET spark.sql.statistics.histogram.numBins = 100;  -- 减少直方图精度

问题5: 统计信息收集影响业务

症状:

复制代码
收集统计信息时,其他查询变慢
集群资源被占用

解决方案:

sql 复制代码
-- 1. 在低峰期收集
-- 安排在凌晨或业务低峰期

-- 2. 限制资源使用
SET spark.sql.shuffle.partitions = 100;
SET spark.executor.cores = 2;

-- 3. 使用队列隔离
-- 将统计信息收集任务放到独立队列

-- 4. 增量收集(分区表)
-- 只收集新分区,减少影响范围

-- 5. 异步收集
-- 使用后台任务收集,不阻塞主流程

自动化方案

方案1: Shell 脚本自动化

bash 复制代码
#!/bin/bash
# collect_statistics.sh

# 配置
SPARK_HOME="/path/to/spark"
TABLE_NAME="sales"
PARTITION_COL="dt"
DATE=$(date +%Y-%m-%d)

# 收集统计信息
$SPARK_HOME/bin/spark-sql <<EOF
-- 收集表级统计
ANALYZE TABLE ${TABLE_NAME} 
PARTITION(${PARTITION_COL}='${DATE}') 
COMPUTE STATISTICS;

-- 收集列级统计
ANALYZE TABLE ${TABLE_NAME} 
PARTITION(${PARTITION_COL}='${DATE}') 
COMPUTE STATISTICS FOR COLUMNS 
product_id, customer_id, amount;
EOF

# 检查结果
if [ $? -eq 0 ]; then
    echo "Statistics collection succeeded for ${DATE}"
else
    echo "Statistics collection failed for ${DATE}"
    exit 1
fi

方案2: Python 自动化脚本

python 复制代码
#!/usr/bin/env python
# collect_statistics.py

from pyspark.sql import SparkSession
from datetime import datetime, timedelta
import sys

def collect_statistics(spark, table_name, partition_col, date):
    """收集指定分区的统计信息"""
    try:
        # 收集表级统计
        spark.sql(f"""
            ANALYZE TABLE {table_name} 
            PARTITION({partition_col}='{date}') 
            COMPUTE STATISTICS
        """)
        
        # 收集列级统计
        spark.sql(f"""
            ANALYZE TABLE {table_name} 
            PARTITION({partition_col}='{date}') 
            COMPUTE STATISTICS FOR COLUMNS 
            product_id, customer_id, amount
        """)
        
        print(f"✓ Statistics collected for {table_name}, {partition_col}={date}")
        return True
    except Exception as e:
        print(f"✗ Failed to collect statistics: {e}")
        return False

def main():
    spark = SparkSession.builder.appName("Statistics Collection").getOrCreate()
    
    # 配置
    table_name = "sales"
    partition_col = "dt"
    date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
    
    # 收集统计信息
    success = collect_statistics(spark, table_name, partition_col, date)
    
    spark.stop()
    sys.exit(0 if success else 1)

if __name__ == "__main__":
    main()

方案3: 集成到 ETL 流程

sql 复制代码
-- 在 ETL 脚本中集成统计信息收集
-- ============================================

-- 1. 数据加载
INSERT OVERWRITE TABLE dwd_order_detail 
PARTITION(dt='${date}')
SELECT ...;

-- 2. 数据质量检查
SELECT COUNT(*) as row_count FROM dwd_order_detail WHERE dt='${date}';
-- 如果 row_count = 0,跳过统计信息收集

-- 3. 收集统计信息(如果数据加载成功)
ANALYZE TABLE dwd_order_detail 
PARTITION(dt='${date}') 
COMPUTE STATISTICS;

ANALYZE TABLE dwd_order_detail 
PARTITION(dt='${date}') 
COMPUTE STATISTICS FOR COLUMNS 
order_id, product_id, customer_id, amount;

-- 4. 验证统计信息
SHOW COLUMN STATS dwd_order_detail PARTITION(dt='${date}');

方案4: 监控和告警

python 复制代码
# monitor_statistics.py
from pyspark.sql import SparkSession
from datetime import datetime, timedelta

def check_statistics_freshness(spark, table_name, max_age_days=7):
    """检查统计信息是否过期"""
    result = spark.sql(f"DESCRIBE EXTENDED {table_name}").collect()
    
    # 解析统计信息
    stats = {}
    for row in result:
        if 'Statistics' in str(row):
            # 解析统计信息
            # 检查时间戳
            pass
    
    # 检查是否过期
    # 如果超过 max_age_days,发送告警
    pass

def send_alert(message):
    """发送告警"""
    # 发送邮件、Slack 等
    print(f"ALERT: {message}")

def main():
    spark = SparkSession.builder.appName("Statistics Monitor").getOrCreate()
    
    tables = ["sales", "orders", "products"]
    
    for table in tables:
        if check_statistics_freshness(spark, table, max_age_days=7):
            send_alert(f"Statistics for {table} may be stale")
    
    spark.stop()

if __name__ == "__main__":
    main()

总结与最佳实践

核心要点

  1. 统计信息是 CBO 的基础

    • 没有统计信息,CBO 无法工作
    • 必须手动收集,不会自动收集
  2. 收集时机很重要

    • 分区表插入后立即收集(开销小)
    • 大表批量或定时收集(避免频繁开销)
  3. 只收集必要的列

    • JOIN 列、过滤列、聚合列
    • 避免收集所有列(开销大)
  4. 定期维护

    • 监控统计信息新鲜度
    • 及时更新过期统计信息

最佳实践清单

复制代码
□ 表级统计信息已收集
□ 关键列统计信息已收集(JOIN 列、过滤列)
□ 分区表插入后立即收集新分区统计
□ 大表使用批量或定时收集策略
□ 统计信息定期更新(不超过 7 天)
□ 收集过程不影响业务查询
□ 有监控和告警机制
□ 有自动化收集脚本

性能优化建议

复制代码
1. 分区表:只收集新分区
2. 只收集关键列
3. 在低峰期收集大表统计
4. 使用并行收集多个分区
5. 监控收集性能,及时优化

常见错误避免

复制代码
❌ 错误1: 开启 CBO 后不收集统计信息
   → 结果:CBO 无法工作

❌ 错误2: 收集所有列的统计信息
   → 结果:开销过大,影响性能

❌ 错误3: 大表每次插入后立即收集全表统计
   → 结果:严重影响性能

❌ 错误4: 不更新过期统计信息
   → 结果:CBO 选择次优计划

✅ 正确做法:
   - 分区表插入后立即收集新分区
   - 只收集关键列
   - 大表批量或定时收集
   - 定期更新统计信息

参考资料

相关推荐
ClouderaHadoop15 小时前
CDH 最隐蔽的坑:NTP 时间同步导致的 5 类故障
hadoop·hbase·kerberos·cloudera·cdh
Gent_倪16 小时前
Hadoop生态组件介绍
大数据·hadoop
DolphinScheduler社区16 小时前
DolphinScheduler 3.3.2 如何调用 DataX 3.0 + SeaTunnel 2.3.12?附 Demo演示!
java·spark·apache·海豚调度·大数据工作流调度
YaBingSec18 小时前
玄机网络安全靶场:Hadoop YARN ResourceManager 未授权 RCE WP
大数据·数据库·hadoop·redis·笔记·分布式·web安全
Leo.yuan18 小时前
数据仓库是什么?数据仓库和大数据平台、数据湖、数据中台、湖仓一体有什么区别?
大数据·数据仓库·spark
heiqizero19 小时前
Spark RDD动作算子
spark
heiqizero21 小时前
Spark RDD转换算子02
spark
heiqizero2 天前
Spark RDD转换算子01
spark
曹牧2 天前
Java Web 开发:servlet-mapping‌
java·数据仓库·hive·hadoop
菜鸟小码3 天前
HDFS 数据块(Block)机制深度解析:从原理到实战
大数据·hadoop·hdfs