Spark:加入两个相同分区的数据帧时,防止混洗/交换

时间:2019-11-25 15:05:14

标签: apache-spark join pyspark apache-spark-sql pyspark-dataframes

我有两个数据帧df1df2,我想在一个称为visitor_id的高基数字段上多次联接这些表。我只想执行一次初始改组,并进行所有联接,而无需在Spark执行程序之间改组/交换数据。

为此,我创建了另一个名为visitor_partition的列,该列始终为每个visitor_id分配[0, 1000)之间的随机值。我使用了一个自定义分区程序来确保对df1df2进行精确分区,以使每个分区都专门包含来自visitor_partition值的行。最初的重新分区是我唯一要对数据进行随机整理的操作。

我已在s3中将每个数据帧保存到镶木地板中,并按访问者分区进行了分区-对于每个数据帧,这将创建以df1/visitor_partition=0df1/visitor_partition=1 ... df1/visitor_partition=999组织的1000个文件。

现在,我从镶木地板中加载每个数据帧,并通过df1.createOrReplaceTempView('df1')将它们注册为临时视图(与df2相同),然后运行以下查询

SELECT
   ...
FROM
  df1 FULL JOIN df1 ON
    df1.visitor_partition = df2.visitor_partition AND
    df1.visitor_id = df2.visitor_id

从理论上讲,查询执行计划者应意识到此处不需要进行改组。例如,单个执行程序可以从df1/visitor_partition=1df2/visitor_partition=2加载数据并在那里连接行。但是,实际上,Spark 2.4.4的查询计划程序在此处执行完整的数据混洗。

有什么办法可以防止这种洗牌的发生?

1 个答案:

答案 0 :(得分:4)

您可以使用DataFrameWriter(bucketBy)的other documentation方法。

在以下示例中,VisitorID列的值将散列到500个存储桶中。通常,对于联接,Spark将基于VisitorID上的哈希执行交换阶段。但是,在这种情况下,您已经使用哈希对数据进行了预分区。

inputRdd = sc.parallelize(list((i, i%200) for i in range(0,1000000)))

schema = StructType([StructField("VisitorID", IntegerType(), True),
                    StructField("visitor_partition", IntegerType(), True)])

inputdf = inputRdd.toDF(schema)

inputdf.write.bucketBy(500, "VisitorID").saveAsTable("bucketed_table")

inputDf1 = spark.sql("select * from bucketed_table")
inputDf2 = spark.sql("select * from bucketed_table")
inputDf3 = inputDf1.alias("df1").join(inputDf2.alias("df2"), col("df1.VisitorID") == col("df2.VisitorID"))

有时Spark查询优化器仍然选择广播交换,因此在我们的示例中,我们禁用自动广播

spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)

实际计划如下:

== Physical Plan ==
*(3) SortMergeJoin [VisitorID#351], [VisitorID#357], Inner
:- *(1) Sort [VisitorID#351 ASC NULLS FIRST], false, 0
:  +- *(1) Project [VisitorID#351, visitor_partition#352]
:     +- *(1) Filter isnotnull(VisitorID#351)
:        +- *(1) FileScan parquet default.bucketed_6[VisitorID#351,visitor_partition#352] Batched: true, DataFilters: [isnotnull(VisitorID#351)], Format: Parquet, Location: InMemoryFileIndex[dbfs:/user/hive/warehouse/bucketed_6], PartitionFilters: [], PushedFilters: [IsNotNull(VisitorID)], ReadSchema: struct<VisitorID:int,visitor_partition:int>, SelectedBucketsCount: 500 out of 500
+- *(2) Sort [VisitorID#357 ASC NULLS FIRST], false, 0
   +- *(2) Project [VisitorID#357, visitor_partition#358]
      +- *(2) Filter isnotnull(VisitorID#357)
         +- *(2) FileScan parquet default.bucketed_6[VisitorID#357,visitor_partition#358] Batched: true, DataFilters: [isnotnull(VisitorID#357)], Format: Parquet, Location: InMemoryFileIndex[dbfs:/user/hive/warehouse/bucketed_6], PartitionFilters: [], PushedFilters: [IsNotNull(VisitorID)], ReadSchema: struct<VisitorID:int,visitor_partition:int>, SelectedBucketsCount: 500 out of 500

做类似的事情:

inputdf.write.partitionBy("visitor_partition").saveAsTable("partitionBy_2")

实际上为每个分区创建一个带有文件夹的结构。但是,由于Spark连接基于哈希,并且无法利用您的自定义结构,因此无法正常工作。

编辑:我误解了您的示例。我相信您是在谈论诸如partitionBy之类的问题,而不是先前版本中提到的重新分区。