如何定义DataFrame的分区?


128

我已经开始在Spark 1.4.0中使用Spark SQL和DataFrames。我想在Scala的DataFrames上定义一个自定义分区程序,但不知道如何做到这一点。

我正在使用的数据表之一包含一个按帐户分类的事务列表,类似于以下示例。

Account   Date       Type       Amount
1001    2014-04-01  Purchase    100.00
1001    2014-04-01  Purchase     50.00
1001    2014-04-05  Purchase     70.00
1001    2014-04-01  Payment    -150.00
1002    2014-04-01  Purchase     80.00
1002    2014-04-02  Purchase     22.00
1002    2014-04-04  Payment    -120.00
1002    2014-04-04  Purchase     60.00
1003    2014-04-02  Purchase    210.00
1003    2014-04-03  Purchase     15.00

至少在最初,大多数计算将在帐户内的交易之间进行。因此,我希望对数据进行分区,以便一个帐户的所有交易都在同一个Spark分区中。

但是我没有找到定义它的方法。DataFrame类具有一个称为“ repartition(Int)”的方法,您可以在其中指定要创建的分区数。但是我没有看到任何可用于为DataFrame定义自定义分区程序的方法,例如可以为RDD指定的方法。

源数据存储在Parquet中。我确实看到,在将DataFrame写入Parquet时,您可以指定要进行分区的列,因此大概我可以通过'Account'列告诉Parquet对其数据进行分区。但是可能有数以百万计的帐户,如果我正确地理解了Parquet,它将为每个帐户创建一个不同的目录,因此这听起来不是一个合理的解决方案。

有没有一种方法可以让Spark对这个DataFrame进行分区,以使一个帐户的所有数据都在同一分区中?



如果您可以告诉Parquet按帐户进行分区,则可以按分区进行分区int(account/someInteger),从而使每个目录获得合理数量的帐户。
保罗

1
@ABC:我确实看到了该链接。正在寻找与该partitionBy(Partitioner)方法等效的方法,但要使用DataFrames而不是RDD。现在,我确实看到partitionBy仅适用于Pair RDD,不确定为什么。

@Paul:我确实考虑过做您描述的事情。有几件事使我退缩:

继续....(1)用于“镶木地板分区”。我找不到任何说明Spark分区实际上将使用Parquet分区的文档。(2)如果我了解Parquet文档,则需要定义一个新字段“ foo”,那么每个Parquet目录将具有一个名称,例如“ foo = 123”。但是,如果我构造一个涉及AccountID的查询,Spark / hive / parquet怎么会知道fooAccountID之间有任何联系?
2015年

Answers:


177

火花> = 2.3.0

SPARK-22614公开范围分区。

val partitionedByRange = df.repartitionByRange(42, $"k")

partitionedByRange.explain
// == Parsed Logical Plan ==
// 'RepartitionByExpression ['k ASC NULLS FIRST], 42
// +- AnalysisBarrier Project [_1#2 AS k#5, _2#3 AS v#6]
// 
// == Analyzed Logical Plan ==
// k: string, v: int
// RepartitionByExpression [k#5 ASC NULLS FIRST], 42
// +- Project [_1#2 AS k#5, _2#3 AS v#6]
//    +- LocalRelation [_1#2, _2#3]
// 
// == Optimized Logical Plan ==
// RepartitionByExpression [k#5 ASC NULLS FIRST], 42
// +- LocalRelation [k#5, v#6]
// 
// == Physical Plan ==
// Exchange rangepartitioning(k#5 ASC NULLS FIRST, 42)
// +- LocalTableScan [k#5, v#6]

SPARK-22389数据源API v2中公开了外部格式分区

火花> = 1.6.0

在Spark> = 1.6中,可以按列使用分区进行查询和缓存。请参阅:SPARK-11410SPARK-4849使用repartition方法:

val df = Seq(
  ("A", 1), ("B", 2), ("A", 3), ("C", 1)
).toDF("k", "v")

val partitioned = df.repartition($"k")
partitioned.explain

// scala> df.repartition($"k").explain(true)
// == Parsed Logical Plan ==
// 'RepartitionByExpression ['k], None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- LogicalRDD [_1#5,_2#6], MapPartitionsRDD[3] at rddToDataFrameHolder at <console>:27
// 
// == Analyzed Logical Plan ==
// k: string, v: int
// RepartitionByExpression [k#7], None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- LogicalRDD [_1#5,_2#6], MapPartitionsRDD[3] at rddToDataFrameHolder at <console>:27
// 
// == Optimized Logical Plan ==
// RepartitionByExpression [k#7], None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- LogicalRDD [_1#5,_2#6], MapPartitionsRDD[3] at rddToDataFrameHolder at <console>:27
// 
// == Physical Plan ==
// TungstenExchange hashpartitioning(k#7,200), None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- Scan PhysicalRDD[_1#5,_2#6]

