我有兴趣使用Spark SQL(1.6)来执行"过滤的equi-joins"形式
A inner join B where A.group_id = B.group_id and pair_filter_udf(A[cols], B[cols])
此处group_id
是粗略的:单个group_id
值可以与A和B中的10,000条记录相关联。
如果equi-join是自己执行的,没有pair_filter_udf
,group_id
的粗糙会产生计算问题。例如,对于A和B中包含10,000条记录的group_id
,连接中将有1亿条目。如果我们有成千上万的这样大的组,我们会生成一个巨大的表,我们很容易耗尽内存。
因此,我们必须将pair_filter_udf
下推到连接中,并在生成它们时对其进行过滤,而不是等到生成所有对。我的问题是Spark SQL是否会这样做。
我设置了一个简单的过滤的equi-join,并询问Spark的查询计划是什么:
# run in PySpark Shell
import pyspark.sql.functions as F
sq = sqlContext
n=100
g=10
a = sq.range(n)
a = a.withColumn('grp',F.floor(a['id']/g)*g)
a = a.withColumnRenamed('id','id_a')
b = sq.range(n)
b = b.withColumn('grp',F.floor(b['id']/g)*g)
b = b.withColumnRenamed('id','id_b')
c = a.join(b,(a.grp == b.grp) & (F.abs(a['id_a'] - b['id_b']) < 2)).drop(b['grp'])
c = c.sort('id_a')
c = c[['grp','id_a','id_b']]
c.explain()
结果:
== Physical Plan ==
Sort [id_a#21L ASC], true, 0
+- ConvertToUnsafe
+- Exchange rangepartitioning(id_a#21L ASC,200), None
+- ConvertToSafe
+- Project [grp#20L,id_a#21L,id_b#24L]
+- Filter (abs((id_a#21L - id_b#24L)) < 2)
+- SortMergeJoin [grp#20L], [grp#23L]
:- Sort [grp#20L ASC], false, 0
: +- TungstenExchange hashpartitioning(grp#20L,200), None
: +- Project [id#19L AS id_a#21L,(FLOOR((cast(id#19L as double) / 10.0)) * 10) AS grp#20L]
: +- Scan ExistingRDD[id#19L]
+- Sort [grp#23L ASC], false, 0
+- TungstenExchange hashpartitioning(grp#23L,200), None
+- Project [id#22L AS id_b#24L,(FLOOR((cast(id#22L as double) / 10.0)) * 10) AS grp#23L]
+- Scan ExistingRDD[id#22L]
这些是该计划的关键路线:
+- Filter (abs((id_a#21L - id_b#24L)) < 2)
+- SortMergeJoin [grp#20L], [grp#23L]
这些行给人的印象是过滤器将在连接后的单独阶段完成,这不是所需的行为。但也许它被隐式地推入了联接,而查询计划只是缺乏那种细节。
在这种情况下,我怎么知道Spark正在做什么?
更新:
我正在运行n = 1e6和g = 1e5的实验,如果Spark没有进行下推,这应该足以让我的笔记本电脑崩溃。由于它没有崩溃,我猜它正在做下推。但是知道它是如何工作以及Spark SQL源的哪些部分负责这个令人敬畏的优化会很有趣。
答案 0 :(得分:5)
很大程度上取决于下推的含义。如果您询问|a.id_a - b.id_b| < 2
是否作为join
旁边a.grp = b.grp
逻辑的一部分执行,则答案为否定。不基于相等性的谓词不直接包含在join
条件中。
您可以说明使用DAG而不是执行计划的一种方式它应该看起来或多或少像这样:
如您所见,filter
作为与SortMergeJoin
的单独转换执行。另一种方法是在删除a.grp = b.grp
时分析执行计划。您会看到它将join
展开为笛卡尔积,然后展开filter
而不进行其他优化:
d = a.join(b,(F.abs(a['id_a'] - b['id_b']) < 2)).drop(b['grp'])
## == Physical Plan ==
## Project [id_a#2L,grp#1L,id_b#5L]
## +- Filter (abs((id_a#2L - id_b#5L)) < 2)
## +- CartesianProduct
## :- ConvertToSafe
## : +- Project [id#0L AS id_a#2L,(FLOOR((cast(id#0L as double) / 10.0)) * 10) AS grp#1L]
## : +- Scan ExistingRDD[id#0L]
## +- ConvertToSafe
## +- Project [id#3L AS id_b#5L]
## +- Scan ExistingRDD[id#3L]
这是否意味着你的代码(不是笛卡尔的代码 - 你真的想在实践中避免这种代码)会生成一个巨大的中间表吗?
不,它没有。 SortMergeJoin
和filter
都作为单个阶段执行(请参阅DAG)。虽然DataFrame
操作的某些细节可以应用在略低的级别,但它基本上只是Scala Iterators
和as shown in a very illustrative way by Justin Pihony上的转换链,可以将不同的操作压缩在一起不添加任何特定于Spark的逻辑。这两种过滤器都可以在一个任务中应用。