Schema Evolution(模式演进)允许用户轻松更改 Hudi 表的当前模式,以适应随时间变化的数据。 从 0.11.0 版本开始,支持 Spark SQL(spark3.1.x 和 spark3.2.1)对 Schema 演进的 DDL 支持并且标志为实验性的。

场景

  • 可以添加、删除、修改和移动列(包括嵌套列)
  • 分区列不能演进
  • 不能对 Array 类型的嵌套列进行添加、删除或操作

SparkSQL模式演进以及语法描述

使用模式演进之前,请先设置spark.sql.extensions,对于spark 3.2.x,需要设置spark.sql.catalog.spark_catalog

# Spark SQL for spark 3.1.x
spark-sql --packages org.apache.hudi:hudi-spark3.1.2-bundle_2.12:0.11.1,org.apache.spark:spark-avro_2.12:3.1.2 \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension' # Spark SQL for spark 3.2.1
spark-sql --packages org.apache.hudi:hudi-spark3-bundle_2.12:0.11.1,org.apache.spark:spark-avro_2.12:3.2.1 \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension' \
--conf 'spark.sql.catalog.spark_catalog=org.apache.spark.sql.hudi.catalog.HoodieCatalog'

启动spark app后,请执行set schema.on.read.enable=true开启模式演进

当前模式演进开启后不能关闭

添加列

语法

-- add columns
ALTER TABLE Table name ADD COLUMNS(col_spec[, col_spec ...])

参数描述

参数 描述
tableName 表名
col_spec 列定义,由五个字段组成,col_name, col_type, nullable, comment, col_position

col_name : 新列名,强制必须存在,如果在嵌套类型中添加子列,请指定子列的全路径

示例

  • 在嵌套类型users struct<name: string, age int>中添加子列col1,设置字段为users.col1
  • 在嵌套map类型member map<string, struct<n: string, a: int>>中添加子列col1, 设置字段为member.value.col1

col_type : 新列的类型

nullable : 新列是否可为null,可为空,当前Hudi中并未使用

comment : 新列的注释,可为空

col_position : 列添加的位置,值可为FIRST或者AFTER 某字段

  • 如果设置为FIRST,那么新加的列在表的第一列
  • 如果设置为AFTER 某字段,将在某字段后添加新列
  • 如果设置为空,只有当新的子列被添加到嵌套列时,才能使用 FIRST。 不要在顶级列中使用 FIRST。 AFTER 的使用没有限制。

示例

alter table h0 add columns(ext0 string);
alter table h0 add columns(new_col int not null comment 'add new column' after col1);
alter table complex_table add columns(col_struct.col_name string comment 'add new column to a struct col' after col_from_col_struct);

修改列

语法

-- alter table ... alter column
ALTER TABLE Table name ALTER [COLUMN] col_old_name TYPE column_type [COMMENT] col_comment[FIRST|AFTER] column_name

参数描述

参数 描述
tableName 表名
col_old_name 待修改的列名
column_type 新的列类型
col_comment 列comment
column_name 列名,放置目标列的新位置。 例如,AFTER column_name 表示目标列放在 column_name 之后

示例

--- Changing the column type
ALTER TABLE table1 ALTER COLUMN a.b.c TYPE bigint --- Altering other attributes
ALTER TABLE table1 ALTER COLUMN a.b.c COMMENT 'new comment'
ALTER TABLE table1 ALTER COLUMN a.b.c FIRST
ALTER TABLE table1 ALTER COLUMN a.b.c AFTER x
ALTER TABLE table1 ALTER COLUMN a.b.c DROP NOT NULL

列类型变更矩阵表

源列类型\目标列类型 long float double string decimal date int
int Y Y Y Y Y N Y
long Y N Y Y Y N N
float N Y Y Y Y N N
double N N Y Y Y N N
decimal N N N Y Y N N
string N N N Y Y Y N
date N N N Y N Y N

删除列

语法

