Я начал использовать Spark SQL и DataFrames в Spark 1.4.0. Я хочу определить пользовательский разделитель на DataFrames в Scala, но не вижу, как это сделать.
Одна из таблиц данных, с которыми я работаю, содержит список транзакций по учетной записи, 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)", где вы можете указать количество создаваемых разделов. Но я не вижу доступных методов для определения пользовательского разделителя для DataFrame, например, для RDD.
Исходные данные хранятся в Парке. Я видел, что при написании DataFrame в Parquet вы можете указать столбец для разделения, так что я предположил, что Parquet может разбить его на столбец "Учетная запись". Но могут быть миллионы аккаунтов, и если я правильно понимаю Паркет, это создало бы отдельный каталог для каждой учетной записи, поэтому это не звучало как разумное решение.
Есть ли способ заставить Spark разбивать этот DataFrame так, чтобы все данные для учетной записи находились в одном разделе?
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 раскрывает разделение внешнего формата в Data Source API v2.
В 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]
В отличие от RDDs
Spark Dataset
(включая Dataset[Row]
a.k.a DataFrame
) не может использовать пользовательский разделитель, как сейчас. Обычно вы можете обратиться к этому, создав столбец искусственного разделения, но он не даст вам такой же гибкости.
Одна вещь, которую вы можете сделать, это предварительно разбить входные данные перед созданием 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
)
Итак, похоже, что это невозможно. Вопрос остается, если он имеет смысл. Я буду утверждать, что большую часть времени это не делает:
Перераспределение - дорогостоящий процесс. В типичном сценарии большая часть данных должна быть сериализована, перетасована и десериализована. С другой стороны, количество операций, которые могут извлечь выгоду из предварительно разделенных данных, относительно невелико и дополнительно ограничено, если внутренний API не предназначен для использования этого свойства.
GROUP BY
- можно уменьшить объем памяти временных буферов **, но общая стоимость намного выше. Более или менее эквивалентно groupByKey.mapValues(_.reduce)
(текущее поведение) vs reduceByKey
(предварительное разбиение). Вряд ли будет полезно на практике.SqlContext.cacheTable
. Поскольку похоже, что используется кодирование длины прогона, применение OrderedRDDFunctions.repartitionAndSortWithinPartitions
может улучшить коэффициент сжатия.Производительность сильно зависит от распределения ключей. Если он искажен, это приведет к субоптимальному использованию ресурсов. В худшем случае невозможно завершить работу вообще.
Разделение с источниками 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
(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>
* По разметке я имею в виду только распределение данных. partitioned
RDD больше не является разделителем.
** Предполагая, что ранняя проекция отсутствует. Если агрегация охватывает только небольшое подмножество столбцов, вероятно, нет никакой выгоды.
In Spark & lt; 1.6 Если вы создаете HiveContext
, а не обычный старый SqlContext
, вы можете использовать HiveQL DISTRIBUTE BY colX...
(гарантирует, что каждый из N редукторов получит неперекрывающиеся диапазоны 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 (обратите внимание, что вам не нужно иметь мета-хранилище улей для использования HiveContext)
EDIT: Spark 1.6+ теперь имеет это в собственном API DataFrame
Используйте DataFrame, возвращенный:
yourDF.orderBy(account)
Нет явного способа использования partitionBy в DataFrame, только на PairRDD, но когда вы сортируете DataFrame, он будет использовать это в нем LogicalPlan, и это поможет, когда вам нужно делать вычисления для каждой учетной записи.
Я просто наткнулся на ту же самую проблему, с фреймворком данных, который я хочу разделить по аккаунту. Я предполагаю, что когда вы говорите: "хотите, чтобы данные были разделены так, чтобы все транзакции для учетной записи находились в одном и том же разделе Spark", вы хотите, чтобы это было для масштаба и производительности, но ваш код не зависит от него (например, используя mapPartitions() и т.д.), правильно?
Я смог сделать это с помощью 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)
Итак, чтобы начать с какого-то ответа:) - Вы не можете
Я не эксперт, но насколько я понимаю DataFrames, они не равны rdd, а DataFrame не имеет такой вещи, как Partitioner.
В целом идея DataFrame заключается в предоставлении другого уровня абстракции, который сам справляется с такими проблемами. Запросы в DataFrame переводятся в логический план, который далее переводится на операции с RDD. Предлагаемое разбиение, вероятно, будет применяться автоматически или, по крайней мере, должно быть.
Если вы не доверяете SparkSQL, что он предоставит какое-то оптимальное задание, вы всегда можете преобразовать DataFrame в RDD [Row], как это предлагается в комментариях.
int(account/someInteger)
и тем самым получить разумное количество учетных записей в каталоге.