Spark SQL CBO(基于成本的优化器)参数深度解析

Spark SQL CBO(基于成本的优化器)参数深度解析

目录

  1. [CBO 核心概念与原理](#CBO 核心概念与原理)
  2. [CBO 总开关详解](#CBO 总开关详解)
  3. [JOIN 重排序机制深度剖析](#JOIN 重排序机制深度剖析)
  4. 星型模式过滤优化
  5. 聚合下推优化
  6. [CBO 与 AQE 的协同工作](#CBO 与 AQE 的协同工作)
  7. 实际场景调优案例

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
  → 使用启发式算法

相关推荐
徐同保40 分钟前
typeorm node后端数据库ORM
前端
艾小码1 小时前
Vue 组件设计纠结症?一招教你告别“数据到底放哪”的烦恼
前端·javascript·vue.js
SVIP111592 小时前
即时通讯WebSocket详解及使用方法
前端·javascript
mCell6 小时前
使用 useSearchParams 同步 URL 和查询参数
前端·javascript·react.js
mCell8 小时前
前端路由详解:Hash vs History
前端·javascript·vue-router
海上彼尚8 小时前
无需绑卡的海外地图
前端·javascript·vue.js·node.js
1024肥宅8 小时前
手写 call、apply、bind 的实现
前端·javascript·ecmascript 6
5***E6859 小时前
【SQL】写SQL查询时,常用到的日期函数
数据库·sql
科杰智能制造9 小时前
纯前端html、js实现人脸检测和表情检测,可直接在浏览器使用
前端·javascript·html