DataFrame के विभाजन को कैसे परिभाषित करें?


128

मैंने स्पार्क एसक्यूएल और डेटाफ्रेम का उपयोग स्पार्क 1.4.0 में करना शुरू कर दिया है। मैं Scala में DataFrames पर एक कस्टम पार्टीशन को परिभाषित करना चाहता हूं, लेकिन यह देखना नहीं है कि यह कैसे करना है।

निम्नलिखित में से एक डेटा तालिकाओं में मैं लेन-देन की सूची में खाता हूँ, सिलिमर।

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

कम से कम शुरुआत में, अधिकांश गणना एक खाते के भीतर लेनदेन के बीच होगी। इसलिए मैं चाहता हूं कि डेटा का विभाजन इसलिए किया जाए ताकि किसी खाते के सभी लेन-देन एक ही स्पार्क विभाजन में हों।

लेकिन मैं इसे परिभाषित करने का एक तरीका नहीं देख रहा हूं। DataFrame वर्ग में एक विधि है, जिसे 'repartition (Int)' कहा जाता है, जहाँ आप विभाजन बनाने की संख्या निर्दिष्ट कर सकते हैं। लेकिन मैं डेटाफ़्रेम के लिए कस्टम पार्टीशनर को परिभाषित करने के लिए कोई भी तरीका उपलब्ध नहीं देख रहा हूं, जैसे कि RDD के लिए निर्दिष्ट किया जा सकता है।

स्रोत डेटा Parquet में संग्रहीत है। मैंने देखा कि जब Parbit को DataFrame लिखते हैं, तो आप विभाजन के लिए एक कॉलम निर्दिष्ट कर सकते हैं, इसलिए संभवतः मैं Parquet को यह बता सकता हूं कि वह 'खाता' कॉलम द्वारा डेटा का विभाजन है। लेकिन लाखों खाते हो सकते हैं, और अगर मैं Parquet को सही तरीके से समझ रहा हूं, तो यह प्रत्येक खाते के लिए एक अलग निर्देशिका बनाएगा, ताकि एक उचित समाधान की तरह आवाज़ न आए।

क्या इस DataFrame को विभाजित करने के लिए स्पार्क प्राप्त करने का एक तरीका है ताकि किसी खाते के सभी डेटा एक ही विभाजन में हों?


इस लिंक की जाँच करें stackoverflow.com/questions/23127329/…
अभिषेक चौधरी

यदि आप खाते से विभाजन के बारे में बता सकते हैं, तो आप संभवतः विभाजन कर सकते हैं int(account/someInteger)और इस प्रकार प्रति निर्देशिका में उचित संख्या में खाते प्राप्त कर सकते हैं ।
पॉल

1
@ABC: मैंने वह लिंक देखा था। उस partitionBy(Partitioner)विधि के समकक्ष की तलाश कर रहा था , लेकिन RDDs के बजाय DataFrames के लिए। अब मुझे लगता partitionByहै कि यह केवल पेयर आरडीडी के लिए उपलब्ध है , निश्चित नहीं है कि ऐसा क्यों है।
रेक

@Paul: आपने जो वर्णन किया है, उसे करने पर मैंने विचार किया। : कुछ बातें मेरे पास वापस आयोजित
रेक

जारी .... (1) जो कि "लकड़ी की छत-विभाजन" के लिए है। मुझे लगता है कि स्पार्क-विभाजन वास्तव में लकड़ी की छत-विभाजन का उपयोग करेगा कि किसी भी डॉक्स को खोजने में सक्षम नहीं था। (२) यदि मैं Parquet डॉक्स को समझता हूं, मुझे एक नए फ़ील्ड "फू" को परिभाषित करने की आवश्यकता है, तो प्रत्येक 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]

SPARK-22389 डेटा स्रोत API v2 में बाहरी स्वरूप विभाजन को उजागर करता है ।

स्पार्क> = 1.6.0

