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 选择次优计划

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

参考资料

相关推荐
mn_kw40 分钟前
Spark SQL CBO(基于成本的优化器)参数深度解析
前端·sql·spark
她说彩礼65万1 小时前
WPF Binding Source
大数据·hadoop·wpf
克喵的水银蛇3 小时前
Flutter 本地存储实战:SharedPreferences+Hive+SQLite
hive·flutter·sqlite
赞奇科技Xsuperzone10 小时前
【首发】DGX Spark 三机互连跑 Qwen3-235B-A22B-FP8!
大数据·分布式·spark
早睡早起早日毕业15 小时前
大数据管理与应用系列丛书《大数据平台架构》之吃透HBase:从原理到架构的深度解剖
hadoop·hbase
LDG_AGI17 小时前
【推荐系统】深度学习训练框架(七):PyTorch DDP(DistributedDataParallel)中,每个rank的batch数必须相同
网络·人工智能·pytorch·深度学习·机器学习·spark·batch
LDG_AGI20 小时前
【推荐系统】深度学习训练框架(六):PyTorch DDP(DistributedDataParallel)数据并行分布式深度学习原理
人工智能·pytorch·分布式·python·深度学习·算法·spark
lucky_syq1 天前
深入Spark核心:Shuffle全剖析与实战指南
大数据·分布式·python·spark
b***67641 天前
深入解析HDFS:定义、架构、原理、应用场景及常用命令
hadoop·hdfs·架构