不像RDDsSpark Dataset(包括Dataset[Row]akaDataFrame)目前无法使用自定义分区程序。通常,您可以通过创建人为的分区列来解决此问题,但这不会为您提供相同的灵活性。

Spark <1.6.0:

您可以做的一件事是在创建数据之前对输入数据进行预分区 DataFrame

import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
import org.apache.spark.HashPartitioner

val schema = StructType(Seq(
  StructField("x", StringType, false),
  StructField("y", LongType, false),
  StructField("z", DoubleType, false)
))

val rdd = sc.parallelize(Seq(
  Row("foo", 1L, 0.5), Row("bar", 0L, 0.0), Row("??", -1L, 2.0),
  Row("foo", -1L, 0.0), Row("??", 3L, 0.6), Row("bar", -3L, 0.99)
))

val partitioner = new HashPartitioner(5) 

val partitioned = rdd.map(r => (r.getString(0), r))
  .partitionBy(partitioner)
  .values

val df = sqlContext.createDataFrame(partitioned, schema)

由于从DataFrame创建RDD仅需要一个简单的地图阶段,因此应保留现有分区布局*:

assert(df.rdd.partitions == partitioned.partitions)

用相同的方法可以对现有分区进行分区 DataFrame

sqlContext.createDataFrame(
  df.rdd.map(r => (r.getInt(1), r)).partitionBy(partitioner).values,
  df.schema
)

因此,看起来并非不可能。问题是否仍然有意义。我会说大多数情况下不会:

  1. 重新分区是一个昂贵的过程。在典型情况下,大多数数据必须进行序列化,混洗和反序列化。另一方面,可以受益于预分区数据的操作数量相对较小,如果内部API并非设计为利用此属性,则操作数量将进一步受到限制。

    • 在某些情况下会加入,但需要内部支持,
    • 窗口函数使用匹配的分区程序进行调用。与上述相同,仅限于单个窗口定义。不过,它已经在内部进行了分区,因此预分区可能是多余的,
    • 简单的聚合GROUP BY-可以减少临时缓冲区的内存占用量**,但总体成本要高得多。或多或少相当于groupByKey.mapValues(_.reduce)(当前行为)与reduceByKey(预分区)。在实践中不太可能有用。
    • 使用进行数据压缩SqlContext.cacheTable。由于看起来好像正在使用游程长度编码,因此应用OrderedRDDFunctions.repartitionAndSortWithinPartitions可以提高压缩率。
  2. 性能在很大程度上取决于密钥的分配。如果偏斜,将导致资源利用不足。在最坏的情况下,根本不可能完成这项工作。

  3. 使用高级声明性API的全部目的是使自己与低级实现细节隔离。正如@dwysakowicz@RomiKuntsman所述,优化是Catalyst Optimizer的工作。这是一种非常复杂的野兽,我真的怀疑您是否可以轻松改进它,而不必深入了解其内部。

相关概念

使用JDBC源进行分区

JDBC数据源支持predicates参数。可以如下使用:

sqlContext.read.jdbc(url, table, Array("foo = 1", "foo = 3"), props)

它为每个谓词创建一个JDBC分区。请记住,如果使用单个谓词创建的集合不是不相交的,您将在结果表中看到重复项。

partitionBy 方法中 DataFrameWriter

Spark DataFrameWriter提供了partitionBy一种可用于在写入时对数据进行“分区”的方法。它使用提供的一组列来分离写入数据

val df = Seq(
  ("foo", 1.0), ("bar", 2.0), ("foo", 1.5), ("bar", 2.6)
).toDF("k", "v")

df.write.partitionBy("k").json("/tmp/foo.json")

这样可以基于键对查询进行谓词下推:

val df1 = sqlContext.read.schema(df.schema).json("/tmp/foo.json")
df1.where($"k" === "bar")

但这不等于DataFrame.repartition。特别是像这样的聚合:

val cnts = df1.groupBy($"k").sum()

仍然需要TungstenExchange

cnts.explain

// == Physical Plan ==
// TungstenAggregate(key=[k#90], functions=[(sum(v#91),mode=Final,isDistinct=false)], output=[k#90,sum(v)#93])
// +- TungstenExchange hashpartitioning(k#90,200), None
//    +- TungstenAggregate(key=[k#90], functions=[(sum(v#91),mode=Partial,isDistinct=false)], output=[k#90,sum#99])
//       +- Scan JSONRelation[k#90,v#91] InputPaths: file:/tmp/foo.json

bucketBy 方法中 DataFrameWriter(火花> = 2.0):

bucketBy具有与相似的应用程序,partitionBy但仅适用于表(saveAsTable)。桶信息可用于优化联接:

// Temporarily disable broadcast joins
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)

df.write.bucketBy(42, "k").saveAsTable("df1")
val df2 = Seq(("A", -1.0), ("B", 2.0)).toDF("k", "v2")
df2.write.bucketBy(42, "k").saveAsTable("df2")

