在Spark DataFrame写入方法中覆盖特定分区


73

我想覆盖特定的分区,而不是全部覆盖。我正在尝试以下命令:

df.write.orc('maprfs:///hdfs-base-path','overwrite',partitionBy='col4')

其中df是具有要覆盖的增量数据的数据帧。

hdfs-base-path包含主数据。

当我尝试上述命令时,它将删除所有分区,并在hdfs路径中的df中插入这些分区。

我的要求是只覆盖指定hdfs路径中df中存在的那些分区。有人可以帮我吗?

Answers:


56

这是一个普遍的问题。Spark最高版本2.0的唯一解决方案是直接写入分区目录,例如,

df.write.mode(SaveMode.Overwrite).save("/root/path/to/data/partition_col=value")

如果您在2.0之前使用Spark,则需要使用以下命令阻止Spark发出元数据文件(因为它们会破坏自动分区发现):

sc.hadoopConfiguration.set("parquet.enable.summary-metadata", "false")

如果使用1.6.2之前的Spark,则还需要删除其中的_SUCCESS文件,/root/path/to/data/partition_col=value否则它的存在将破坏自动分区发现。(我强烈建议使用1.6.2或更高版本。)

在我的Spark Summit关于Bulletproof Jobs的演讲中,您可以获得有关如何管理大型分区表的更多详细信息。


1
非常感谢Sim的回答。再有几个疑问,如果假设初始数据帧具有大约100个分区的数据,那么我是否必须将此数据帧拆分为具有各自分区值的另外100个数据帧,然后直接插入分区目录中。可以同时保存这100个分区吗?另外,我正在使用Spark 1.6.1,如果我使用的是orc文件格式,那么我该如何停止发送元数据文件,与前面提到的镶木地板一样吗?
yatin

回复:元数据,不,ORC是一种不同的格式,我认为它不会产生非数据文件。使用1.6.1,您仅需要分区树的子目录中的ORC文件。因此,您必须_SUCCESS手动删除。您可以并行写入多个分区,但不能来自同一作业。根据您的平台功能(例如,使用REST API)启动多个作业。
Sim

7
有什么更新吗?saveToTable()是否仅覆盖特定分区?spark是否足够聪明以找出覆盖了哪些分区?
David H

133

最后!现在,这是Spark 2.3.0的功能:https : //issues.apache.org/jira/browse/SPARK-20236

要使用它,您需要将spark.sql.sources.partitionOverwriteMode设置设置为dynamic,需要对数据集进行分区,并且写入模式必须被overwrite。例:

spark.conf.set("spark.sql.sources.partitionOverwriteMode","dynamic")
data.write.mode("overwrite").insertInto("partitioned_table")

我建议在写入之前根据您的分区列进行重新分区,因此每个文件夹最终不会包含400个文件。

在Spark 2.3.0之前,最好的解决方案是启动SQL语句以删除这些分区,然后使用模式追加将其写入。


2
这是我很难找到使用这个设定,所以离开这里的参考:stackoverflow.com/questions/50006526/...
玛达瓦卡里洛

1
您可以编辑答案以显示JIRA的示例代码吗?
OneCricketeer

不起作用 HDFS中尚未存在的新数据不会写入其中。
hey_you

1
如果我要覆盖一个分区,并且知道该分区的名称apriori,是否可以指定一种方法来spark像我们可以做到的那样Hive?我之所以这样问是因为,这将给我带来很多保证和工作量,例如,健全性检查,另外,我相信也会有一些性能上的好处(因为不需要为每个记录对分区进行运行时解析)
y2k- shubham