स्पार्क> = 1.6 में क्वेरी और कैशिंग के लिए कॉलम द्वारा विभाजन का उपयोग करना संभव है। देखें: SPARK-11410 और SPARK-4849repartition विधि का उपयोग कर :

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. पुनर्मूल्यांकन एक महंगी प्रक्रिया है। एक विशिष्ट परिदृश्य में अधिकांश डेटा को क्रमबद्ध, फेरबदल और deserialized किया जाना होता है। दूसरी ओर से ऑपरेशनों की संख्या जो कि एक पूर्व-विभाजित डेटा से लाभ उठा सकती है, अपेक्षाकृत कम है और इस तक सीमित है यदि आंतरिक एपीआई को इस संपत्ति का लाभ उठाने के लिए डिज़ाइन नहीं किया गया है।

    • कुछ परिदृश्यों में शामिल होता है, लेकिन इसके लिए आंतरिक समर्थन की आवश्यकता होती है,
    • विंडो फंक्शंस मैचिंग पार्टीशनर के साथ कॉल करते हैं। ऊपर के रूप में ही, एक एकल खिड़की परिभाषा तक सीमित। यह पहले से ही आंतरिक रूप से विभाजित है, लेकिन पूर्व विभाजन निरर्थक हो सकता है,
    • के साथ सरल एकत्रीकरण GROUP BY- यह अस्थायी बफ़र्स ** की स्मृति पदचिह्न को कम करना संभव है, लेकिन समग्र लागत बहुत अधिक है। कमोबेश groupByKey.mapValues(_.reduce)(वर्तमान व्यवहार) बनामreduceByKey (पूर्व विभाजन)। व्यवहार में उपयोगी होने के लिए अयोग्य।
    • के साथ डेटा संपीड़न SqlContext.cacheTable। चूंकि ऐसा लगता है कि यह रन लंबाई एन्कोडिंग का उपयोग कर रहा है, इसलिए आवेदन OrderedRDDFunctions.repartitionAndSortWithinPartitionsसंपीड़न अनुपात में सुधार कर सकता है।
  2. प्रदर्शन कुंजी के वितरण पर अत्यधिक निर्भर है। यदि इसे तिरछा किया जाता है, तो यह एक उप-स्तरीय संसाधन उपयोग के परिणामस्वरूप होगा। सबसे खराब स्थिति में नौकरी खत्म करना असंभव होगा।

  3. एक उच्च स्तरीय घोषणात्मक एपीआई का उपयोग करने का एक पूरा बिंदु खुद को निम्न स्तर के कार्यान्वयन विवरण से अलग करना है। जैसा कि पहले ही @dwysakowicz और @RomiKuntsman द्वारा उल्लिखित एक अनुकूलन एक काम है कैटालिस्ट ऑप्टिमाइज़र । यह एक बहुत ही परिष्कृत जानवर है और मुझे वास्तव में संदेह है कि आप आसानी से उस पर सुधार कर सकते हैं, बिना इसके इंटर्नल में अधिक गहराई तक।

संबंधित अवधारणाएँ

जेडीबीसी स्रोतों के साथ विभाजन :

JDBC डेटा स्रोत predicatesतर्क का समर्थन करते हैं । इसका उपयोग इस प्रकार किया जा सकता है:

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

यह प्रति विधेय के लिए एक एकल JDBC विभाजन बनाता है। ध्यान रखें कि यदि व्यक्तिगत विधेय का उपयोग करके बनाए गए सेट असंतुष्ट नहीं हैं, तो आप परिणामी तालिका में डुप्लिकेट देखेंगे।

partitionBy में विधि DataFrameWriter :

स्पार्क विधि 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 का अब कोई पार्टीशनर नहीं है। ** कोई प्रारंभिक प्रक्षेपण नहीं है। यदि एकत्रीकरण केवल स्तंभों के छोटे उपसमूह को कवर करता है तो संभवतः कोई लाभ नहीं है।


@ लाइकेंस हाँ और ना। डेटा लेआउट संरक्षित किया जाएगा, लेकिन AFAIK यह आपको विभाजन छंटाई जैसे लाभ नहीं देगा।
शून्य 323

@ zero323 धन्यवाद, क्या df.save.write को वास्तव में लेआउट को बचाने के लिए Parquet फ़ाइल के विभाजन आवंटन की जांच करने का एक तरीका है? और अगर मैं df.repartition ("A") करता हूं, तो df.write.repartitionBy ("B") करते हैं, भौतिक फ़ोल्डर संरचना B द्वारा विभाजित की जाएगी, और प्रत्येक B मान फ़ोल्डर के भीतर, यह तब भी विभाजन को रखेगा। ए?
bychance

2
@bychance DataFrameWriter.partitionByतार्किक रूप से समान नहीं है DataFrame.repartition। पूर्व में फेरबदल नहीं करता है, यह केवल आउटपुट को अलग करता है। पहले प्रश्न के बारे में ।- डेटा प्रति विभाजन में सहेजा जाता है और कोई फेरबदल नहीं होता है। आप व्यक्तिगत फ़ाइलों को पढ़कर आसानी से देख सकते हैं। लेकिन अकेले स्पार्क के पास इसके बारे में जानने का कोई तरीका नहीं है अगर यह वही है जो आप वास्तव में चाहते हैं।
शून्य 323

11

