Як визначити розділення DataFrame?


128

Я почав використовувати 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 має метод, який називається "перерозподіл (Int)", де ви можете вказати кількість створених розділів. Але я не бачу жодного методу для визначення спеціального учасника для DataFrame, такого як можна вказати для RDD.

Вихідні дані зберігаються в Паркеті. Я бачив, що, записуючи DataFrame в Parquet, ви можете вказати стовпчик для розділу за допомогою, тому, імовірно, я міг би повідомити Parquet для розділу його дані в стовпці "Account". Але може бути мільйони облікових записів, і якщо я правильно розумію Паркет, він створив би окремий каталог для кожного облікового запису, так що це не здавалося розумним рішенням.

Чи є спосіб отримати Spark для розділу цього DataFrame, щоб усі дані для облікового запису були в одному розділі?


перевірити це посилання stackoverflow.com/questions/23127329/…
Abhishek Choudhary

Якщо ви можете повідомити Паркет до розділу за обліковим записом, ви, ймовірно, можете розділити його int(account/someInteger)і тим самим отримати розумну кількість облікових записів у каталозі.
Пол

1
@ABC: я бачив це посилання. partitionBy(Partitioner)Шукав еквівалент цього методу, але для DataFrames замість RDD. Я тепер бачу , що partitionByє тільки для парного РДА, не впевнене , чому це так.
граблі

@Paul: Я думав робити те, що ви описуєте. Кілька речей мене стримували:
граблі

продовження .... (1) Це для "Паркет-перегородки". Я не зміг знайти жодного документа, який би стверджував, що Spark-partitioning насправді використовуватиме Parquet-partitioning. (2) Якщо я розумію документи Parquet, мені потрібно визначити нове поле "foo", то кожен каталог Parquet мав би назву типу "foo = 123". Але якщо я будую запит за участю AccountID , як Spark / hive / parquet знає, що між foo та AccountID існує зв'язок ?
граблі

Відповіді:


177

Іскра> = 2.3.0

СПАРК-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]

СПАРК-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]

На відміну від RDDsІскриDataset (включаючи Dataset[Row]ака DataFrame), не можна використовувати спеціальний роздільник, як зараз. Зазвичай ви можете вирішити це, створивши стовпчик штучного розподілу, але це не дасть вам такої ж гнучкості.

Іскра <1.6.0:

Одне, що ви можете зробити - це попередньо розділити вхідні дані перед створенням 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 , оптимізація - це робота оптимізатора каталізаторів . Це досить витончений звір, і я дуже сумніваюся, що ви можете легко вдосконалити це, не занурившись всередину.

Пов'язані поняття

Розмежування з джерелами JDBC :

predicatesАргумент підтримки джерел даних JDBC . Його можна використовувати наступним чином:

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 (Іскра> = 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 Так і ні. Макет даних буде збережений, але AFAIK не принесе таких переваг, як обрізка розділів.
нуль323

@ zero323 Спасибі, чи є спосіб перевірити розподіл файлів паркету для паркету, щоб перевірити df.save.write дійсно зберегти макет? І якщо я буду df.repartition ("A"), то виконую df.write.repartitionBy ("B"), фізична структура папки буде розділена на B, і всередині кожної папки зі значенням B вона все ще буде зберігати розділ на А?
вихідний

2
@bychance DataFrameWriter.partitionByлогічно не те саме, що DataFrame.repartition. Колишній параметр не переміщується, він просто розділяє вихід. Щодо першого питання. - Дані зберігаються на розділі, і перетасування не відбувається. Ви можете легко перевірити це, прочитавши окремі файли. Але тільки Spark не має змоги дізнатися про це, якщо це те, що ви насправді хочете.
нуль323

11

В Spark <1.6 Якщо ви створили a HiveContext, а не простий старий, SqlContextви можете використовувати HiveQL 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 вам не потрібно мати металевий вулик).

EDIT: Spark 1.6+ тепер має це в рідному API DataFrame


1
Чи зберігаються розділи під час збереження фрейму даних?
Сім

як ви керуєте, скільки розділів ви можете мати у прикладі вулика ql? наприклад, у підході до парного RDD, ви можете зробити це, щоб створити 5 розділів: val partitor = new HashPartitioner (5)
Minnie

ОК, знайшов відповідь, це можна зробити так: sqlContext.setConf ("spark.sql.shuffle.partitions", "5") Я не зміг редагувати попередній коментар, оскільки я пропустив 5-хвилинну межу
Minnie

7

Отже, для початку відповідь:) - Ти не можеш

Я не фахівець, але наскільки я розумію DataFrames, вони не рівні rdd, а DataFrame не має такого поняття як Partitor.

Як правило, ідея DataFrame полягає у наданні іншого рівня абстракції, який сам вирішує подібні проблеми. Запити на DataFrame переводяться в логічний план, який далі перекладається на операції над RDD. Пропонований вами розділ, ймовірно, буде застосований автоматично або принаймні повинен бути.

Якщо ви не довіряєте SparkSQL, що це забезпечить якусь оптимальну роботу, ви завжди можете перетворити DataFrame в RDD [рядок], як це запропоновано в коментарях.


7

Використовуйте DataFrame, повернений:

yourDF.orderBy(account)

Немає явного способу використання partitionByу DataFrame, лише на PairRDD, але коли ви сортуєте DataFrame, він використовуватиме це у своєму LogicalPlan, і це допоможе, коли вам потрібно зробити розрахунки для кожного облікового запису.

Я просто натрапив на ту саму точну проблему з фреймом даних, який я хочу розділити за рахунком. Я припускаю, що коли ви говорите "хочете, щоб дані були розділені таким чином, щоб усі транзакції для облікового запису були в одному і тому ж розділі Spark", ви хочете, щоб вони були масштабними та ефективними, але ваш код не залежить від цього (наприклад, використання mapPartitions()тощо), правда?


3
Що робити, якщо ваш код залежить від нього, оскільки ви використовуєте mapPartitions?
NightWolf

2
Ви можете перетворити DataFrame в RDD, а потім розділити його (наприклад, використовуючи
agregatByKey

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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.