Я просмотрел документацию здесь: https: //spark.apache .org / docs / latest / api / python / pyspark.sql.html

Он говорит:

  • для повторного разбиения: полученный DataFrame разделен по хешу.
  • для repartitionByRange: результирующий DataFrame разделен по диапазону.

И в предыдущем вопросе это также упоминается. Однако я до сих пор не понимаю, чем именно они отличаются и какое влияние окажет выбор одного из них?

Что еще более важно , если повторное разбиение приводит к разбиению хэша, какое влияние оказывает предоставление столбцов в качестве аргумента?

8
pallupz 20 Янв 2021 в 15:50

2 ответа

Лучший ответ

Я думаю, что лучше всего посмотреть на разницу экспериментальным путем.

Тестовые кадры данных

Для этого эксперимента я использую следующие два фрейма данных (я показываю код на Scala, но концепция идентична API Python):

// Dataframe with one column "value" containing the values ranging from 0 to 1000000
val df = Seq(0 to 1000000: _*).toDF("value")

// Dataframe with one column "value" containing 1000000 the number 0 in addition to the numbers 5000, 10000 and 100000
val df2 = Seq((0 to 1000000).map(_ => 0) :+ 5000 :+ 10000 :+ 100000: _*).toDF("value")

Теория

  • repartition применяет HashPartitioner, когда предоставлен один или несколько столбцов, и RoundRobinPartitioner, который распределяет данные равномерно по предоставленному количеству разделов. Если предоставлен один столбец (или несколько), эти значения будут хешированы и использованы для определения номера раздела путем вычисления чего-то вроде partition = hash(columns) % numberOfPartitions.

  • repartitionByRange разделит данные на основе диапазона значений столбца. Обычно это используется для непрерывных (не дискретных) значений, таких как любые числа.

Также стоит упомянуть, что для обоих методов, если не указан numPartitions, по умолчанию он разделяет данные Dataframe на spark.sql.shuffle.partitions, настроенный в вашем сеансе Spark, и может быть объединен с помощью Adaptive Query Execution (доступного с Spark 3.х).

Испытательная установка

На основе данных Testdata я всегда применяю один и тот же код:

val testDf = df
// here I will insert the partition logic
    .withColumn("partition", spark_partition_id()) // applying SQL built-in function to determin actual partition
    .groupBy(col("partition"))
    .agg(
      count(col("value")).as("count"),
      min(col("value")).as("min_value"),
      max(col("value")).as("max_value"))
    .orderBy(col("partition"))

testDf.show(false)

Результаты теста

Df.repartition (4, col ("значение"))

Как и ожидалось, мы получаем 4 раздела, и поскольку значения df находятся в диапазоне от 0 до 1000000, мы видим, что их хешированные значения приведут к хорошо распределенному фрейму данных.

+---------+------+---------+---------+
|partition|count |min_value|max_value|
+---------+------+---------+---------+
|0        |249911|12       |1000000  |
|1        |250076|6        |999994   |
|2        |250334|2        |999999   |
|3        |249680|0        |999998   |
+---------+------+---------+---------+

Df.repartitionByRange (4, col ("значение"))

Также в этом случае мы получаем 4 раздела, но на этот раз минимальные и максимальные значения четко показывают диапазоны значений внутри раздела. Он почти равномерно распределен - 250000 значений на раздел.

+---------+------+---------+---------+
|partition|count |min_value|max_value|
+---------+------+---------+---------+
|0        |244803|0        |244802   |
|1        |255376|244803   |500178   |
|2        |249777|500179   |749955   |
|3        |250045|749956   |1000000  |
+---------+------+---------+---------+

Df2.repartition (4, col ("значение"))

Теперь мы используем другой фрейм данных df2. Здесь алгоритм хеширования хеширует только значения 0, 5000, 10000 или 100000. Конечно, хэш значения 0 всегда будет одинаковым, поэтому все нули попадают в один и тот же раздел (в данном случае раздел 3 ). Два других раздела содержат только одно значение.

+---------+-------+---------+---------+
|partition|count  |min_value|max_value|
+---------+-------+---------+---------+
|0        |1      |100000   |100000   |
|1        |1      |10000    |10000    |
|2        |1      |5000     |5000     |
|3        |1000001|0        |0        |
+---------+-------+---------+---------+

Df2.repartition (4)

