Spark 性能优化 (三):RBO 与 CBO

1. RBO 的核心概念

在 Apache Spark 的查询优化过程中,规则优化(Rule-Based Optimization, RBO) 是 Catalyst 优化器的一个关键组成部分。它主要依赖于一组固定的规则进行优化,而不是基于统计信息(如 CBO - Cost-Based Optimization)。

RBO 主要通过一系列 逻辑规则(Logical Rules)物理规则(Physical Rules) 来转换和优化查询计划。这些规则在不改变查询结果的情况下,优化查询逻辑,使查询执行得更高效。

RBO 适用于优化以下方面:

  • 谓词下推(Predicate Pushdown):减少不必要的数据扫描
  • 常量折叠(Constant Folding):减少计算量
  • 投影下推(Projection Pruning):减少数据传输
  • 消除无效操作(Eliminate Redundant Operations):去掉无用的计算

2. RBO 在 Catalyst 优化器中的角色

Spark 的 Catalyst 查询优化器由四个阶段组成:

  1. 解析(Parsing):将 SQL 语句解析成抽象语法树(AST)。
  2. 分析(Analysis):使用 Catalog 解析表和列,并检查语义错误,生成逻辑计划。
  3. 优化(Optimization)
    • 规则优化(RBO)
    • 基于代价的优化(CBO)
  4. 物理规划(Physical Planning):选择合适的执行计划(如 Hash Join、Sort Merge Join 等)。

RBO 主要在 优化阶段(Optimization) 进行,它会对逻辑计划进行一系列转换,以减少计算成本。

3. 常见的 RBO 规则

Spark 提供了大量的规则优化,以下是几个典型的 RBO 规则:

① 谓词下推(Predicate Pushdown)

WHERE 条件尽早下推到数据源,减少数据扫描量。例如:

复制代码
SELECT * FROM orders WHERE order_status = 'shipped';

Spark 在 RBO 阶段会将 order_status = 'shipped' 下推到数据源层,比如 Parquet、ORC、JDBC 源等,从而减少数据扫描量。

② 常量折叠(Constant Folding)

计算可以提前执行的表达式,减少运行时计算。例如:

复制代码
SELECT 1 + 2 * 3;

在 RBO 阶段,Spark 会直接优化成:

复制代码
SELECT 7;

这避免了运行时计算,提升查询性能。

③ 消除无效的 LIMIT 操作(Eliminate No-op Limit)

如果 LIMIT 操作不会影响查询结果,则直接去掉。例如:

复制代码
SELECT * FROM orders LIMIT 1000

如果 orders 只有 500 条记录,则 LIMIT 1000 无意义,Spark 可能会优化掉这个 LIMIT

④ 消除无效的 Sort 操作(Eliminate No-op Sort)

如果数据已经按照 ORDER BY 排序过,则去掉多余的排序。例如:

复制代码
SELECT * FROM orders ORDER BY order_date

如果 orders 数据表已经按照 order_date 排序,Spark 可能会优化掉 ORDER BY 操作。

⑤ 列裁剪(Column Pruning)

减少不必要的数据传输和计算。例如:

复制代码
SELECT customer_id FROM orders;

如果 orders 表有 50 列,而查询只需要 customer_id,Spark 会在 RBO 过程中移除不必要的列,减少数据扫描和传输的成本。

⑥ 过滤 NULL 值(Simplify Filters)

复制代码
SELECT * FROM users WHERE age > 18 AND age IS NOT NULL;

如果 age 列设置了 NOT NULL 约束,则 age IS NOT NULL 这个条件可以去掉。

4. RBO 代码示例:

Spark 提供了 explain 方法来查看 RBO 规则应用情况:

复制代码
from pyspark.sql import SparkSession

# 创建 SparkSession
spark = SparkSession.builder.appName("Spark RBO").getOrCreate()

# 创建测试 DataFrame
data = [(1, "Alice", 23), (2, "Bob", 25), (3, "Cathy", 30)]
df = spark.createDataFrame(data, ["id", "name", "age"])

