对于在分区内进行分区和排序的缓存数据帧,使用1 Drink1 Beef1
2 Drink2 Beef2
3 Beef3
子句查询键时,我获得了良好的性能,但是使用同一键上的一个小表执行联接时,性能却下降了。
请参阅下面的示例数据集where
,其中有10Kx44K = 438M行。
dftest
以下查询现在大约需要一秒钟(在具有2个工作程序的小型集群上):
sqlContext.sql(f'set spark.sql.shuffle.partitions={32}')
sqlContext.clearCache()
sc.setCheckpointDir('/checkpoint/temp')
import datetime
from pyspark.sql.functions import *
from pyspark.sql import Row
start_date = datetime.date(1900, 1, 1)
end_date = datetime.date(2020, 1, 1)
dates = [ start_date + datetime.timedelta(n) for n in range(int ((end_date - start_date).days))]
dfdates=spark.createDataFrame(list(map(lambda x: Row(date=x), dates))) # some dates
dfrange=spark.createDataFrame(list(map(lambda x: Row(number=x), range(10000)))) # some number range
dfjoin = dfrange.crossJoin(dfdates)
dftest = dfjoin.withColumn("random1", round(rand()*(10-5)+5,0)).withColumn("random2", round(rand()*(10-5)+5,0)).withColumn("random3", round(rand()*(10-5)+5,0)).withColumn("random4", round(rand()*(10-5)+5,0)).withColumn("random5", round(rand()*(10-5)+5,0)).checkpoint()
dftest = dftest.repartition("number").sortWithinPartitions("number", "date").cache()
dftest.count() # 438,290,000 rows
但是,当我编写与联接类似的条件时,它需要2分钟:
dftest.where("number = 1000 and date = \"2001-04-04\"").count()
我原本希望它能快得差不多。特别是因为我希望更大的数据帧已经被群集和缓存。查看计划:
dfsub = spark.createDataFrame([(10,"1900-01-02",1),
(1000,"2001-04-04",2),
(4000,"2002-05-05",3),
(5000,"1950-06-06",4),
(9875,"1980-07-07",5)],
["number","date", "dummy"]).repartition("number").sortWithinPartitions("number", "date").cache()
df_result = dftest.join(dfsub, ( dftest.number == dfsub.number ) & ( dftest.date == dfsub.date ), 'inner').cache()
df_result.count() # takes 2 minutes (result = 5)
似乎花费大量时间按数字和日期对较大的数据框进行排序(此行:== Physical Plan ==
InMemoryTableScan [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797, number#945L, date#946, dummy#947L]
+- InMemoryRelation [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797, number#945L, date#946, dummy#947L], StorageLevel(disk, memory, deserialized, 1 replicas)
+- *(3) SortMergeJoin [number#771L, cast(date#769 as string)], [number#945L, date#946], Inner
:- *(1) Sort [number#771L ASC NULLS FIRST, cast(date#769 as string) ASC NULLS FIRST], false, 0
: +- *(1) Filter (isnotnull(number#771L) && isnotnull(date#769))
: +- InMemoryTableScan [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797], [isnotnull(number#771L), isnotnull(date#769)]
: +- InMemoryRelation [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797], StorageLevel(disk, memory, deserialized, 1 replicas)
: +- Sort [number#771L ASC NULLS FIRST, date#769 ASC NULLS FIRST], false, 0
: +- Exchange hashpartitioning(number#771L, 32)
: +- *(1) Scan ExistingRDD[number#771L,date#769,random1#775,random2#779,random3#784,random4#790,random5#797]
+- *(2) Filter (isnotnull(number#945L) && isnotnull(date#946))
+- InMemoryTableScan [number#945L, date#946, dummy#947L], [isnotnull(number#945L), isnotnull(date#946)]
+- InMemoryRelation [number#945L, date#946, dummy#947L], StorageLevel(disk, memory, deserialized, 1 replicas)
+- Sort [number#945L ASC NULLS FIRST, date#946 ASC NULLS FIRST], false, 0
+- Exchange hashpartitioning(number#945L, 32)
+- *(1) Scan ExistingRDD[number#945L,date#946,dummy#947L]
)。它给我留下了以下问题:
Sort [number#771L ASC NULLS FIRST, date#769 ASC NULLS FIRST], false, 0
和repartition
的信息。在此类情况下使用sortWithinPartitions
是否有意义?答案 0 :(得分:1)
让我尝试回答您的三个问题:
在分区内,左侧和右侧的排序顺序完全相同,并且对于JOIN子句来说是最佳的,为什么Spark仍会再次对分区进行排序?
由于排序列date
中的数据类型不同,两个DataFrame中的排序顺序不同,在dfsub
中是StringType
,在dftest
中是DateType
,因此在连接期间,Spark会发现两个分支中的顺序是不同的,因此强制使用Sort
。
由于5个连接记录匹配(最多)5个分区,为什么要评估所有分区?
在查询计划处理期间,Spark不知道小型DataFrame中有多少个非空分区,因此它必须处理所有分区。
Catalyst似乎没有使用缓存数据帧的repartition和sortWithinPartitions信息。在这种情况下使用sortWithinPartitions是否有意义?
Spark优化器正在使用repartition
和sortWithinPartitions
中的信息,但是有一些有关其工作原理的警告。要修正您的查询,对联接中使用的同一列(包括两列)进行重新分区(而不仅仅是一列)也很重要。原则上,这不是必须的,并且正在解决相关的jira。
这是我对查询的建议更改:
将date
列的类型更改为dftest
中的StringType(或类似地更改为dfsub
中的DateType):
dftest.withColumn("date", col("date").cast('string'))
两个数据框都发生变化
.repartition("number")
到
.repartition("number", "date")
完成这些更改后,您应该获得如下计划:
*(3) SortMergeJoin [number#1410L, date#1653], [number#1661L, date#1662], Inner
:- Sort [number#1410L ASC NULLS FIRST, date#1653 ASC NULLS FIRST], false, 0
: +- Exchange hashpartitioning(number#1410L, date#1653, 200)
: +- *(1) Project [number#1410L, cast(date#1408 as string) AS date#1653, random1#1540, random2#1544, random3#1549, random4#1555, random5#1562]
: +- *(1) Filter (isnotnull(number#1410L) && isnotnull(cast(date#1408 as string)))
: +- *(1) Scan ExistingRDD[number#1410L,date#1408,random1#1540,random2#1544,random3#1549,random4#1555,random5#1562]
+- Sort [number#1661L ASC NULLS FIRST, date#1662 ASC NULLS FIRST], false, 0
+- Exchange hashpartitioning(number#1661L, date#1662, 200)
+- *(2) Filter (isnotnull(number#1661L) && isnotnull(date#1662))
+- *(2) Scan ExistingRDD[number#1661L,date#1662,dummy#1663L]
因此,该计划的每个分支中只有一个Exchange
和一个Sort
,这两个都来自您在自己的调用中调用的repartition
和sortWithinPartition
转换和连接不会引起更多排序或改组。另外请注意,由于我没有使用缓存,因此在我的计划中没有InMemoryTableScan
。