Без использования содержимого столбца «значение» метод repartition будет распространять сообщения на основе RoundRobin. На всех разделах почти одинаковое количество данных.

+---------+------+---------+---------+
|partition|count |min_value|max_value|
+---------+------+---------+---------+
|0        |250002|0        |5000     |
|1        |250002|0        |10000    |
|2        |249998|0        |100000   |
|3        |250002|0        |0        |
+---------+------+---------+---------+

Df2.repartitionByRange (4, col ("значение"))

Этот случай показывает, что Dataframe df2 плохо определен для повторного разбиения по диапазонам, поскольку почти все значения равны 0. Следовательно, у нас даже получается только два раздела, тогда как раздел 0 содержит все нули.

+---------+-------+---------+---------+
|partition|count  |min_value|max_value|
+---------+-------+---------+---------+
|0        |1000001|0        |0        |
|1        |3      |5000     |100000   |
+---------+-------+---------+---------+
2
mike 20 Янв 2021 в 20:33

Используя df.explain, вы можете получить много информации об этих операциях.

Я использую этот DataFrame для примера:

df = spark.createDataFrame([(i, f"value {i}") for i in range(1, 22, 1)], ["id", "value"])

Переразметка

В зависимости от того, указано ли ключевое выражение (столбец) или нет, метод разделения будет разным. Как вы сказали, это не всегда хеш-секционирование.

df.repartition(3).explain(True)

== Parsed Logical Plan ==
Repartition 3, true
+- LogicalRDD [id#0L, value#1], false

== Analyzed Logical Plan ==
id: bigint, value: string
Repartition 3, true
+- LogicalRDD [id#0L, value#1], false

== Optimized Logical Plan ==
Repartition 3, true
+- LogicalRDD [id#0L, value#1], false

== Physical Plan ==
Exchange RoundRobinPartitioning(3)
+- Scan ExistingRDD[id#0L,value#1]

На сгенерированном физическом плане видно, что RoundRobinPartitioning используется:

Представляет разбиение, в котором строки равномерно распределяются по выходным разделам, начиная со случайного номера целевого раздела и распределяя строки циклическим способом. Это разбиение используется при реализации оператора DataFrame.repartition ().

При использовании повторного разбиения по выражению столбца:

df.repartition(3, "id").explain(True)

== Parsed Logical Plan ==
'RepartitionByExpression ['id], 3
+- LogicalRDD [id#0L, value#1], false

== Analyzed Logical Plan ==
id: bigint, value: string
RepartitionByExpression [id#0L], 3
+- LogicalRDD [id#0L, value#1], false

== Optimized Logical Plan ==
RepartitionByExpression [id#0L], 3
+- LogicalRDD [id#0L, value#1], false

== Physical Plan ==
Exchange hashpartitioning(id#0L, 3)
+- Scan ExistingRDD[id#0L,value#1]

Теперь выбранный метод разбиения: hashpartitioning. В методе хэш-секционирования Java Object.hashCode вычисляется для каждого ключевого выражения, чтобы определить место назначения partition_id путем вычисления по модулю: key.hashCode % numPartitions.

RepartitionByRange

Этот метод разделения создает numPartitions последовательных и неперекрывающихся диапазонов значений на основе ключа разделения. Таким образом, требуется по крайней мере одно ключевое выражение, которое должно быть упорядочено.

df.repartitionByRange(3, "id").explain(True)

== Parsed Logical Plan ==
'RepartitionByExpression ['id ASC NULLS FIRST], 3
+- LogicalRDD [id#0L, value#1], false

== Analyzed Logical Plan ==
id: bigint, value: string
RepartitionByExpression [id#0L ASC NULLS FIRST], 3
+- LogicalRDD [id#0L, value#1], false

== Optimized Logical Plan ==
RepartitionByExpression [id#0L ASC NULLS FIRST], 3
+- LogicalRDD [id#0L, value#1], false

== Physical Plan ==
Exchange rangepartitioning(id#0L ASC NULLS FIRST, 3)
+- Scan ExistingRDD[id#0L,value#1]

Глядя на сгенерированный физический план, мы видим, что rangepartitioning отличается от двух других, описанных выше, наличием предложения упорядочения в выражении разделения. Если в выражении не указан явный порядок сортировки, по умолчанию используется возрастающий порядок.

Несколько интересных ссылок:

2
blackbishop 20 Янв 2021 в 20:30