-- alter table ... drop columns
ALTER TABLE tableName DROP COLUMN|COLUMNS cols

示例

ALTER TABLE table1 DROP COLUMN a.b.c
ALTER TABLE table1 DROP COLUMNS a.b.c, x, y

修改列名

语法

-- alter table ... rename column
ALTER TABLE tableName RENAME COLUMN old_columnName TO new_columnName

示例

ALTER TABLE table1 RENAME COLUMN a.b.c TO x

修改表属性

语法

-- alter table ... set|unset
ALTER TABLE Table name SET|UNSET tblproperties

示例

ALTER TABLE table SET TBLPROPERTIES ('table_property' = 'property_value')
ALTER TABLE table UNSET TBLPROPERTIES [IF EXISTS] ('comment', 'key')

修改表名

语法

-- alter table ... rename
ALTER TABLE tableName RENAME TO newTableName

示例

ALTER TABLE table1 RENAME TO table2

0.11.0之前的模式演进

模式演进是数据管理的一个非常重要的方面。 Hudi 支持开箱即用的常见模式演进场景,例如添加可为空的字段或提升字段的数据类型。 此外,演进后的模式可以跨引擎查询,例如 Presto、Hive 和 Spark SQL。 下表总结了与不同 Hudi 表类型兼容的Schema变更类型。

Schema变更 COW MOR 说明
在最后的根级别添加一个新的可为空列 Yes Yes Yes意味着具有演进模式的写入成功并且写入之后的读取成功读取整个数据集
向内部结构添加一个新的可为空列(最后) Yes Yes
添加具有默认值的新复杂类型字段(map和array) Yes Yes
添加新的可为空列并更改字段的顺序 No No 如果使用演进模式的写入仅更新了一些基本文件而不是全部,则写入成功但读取失败。 目前Hudi 不维护模式注册表,其中包含跨基础文件的更改历史记录。 然而如果 upsert 触及所有基本文件,则读取将成功
添加自定义可为空的 Hudi 元列,例如 _hoodie_meta_col Yes Yes
将根级别字段的数据类型从 int 提升为 long Yes Yes 对于其他类型,Hudi 支持与Avro相同 Avro schema resolution
.
将嵌套字段的数据类型从 int 提升为 long Yes Yes
对于复杂类型(map或array的值),将数据类型从 int 提升为 long Yes Yes
在最后的根级别添加一个新的不可为空的列 No No 对于Spark数据源的MOR表,写入成功但读取失败。 作为一种解决方法,您可以使该字段为空
向内部结构添加一个新的不可为空的列(最后) No No
将嵌套字段的数据类型从 long 更改为 int No No
将复杂类型的数据类型从 long 更改为 int(映射或数组的值) No No