2
@ y2k-shubham是的,使用spark.sql('insert overwrite table TABLE_NAME partition(PARTITION_NAME=PARTITION_VALUE) YOUR SELECT STATEMENT)此功能至少适用于2.2,如果较早的版本支持此功能,则不建议使用。
Tetlanesh

11
spark.conf.set("spark.sql.sources.partitionOverwriteMode","dynamic")
data.toDF().write.mode("overwrite").format("parquet").partitionBy("date", "name").save("s3://path/to/somewhere")

这对我适用于AWS Glue ETL作业(Glue 1.0-Spark 2.4-Python 2)


这种方法在工作书签中的表现如何?假设您有一个现有分区(例如一天),该分区仅当天的前12个小时的数据,并且源文件已到达第二十二个小时的新文件,则应将其添加到该分区,我担心胶水工作书签非常幼稚,最终只能在接下来的12小时内从新文件中写入数据。还是不使用工作书签?
达沃斯

1
好问题!我也有同样的担忧。我的用例是我专门要求Glue重新处理某些分区并重新写入结果(使用以上两行)。启用作业书签后,它拒绝重新处理“旧”数据。
扎克

所以您不使用书签?这几乎是我可以看到的对glugContext而不是仅仅坚持使用Spark的唯一原因。我不想管理已处理的状态,但是我发现书签不可靠,依赖于文件修改的时间戳,除了残酷的重置之外,没有办法同步它。为什么是Python 2,而不是3?
达沃斯

1
是的,工作书签已经困扰了我一段时间。这对于一些低调的日常工作很有用。但是,一旦您采取了一些“越野”操作,那件事就变得毫无用处了。关于Python版本,从Glue 0.9升级时,查看两个选项(Python 2 vs 3),我只是不想破坏任何东西,因为代码是用Python 2时代编写的^ _ ^
Zach

指出“少于无用”。除此之外print is a functionunicode done properlyliteral long not necessary那里去2-> 3多不。Pyspark DSL语法似乎相同。2020年正式不支持Python 2,该放弃它了。
达沃斯

9

在insertInto语句中添加'overwrite = True'参数可以解决此问题:

hiveContext.setConf("hive.exec.dynamic.partition", "true")
hiveContext.setConf("hive.exec.dynamic.partition.mode", "nonstrict")

df.write.mode("overwrite").insertInto("database_name.partioned_table", overwrite=True)

默认情况下overwrite=False。将其更改为True允许我们覆盖dfpartioned_table中和其中包含的特定分区。这有助于我们避免使用覆盖partioned_table的全部内容df


似乎已经改变了这种方法。
thebluephantom

这对我
有用

8

使用Spark 1.6 ...

HiveContext可以大大简化此过程。关键是您必须首先使用已CREATE EXTERNAL TABLE定义分区的语句在Hive中创建表。例如:

# Hive SQL
CREATE EXTERNAL TABLE test
(name STRING)
PARTITIONED BY
(age INT)
STORED AS PARQUET
LOCATION 'hdfs:///tmp/tables/test'

从这里,假设您有一个数据框,其中有一个特定分区(或多个分区)的新记录。您可以使用HiveContext SQL语句执行INSERT OVERWRITE此Dataframe的使用,这将仅覆盖Dataframe中包含的分区的表:

# PySpark
hiveContext = HiveContext(sc)
update_dataframe.registerTempTable('update_dataframe')

hiveContext.sql("""INSERT OVERWRITE TABLE test PARTITION (age)
                   SELECT name, age
                   FROM update_dataframe""")

注意:update_dataframe在此示例中,具有与目标test表匹配的架构。

使用此方法容易犯的一个错误是跳过CREATE EXTERNAL TABLEHive中的步骤,仅使用Dataframe API的write方法创建表。特别是对于基于Parquet的表,将无法正确定义该表以支持Hive的INSERT OVERWRITE... PARTITION功能。

希望这可以帮助。


我尝试了上述方法,但遇到了类似Dynamic partition strict mode requires at least one static partition column. To turn this off set hive.exec.dynamic.partition.mode=nonstrict
Shankar

我没有任何静态分区列
Shankar

6

使用Scala在Spark 2.3.1上进行了测试。上面的大多数答案都正在写入Hive表。但是,我想直接写入到disk,该磁盘external hive table位于此文件夹的顶部。

首先需要配置

val sparkSession: SparkSession = SparkSession
      .builder
      .enableHiveSupport()
      .config("spark.sql.sources.partitionOverwriteMode", "dynamic") // Required for overwriting ONLY the required partitioned folders, and not the entire root folder
      .appName("spark_write_to_dynamic_partition_folders")

这里的用法:

DataFrame
.write
.format("<required file format>")
.partitionBy("<partitioned column name>")
.mode(SaveMode.Overwrite) // This is required.
.save(s"<path_to_root_folder>")

3

我尝试了以下方法来覆盖HIVE表中的特定分区。

### load Data and check records
    raw_df = spark.table("test.original")
    raw_df.count()

lets say this table is partitioned based on column : **c_birth_year** and we would like to update the partition for year less than 1925


### Check data in few partitions.
    sample = raw_df.filter(col("c_birth_year") <= 1925).select("c_customer_sk", "c_preferred_cust_flag")
    print "Number of records: ", sample.count()
    sample.show()


### Back-up the partitions before deletion
    raw_df.filter(col("c_birth_year") <= 1925).write.saveAsTable("test.original_bkp", mode = "overwrite")


### UDF : To delete particular partition.
    def delete_part(table, part):
        qry = "ALTER TABLE " + table + " DROP IF EXISTS PARTITION (c_birth_year = " + str(part) + ")"
        spark.sql(qry)


### Delete partitions
    part_df = raw_df.filter(col("c_birth_year") <= 1925).select("c_birth_year").distinct()
    part_list = part_df.rdd.map(lambda x : x[0]).collect()

    table = "test.original"
    for p in part_list:
        delete_part(table, p)


### Do the required Changes to the columns in partitions
    df = spark.table("test.original_bkp")
    newdf = df.withColumn("c_preferred_cust_flag", lit("Y"))
    newdf.select("c_customer_sk", "c_preferred_cust_flag").show()


### Write the Partitions back to Original table
    newdf.write.insertInto("test.original")


### Verify data in Original table
    orginial.filter(col("c_birth_year") <= 1925).select("c_customer_sk", "c_preferred_cust_flag").show()



Hope it helps.

Regards,

Neeraj

2

作为jatin Wrote,您可以从配置单元和路径中删除分区,然后追加数据。由于我浪费了太多时间,因此为其他spark用户添加了以下示例。我在Scala 2.2.1中使用了Scala

  import org.apache.hadoop.conf.Configuration
  import org.apache.hadoop.fs.Path
  import org.apache.spark.SparkConf
  import org.apache.spark.sql.{Column, DataFrame, SaveMode, SparkSession}

  case class DataExample(partition1: Int, partition2: String, someTest: String, id: Int)

 object StackOverflowExample extends App {
//Prepare spark & Data
val sparkConf = new SparkConf()
sparkConf.setMaster(s"local[2]")
val spark = SparkSession.builder().config(sparkConf).getOrCreate()
val tableName = "my_table"

val partitions1 = List(1, 2)
val partitions2 = List("e1", "e2")
val partitionColumns = List("partition1", "partition2")
val myTablePath = "/tmp/some_example"

val someText = List("text1", "text2")
val ids = (0 until 5).toList

val listData = partitions1.flatMap(p1 => {
  partitions2.flatMap(p2 => {
    someText.flatMap(
      text => {
        ids.map(
          id => DataExample(p1, p2, text, id)
        )
      }
    )
  }
  )
})

val asDataFrame = spark.createDataFrame(listData)

//Delete path function
def deletePath(path: String, recursive: Boolean): Unit = {
  val p = new Path(path)
  val fs = p.getFileSystem(new Configuration())
  fs.delete(p, recursive)
}

def tableOverwrite(df: DataFrame, partitions: List[String], path: String): Unit = {
  if (spark.catalog.tableExists(tableName)) {
    //clean partitions
    val asColumns = partitions.map(c => new Column(c))
    val relevantPartitions = df.select(asColumns: _*).distinct().collect()
    val partitionToRemove = relevantPartitions.map(row => {
      val fields = row.schema.fields
      s"ALTER TABLE ${tableName} DROP IF EXISTS PARTITION " +
        s"${fields.map(field => s"${field.name}='${row.getAs(field.name)}'").mkString("(", ",", ")")} PURGE"
    })

    val cleanFolders = relevantPartitions.map(partition => {
      val fields = partition.schema.fields
      path + fields.map(f => s"${f.name}=${partition.getAs(f.name)}").mkString("/")
    })

    println(s"Going to clean ${partitionToRemove.size} partitions")
    partitionToRemove.foreach(partition => spark.sqlContext.sql(partition))
    cleanFolders.foreach(partition => deletePath(partition, true))
  }
  asDataFrame.write
    .options(Map("path" -> myTablePath))
    .mode(SaveMode.Append)
    .partitionBy(partitionColumns: _*)
    .saveAsTable(tableName)
}

//Now test
tableOverwrite(asDataFrame, partitionColumns, tableName)
spark.sqlContext.sql(s"select * from $tableName").show(1000)
tableOverwrite(asDataFrame, partitionColumns, tableName)

import spark.implicits._

val asLocalSet = spark.sqlContext.sql(s"select * from $tableName").as[DataExample].collect().toSet
if (asLocalSet == listData.toSet) {
  println("Overwrite is working !!!")
}

}


1

如果使用DataFrame,则可能要对数据使用Hive表。在这种情况下,您只需要调用方法

df.write.mode(SaveMode.Overwrite).partitionBy("partition_col").insertInto(table_name)

它将覆盖DataFrame包含的分区。

无需指定格式(orc),因为Spark将使用Hive表格式。

在Spark版本1.6中工作正常


1
如果先前的分区不在当前数据框中,则将其删除。
卡洛斯·韦尔德

如果表是根据多列进行分区(例如年,月,而我只想根据年覆盖),则如何更新数据?
neeraj bhadani

我也收到错误消息:AnalysisException:u“ insertInto()不能与partitionBy()一起使用。已经为该表定义了分区列。没有必要使用partitionBy()。”
neeraj bhadani

没有partitionBy我即使使用mode(“ overwrite”)
也会

这是部分正确的。请参阅Surya Murali注释,以获得我需要添加的其他设置才能使其正常运行。至少在我看来是可行的(火花1.6,斯卡拉)
Costin Aldea

1

我建议您创建一个类似于目标表的临时表,然后在其中插入数据,而不是直接写入目标表。

CREATE TABLE tmpTbl LIKE trgtTbl LOCATION '<tmpLocation';

创建表后,您将数据写入到 tmpLocation

df.write.mode("overwrite").partitionBy("p_col").orc(tmpLocation)

然后,您将通过执行以下命令恢复表分区路径:

MSCK REPAIR TABLE tmpTbl;

通过查询Hive元数据来获取分区路径,例如:

SHOW PARTITONS tmpTbl;

从中删除这些分区,trgtTbl并将目录从中tmpTbl移至trgtTbl


0

您可以执行以下操作使工作可重新进入(幂等):(在spark 2.2上进行了尝试)

# drop the partition
drop_query = "ALTER TABLE table_name DROP IF EXISTS PARTITION (partition_col='{val}')".format(val=target_partition)
print drop_query
spark.sql(drop_query)

# delete directory
dbutils.fs.rm(<partition_directoy>,recurse=True)

# Load the partition
df.write\
  .partitionBy("partition_col")\
  .saveAsTable(table_name, format = "parquet", mode = "append", path = <path to parquet>)

为什么选择Python 2?同样,这看起来像Databricks特有的,值得一提的是,对于其他未使用该平台的人。我喜欢幂等,但这是真的吗?如果删除目录成功但追加失败,该怎么办?您如何保证df包含已删除分区的数据?
达沃斯

0

我建议您进行清理,然后使用Append模式编写新分区:

import scala.sys.process._
def deletePath(path: String): Unit = {
    s"hdfs dfs -rm -r -skipTrash $path".!
}

df.select(partitionColumn).distinct.collect().foreach(p => {
    val partition = p.getAs[String](partitionColumn)
    deletePath(s"$path/$partitionColumn=$partition")
})

df.write.partitionBy(partitionColumn).mode(SaveMode.Append).orc(path)

这将仅删除新分区。写入数据后,如果需要更新元存储,请运行以下命令:

sparkSession.sql(s"MSCK REPAIR TABLE $db.$table")

注意: deletePath假定该hfds命令在您的系统上可用。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.