स्पार्क <1.6 में, यदि आप एक बनाते हैं HiveContext, तो सादा पुराना नहीं जो SqlContextआप हाइवेकेल का उपयोग कर सकते हैं DISTRIBUTE BY colX...(यह सुनिश्चित करता है कि प्रत्येक एन रिड्यूसर को एक्स की गैर-अतिव्यापी सीमाएं मिलती हैं) और CLUSTER BY colX...उदाहरण के लिए (डिस्ट्रीब्यूट बाय और सॉर्ट के लिए शॉर्टकट) उदाहरण के लिए;

df.registerTempTable("partitionMe")
hiveCtx.sql("select * from partitionMe DISTRIBUTE BY accountId SORT BY accountId, date")

यकीन नहीं होता कि स्पार्क डीएफ एपी के साथ यह कैसे फिट बैठता है। ये कीवर्ड सामान्य SqlContext में समर्थित नहीं हैं (ध्यान दें कि आपको HiveContext का उपयोग करने के लिए हाइव मेटा स्टोर की आवश्यकता नहीं है)

संपादित करें: स्पार्क 1.6+ में अब यह देशी डेटाफ्रेम एपीआई में है


1
क्या डेटाफ़्रेम सहेजे जाने के लिए विभाजन संरक्षित हैं?
सिम

हाइव क्यूएल उदाहरण में आपके पास कितने विभाजन हो सकते हैं? उदाहरण के लिए RDD दृष्टिकोण में, आप 5 विभाजन बनाने के लिए ऐसा कर सकते हैं: val विभाजनकर्ता = नया HashPartitioner (5)
Minnie

ठीक है, पाया गया उत्तर, यह इस तरह से किया जा सकता है: sqlContext.setConf ("spark.sql.shuffle.partitions", "5") मैं पिछली टिप्पणी को संपादित नहीं कर सका क्योंकि मैं 5 मिनट की सीमा से चूक गया था
Minnie

7

तो किसी तरह के उत्तर से शुरू करने के लिए:) - आप नहीं कर सकते

मैं एक विशेषज्ञ नहीं हूं, लेकिन जहां तक ​​मैं डेटाफ्रेम को समझता हूं, वे rdd के बराबर नहीं हैं और DataFrame में पार्टीशनर जैसी कोई चीज नहीं है।

आम तौर पर डेटाफ़्रेम का विचार अमूर्तता का एक और स्तर प्रदान करना है जो इस तरह की समस्याओं को स्वयं संभालता है। DataFrame के प्रश्नों को तार्किक योजना में अनुवादित किया जाता है जिसे RDDs के संचालन में आगे अनुवाद किया जाता है। आपके द्वारा सुझाए गए विभाजन को संभवतः स्वचालित रूप से लागू किया जाएगा या कम से कम होना चाहिए।

यदि आप स्पार्कक्यूसी पर भरोसा नहीं करते हैं कि यह किसी प्रकार का इष्टतम काम प्रदान करेगा, तो आप हमेशा टिप्पणी के अनुसार डेटाफ्रेम को आरडीडी [रो] में बदल सकते हैं।


7

द्वारा दिए गए DataFrame का उपयोग करें:

yourDF.orderBy(account)

उपयोग करने का कोई स्पष्ट तरीका नहीं है partitionByएक DataFrame पर, केवल PairRDD पर , लेकिन जब आप एक DataFrame सॉर्ट करते हैं, तो यह उस LogicalPlan का उपयोग करेगा और जब आपको प्रत्येक खाते पर गणना करने की आवश्यकता होगी, तो यह मदद करेगा।

मैं एक ही सटीक मुद्दे पर ठोकर खाई, एक डेटाफ्रेम के साथ जिसे मैं खाते से विभाजित करना चाहता हूं। मुझे लगता है कि जब आप कहते हैं "डेटा को विभाजित करना चाहते हैं ताकि एक खाते के सभी लेनदेन समान स्पार्क विभाजन में हों", तो आप इसे पैमाने और प्रदर्शन के लिए चाहते हैं, लेकिन आपका कोड इस पर निर्भर नहीं करता है (जैसे उपयोग करना mapPartitions()आदि), सही है?


3
क्या होगा अगर आपका कोड इस पर निर्भर करता है क्योंकि आपके मैप मैप का उपयोग कर रहे हैं?
नाइटवॉल्फ

2
आप DataFrame को RDD में रूपांतरित कर सकते हैं, और फिर इसे विभाजन कर सकते हैं (उदाहरण के लिए समुच्चयबाई () का उपयोग कर और एक कस्टम विभाजनकर्ता पास करें)
रोमी कुंट्समैन

5

मैं RDD का उपयोग करके ऐसा करने में सक्षम था। लेकिन मुझे नहीं पता कि यह आपके लिए स्वीकार्य समाधान है। एक बार जब आपके पास DFD 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 Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.