DataFrameのパーティションを定義するにはどうすればよいですか?


128

Spark 1.4.0でSpark SQLとDataFramesを使い始めました。ScalaのDataFrameでカスタムパーティショナーを定義したいのですが、その方法がわかりません。

私が使用しているデータテーブルの1つには、次の例のsilimarのトランザクションのリストがアカウントごとに含まれています。

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)'というメソッドがあり、作成するパーティションの数を指定できます。しかし、RDDに指定できるような、DataFrameのカスタムパーティショナーを定義するために利用できるメソッドはありません。

ソースデータはParquetに保存されます。DataFrameをParquetに書き込むときに、パーティション化する列を指定できることがわかったので、おそらく、Parquetにデータを「アカウント」列でパーティション化するように指示できます。しかし、数百万のアカウントが存在する可能性があり、Parquetを正しく理解している場合は、アカウントごとに個別のディレクトリが作成されるため、適切な解決策とは思えません。

アカウントのすべてのデータが同じパーティションにあるように、SparkでこのDataFrameをパーティション分割する方法はありますか?


このリンクを確認してください。stackoverflow.com
questions/23127329/

Parquetにアカウントでパーティションを作成するように指示できる場合は、パーティションを作成してint(account/someInteger)、ディレクトリごとに適切な数のアカウントを取得できます。
ポール

1
@ABC:私はそのリンクを見ました。そのpartitionBy(Partitioner)方法と同等のものを探していましたが、RDDの代わりにデータフレームを探していました。現在partitionByペア RDDでのみ使用できることがわかりますが、それがなぜかはわかりません。
レーキ

@Paul:私はあなたが説明することをすることを考えました。いくつかのことが私
レーキ

続く....(1)「寄木張り」です。Sparkパーティション化が実際にParquetパーティション化を使用することを示すドキュメントを見つけることができませんでした。(2)Parquetのドキュメントを理解している場合、新しいフィールド「foo」を定義する必要があります。各Parquetディレクトリの名前は「foo = 123」のようになります。しかし、AccountIDを含むクエリを作成した場合、Spark / hive / parquetはfooAccountIDの間にリンケージがあったことをどのようにして知るのでしょうか?
レーキ

回答:


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-11410およびSPARK-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 DatasetDataset[Row]別名を含むDataFrame)とは異なり、現時点ではカスタムパーティショナーを使用できません。通常は、人工的なパーティション列を作成することで対処できますが、同じ柔軟性は得られません。

スパーク<1.6.0:

できることの1つは、作成する前に入力データを事前分割することです。 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オプティマイザの仕事です。それはかなり洗練された獣であり、私はあなたがその内部を深く掘り下げることなく簡単にそれを改善できることを本当に疑っています。

関連する概念

JDBCソースを使用したパーティション化

JDBCデータソースはpredicates引数をサポートします。次のように使用できます。

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

述語ごとに単一のJDBCパーティションを作成します。個々の述語を使用して作成されたセットがばらばらでない場合、結果のテーブルに重複が表示されることに注意してください。

partitionBy の方法 DataFrameWriter

Spark DataFrameWriterpartitionBy、書き込み時にデータを「パーティション化」するために使用できるメソッドを提供します。提供された列のセットを使用して書き込み時にデータを分離します

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

bucketByDataFrameWriter(Spark> = 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はい、いいえ。データレイアウトは保持されますが、私の知る限り、パーティションプルーニングのような利点はありません。
zero323 2016年

@ zero323おかげで、寄木細工のファイルのパーティション割り当てをチェックしてdf.save.writeを検証する方法はありますか?そして、もし私がdf.repartition( "A")を実行し、次にdf.write.repartitionBy( "B")を実行すると、物理フォルダー構造はBによってパーティション化され、各B値フォルダー内で、 A?
bychance

2
@bychance DataFrameWriter.partitionByは論理的にはと同じではありませんDataFrame.repartition。以前はシャッフルせず、単に出力を分離します。最初の質問について。-データはパーティションごとに保存され、シャッフルはありません。個々のファイルを読むことで簡単に確認できます。しかし、Sparkだけでは、これが本当に必要なものであるかどうかを知る方法がありません。
zero323 2016年

11

Spark <1.6 HiveContextでは、プレーンな古いものではなくを作成する場合SqlContextHiveQL を使用できますDISTRIBUTE BY colX...(N個のレデューサーのそれぞれがxの重複しない範囲を取得することを保証します)&CLUSTER BY colX...(配布元と並べ替えのショートカット)。

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に追加しました


1
データフレームが保存されるときにパーティションは保持されますか?
Sim

hive qlの例で使用できるパーティションの数をどのように制御しますか?たとえばペアRDDアプローチでは、これを実行して5つのパーティションを作成できます。val partitioner = new HashPartitioner(5)
Minnie

わかりました、答えが見つかりました。次のように実行できます:sqlContext.setConf( "spark.sql.shuffle.partitions"、 "5")5分制限を逃したため、以前のコメントを編集できませんでした
Minnie

7

だから、ある種の答えから始めるには:)-あなたはできません

私は専門家ではありませんが、DataFrameを理解している限り、それらはrddとは異なり、DataFrameにはPartitionerのようなものはありません。

一般に、DataFrameのアイデアは、そのような問題自体を処理する別のレベルの抽象化を提供することです。DataFrameのクエリは論理プランに変換され、さらにRDDの操作に変換されます。提案したパーティショニングはおそらく自動的に適用されるか、少なくとも適用されるはずです。

SparkSQLが何らかの最適なジョブを提供すると信頼していない場合は、コメントで提案されているように、常にDataFrameをRDD [Row]に変換できます。


7

以下によって返されるDataFrameを使用します。

yourDF.orderBy(account)

明示的な使用方法はありません partitionByDataFrameで、PairRDDでのみできますが、DataFrameを並べ替えると、LogicalPlanでそれを使用し、各アカウントで計算を行う必要があるときに役立ちます。

同じ正確な問題に遭遇しました。アカ​​ウントごとに分割したいデータフレームがあります。「アカウントのすべてのトランザクションが同じSparkパーティションにあるようにデータをパーティション化したい」と言ったときに、スケールとパフォーマンスのためにそれが必要であると思いますが、コードはそれに依存していません(たとえばmapPartitions()など)、そうですか?


3
mapPartitionsを使用しているために、コードがコードに依存している場合はどうでしょうか?
NightWolf 2015

2
DataFrameをRDDに変換してからパーティション化できます(たとえば、aggregatByKey()を使用してカスタムパーティ
ショナーを渡します

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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.