让我们通过一个示例来演示 Hudi 中的模式演进支持。 在下面的示例中,我们将添加一个新的字符串字段并将字段的数据类型从 int 更改为 long。

Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 3.1.2
/_/ Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_292)
Type in expressions to have them evaluated.
Type :help for more information. scala> import org.apache.hudi.QuickstartUtils._
import org.apache.hudi.QuickstartUtils._ scala> import scala.collection.JavaConversions._
import scala.collection.JavaConversions._ scala> import org.apache.spark.sql.SaveMode._
import org.apache.spark.sql.SaveMode._ scala> import org.apache.hudi.DataSourceReadOptions._
import org.apache.hudi.DataSourceReadOptions._ scala> import org.apache.hudi.DataSourceWriteOptions._
import org.apache.hudi.DataSourceWriteOptions._ scala> import org.apache.hudi.config.HoodieWriteConfig._
import org.apache.hudi.config.HoodieWriteConfig._ scala> import org.apache.spark.sql.types._
import org.apache.spark.sql.types._ scala> import org.apache.spark.sql.Row
import org.apache.spark.sql.Row scala> val tableName = "hudi_trips_cow"
tableName: String = hudi_trips_cow
scala> val basePath = "file:///tmp/hudi_trips_cow"
basePath: String = file:///tmp/hudi_trips_cow
scala> val schema = StructType( Array(
| StructField("rowId", StringType,true),
| StructField("partitionId", StringType,true),
| StructField("preComb", LongType,true),
| StructField("name", StringType,true),
| StructField("versionId", StringType,true),
| StructField("intToLong", IntegerType,true)
| ))
schema: org.apache.spark.sql.types.StructType = StructType(StructField(rowId,StringType,true), StructField(partitionId,StringType,true), StructField(preComb,LongType,true), StructField(name,StringType,true), StructField(versionId,StringType,true), StructField(intToLong,IntegerType,true)) scala> val data1 = Seq(Row("row_1", "part_0", 0L, "bob", "v_0", 0),
| Row("row_2", "part_0", 0L, "john", "v_0", 0),
| Row("row_3", "part_0", 0L, "tom", "v_0", 0))
data1: Seq[org.apache.spark.sql.Row] = List([row_1,part_0,0,bob,v_0,0], [row_2,part_0,0,john,v_0,0], [row_3,part_0,0,tom,v_0,0]) scala> var dfFromData1 = spark.createDataFrame(data1, schema)
scala> dfFromData1.write.format("hudi").
| options(getQuickstartWriteConfigs).
| option(PRECOMBINE_FIELD_OPT_KEY.key, "preComb").
| option(RECORDKEY_FIELD_OPT_KEY.key, "rowId").
| option(PARTITIONPATH_FIELD_OPT_KEY.key, "partitionId").
| option("hoodie.index.type","SIMPLE").
| option(TABLE_NAME.key, tableName).
| mode(Overwrite).
| save(basePath) scala> var tripsSnapshotDF1 = spark.read.format("hudi").load(basePath + "/*/*")
tripsSnapshotDF1: org.apache.spark.sql.DataFrame = [_hoodie_commit_time: string, _hoodie_commit_seqno: string ... 9 more fields] scala> tripsSnapshotDF1.createOrReplaceTempView("hudi_trips_snapshot") scala> spark.sql("desc hudi_trips_snapshot").show()
+--------------------+---------+-------+
| col_name|data_type|comment|
+--------------------+---------+-------+
| _hoodie_commit_time| string| null|
|_hoodie_commit_seqno| string| null|
| _hoodie_record_key| string| null|
|_hoodie_partition...| string| null|
| _hoodie_file_name| string| null|
| rowId| string| null|
| partitionId| string| null|
| preComb| bigint| null|
| name| string| null|
| versionId| string| null|
| intToLong| int| null|
+--------------------+---------+-------+ scala> spark.sql("select rowId, partitionId, preComb, name, versionId, intToLong from hudi_trips_snapshot").show()
+-----+-----------+-------+----+---------+---------+
|rowId|partitionId|preComb|name|versionId|intToLong|
+-----+-----------+-------+----+---------+---------+
|row_3| part_0| 0| tom| v_0| 0|
|row_2| part_0| 0|john| v_0| 0|
|row_1| part_0| 0| bob| v_0| 0|
+-----+-----------+-------+----+---------+---------+ // In the new schema, we are going to add a String field and
// change the datatype `intToLong` field from int to long.
scala> val newSchema = StructType( Array(
| StructField("rowId", StringType,true),
| StructField("partitionId", StringType,true),
| StructField("preComb", LongType,true),
| StructField("name", StringType,true),
| StructField("versionId", StringType,true),
| StructField("intToLong", LongType,true),
| StructField("newField", StringType,true)
| ))
newSchema: org.apache.spark.sql.types.StructType = StructType(StructField(rowId,StringType,true), StructField(partitionId,StringType,true), StructField(preComb,LongType,true), StructField(name,StringType,true), StructField(versionId,StringType,true), StructField(intToLong,LongType,true), StructField(newField,StringType,true)) scala> val data2 = Seq(Row("row_2", "part_0", 5L, "john", "v_3", 3L, "newField_1"),
| Row("row_5", "part_0", 5L, "maroon", "v_2", 2L, "newField_1"),
| Row("row_9", "part_0", 5L, "michael", "v_2", 2L, "newField_1"))
data2: Seq[org.apache.spark.sql.Row] = List([row_2,part_0,5,john,v_3,3,newField_1], [row_5,part_0,5,maroon,v_2,2,newField_1], [row_9,part_0,5,michael,v_2,2,newField_1]) scala> var dfFromData2 = spark.createDataFrame(data2, newSchema)
scala> dfFromData2.write.format("hudi").
| options(getQuickstartWriteConfigs).
| option(PRECOMBINE_FIELD_OPT_KEY.key, "preComb").
| option(RECORDKEY_FIELD_OPT_KEY.key, "rowId").
| option(PARTITIONPATH_FIELD_OPT_KEY.key, "partitionId").
| option("hoodie.index.type","SIMPLE").
| option(TABLE_NAME.key, tableName).
| mode(Append).
| save(basePath) scala> var tripsSnapshotDF2 = spark.read.format("hudi").load(basePath + "/*/*")
tripsSnapshotDF2: org.apache.spark.sql.DataFrame = [_hoodie_commit_time: string, _hoodie_commit_seqno: string ... 10 more fields] scala> tripsSnapshotDF2.createOrReplaceTempView("hudi_trips_snapshot") scala> spark.sql("desc hudi_trips_snapshot").show()
+--------------------+---------+-------+
| col_name|data_type|comment|
+--------------------+---------+-------+
| _hoodie_commit_time| string| null|
|_hoodie_commit_seqno| string| null|
| _hoodie_record_key| string| null|
|_hoodie_partition...| string| null|
| _hoodie_file_name| string| null|
| rowId| string| null|
| partitionId| string| null|
| preComb| bigint| null|
| name| string| null|
| versionId| string| null|
| intToLong| bigint| null|
| newField| string| null|
+--------------------+---------+-------+ scala> spark.sql("select rowId, partitionId, preComb, name, versionId, intToLong, newField from hudi_trips_snapshot").show()
+-----+-----------+-------+-------+---------+---------+----------+
|rowId|partitionId|preComb| name|versionId|intToLong| newField|
+-----+-----------+-------+-------+---------+---------+----------+
|row_3| part_0| 0| tom| v_0| 0| null|
|row_2| part_0| 5| john| v_3| 3|newField_1|
|row_1| part_0| 0| bob| v_0| 0| null|
|row_5| part_0| 5| maroon| v_2| 2|newField_1|
|row_9| part_0| 5|michael| v_2| 2|newField_1|
+-----+-----------+-------+-------+---------+---------+----------+