// == Physical Plan ==
// *Project [k#41, v#42, v2#47]
// +- *SortMergeJoin [k#41], [k#46], Inner
//    :- *Sort [k#41 ASC NULLS FIRST], false, 0
//    :  +- *Project [k#41, v#42]
//    :     +- *Filter isnotnull(k#41)
//    :        +- *FileScan parquet default.df1[k#41,v#42] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/spark-warehouse/df1], PartitionFilters: [], PushedFilters: [IsNotNull(k)], ReadSchema: struct<k:string,v:int>
//    +- *Sort [k#46 ASC NULLS FIRST], false, 0
//       +- *Project [k#46, v2#47]
//          +- *Filter isnotnull(k#46)
//             +- *FileScan parquet default.df2[k#46,v2#47] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/spark-warehouse/df2], PartitionFilters: [], PushedFilters: [IsNotNull(k)], ReadSchema: struct<k:string,v2:double>

* 分区布局是指仅数据分布。partitionedRDD不再具有分区程序。**假设没有早期预测。如果聚合仅覆盖列的一小部分,则可能毫无收益。


@bychance是和否。数据布局将保留,但AFAIK不会给您带来诸如分区修剪之类的好处。
zero323 '16

@ zero323谢谢,有没有一种方法可以检查镶木地板文件的分区分配以验证df.save.write确实保存了布局?如果我执行df.repartition(“ A”),然后执行df.write.repartitionBy(“ B”),则物理文件夹结构将按B进行分区,并且在每个B值文件夹中,仍将按一个?
bychance

2
@bychance DataFrameWriter.partitionBy在逻辑上与并不相同DataFrame.repartition。前者不会洗牌,它只是将输出分开。关于第一个问题。-每个分区都保存有数据,没有随机播放。您可以通过读取单个文件轻松地进行检查。但是,仅Spark一个人就无法知道这是否是您真正想要的。
zero323 '16

11

在Spark <1.6中,如果您创建HiveContext,而不是普通的SqlContext,则可以使用HiveQL DISTRIBUTE BY colX...(确保N个reducer的每个获得x的不重叠范围)和CLUSTER BY colX...(Distribute By和Sort By的快捷方式);例如,

df.registerTempTable("partitionMe")
hiveCtx.sql("select * from partitionMe DISTRIBUTE BY accountId SORT BY accountId, date")

不知道如何配合Spark DF API。普通的SqlContext不支持这些关键字(请注意,您不需要具有Hive Meta存储即可使用HiveContext)

编辑: Spark 1.6+现在在本机DataFrame API中具有此功能


1
保存数据框时是否保留了分区?
2015年

您如何控制在蜂巢ql示例中可以有多少个分区?例如,在RDD对中,您可以执行以下操作来创建5个分区:val partitioner = new HashPartitioner(5)
Minnie

好,找到答案了,可以这样完成:sqlContext.setConf(“ spark.sql.shuffle.partitions”,“ 5”)我无法编辑以前的注释,因为我错过了5分钟的限制
Minnie

7

因此,从某种答案开始:)-您不能

我不是专家,但据我了解DataFrames,它们不等于rdd,DataFrame没有Partitioner之类的东西。

通常,DataFrame的想法是提供另一个层次的抽象本身来处理此类问题。将DataFrame上的查询转换为逻辑计划,然后将其进一步转换为对RDD的操作。您建议的分区可能会自动应用,或者至少应该应用。

如果您不信任SparkSQL会提供某种最佳工作,则可以始终按照注释中的建议将DataFrame转换为RDD [Row]。


7

使用以下方式返回的DataFrame:

yourDF.orderBy(account)

没有明确的使用方法 partitionBy在DataFrame上,仅在PairRDD上可以使用,但是对DataFrame进行排序时,它将在LogicalPlan中使用它,并且在需要对每个帐户进行计算时会有所帮助。

我只是偶然发现了一个完全相同的问题,即要按帐户划分的数据框。我假设当您说“希望对数据进行分区,以使一个帐户的所有交易都在同一个Spark分区中”时,您希望它具有扩展性和性能,但是您的代码并不依赖于它(例如使用mapPartitions()等),对吗?


3
如果您使用的是MapPartitions,那么如果您的代码确实依赖它怎么办?
NightWolf 2015年

2
您可以将DataFrame转换为RDD,然后对其进行分区(例如,使用aggregatByKey()并传递自定义分区程序)
Romi Kuntsman 2015年

5

我能够使用RDD做到这一点。但是我不知道这是否是您可以接受的解决方案。将DF用作RDD后,您可以申请repartitionAndSortWithinPartitions执行数据的自定义重新分区。

这是我使用的示例:

class DatePartitioner(partitions: Int) extends Partitioner {

  override def getPartition(key: Any): Int = {
    val start_time: Long = key.asInstanceOf[Long]
    Objects.hash(Array(start_time)) % partitions
  }

  override def numPartitions: Int = partitions
}

myRDD
  .repartitionAndSortWithinPartitions(new DatePartitioner(24))
  .map { v => v._2 }
  .toDF()
  .write.mode(SaveMode.Overwrite)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.