我有一个按列分区的数据(比如id
),我把这个数据集保存了一些地方。我不时地得到一个具有相同结构的较小的增量数据集,我基本上必须根据我的id
以date
列来确定哪条记录是最新的。 (我不会把它写在同一个地方,我将整个新blob保存在其他地方。)
我有两种方法可以做到这一点 - 在窗口中分组并获取最高date
的行。或者通过dropDuplicates
,依靠事实,我的数据是订购的。 (我宁愿使用前者,但我一直在尝试各种各样的事情。)
一个大问题是每个id
组都不可忽略不计(几千兆字节),所以我希望Spark(有n
名工作人员)会理解,因为我正在阅读{ {1}} - 分区数据并编写id
- 分区数据,它会立即处理id
个ID并不断将它们写入我的存储空间,因为它已经完成了之前的新内容的。
不幸的是,似乎正在发生的事情是,在将任何内容写入磁盘之前,Spark会在一个大作业中处理所有n
组(并自然地溢出到磁盘)。它真的很慢。
问题是:有没有办法强制Spark处理这些群组并在他们准备好后立即编写?同样,它们是分区的,因此其他任务都不会影响我的分区。
这里有一些代码可以重现问题:
id
# generate dummy data first
import random
from typing import List
from datetime import datetime, timedelta
from pyspark.sql.functions import desc, col, row_number
from pyspark.sql.window import Window
from pyspark.sql.dataframe import DataFrame
def gen_data(n: int) -> List[tuple]:
names = 'foo, bar, baz, bak'.split(', ')
return [(random.randint(1, 25), random.choice(names), datetime.today() - timedelta(days=random.randint(1, 100))) \
for j in range(n)]
def get_df(n: int) -> DataFrame:
return spark.createDataFrame(gen_data(n), ['id', 'name', 'date'])
n = 10_000
df = get_df(n)
dd = get_df(n*10)
df.write.mode('overwrite').partitionBy('id').parquet('outputs/first')
dd.write.mode('overwrite').partitionBy('id').parquet('outputs/second')
和d1
都由d2
分区,结果数据集也是如此,但它没有反映在计划中:
id
我还试图明确说明分区键(否则代码是相同的):
w = Window().partitionBy('id').orderBy(desc('date'))
d1 = spark.read.parquet('outputs/first')
d2 = spark.read.parquet('outputs/second')
d1.union(d2).\
withColumn('rn', row_number().over(w)).filter(col('rn') == 1).drop('rn').\
write.mode('overwrite').partitionBy('id').parquet('outputs/window')
这里使用d1 = spark.read.parquet('outputs/first').repartition('id')
d2 = spark.read.parquet('outputs/second').repartition('id')
d1.union(d2).\
withColumn('rn', row_number().over(w)).filter(col('rn') == 1).drop('rn').\
write.mode('overwrite').partitionBy('id').parquet('outputs/window2')
:
dropDuplicates
我也试着强调我的联盟仍然使用类似的东西进行分区,但是再次无济于事:
d1 = spark.read.parquet('outputs/first')
d2 = spark.read.parquet('outputs/second')
d1.union(d2).\
dropDuplicates(subset=['id']).\
write.mode('overwrite').partitionBy('id').parquet('outputs/window3')
我可以列出所有分区(df.union(d2).repartition('id').\
.withColumn...
s),逐个加载它们,同时利用分区修剪,重复数据删除和写入。但这似乎是不必要的额外样板。或者是否可以通过id
?
更新(2018-03-27):
事实证明,有关分区的信息确实以某种方式存在于窗口功能中,因为当我在最后进行过滤时,会对输入进行分区修剪:
foreach
结果
d1 = spark.read.parquet('outputs/first')
d2 = spark.read.parquet('outputs/second')
w = Window().partitionBy('id', 'name').orderBy(desc('date'))
d1.union(d2).withColumn('rn', row_number().over(w)).filter(col('rn') == 1).filter(col('id') == 12).explain(True)
所以它确实只读取了两个分区,每个文件一个。所以我可以,而不是循环,只需一次运行一个过滤器的代码(过滤器在窗口函数和== Physical Plan ==
*(4) Filter (isnotnull(rn#387) && (rn#387 = 1))
+- Window [row_number() windowspecdefinition(id#187, name#185, date#186 DESC NULLS LAST, specifiedwindowframe(RowFrame, unboundedpreceding$(), currentrow$())) AS rn#387], [id#187, name#185], [date#186 DESC NULLS LAST]
+- *(3) Sort [id#187 ASC NULLS FIRST, name#185 ASC NULLS FIRST, date#186 DESC NULLS LAST], false, 0
+- Exchange hashpartitioning(id#187, name#185, 200)
+- Union
:- *(1) FileScan parquet [name#185,date#186,id#187] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/.../spark_perf_partitions/outputs..., PartitionCount: 1, PartitionFilters: [isnotnull(id#187), (id#187 = 12)], PushedFilters: [], ReadSchema: struct<name:string,date:timestamp>
+- *(2) FileScan parquet [name#191,date#192,id#193] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/.../spark_perf_partitions/outputs..., PartitionCount: 1, PartitionFilters: [isnotnull(id#193), (id#193 = 12)], PushedFilters: [], ReadSchema: struct<name:string,date:timestamp>
之间)。单调乏味且不太实用,但可能比将所有内容泄露到磁盘上更快。
答案 0 :(得分:0)
是的,这正是火花分区的工作方式。因此,它计算整个谱系,然后在磁盘上以分区形式写入。这有几个优点。其中一个重要原因是并行写入。因此,当计算完成时,spark可以在磁盘上并行写入所有分区。这显着改善了性能。
如果你想在数据就绪时写入,你也可以通过不同的Ids过滤数据帧,并在循环中计算进程并写入。但是,根据我的经验,这种方法需要在同一数据帧上进行多次迭代,从而导致巨大的性能损失。