详解 Apache Hudi Schema Evolution(模式演进)的更多相关文章

  1. 详解Apache Hudi如何配置各种类型分区

    1. 引入 Apache Hudi支持多种分区方式数据集,如多级分区.单分区.时间日期分区.无分区数据集等,用户可根据实际需求选择合适的分区方式,下面来详细了解Hudi如何配置何种类型分区. 2. 分 ...

  2. LVS原理详解(3种工作模式及8种调度算法)

    2017年1月12日, 星期四 LVS原理详解(3种工作模式及8种调度算法)   LVS原理详解及部署之二:LVS原理详解(3种工作方式8种调度算法) 作者:woshiliwentong  发布日期: ...

  3. 图文详解AO打印(标准模式)

    一.概述   AO打印是英文Active-Online Print的简称,也称主动在线打印.打印前支持AO通讯协议的AO打印机(购买地址>>)首先通过普通网络与C-Lodop服务保持在线链 ...

  4. Java8初体验(二)Stream语法详解---符合人的思维模式,数据源--》stream-->干什么事(具体怎么做,就交给Stream)--》聚合

    Function.identity()是什么? // 将Stream转换成容器或Map Stream<String> stream = Stream.of("I", & ...

  5. 大数据入门第八天——MapReduce详解(四)本地模式运行与join实例

    一.本地模式调试MR程序 1.准备 参考之前随笔的windows开发说明处:http://www.cnblogs.com/jiangbei/p/8366238.html 2.流程 最重要的是设置Loc ...

  6. 详解Apache服务与高级配置,(主配置文件每行都有描述)

    HTTP服务---> http://httpd.apache.org/(官方网站) httpd  service :纯粹的web服务器,同时开源(不是GPL). 特性:1.在进程特性上通常是事先 ...

  7. 详解ThinkPHP支持的URL模式有四种普通模式、PATHINFO、REWRITE和兼容模式

    URL模式     URL_MODEL设置 普通模式    0 PATHINFO模式     1 REWRITE模式     2 兼容模式     3 如果你整个应用下面的模块都是采用统一的URL模式 ...

  8. 详解apache的allow和deny

    今天看了一篇关于apache allow,deny的文章收获匪浅,防止被删,我直接摘过来了,原文地址!!! !http://www.cnblogs.com/top5/archive/2009/09/2 ...

  9. android中的LaunchMode详解----四种加载模式

    Activity有四种加载模式: standard singleTop singleTask singleInstance 配置加载模式的位置在AndroidManifest.xml文件中activi ...

