Scala 如何定义数据帧的分区?

Scala 如何定义数据帧的分区?,scala,apache-spark,dataframe,apache-spark-sql,partitioning,Scala,Apache Spark,Dataframe,Apache Spark Sql,Partitioning,我已经开始在Spark 1.4.0中使用Spark SQL和数据帧。我想在Scala中定义数据帧上的自定义分区器,但不知道如何实现 我正在使用的一个数据表包含一个事务列表,按帐户分类,如下例所示 Account Date Type Amount 1001 2014-04-01 Purchase 100.00 1001 2014-04-01 Purchase 50.00 1001 2014-04-05 Purchase 70

我已经开始在Spark 1.4.0中使用Spark SQL和数据帧。我想在Scala中定义数据帧上的自定义分区器,但不知道如何实现

我正在使用的一个数据表包含一个事务列表,按帐户分类,如下例所示

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类有一个名为“重新分区(Int)”的方法,您可以在其中指定要创建的分区数。但我没有看到任何方法可用于为数据帧定义自定义分区器,例如可以为RDD指定

源数据存储在拼花地板中。我确实看到,当向Parquet写入数据帧时,您可以指定一个列来进行分区,因此我可以告诉Parquet按“Account”列对其数据进行分区。但可能有数百万个帐户,如果我正确理解拼花,它会为每个帐户创建一个不同的目录,所以这听起来不是一个合理的解决方案


有没有办法让Spark对此数据帧进行分区,以便帐户的所有数据都在同一分区中?

使用以下方法返回的数据帧:

yourDF.orderBy(account)
没有明确的方法在数据帧上使用
partitionBy
,仅在PairRDD上使用,但是当您对数据帧进行排序时,它将在其逻辑计划中使用该方法,这将有助于您对每个帐户进行计算

我只是偶然发现了同样的问题,有一个我想按帐户划分的数据帧。
我假设,当您说“希望对数据进行分区,以便一个帐户的所有事务都在同一个Spark分区中”时,您希望它具有规模和性能,但您的代码并不依赖于它(例如使用
mapPartitions()
等),对吗?

在Spark<1.6中,如果您创建
HiveContext
,不是普通的老式
SqlContext
例如,您可以使用
distributed BY colX…
(确保N个还原器中的每一个都获得不重叠的x范围)和
CLUSTER BY colX…
(distributed BY和Sort BY的快捷方式)

df.registerTempTable("partitionMe")
hiveCtx.sql("select * from partitionMe DISTRIBUTE BY accountId SORT BY accountId, date")
不确定这是否适合Spark DF api。在普通SqlContext中不支持这些关键字(注意,使用HiveContext不需要配置单元元存储)


编辑:Spark 1.6+现在在本机DataFrame API中有这样的功能,所以首先要回答:)-你不能

我不是专家,但就我所了解的数据帧而言,它们并不等于rdd,并且数据帧没有分区器

一般来说,DataFrame的想法是提供另一个抽象级别来处理这些问题。DataFrame上的查询被转换为逻辑计划,然后再转换为RDD上的操作。您建议的分区可能会自动应用,或者至少应该自动应用


如果您不相信SparkSQL会提供某种最佳的工作,那么您可以按照注释中的建议,将DataFrame转换为RDD[Row]。但我不知道这对你来说是否是一个可接受的解决方案。 一旦DF作为RDD可用,就可以应用来执行数据的自定义重新分区

以下是我使用的一个示例:

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)
火花>=2.3.0 公开范围分区

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]
在中公开外部格式分区

火花>=1.6.0 在Spark>=1.6中,可以使用按列划分的方法进行查询和缓存。请参阅:并使用
重新分区
方法:

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]
由于从
RDD
创建
DataFrame
只需要简单的映射阶段,因此应保留现有分区布局*:

assert(df.rdd.partitions == partitioned.partitions)
与重新分区现有数据帧的方法相同:

sqlContext.createDataFrame(
  df.rdd.map(r => (r.getInt(1), r)).partitionBy(partitioner).values,
  df.schema
)
所以看起来这不是不可能的。问题在于它是否有意义。我会说,大多数情况下,它不会:

  • 重新划分是一个昂贵的过程。在一个典型的场景中,大多数数据必须被序列化、洗牌和反序列化。另一方面,可以从预分区数据中受益的操作数量相对较少,如果内部API未设计为利用此属性,则会进一步受到限制

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

  • 使用高级声明性API的一个要点是将自己与低级实现细节隔离开来。正如前面提到的,优化是的工作。这是一个相当复杂的野兽,我真的怀疑你能在不深入其内部的情况下轻松改进它
  • 相关概念 使用JDBC源进行分区

    JDBC数据源支持。它可以按如下方式使用:

    sqlContext.read.jdbc(url, table, Array("foo = 1", "foo = 3"), props)
    
    它创造
    sqlContext.read.jdbc(url, table, Array("foo = 1", "foo = 3"), props)
    
    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")
    
    val cnts = df1.groupBy($"k").sum()
    
    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
    
    // 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>