# 运行查询并查看优化后的逻辑计划
df.select("id", "name").explain(mode="extended")

输出示例:

复制代码
== Optimized Logical Plan ==
Project [id, name]
+- LocalRelation [id, name, age]

可以看到 Spark 在 RBO 过程中自动去掉了 age 列。

5. CBO 的核心概念

在 Apache Spark 的查询优化过程中,基于代价的优化(Cost-Based Optimization, CBO) 是 Catalyst 优化器的一个关键部分。CBO 主要依赖 统计信息 (Statistics)来选择更高效的查询执行计划,相比 RBO(Rule-Based Optimization,基于规则的优化),CBO 能够更智能地优化查询性能,特别是在 Join 选择、聚合优化、谓词优化 方面。

Spark CBO 主要通过统计信息计算不同查询计划的代价(cost),并选择代价最小的执行方案。它适用于:

  • Join 重新排序(Reorder Joins)------ 选择最佳的 Join 顺序,提高执行效率。
  • 选择最佳 Join 方式(Broadcast Hash Join vs. Sort Merge Join)
  • 聚合优化(Aggregation Optimization)
  • 列裁剪优化(Column Pruning)------ 减少不必要的列传输
  • 谓词优化(Predicate Optimization)

CBO 依赖 表的统计信息,如:

  • 行数(Row Count)
  • 列的基数(Cardinality)
  • NULL 值的数量
  • 最大/最小值
  • 直方图(Histogram)

6. CBO 在 Catalyst 优化器中的作用

Spark Catalyst 查询优化器由四个阶段组成:

  1. 解析(Parsing):将 SQL 解析成抽象语法树(AST)。
  2. 分析(Analysis):使用 Catalog 解析表和列,并检查语义错误。
  3. 优化(Optimization)
    • 规则优化(RBO)------不依赖统计信息
    • 基于代价的优化(CBO)------依赖统计信息
  4. 物理计划生成(Physical Planning):选择最优执行计划。

在优化阶段,RBO 先执行 ,然后 CBO 基于统计信息 进一步优化查询计划,使执行更加高效。

7. Spark CBO 关键优化策略

① Join 重新排序(Reorder Joins)

CBO 通过统计信息计算不同 Join 顺序的代价,选择代价最低的 Join 执行方式。例如:

复制代码
SELECT * 
FROM orders o 
JOIN customers c ON o.customer_id = c.customer_id
JOIN products p ON o.product_id = p.product_id;

如果 orders 表有 1 亿行,customers 表有 100 万行,products 表有 10 万行,理想的 Join 顺序是:

  1. 先 Join orderscustomers(较小的表优先参与 Join)
  2. 再 Join products

CBO 通过 统计信息 来决定 Join 的最佳顺序,避免大表 Join 造成的性能问题。

相关参数:

复制代码
spark.sql.cbo.enabled = true  # 启用 CBO
spark.sql.cbo.joinReorder.enabled = true  # 允许 Join 重新排序

② 选择最佳 Join 方式

Spark 支持多种 Join 方式:

  • Broadcast Hash Join(适用于小表)
  • Sort Merge Join(适用于大表)
  • Shuffle Hash Join
  • Nested Loop Join(适用于非等值 Join)

CBO 通过统计信息选择合适的 Join 方式。例如,如果 customers 表小于 spark.sql.autoBroadcastJoinThreshold(默认 10MB),CBO 会选择 Broadcast Hash Join,避免 Shuffle,提高性能。

相关参数:

复制代码
spark.sql.autoBroadcastJoinThreshold = 10MB  # 小于 10MB 的表自动广播

③ 聚合优化(Aggregation Optimization)

如果 CBO 发现:

  • 过滤后的数据量很小
  • 适合 Hash Aggregate(内存中完成聚合)

则 Spark 会选择 Hash Aggregate 代替 Sort Aggregate,提升查询性能。

复制代码
SELECT category, COUNT(*) 
FROM products 
GROUP BY category;

