Spark SQL 统计信息收集深度解析
目录
统计信息核心概念
什么是统计信息?
统计信息(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()
总结与最佳实践
核心要点
-
统计信息是 CBO 的基础
- 没有统计信息,CBO 无法工作
- 必须手动收集,不会自动收集
-
收集时机很重要
- 分区表插入后立即收集(开销小)
- 大表批量或定时收集(避免频繁开销)
-
只收集必要的列
- JOIN 列、过滤列、聚合列
- 避免收集所有列(开销大)
-
定期维护
- 监控统计信息新鲜度
- 及时更新过期统计信息
最佳实践清单
□ 表级统计信息已收集
□ 关键列统计信息已收集(JOIN 列、过滤列)
□ 分区表插入后立即收集新分区统计
□ 大表使用批量或定时收集策略
□ 统计信息定期更新(不超过 7 天)
□ 收集过程不影响业务查询
□ 有监控和告警机制
□ 有自动化收集脚本
性能优化建议
1. 分区表:只收集新分区
2. 只收集关键列
3. 在低峰期收集大表统计
4. 使用并行收集多个分区
5. 监控收集性能,及时优化
常见错误避免
❌ 错误1: 开启 CBO 后不收集统计信息
→ 结果:CBO 无法工作
❌ 错误2: 收集所有列的统计信息
→ 结果:开销过大,影响性能
❌ 错误3: 大表每次插入后立即收集全表统计
→ 结果:严重影响性能
❌ 错误4: 不更新过期统计信息
→ 结果:CBO 选择次优计划
✅ 正确做法:
- 分区表插入后立即收集新分区
- 只收集关键列
- 大表批量或定时收集
- 定期更新统计信息