随机推荐

  1. Go 语言快速开发入门

    目录 需求 开发的步骤 linux下如何开发Go程序 MAC下如何开发Go程序 Golang执行流程分析 编译和运行说明 Go程序开发的注意事项 Go语言的转义字符(escapechar) Golan ...

  2. 腾讯tbs 内存泄露

    一.背景 TBS(腾讯浏览服务)是腾讯提供的移动端webview体验的整套解决方案(https://x5.tencent.com/docs/index.html),可以用于移动端加载doc.xls.p ...

  3. 一个程序的自我修养「GitHub 热点速览 v.22.19」

    一个程序要诞生涉及前后端技术,比如,你可以用可视化网页搭建工具 tmagic-editor 完成前端部分,而后端部分的数据库以及数据处理可能就要用到 jsonhero-web 和 directus.知 ...

  4. Python3获取5000个元素的单字符表

    技术背景 此前考虑过一个问题,有没有办法获取到python里面所有定义好的单字符的表,比如我们获取5000个不一样的单字符,但是常用的chr(number)的方法里面包含了太多的非字母条目,比如缩进换 ...

  5. c++动态内存分配需要注意的地方,可以不用尽量不用

    在实际开发中,需要动态分配内存的场景极少,string和vector已经足够方便,如果不是非用不可的情况,采用动态分配内存就是给自己挖坑. 如果应用开发中一定要用动态内存分配技术,建议把它封装在类中, ...

  6. Node.js躬行记(20)——KOA源码分析(下)

    在上一篇中,主要分析了package.json和application.js文件,本文会分析剩下的几个文件. 一.context.js 在context.js中,会处理错误,cookie,JSON格式 ...

  7. 【多线程】线程强制执行 join()

    线程强制执行 join() Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞 : 可以想象成插队. 代码示例: /** * @Description 测试join方法 * @Auth ...

  8. 日期和时间API - 读《Java 8实战》

    日期与时间 LocalDate 创建一个LocalDate对象并读取其值 // 根据年月日创建日期 LocalDate date1 = LocalDate.of(2014, 3, 18); // 读取 ...

  9. monit 命令详解(monit)

    monit是Monit软件的主操作控制命令. 语法 monit [options]+ [command] 选项(options) -c file 指定要使用的配置文件 -d n 每间隔多少秒运行一次M ...

  10. 高危!Fastjson反序列化远程代码执行漏洞风险通告,请尽快升级

    据国家网络与信息安全信息通报中心监测发现,开源Java开发组件Fastjson存在反序列化远程代码执行漏洞.攻击者可利用上述漏洞实施任意文件写入.服务端请求伪造等攻击行为,造成服务器权限被窃取.敏感信 ...