如果 category 基数小,CBO 可能选择 Hash Aggregate,避免排序消耗。

④ 谓词优化(Predicate Optimization)

CBO 通过统计信息判断谓词是否能减少扫描数据量。例如:

复制代码
SELECT * FROM orders WHERE order_date >= '2024-01-01';

如果 order_date 是一个 高基数列(High Cardinality) ,CBO 可能建议 索引扫描(Index Scan),而不是全表扫描。

⑤ 列裁剪(Column Pruning)

CBO 可以自动裁剪不必要的列,减少数据传输。例如:

复制代码
SELECT customer_id FROM orders;

如果 orders 表有 50 列,而查询只涉及 customer_id,CBO 会裁剪掉其他 49 列,减少 I/O 和计算成本。

8. 如何启用 CBO

默认情况下,CBO 是 关闭 的。需要显式开启:

复制代码
spark.conf.set("spark.sql.cbo.enabled", "true")
spark.conf.set("spark.sql.cbo.joinReorder.enabled", "true")  # 允许 Join 重新排序
spark.conf.set("spark.sql.statistics.histogram.enabled", "true")  # 启用直方图

此外,CBO 依赖 表的统计信息,需要手动收集:

复制代码
ANALYZE TABLE orders COMPUTE STATISTICS;
ANALYZE TABLE orders COMPUTE STATISTICS FOR COLUMNS customer_id, order_date;

9. CBO 代码示例

复制代码
from pyspark.sql import SparkSession

# 创建 SparkSession
spark = SparkSession.builder \
    .appName("Spark CBO") \
    .config("spark.sql.cbo.enabled", "true") \
    .config("spark.sql.cbo.joinReorder.enabled", "true") \
    .getOrCreate()

# 创建示例表
data1 = [(1, "Alice", 1000), (2, "Bob", 2000), (3, "Cathy", 3000)]
data2 = [(1, "Product A"), (2, "Product B"), (3, "Product C")]

df1 = spark.createDataFrame(data1, ["id", "name", "salary"])
df2 = spark.createDataFrame(data2, ["id", "product_name"])

# Join 并查看执行计划
df = df1.join(df2, "id")
df.explain(mode="extended")

如果统计信息已收集,CBO 会优化 Join 方式并选择最佳执行计划。

10. CBO vs. RBO

优化类型 CBO(基于代价优化) RBO(规则优化)
依赖统计信息 ✅ 依赖统计信息 ❌ 不依赖统计信息
适用场景 Join 选择、聚合优化、列裁剪 谓词下推、常量折叠、投影下推
计算成本 较高(需要计算代价)
典型优化 Join 重新排序、选择最佳 Join 算法 谓词下推、常量折叠

RBO 适用于基本优化,而 CBO 在大数据查询中至关重要,能够智能选择最佳执行计划。

相关推荐
你听得到113 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
亲亲小宝宝鸭6 天前
前端性能监控:web-vitals
前端·性能优化·监控
TrisighT10 天前
Electron 跑在鸿蒙 PC 上,单窗口和多窗口内存差 800MB?我抓了 5 组数据
性能优化·electron·harmonyos
jump_jump14 天前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化
小小工匠15 天前
Redis - 事务机制:能实现 ACID 属性吗
数据结构·redis·性能优化·并发·持久化
极光代码工作室15 天前
基于数据仓库的电商数据分析平台
大数据·hadoop·python·spark·数据可视化
JLWcai2025100915 天前
铸造领域树脂砂轮|金利威多场景解决方案,20 + 配方覆盖全需求
mongodb·zookeeper·eureka·spark·rabbitmq·memcached·storm
大鱼>15 天前
地平线BPU部署实战:YOLOv8在J5/X3上的算法适配与性能优化
算法·yolo·性能优化
醉颜凉15 天前
Elasticsearch高性能优化:Bulk API大规模数据导入性能调优全攻略
elasticsearch·性能优化·jenkins
隔窗听雨眠15 天前
C语言函数递归从入门到精通(下):性能优化与工程实践
c语言·算法·性能优化