Spark SQL CBO(基于成本的优化器)参数深度解析
目录
- [CBO 核心概念与原理](#CBO 核心概念与原理)
- [CBO 总开关详解](#CBO 总开关详解)
- [JOIN 重排序机制深度剖析](#JOIN 重排序机制深度剖析)
- 星型模式过滤优化
- 聚合下推优化
- [CBO 与 AQE 的协同工作](#CBO 与 AQE 的协同工作)
- 实际场景调优案例
CBO 核心概念与原理
什么是 CBO?
CBO(Cost-Based Optimizer) 是 Spark SQL 的基于成本的查询优化器,它在编译时(查询计划生成阶段)根据表统计信息计算不同执行计划的成本,选择最优计划。
CBO vs 规则优化器(Rule-Based Optimizer)
规则优化器(RBO):
→ 基于启发式规则
→ 不考虑数据大小和分布
→ 可能选择次优计划
基于成本的优化器(CBO):
→ 基于统计信息计算成本
→ 考虑数据大小、分布、选择性
→ 选择成本最低的计划
CBO 工作流程
阶段1: 收集统计信息
↓
ANALYZE TABLE table_name COMPUTE STATISTICS;
→ 收集:行数、列统计、直方图等
↓
阶段2: 生成候选执行计划
↓
→ 不同的 JOIN 顺序
→ 不同的 JOIN 策略
→ 不同的过滤顺序
↓
阶段3: 计算每个计划的成本
↓
成本 = CPU 成本 + I/O 成本 + 网络成本
↓
阶段4: 选择成本最低的计划
↓
执行最优计划
统计信息的重要性
CBO 的优化效果完全依赖统计信息的准确性:
缺少统计信息:
→ CBO 无法计算准确成本
→ 可能回退到规则优化
→ 优化效果大打折扣
准确的统计信息:
→ CBO 能准确评估成本
→ 选择最优执行计划
→ 性能提升 2-10 倍
CBO 总开关详解
1. spark.sql.cbo.enabled = true
作用范围
启用 CBO 后,以下优化会被激活:
1. JOIN 顺序优化(Join Reordering)
2. JOIN 策略选择(Join Strategy Selection)
3. 过滤下推优化(Predicate Pushdown)
4. 列裁剪优化(Column Pruning)
5. 常量折叠(Constant Folding)
6. 投影下推(Projection Pushdown)
成本模型
CBO 使用多维度成本模型:
总成本 = CPU 成本 + I/O 成本 + 网络成本 + 内存成本
CPU 成本:
- 数据扫描开销
- 过滤操作开销
- JOIN 操作开销
- 聚合操作开销
I/O 成本:
- 磁盘读取开销
- 磁盘写入开销
- 缓存命中率
网络成本:
- Shuffle 数据传输
- Broadcast 数据传输
内存成本:
- Hash 表构建
- 排序缓冲区
- 缓存占用
性能影响示例
场景:3 表 JOIN 查询
sql
SELECT *
FROM large_table l
JOIN medium_table m ON l.id = m.id
JOIN small_table s ON m.key = s.key
WHERE l.date = '2024-01-01'
关闭 CBO:
执行计划(固定顺序):
large_table → JOIN medium_table → JOIN small_table
中间结果:100GB
执行时间:45 分钟
开启 CBO:
执行计划(优化顺序):
small_table → JOIN medium_table → JOIN large_table
中间结果:5GB(先过滤,再 JOIN)
执行时间:8 分钟
性能提升:5.6 倍!
统计信息收集
必须收集的统计信息:
sql
-- 表级统计信息
ANALYZE TABLE table_name COMPUTE STATISTICS;
-- 列级统计信息(包含直方图)
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2, ...;
-- 分区统计信息
ANALYZE TABLE table_name PARTITION(partition_col='value') COMPUTE STATISTICS;
统计信息内容:
表级统计:
- 总行数(rowCount)
- 总大小(sizeInBytes)
列级统计:
- 不同值数量(distinctCount)
- 空值数量(nullCount)
- 最小值/最大值(min/max)
- 平均长度(avgLen)
- 直方图(histogram)- 数据分布
常见问题
问题1: CBO 启用但效果不明显
可能原因:
1. 统计信息未收集或过期
→ 解决:定期收集统计信息
2. 统计信息不准确
→ 解决:使用 FOR COLUMNS 收集详细统计
3. 数据变化频繁
→ 解决:增量更新统计信息
问题2: CBO 选择次优计划
可能原因:
1. 成本模型不准确
→ 解决:调整成本权重参数
2. 统计信息缺失
→ 解决:补充缺失的统计信息
3. 数据倾斜未考虑
→ 解决:结合 AQE 使用
JOIN 重排序机制深度剖析
2. spark.sql.cbo.joinReorder.enabled = true
JOIN 顺序的重要性
为什么 JOIN 顺序很重要?
场景:3 表 JOIN
Table A: 100GB
Table B: 10GB
Table C: 1GB
顺序1: A JOIN B JOIN C
中间结果1: A JOIN B = 50GB
中间结果2: 50GB JOIN C = 5GB
总 Shuffle: 100GB + 50GB = 150GB
顺序2: C JOIN B JOIN A
中间结果1: C JOIN B = 0.5GB
中间结果2: 0.5GB JOIN A = 0.3GB
总 Shuffle: 1GB + 0.5GB = 1.5GB
性能差异:100 倍!
重排序算法
动态规划算法(DP - Dynamic Programming)
步骤1: 构建 JOIN 图
A ──┐
├── JOIN
B ──┘
│
C ──┘
步骤2: 枚举所有可能的 JOIN 顺序
顺序1: (A JOIN B) JOIN C
顺序2: (A JOIN C) JOIN B
顺序3: (B JOIN C) JOIN A
...
步骤3: 计算每个顺序的成本
成本 = 扫描成本 + JOIN 成本 + Shuffle 成本
步骤4: 选择成本最低的顺序
算法复杂度
表数量: n
可能的 JOIN 顺序数: (2n-2)! / (n-1)!
n=3: 2 种顺序
n=4: 12 种顺序
n=5: 120 种顺序
n=6: 1680 种顺序
n=7: 30240 种顺序
...
复杂度: O(n!)
→ 表数量多时,需要限制搜索空间
3. spark.sql.cbo.joinReorder.dp.threshold = 12
阈值的作用
为什么需要阈值?
无阈值限制:
10 个表 JOIN
→ 可能的顺序数:3628800 种
→ 计算时间:数小时
→ 优化时间 > 执行时间(得不偿失)
有阈值限制(threshold = 12):
表数 ≤ 12:完整搜索
表数 > 12:使用启发式算法
→ 平衡优化效果和优化时间
搜索策略
表数 ≤ threshold(完整搜索):
使用动态规划算法:
→ 枚举所有可能的 JOIN 顺序
→ 计算每个顺序的成本
→ 选择最优顺序
优点:找到全局最优解
缺点:计算开销大
表数 > threshold(启发式搜索):
使用贪心算法:
1. 从最小的表开始
2. 每次选择成本最低的 JOIN
3. 逐步构建 JOIN 树
优点:计算速度快
缺点:可能不是全局最优
阈值选择建议
| 表数量 | 推荐阈值 | 原因 |
|---|---|---|
| ≤ 5 | 5-8 | 表少,可以完整搜索 |
| 6-10 | 10-12 | 平衡优化效果和时间 |
| 11-15 | 12-15 | 需要更多搜索空间 |
| > 15 | 15-20 | 大量表,需要限制搜索 |
注意:
- 阈值越大,优化时间越长
- 阈值太小,可能错过最优计划
- 需要根据实际场景调整
实际示例
场景:8 表 JOIN
threshold = 12(当前配置):
→ 8 < 12,使用完整搜索
→ 枚举所有 5040 种顺序
→ 计算时间:~5 秒
→ 找到最优顺序
→ 执行时间:10 分钟
threshold = 5:
→ 8 > 5,使用启发式搜索
→ 计算时间:~0.1 秒
→ 可能不是最优顺序
→ 执行时间:12 分钟(慢 20%)
星型模式过滤优化
4. spark.sql.cbo.joinReorder.dp.star.filter = false
星型模式(Star Schema)
什么是星型模式?
星型模式结构:
- 1 个事实表(Fact Table):大表,包含度量数据
- N 个维度表(Dimension Tables):小表,包含描述信息
示例:
事实表:sales (100GB)
- sale_id, product_id, customer_id, date_id, amount
维度表:
- products (10MB)
- customers (50MB)
- dates (1MB)
星型模式 JOIN 特点
典型查询:
SELECT s.amount, p.name, c.name, d.date
FROM sales s
JOIN products p ON s.product_id = p.id
JOIN customers c ON s.customer_id = c.id
JOIN dates d ON s.date_id = d.id
WHERE p.category = 'Electronics'
AND c.region = 'North'
AND d.year = 2024
特点:
- 多个维度表过滤条件
- 过滤后维度表很小
- 先过滤维度表,再 JOIN 事实表,效果最好
star.filter 参数的作用
star.filter = true(启用):
优化策略:
1. 识别星型模式结构
2. 先对维度表应用过滤条件
3. 过滤后的维度表 JOIN 事实表
4. 大幅减少中间结果
执行顺序:
products (filtered) → JOIN sales
customers (filtered) → JOIN sales
dates (filtered) → JOIN sales
→ 中间结果:从 100GB 降到 1GB
star.filter = false(当前配置):
使用标准 JOIN 重排序:
→ 基于成本选择 JOIN 顺序
→ 不特殊处理星型模式
→ 可能选择次优顺序
可能的问题:
- 先 JOIN 事实表,再过滤维度表
- 中间结果很大
- 性能较差
为什么当前设置为 false?
可能的原因:
1. 不是星型模式场景
→ 如果查询不是星型模式,启用反而可能影响优化
2. 维度表过滤效果不明显
→ 如果维度表过滤后仍然很大,优化效果有限
3. 兼容性问题
→ 某些场景下可能有问题
何时启用?
建议启用的场景:
✓ 明确的星型模式数据仓库
✓ 维度表有强过滤条件
✓ 维度表过滤后显著变小(< 10%)
✓ 事实表很大(> 10GB)
不建议启用的场景:
✗ 不是星型模式
✗ 维度表过滤效果不明显
✗ 查询模式复杂多变
性能对比示例
场景:星型模式查询
sql
SELECT s.amount, p.name, c.name
FROM sales s (100GB)
JOIN products p (10MB) ON s.product_id = p.id
JOIN customers c (50MB) ON s.customer_id = c.id
WHERE p.category = 'Electronics' -- 过滤后 1MB
AND c.region = 'North' -- 过滤后 5MB
star.filter = false:
执行顺序:sales JOIN products JOIN customers
中间结果1: 100GB JOIN 10MB = 50GB
中间结果2: 50GB JOIN 50MB = 25GB
然后应用过滤条件
最终结果: 1GB
执行时间: 30 分钟
star.filter = true:
执行顺序:
1. 过滤 products: 10MB → 1MB
2. 过滤 customers: 50MB → 5MB
3. 1MB JOIN sales = 0.5GB
4. 0.5GB JOIN 5MB = 0.3GB
最终结果: 1GB
执行时间: 5 分钟
性能提升:6 倍!
聚合下推优化
5. spark.sql.distinctAggPushdown.enabled = true
聚合下推(Aggregation Pushdown)
什么是聚合下推?
将聚合操作尽可能下推到数据源或更早的执行阶段
→ 减少需要处理的数据量
→ 提高查询性能
distinctAggPushdown 的作用
启用后,优化 DISTINCT 聚合操作:
sql
-- 原始查询
SELECT
customer_id,
COUNT(DISTINCT product_id) as unique_products,
SUM(amount) as total_amount
FROM sales
GROUP BY customer_id
不启用(标准执行):
执行流程:
1. 扫描 sales 表(100GB)
2. Shuffle 按 customer_id 分组
3. 在每个分区计算 COUNT(DISTINCT product_id)
4. 再次 Shuffle 合并结果
5. 计算 SUM(amount)
问题:
- COUNT(DISTINCT) 需要两阶段聚合
- 中间数据量大
- Shuffle 开销大
启用(优化执行):
执行流程:
1. 扫描 sales 表时,在 Map 端预聚合
2. 对每个 customer_id,先计算部分 DISTINCT
3. Shuffle 数据量减少(去重后)
4. Reduce 端合并 DISTINCT 结果
5. 计算 SUM(amount)
优势:
- 减少 Shuffle 数据量(30-70%)
- 减少网络传输
- 提高执行效率
优化原理
两阶段 DISTINCT 聚合:
阶段1: Map 端预聚合(Partial Aggregation)
输入:原始数据
输出:部分去重结果
例如:customer_id=1, product_id={1,2,3} (Map 端)
阶段2: Reduce 端最终聚合(Final Aggregation)
输入:部分去重结果
输出:完全去重结果
例如:customer_id=1, product_id={1,2,3,4,5} (合并所有 Map 结果)
性能影响
场景:1 亿条销售记录,1000 万客户,100 万产品
不启用:
Shuffle 数据量:100GB(原始数据)
执行时间:45 分钟
启用:
Shuffle 数据量:30GB(预聚合后)
执行时间:15 分钟
性能提升:3 倍!
适用场景
适合的场景:
✓ COUNT(DISTINCT column)
✓ 多个 DISTINCT 聚合
✓ 数据重复度高(去重效果好)
✓ 大表聚合查询
不适合的场景:
✗ 数据重复度低(去重效果不明显)
✗ 小表查询(优化开销 > 收益)
✗ 简单的 SUM/AVG(不需要 DISTINCT)
与其他优化的协同
结合 GROUP BY 下推:
sql
SELECT
c.region,
COUNT(DISTINCT s.product_id) as unique_products
FROM sales s
JOIN customers c ON s.customer_id = c.id
WHERE c.region IN ('North', 'South')
GROUP BY c.region
优化流程:
1. 过滤 customers(WHERE 下推)
2. JOIN sales(过滤后的 customers)
3. DISTINCT 聚合下推(Map 端预聚合)
4. GROUP BY 聚合
多重优化叠加效果:
- 过滤减少数据量:90%
- DISTINCT 下推减少 Shuffle:60%
- 总体性能提升:10-20 倍
CBO 与 AQE 的协同工作
编译时优化 vs 运行时优化
CBO(编译时):
→ 基于统计信息
→ 生成初始执行计划
→ 选择 JOIN 顺序、策略等
AQE(运行时):
→ 基于实际数据
→ 动态调整执行计划
→ 处理数据倾斜、分区合并等
协同工作:
CBO 生成好的初始计划
↓
AQE 根据实际情况调整
↓
最优执行效果
协同示例
场景:多表 JOIN + 数据倾斜
sql
SELECT *
FROM large_table l
JOIN medium_table m ON l.id = m.id
JOIN small_table s ON m.key = s.key
WHERE l.date = '2024-01-01'
阶段1: CBO 优化(编译时)
CBO 分析:
- large_table: 100GB
- medium_table: 10GB
- small_table: 1GB
- 过滤条件:l.date = '2024-01-01'(选择性 10%)
CBO 决策:
1. 先过滤 large_table:100GB → 10GB
2. JOIN 顺序:small_table → medium_table → large_table
3. JOIN 策略:Broadcast Join(small_table < 10MB)
生成执行计划
阶段2: AQE 优化(运行时)
AQE 监控:
- 发现 medium_table JOIN large_table 时数据倾斜
- 某些分区 200MB,某些 10MB
AQE 调整:
1. 拆分倾斜分区
2. 动态调整分区大小
3. 优化 Shuffle 策略
最终执行
性能对比:
仅 CBO:30 分钟(被倾斜拖累)
仅 AQE:35 分钟(初始计划不佳)
CBO + AQE:8 分钟(最优组合)
协同效果:3.75 倍提升!
参数配置建议
完整优化配置:
# CBO 配置
spark.sql.cbo.enabled = true
spark.sql.cbo.joinReorder.enabled = true
spark.sql.cbo.joinReorder.dp.threshold = 12
spark.sql.cbo.joinReorder.dp.star.filter = false # 根据场景调整
spark.sql.distinctAggPushdown.enabled = true
# AQE 配置
spark.sql.adaptive.enabled = true
spark.sql.adaptive.coalescePartitions.enabled = true
spark.sql.adaptive.skewJoin.enabled = true # 建议启用
# 统计信息收集
定期执行:
ANALYZE TABLE table_name COMPUTE STATISTICS;
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2;
实际场景调优案例
案例1: 多表 JOIN 性能优化
问题描述:
- 5 表 JOIN 查询,执行时间 2 小时
- 中间结果很大,Shuffle 开销大
当前配置分析:
cbo.enabled = true ✓
joinReorder.enabled = true ✓
joinReorder.dp.threshold = 12 ✓
5 表 < 12,会完整搜索
诊断步骤:
1. 检查统计信息:
SHOW TABLE EXTENDED LIKE 'table_name';
→ 发现部分表缺少统计信息
2. 收集统计信息:
ANALYZE TABLE table1 COMPUTE STATISTICS;
ANALYZE TABLE table2 COMPUTE STATISTICS FOR COLUMNS id, date;
...
3. 查看执行计划:
EXPLAIN COST SELECT ...;
→ 发现 JOIN 顺序不是最优
优化方案:
1. 收集完整统计信息:
- 表级统计
- 列级统计(JOIN 列和过滤列)
- 分区统计(如果分区表)
2. 验证 CBO 优化:
- 查看执行计划中的成本信息
- 确认 JOIN 顺序已优化
3. 预期效果:
执行时间:2 小时 → 30 分钟
性能提升:4 倍
案例2: 星型模式查询优化
问题描述:
- 数据仓库星型模式查询
- 事实表 500GB,5 个维度表
- 查询耗时 1.5 小时
当前配置问题:
star.filter = false ✗
→ 未启用星型模式优化
优化方案:
1. 启用星型模式优化:
spark.sql.cbo.joinReorder.dp.star.filter = true
2. 收集维度表统计信息:
ANALYZE TABLE dim_table COMPUTE STATISTICS FOR COLUMNS id, category;
3. 验证优化效果:
- 查看执行计划
- 确认先过滤维度表,再 JOIN 事实表
4. 预期效果:
执行时间:1.5 小时 → 20 分钟
性能提升:4.5 倍
案例3: DISTINCT 聚合优化
问题描述:
- 大表 COUNT(DISTINCT) 查询
- Shuffle 数据量大,执行慢
当前配置分析:
distinctAggPushdown.enabled = true ✓
→ 已启用,但效果不明显
诊断:
1. 检查执行计划:
EXPLAIN SELECT COUNT(DISTINCT col) FROM table;
→ 确认是否使用了预聚合
2. 检查数据分布:
→ 发现数据重复度低(去重效果不明显)
3. 检查统计信息:
→ 发现表统计信息过期
优化方案:
1. 更新统计信息:
ANALYZE TABLE table_name COMPUTE STATISTICS;
2. 如果数据重复度确实低:
→ distinctAggPushdown 优化效果有限
→ 考虑其他优化:
- 增加 Executor 数量
- 调整 Shuffle 分区数
- 使用 AQE 分区合并
3. 如果数据重复度高但效果不明显:
→ 检查是否有其他瓶颈
→ 网络带宽、磁盘 I/O 等
案例4: 复杂查询综合优化
问题描述:
- 包含多个 CTE、多表 JOIN、复杂聚合
- 执行时间 3 小时
完整优化方案:
1. CBO 优化:
✓ 收集所有表的统计信息
✓ 启用 JOIN 重排序
✓ 调整 threshold(如果表多)
2. AQE 优化:
✓ 启用分区合并
✓ 启用倾斜处理
✓ 启用动态分区裁剪
3. 其他优化:
✓ CTE 缓存
✓ 广播小表
✓ 过滤下推
4. 预期效果:
执行时间:3 小时 → 25 分钟
性能提升:7.2 倍
统计信息管理最佳实践
统计信息收集策略
初始收集:
sql
-- 全表统计
ANALYZE TABLE table_name COMPUTE STATISTICS;
-- 关键列统计(JOIN 列、过滤列、分组列)
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS
join_col1, join_col2,
filter_col1, filter_col2,
group_col1, group_col2;
增量更新:
sql
-- 分区表增量更新
ANALYZE TABLE table_name PARTITION(date='2024-01-01') COMPUTE STATISTICS;
-- 定期全量更新(如每天)
ANALYZE TABLE table_name COMPUTE STATISTICS;
统计信息维护
监控统计信息:
sql
-- 查看表统计信息
DESCRIBE EXTENDED table_name;
-- 查看列统计信息
SHOW COLUMN STATS table_name;
统计信息过期检测:
指标:
- 数据变化率 > 10%
- 上次统计时间 > 7 天
- 查询性能下降
处理:
→ 重新收集统计信息
自动化脚本示例
sql
-- 收集所有表的统计信息
SET spark.sql.adaptive.enabled = true;
-- 表列表
SHOW TABLES;
-- 对每个表执行
ANALYZE TABLE ${table_name} COMPUTE STATISTICS;
-- 对关键列执行
ANALYZE TABLE ${table_name} COMPUTE STATISTICS FOR COLUMNS ${key_columns};
总结与最佳实践
CBO 参数配置检查清单
□ CBO 总开关已启用
□ JOIN 重排序已启用
□ JOIN 重排序阈值设置合理(根据表数量)
□ 星型模式过滤已配置(如果是星型模式)
□ DISTINCT 聚合下推已启用
□ 统计信息已收集(表级 + 列级)
□ 统计信息定期更新
□ 结合 AQE 使用
性能监控指标
关键指标:
1. 执行计划成本(EXPLAIN COST)
2. JOIN 顺序是否优化
3. Shuffle 数据量
4. 统计信息准确性
5. 查询执行时间
调优流程
1. 基线测试:记录当前性能
2. 收集统计信息:表级 + 列级
3. 启用 CBO 相关参数
4. 查看执行计划:EXPLAIN COST
5. 验证优化效果:对比性能
6. 持续监控:定期更新统计信息
常见问题排查
问题1: CBO 未生效
→ 检查统计信息是否收集
→ 检查参数是否启用
→ 查看执行计划
问题2: 优化效果不明显
→ 检查统计信息准确性
→ 检查数据分布
→ 考虑结合 AQE
问题3: 优化时间过长
→ 降低 joinReorder.dp.threshold
→ 使用启发式算法