webdevqa.jp.net

Spark foldLeft&withColumnを使用してgroupby / pivot / agg / collect_listに代わるSQLにより、パフォーマンスを向上

私はSpark 3つの列で構成されるDataFrameを持っています:

_ id | col1 | col2 
-----------------
 x  |  p1  |  a1  
-----------------
 x  |  p2  |  b1
-----------------
 y  |  p2  |  b2
-----------------
 y  |  p2  |  b3
-----------------
 y  |  p3  |  c1
_

df.groupBy("id").pivot("col1").agg(collect_list("col2"))を適用した後、次のデータフレーム(aggDF)を取得しています。

_+---+----+--------+----+
| id|  p1|      p2|  p3|
+---+----+--------+----+
|  x|[a1]|    [b1]|  []|
|  y|  []|[b2, b3]|[c1]|
+---+----+--------+----+
_

次に、id列以外の列の名前を見つけます。

_val cols = aggDF.columns.filter(x => x != "id")
_

その後、cols.foldLeft(aggDF)((df, x) => df.withColumn(x, when(size(col(x)) > 0, col(x)).otherwise(lit(null))))を使用して空の配列をnullに置き換えます。列数が増えると、このコードのパフォーマンスが低下します。さらに、文字列列の名前val stringColumns = Array("p1","p3")があります。次の最終データフレームを取得したいと思います。

_+---+----+--------+----+
| id|  p1|      p2|  p3|
+---+----+--------+----+
|  x| a1 |    [b1]|null|
|  y|null|[b2, b3]| c1 |
+---+----+--------+----+
_

最終的なデータフレームを達成するために、この問題のより良い解決策はありますか?

5

現在のコードは、構造化された2つのパフォーマンスコストを支払います

  • Alexandrosが述べたように、DataFrame変換ごとに1つの触媒分析を支払うため、他の数百または数千の列をループすると、ジョブが実際に送信される前にドライバーに費やされる時間がわかります。これが重要な問題である場合は、withColumnsでfoldLeftの代わりに単一の選択ステートメントを使用できますが、次の点のため、これによって実行時間が大きく変わることはありません

  • When()。otherwise()などの式を単一の選択ステートメントとして最適化できる列で使用すると、コードジェネレーターはすべての列を処理する単一の大きなメソッドを生成します。数百列を超える場合、結果として得られるメソッドはデフォルトでJVMによってJITコンパイルされず、実行パフォーマンスが非常に遅くなる可能性があります(最大JIT対応メソッドはHotspotで8kバイトコードです)。

エグゼキューターログを調べて、2番目の問題が発生したかどうかを検出し、JITできない大きすぎるメソッドで警告が表示されるかどうかを確認できます。

これを試して解決するにはどうすればよいですか?

1-ロジックを変更する

ウィンドウ変換を使用して、ピボットの前の空のセルをフィルタリングできます

import org.Apache.spark.sql.expressions.Window

val finalDf = df
  .withColumn("count", count('col2) over Window.partitionBy('id,'col1)) 
  .filter('count > 0)
  .groupBy("id").pivot("col1").agg(collect_list("col2"))

これは実際のデータセットに応じて高速になる場合とそうでない場合があります。ピボット自体も大きなselectステートメント式を生成するため、col1の値が約500を超えると、メソッドの大きなしきい値に達する可能性があります。これをオプション2と組み合わせることもできます。

2-JVMを試してみてください

エグゼキューターにextraJavaOptionを追加して、JVMに8kを超えるホットメソッドを試してJITに依頼するように要求できます。

たとえば、spark-submitにオプション--conf "spark.executor.extraJavaOptions=-XX:-DontCompileHugeMethods"を追加して、ピボット実行時間にどのように影響するかを確認します。

実際のデータセットの詳細なしに大幅な速度向上を保証することは困難ですが、一見の価値があります。

1
rluta

https://medium.com/@manuzhang/the-hidden-cost-of-spark-withcolumn-8ffea517c015 を見ると、foldLeftを持つwithColumnには既知のパフォーマンス問題があることがわかります。以下に示すように、varargsを使用してSelectを選択します。

Collect_listが問題であると確信していない。ロジックの最初のセットも保持しました。ピボットはジョブを開始して、ピボットの個別の値を取得します。これは受け入れられているアプローチです。自分でロールしようとしても意味がないように見えますが、他の答えが間違っているか、Spark 2.4が改善されました。

import spark.implicits._ 
import org.Apache.spark.sql.functions._

// Your code & assumig id is only col of interest as in THIS question. More elegant than 1st posting.
val df = Seq( ("x","p1","a1"), ("x","p2","b1"), ("y","p2","b2"), ("y","p2","b3"), ("y","p3","c1")).toDF("id", "col1", "col2")
val aggDF = df.groupBy("id").pivot("col1").agg(collect_list("col2")) 
//aggDF.show(false)

val colsToSelect = aggDF.columns  // All in this case, 1st col id handled by head & tail

val aggDF2 = aggDF.select((col(colsToSelect.head) +: colsToSelect.tail.map(col => when(size(aggDF(col)) === 0,lit(null)).otherwise(aggDF(col)).as(s"$col"))):_*)
aggDF2.show(false)

戻り値:

+---+----+--------+----+
|id |p1  |p2      |p3  |
+---+----+--------+----+
|x  |[a1]|[b1]    |null|
|y  |null|[b2, b3]|[c1]|
+---+----+--------+----+

また、素敵な読み取りBTW: https://lansalo.com/2018/05/13/spark-how-to-add-multiple-columns-in-dataframes-and-how-not-to/ =。列の数が多いほど、効果が顕著になります。最後に読者が関連するポイントを作成します。

列数が多いほど、選択アプローチを使用するとパフォーマンスが向上すると思います。

UPD:休暇中、私は両方のアプローチをSpark 2.4.xで試してみましたが、最大1000列まで観察可能な違いはありませんでした。

0
thebluephantom