3 Paimon数据湖中的表类型详解_paimon表和hive区别

  //Row.ofKind(RowKind.DELETE, "jack", Int.box(11))//-D
)(Types.ROW_NAMED(Array("name", "age"),Types.STRING,Types.INT))


//将DataStream转换为Table
val schema = Schema.newBuilder()
  .column("name", DataTypes.STRING().notNull())//主键非空
  .column("age", DataTypes.INT())
  .primaryKey("name")//指定主键
  .build()
val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

//创建Paimon类型的Catalog
tEnv.executeSql(
  """
    |CREATE CATALOG paimon_catalog WITH (
    |    'type'='paimon',
    |    'warehouse'='hdfs://bigdata01:9000/paimon'
    |)
    |""".stripMargin)
tEnv.executeSql("USE CATALOG paimon_catalog")

//注册临时表
tEnv.createTemporaryView("t1",table)

//创建Paimon类型的表
tEnv.executeSql(
  """
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS `changelog_input` (
    |    name STRING,
    |    age INT,
    |    PRIMARY KEY (name) NOT ENFORCED
    |) WITH (
    |    'changelog-producer' = 'input'
    |)
    |""".stripMargin)

//向Paimon表中写入数据
tEnv.executeSql(
  """
    |INSERT INTO `changelog_input`
    |SELECT name,age FROM t1
    |""".stripMargin)

}

}


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多种类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForInput`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.changelogproducer.input

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForInput {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM changelog_input – 此时默认只能查到数据的最新值
    |-- /*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来先运行`FlinkDataStreamWriteToPaimonForInput`向Paimon表中写入`+I`类型的数据。


再运行`FlinkDataStreamReadFromPaimonForInput`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±------------+
| op | name | age |
±—±-------------------------------±------------+
| +I | jack | 10 |


来看一下这个Flink任务的Web UI界面  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/426d219dcf2844b0a56ca8dac531ec25.png#pic_center)


在这可以发现,此时这个任务中没有产生Changelog Normalize物化节点,因为我们在Paimon表中指定了`changelog-producer=input`,所以这个Paimon表内部会自己存储Changelog数据。


此时到这个Paimon表的hdfs数据目录中查看一下:



[root@bigdata04 ~]# hdfs dfs -ls /paimon/default.db/changelog_input/bucket-0
Found 2 items
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 11:14 /paimon/default.db/changelog_input/bucket-0/changelog-bc3740e4-6adf-4e94-9d4e-c1ece10ed114-0.orc
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 11:14 /paimon/default.db/changelog_input/bucket-0/data-bc3740e4-6adf-4e94-9d4e-c1ece10ed114-1.orc


在这里可以发现里面有两个文件,一个以data开头的文件,里面存储的是数据自身。还有一个以changelog开头的文件,里面存储的是changelog变更数据。


修改`FlinkDataStreamWriteToPaimonForInput`中的代码,继续执行,向Paimon表中写入`-U`类型的数据。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
Row.ofKind(RowKind.UPDATE_BEFORE, “jack”, Int.box(10))//-U
//Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
//Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForInput`的控制台看到如下结果:



| -U | jack | 10 |


再修改`FlinkDataStreamWriteToPaimonForInput`中的代码,继续执行,向Paimon表中写入`+U`类型的数据。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
//Row.ofKind(RowKind.UPDATE_BEFORE, “jack”, Int.box(10))//-U
Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
//Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForInput`的控制台看到如下结果:



| +U | jack | 11 |


再修改`FlinkDataStreamWriteToPaimonForInput`中的代码,继续执行,向Paimon表中写入`-D`类型的数据。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
//Row.ofKind(RowKind.UPDATE_BEFORE, “jack”, Int.box(10))//-U
//Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForInput`的控制台看到如下结果:



| -D | jack | 11 |


下面我们停止`FlinkDataStreamReadFromPaimonForInput`这个实时读取任务。


停止了之后,修改一下代码,因为默认只会读取最新的数据快照



val execSql =
“”"
|SELECT * FROM changelog_input – 此时默认只能查到数据的最新值
|/*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
|“”".stripMargin


再重新运行`FlinkDataStreamReadFromPaimonForNone`这个实时读取任务,可以看到这个结果:



±—±-------------------------------±------------+
| op | name | age |
±—±-------------------------------±------------+
| +I | jack | 10 |
| -U | jack | 10 |
| +U | jack | 11 |
| -D | jack | 11 |


注意:此时可以看到完整的数据变更情况,这是依赖于Paimon表中存储的changelog文件实现的,没有依赖于Flink任务中的`Changelog Normalize`物化节点。


所以说,如果我们数据源中可以提供完整的changelog数据,那么建议给存储数据的Paimon表设置`changelog-producer=input`,这样下游任务读取这个Paimon表的时候就可以直接从表中changelog文件里面获取变更数据了,不需要自己维护,效率比较高。


###### (3)Lookup


如果数据源中没有提供完整的 Changelog,并且我们也不想让下游任务在读取数据时通过Changelog Normalize物化节点来生成,那么这个时候我们可以考虑在Paimon表中配置 `changelog-producer=lookup`。


这样可以通过Lookup(查找)的方式在向Paimon表中写入数据的时候生成 Changelog。


但是需要注意:Lookup这种方式目前处于实验阶段,还没有经过大量的生产环境验证。


![在这里插入图片描述](https://img-blog.csdnimg.cn/1fbd5376a423421d8e721e725743ceb7.png#pic_center)


看这个图,此时这个数据源中没有提供完整的Changelog,这个数据源可以是任意类型的数据源,数据源中可能只有`+I、+U、-D`类型的数据,缺少了`-U`类型的数据。


但是由于我们在Paimon表中设置了`changelog-producer=lookup`,所以在通过`SinkWriter`向Paimon表中写入数据的时候,底层会通过Lookup的方式查找表中已有的数据,自动生成`Changelog File`,补全`-U`类型的变更日志。


这样下游任务在读取这个Paimon表的时候就可以直接从表对应的`Changelog File`中读取到完整的`+I、-U、+U、-D`类型的数据了。


下面我们来具体演示一下建表语句中指定`changelog-producer=lookup`时的效果


创建package:`tech.xuwei.paimon.changelogproducer.lookup`


基于创建Object:`FlinkDataStreamWriteToPaimonForLookup`


这个Object负责向Paimon表中模拟写入数据。


代码如下:



package tech.xuwei.paimon.changelogproducer.lookup

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 使用Flink DataStream API向Paimon表中写入数据

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForLookup {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
    //Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
    //Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
    )(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING().notNull())//主键非空
    .column(“age”, DataTypes.INT())
    .primaryKey(“name”)//指定主键
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS changelog_lookup (
    | name STRING,
    | age INT,
    | PRIMARY KEY (name) NOT ENFORCED
    |) WITH (
    | ‘changelog-producer’ = ‘lookup’
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO changelog_lookup
    |SELECT name,age FROM t1
    |“”".stripMargin)
    }

}


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多种类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForLookup`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.changelogproducer.lookup

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForLookup {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM changelog_lookup – 此时默认只能查到数据的最新值
    |-- /*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来先运行`FlinkDataStreamWriteToPaimonForLookup`向Paimon表中写入+I类型的数据。


再运行`FlinkDataStreamReadFromPaimonForLookup`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±------------+
| op | name | age |
±—±-------------------------------±------------+
| +I | jack | 10 |


来看一下这个Flink任务的Web UI界面  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/896ac30e704548078972651a826ca196.png#pic_center)


在这可以发现,此时这个任务中没有产生`Changelog Normalize`物化节点,因为我们在Paimon表中指定了`changelog-producer=lookup`,Changelog数据会在我们向Paimon表中写入数据的时候通过Lookup产生。


到这个Paimon表的hdfs数据目录里面查看一下:



[root@bigdata04 ~]# hdfs dfs -ls /paimon/default.db/changelog_lookup/bucket-0
Found 3 items
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 12:01 /paimon/default.db/changelog_lookup/bucket-0/changelog-edb23cdc-09be-4437-b2ac-716e06e25c6d-1.orc
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 12:01 /paimon/default.db/changelog_lookup/bucket-0/data-edb23cdc-09be-4437-b2ac-716e06e25c6d-0.orc
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 12:01 /paimon/default.db/changelog_lookup/bucket-0/data-f07e00b5-a815-4d64-b8d6-1b8a2e64dab6-0.orc


在这可以发现,里面有1个changelog开头的文件,这个就是Lookup产生的。


修改`FlinkDataStreamWriteToPaimonForLookup`中的代码,继续执行,向Paimon表中写入`+U`类型的数据。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
//Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForLookup`的控制台看到如下结果:



| -U | jack | 10 |
| +U | jack | 11 |


注意:虽然我们向Paimon表中只写入了+U类型的数据,但是Lookup在生成changelog的时候会自动补全`-U`类型的数据。


后面的`-D`类型的数据就不再演示了,效果和前面是一样的。


所以说,Lookup这种方式属于一种折中方案,数据源里面无法提供完整的changelog变更日志,所以无法使用Input,但是我们还想摆脱昂贵的`Changelog Normalize`物化节点,这个时候就可以考虑Lookup了。


最后还需要注意,Lookup这种方式虽然不需要产生`Changelog Normalize`物化节点,但是他在生成Changelog的时候依然会消耗一部分资源的,因为它需要触发数据查找这个过程,只不过消耗的资源比`Changelog Normalize`物化节点这种方式低一些。


###### (4)Full Compaction


如果你的数据源无法提供完整的changelog变更日志数据,并且你觉得Lookup这种方式还是比较消耗资源,此时可以考虑使用`Full Compaction`这种方式,在创建Paimon表的时候指定`changelog-producer=full-compaction`。


Full Compaction这种方式可以解耦写入数据和生成changelog这两个步骤。  
 也就是说我们会先把数据写入到Paimon表中,当表中的数据触发完全压缩之后,Paimon 会比较两次完全压缩之间的结果并生成差异作为changelog(变更日志),生成changelog的延迟会受到完全压缩频率的影响。


通过指定`full-compaction.delta-commits`表属性,表示在增量提交Checkpoint后将会触发完全压缩。默认情况下值为1,所以每次提交Checkpoint都会进行完全压缩并生成changelog。  
 这样其实对生成changelog的延迟没有特别大的影响。


Full Compaction这种方式可以为任何类型的数据源生成完整的changelog变更日志。但是它没有Input方式的效率高,并且生成changelog的延迟可能会比较高。


不过Full Compaction这种方式解耦了写入数据和生成changelog这两个步骤,他的资源消耗比Lookup这种方式要低一些。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5973104d317d4ca4a04174ad93ede03d.png#pic_center)


看这个图,此时这个数据源中没有提供完整的Changelog,这个数据源可以是任意类型的数据源,数据源中可能只有`+I、+U、-D`的数据,缺少了`-U`类型的数据。


但是由于我们在Paimon表中设置了`changelog-producer=full-compaction`,所以Paimon会周期性的比较两次完全压缩(Full Compaction)之间的结果并生成差异作为changelog(变更日志),并且在Changelog中补全缺失的变更日志。


这样下游任务在读取这个Paimon表的时候就可以从表对应的Changelog File中读取到完整的`+I、-U、+U、-D`类型的数据了。


下面我们来具体演示一下建表语句中指定`changelog-producer=full-compaction`时的效果


创建package:`tech.xuwei.paimon.changelogproducer.fullcompaction`


创建object:`FlinkDataStreamWriteToPaimonForFullcompaction`


这个Object负责向Paimon表中模拟写入数据。


代码如下:



package tech.xuwei.paimon.changelogproducer.fullcompaction

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 使用Flink DataStream API向Paimon表中写入数据

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForFullcompaction {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
    //Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
    //Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
    )(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING().notNull())//主键非空
    .column(“age”, DataTypes.INT())
    .primaryKey(“name”)//指定主键
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS changelog_fullcompaction (
    | name STRING,
    | age INT,
    | PRIMARY KEY (name) NOT ENFORCED
    |) WITH (
    | ‘changelog-producer’ = ‘full-compaction’,
    | ‘full-compaction.delta-commits’ = ‘1’
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO changelog_fullcompaction
    |SELECT name,age FROM t1
    |“”".stripMargin)
    }

}


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多种类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForFullcompaction`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.changelogproducer.fullcompaction

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForFullcompaction {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM changelog_fullcompaction – 此时默认只能查到数据的最新值
    |–/*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来先运行`FlinkDataStreamWriteToPaimonForFullcompaction`向Paimon表中写入`+I`类型的数据。


再运行`FlinkDataStreamReadFromPaimonForFullcompaction`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±------------+
| op | name | age |
±—±-------------------------------±------------+
| +I | jack | 10 |


来看一下这个Flink任务的Web UI界面  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/50edb2b1d63141cf836e64cfbbdf7f61.png#pic_center)


在这可以发现,此时这个任务中没有产生`Changelog Normalize`物化节点,其实只有我们把`Changelog Producer`设置为`none`的时候Flink任务才会产生`Changelog Normalize`物化节点。


那此时我们到这个Paimon表的hdfs数据目录里面查看一下有没有产生changelog文件:



[root@bigdata04 ~]# hdfs dfs -ls /paimon/default.db/changelog_fullcompaction/bucket-0
Found 3 items
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 16:20 /paimon/default.db/changelog_fullcompaction/bucket-0/changelog-264c4b74-10dd-493d-95e0-8f5760e90dc8-1.orc
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 16:20 /paimon/default.db/changelog_fullcompaction/bucket-0/data-264c4b74-10dd-493d-95e0-8f5760e90dc8-0.orc
-rw-r–r-- 3 yehua supergroup 566 2028-12-11 16:20 /paimon/default.db/changelog_fullcompaction/bucket-0/data-d7adcc2a-804a-4a13-876a-fb77dc4a0952-0.orc


在这可以发现,里面有1个changelog开头的文件,这个就是Full Compaction这种方式产生的。


修改`FlinkDataStreamWriteToPaimonForFullcompaction`中的代码,继续执行,向Paimon表中写入`+U`类型的数据。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
//Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForFullcompaction`的控制台看到如下结果:



| -U | jack | 10 |
| +U | jack | 11 |


注意:这块可能会有一些延迟,具体的延迟程度要看完全压缩触发的频率,我们前面指定了`full-compaction.delta-commits`的值为1,表示在每次提交Checkpoint都会进行完全压缩并生成changelog,所以目前的延迟是比较低的。


但是我们需要注意:完全压缩是一个资源密集型的过程,会消耗一定的`CPU`和`磁盘IO`,因此过于频繁的完全压缩可能会导致写入速度变慢,所以这块也需要均衡考虑。


后面的`-D`类型的数据就不再演示了,效果和前面是一样的。


(5)总结  
 咱们前面一共讲了4种Changelog Producer。


* 在实际工作中None这种方式基本上是不使用的,成本太高。
* 如果数据源是完整的CDC数据,直接使用Input这种方式即可,成本最低,效率最高。
* 如果数据源中无法提供完整的Changelog,此时可以考虑使用Lookup和Full Compaction。
* 如果你觉得使用Lookup来实时生成 Changelog 成本过大,可以考虑通过Full Compaction和对应较大的延迟,以非常低的成本生成 Changelog。


##### 3.2.1.3 Merge Engines


Merge Engines:可以翻译为合并引擎。


针对多条相同主键的数据,Paimon主键表收到之后,应该如何进行合并处理?


针对这块的处理逻辑,Paimon提供了参数`merge-engine`,通过这个参数来指定如何合并数据。


`merge-engine`一共支持3种取值:


* deduplicate:默认值,表示去重,也就是说主键表默认只会保留相同主键最新的数据。
* partial-update:表示局部更新,通过相同主键的多条数据来更新不同字段的值。
* aggregation:表示聚合,可以对相同主键的多条数据根据指定的字段进行聚合。


下面我们来详细分析一下这几种合并引擎。


###### (1)Deduplicate


如果我们在Paimon中创建主键表时不指定`merge-engine`参数,那么默认值就是`deduplicate` 。


此时只保留主键最新的数据,之前表中相同主键的数据会被丢弃。


注意:如果主键最新的数据是`-D`类型的,那么这个主键的所有数据都会被删除。


下面我们来具体演示一下。  
 核心的思路是这样的:我们通过数据源模拟产生2条相同主键的+I类型的数据,依次写入到主键表中,最终发现主键表中只会保留最新的那一条数据。


创建package:`tech.xuwei.paimon.mergeengine.deduplicate`  
 创建object:`FlinkDataStreamWriteToPaimonForDeduplicate`


这个Object负责向Paimon表中模拟写入数据。  
 代码如下:



package tech.xuwei.paimon.mergeengine.deduplicate

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 使用Flink DataStream API向Paimon表中写入数据

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForDeduplicate {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
    //Row.ofKind(RowKind.INSERT, “jack”, Int.box(12))//+I
    )(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING().notNull())//主键非空
    .column(“age”, DataTypes.INT())
    .primaryKey(“name”)//指定主键
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS merge_engine_deduplicate (
    | name STRING,
    | age INT,
    | PRIMARY KEY (name) NOT ENFORCED
    |) WITH (
    | ‘merge-engine’ = ‘deduplicate’ – 注意:值为deduplicate时这一行配置可以省略不写
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO merge_engine_deduplicate
    |SELECT name,age FROM t1
    |“”".stripMargin)
    }

}


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多条`+I`类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForDeduplicate`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.mergeengine.deduplicate

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForDeduplicate {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM merge_engine_deduplicate – 此时默认只能查到数据的最新值
    |-- /*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来先运行`FlinkDataStreamWriteToPaimonForDeduplicate`向Paimon表中写入一条`+I`类型的数据。


再运行`FlinkDataStreamReadFromPaimonForDeduplicate`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±------------+
| op | name | age |
±—±-------------------------------±------------+
| +I | jack | 10 |


修改`FlinkDataStreamWriteToPaimonForDeduplicate`中的代码,继续执行,向Paimon表中写入第2条`+I`类型的数据。  
 注意:这两条数据的主键是相同的。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(12))//+I
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForDeduplicate`的控制台看到如下结果:



| -U | jack | 10 |
| +U | jack | 12 |


从这可以看出来,之前的数据被删除了,新增了一条年龄为12的数据。  
 所以`deduplicate`这种表引擎只会保留相同主键最新的数据。


###### (2)Partial Update


如果我们在Paimon中创建主键表时指定`merge-engine`的值为`partial-update`,那么就可以实现局部更新数据字段的效果。


举个例子:使用多个 Flink流任务去更新同一张表,每个流任务只更新一张表的部分列,最终实现一行完整数据的更新。对于需要构建宽表的业务场景,使用`partial-update`是非常合适的,并且构建宽表的操作也比较简单。



> 
> 注意:这里所说的多个Flink 流任务并不是指多个Flink Job并发写同一张Paimon表,这样比较麻烦。目前推荐的是将多个Flink流任务 `UNION ALL` 起来,最终启动一个Flink Job 向Paimon表中写入数据。
> 
> 
> 


还有一点需要注意的是:`partial-update`这种表引擎不支持流读,需要结合`Lookup`或者`full-compaction`变更日志生产者一起使用才可以支持流读。


同时由于`partial-update`不能接收和处理`DELETE`消息,为了避免接收到DELETE消息报错,需要在建表语句中配置`partial-update.ignore-delete= true`表示忽略 DELETE消息。


下面我们来具体演示一下:


核心思路是这样的,准备模拟产生3条+I类型的数据,数据内容大致是这样的。



<jack, 10, 175, null>
<jack, null, null, beijing>
<jack, 11, null, null>


将这3条数据写入到Paimon主键表之后,会得到什么结果呢?  
 结果是这样的:`<jack, 11, 175, beijing>`


为什么呢?因为`null`字段不会覆盖更新字段的值。


创建package:`tech.xuwei.paimon.mergeengine.partialupdate`  
 创建object:`FlinkDataStreamWriteToPaimonForPartialupdate`


这个Object负责向Paimon表中模拟写入数据。  
 代码如下:



package tech.xuwei.paimon.mergeengine.partialupdate

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 使用Flink DataStream API向Paimon表中写入数据

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForPartialupdate {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(10),Int.box(175),null)//+I
    //Row.ofKind(RowKind.INSERT, “jack”, null,null,“beijing”)//+I
    //Row.ofKind(RowKind.INSERT, “jack”, Int.box(11),null,null)//+I
    )(Types.ROW_NAMED(Array(“name”, “age”, “height”, “city”),Types.STRING,Types.INT,Types.INT,Types.STRING))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING().notNull())//主键非空
    .column(“age”, DataTypes.INT())
    .column(“height”, DataTypes.INT())
    .column(“city”, DataTypes.STRING())
    .primaryKey(“name”)//指定主键
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS merge_engine_partialupdate (
    | name STRING,
    | age INT,
    | height INT,
    | city STRING,
    | PRIMARY KEY (name) NOT ENFORCED
    |) WITH (
    | ‘merge-engine’ = ‘partial-update’,
    | ‘partial-update.ignore-delete’ = ‘true’
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO merge_engine_partialupdate
    |SELECT name,age,height,city FROM t1
    |“”".stripMargin)
    }

}


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多条`+I`类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForPartialupdate`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.mergeengine.partialupdate

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForPartialupdate {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM merge_engine_partialupdate – 此时默认只能查到数据的最新值
    |-- /*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来先运行`FlinkDataStreamWriteToPaimonFoPartialupdate`向Paimon表中写入一条`+I`类型的数据。


再运行`FlinkDataStreamReadFromPaimonForPartialupdate`负责读取数据。  
 结果发现代码报错了,错误日志如下:



Exception in thread “main” java.lang.RuntimeException: Partial update streaming reading is not supported. You can use ‘lookup’ or ‘full-compaction’ changelog producer to support streaming reading.
at org.apache.paimon.flink.utils.TableScanUtils.streamingReadingValidate(TableScanUtils.java:45)
at org.apache.paimon.flink.source.FlinkSourceBuilder.build(FlinkSourceBuilder.java:170)
at org.apache.paimon.flink.source.AbstractDataTableSource.configureSource(AbstractDataTableSource.java:233)
at org.apache.paimon.flink.source.AbstractDataTableSource.lambda$getScanRuntimeProvider$0(AbstractDataTableSource.java:210)
at org.apache.paimon.flink.PaimonDataStreamScanProvider.produceDataStream(PaimonDataStreamScanProvider.java:44)


通过错误日志可以看出来,`Partial update`表引擎默认不支持流读,我们现在在代码中指定了运行模式为`STREAMING`,就是流式读取的意思。


我们可以在表中指定使用`lookup`或者`full-compaction`变更日志生产者来支持流读。



> 
> 注意:如果不需要流读的话,可以在代码中指定运行模式为`BATCH`,此时执行是不报错的。
> 
> 
> 


如果想要使用流读,就需要在建表语中修改变更日志生产者了。



//创建Paimon类型的表
tEnv.executeSql(
“”"
|-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
|CREATE TABLE IF NOT EXISTS merge_engine_partialupdate (
| name STRING,
| age INT,
| height INT,
| city STRING,
| PRIMARY KEY (name) NOT ENFORCED
|) WITH (
| ‘changelog-producer’ = ‘lookup’,-- 注意:partial-update表引擎需要和lookup或者full-compaction一起使用时才支持流读
| ‘merge-engine’ = ‘partial-update’,
| ‘partial-update.ignore-delete’ = ‘true’
|)
|“”".stripMargin)


`merge_engine_partialupdate`这个表我们已经创建过了,所以我们需要删除这个表。其实有一种快捷方式,我们直接到HDFS中删除这个表对应的目录其实就可以了。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/8876889bd285490cbf826c9b6e5c29c0.png#pic_center)


接下来继续重新运行`FlinkDataStreamWriteToPaimonForPartialupdate`向Paimon表中写入一条`+I`类型的数据。


再运行`FlinkDataStreamReadFromPaimonForPartialupdate`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±------------±------------±-------------------------------+
| op | name | age | height | city |
±—±-------------------------------±------------±------------±-------------------------------+
| +I | jack | 10 | 175 | |


注意:此时`city`字段的值为`null`。


修改`FlinkDataStreamWriteToPaimonForDeduplicate`中的代码,继续执行,向Paimon表中写入第2条`+I`类型的数据。  
 注意:这条数据的主键和前面的数据是相同的。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10),Int.box(175),null)//+I
Row.ofKind(RowKind.INSERT, “jack”, null,null,“beijing”)//+I
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(11),null,null)//+I
)(Types.ROW_NAMED(Array(“name”, “age”, “height”, “city”),Types.STRING,Types.INT,Types.INT,Types.STRING))


此时可以在`FlinkDataStreamReadFromPaimonForPartialupdate`的控制台看到如下结果:



| -U | jack | 10 | 175 | |
| +U | jack | 10 | 175 | beijing |


在这里看到最新数据中的`city`字段有值了,其实刚才这一条数据相当于局部更新了`city`字段的值。  
 注意:其他几个为null的字段不会覆盖之前的字段的值,那也就意味着,如果我们指定了字段的值为null,说明不需要覆盖更新这个字段的值。


修改`FlinkDataStreamWriteToPaimonForDeduplicate`中的代码,继续执行,向Paimon表中写入第3条`+I`类型的数据。  
 注意:这条数据的主键和前面的数据是相同的。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10),Int.box(175),null)//+I
//Row.ofKind(RowKind.INSERT, “jack”, null,null,“beijing”)//+I
Row.ofKind(RowKind.INSERT, “jack”, Int.box(11),null,null)//+I
)(Types.ROW_NAMED(Array(“name”, “age”, “height”, “city”),Types.STRING,Types.INT,Types.INT,Types.STRING))


此时可以在`FlinkDataStreamReadFromPaimonForPartialupdate`的控制台看到如下结果:



| -U | jack | 10 | 175 | beijing |
| +U | jack | 11 | 175 | beijing |


在这里可以发现,`age`字段的值被修改为了`11`,其他字段的值没变。


这样我们就实现了局部更新数据字段的效果,这种业务场景和构建宽表的场景是非常类似的,所以`Partial Update`这个表引擎适合用于构建宽表的业务场景。


###### (3)Aggregation


如果我们在Paimon中创建主键表时指定`merge-engine`的值为`aggregation`,那么就可以实现指定列数据预聚合的效果了。


此时可以通过聚合函数做一些预聚合,除了主键以外的每个列都可以指定一个聚合函数,相同主键的数据就可以按照列上指定的聚合函数进行相应的预聚合;


常见的聚合函数包括`sum、max、min`等。


如果没有给列指定聚合函数,则默认使用`last-non-null-value`这个聚合函数,此时表示只保存最新非空值,空值不会覆盖。


注意:除了`sum`这个聚合函数,其他的聚合函数都不支持读取回撤数据,为了避免接收到DELETE和UPDATE BEFORE类型的消息报错,我们需要在建表语句中给指定字段进行配置`fields.${field_name}.ignore-retract = true` 忽略回撤数据。


还有一点需要注意:Aggregation表引擎也需要和`Lookup`或者`full-compaction`变更日志生产者一起使用。


下面我们来具体演示一下:


核心思路是这样的,准备模拟产生2条+I类型的数据,数据内容大致是这样的。



<1, 3.4, 10>
<1, 2.12, 15>


解释:`<商品id(id),商品价格(price),商品数量(count)>`


针对商品价格,我们希望统计最大值,针对商品数量,我们希望统计累加的和。


将这2条数据写入到Paimon主键表之后,会得到什么结果呢?  
 结果是这样的:`<1,3.4,25>`


创建package:`tech.xuwei.paimon.mergeengine.Aggregation`  
 创建object:`FlinkDataStreamWriteToPaimonForAggregation`


这个Object负责向Paimon表中模拟写入数据。  
 代码如下:



package tech.xuwei.paimon.mergeengine.aggregation

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 使用Flink DataStream API向Paimon表中写入数据

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForAggregation {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “1”, Double.box(3.4), Int.box(10))//+I
    //Row.ofKind(RowKind.INSERT, “1”, Double.box(2.12), Int.box(15))//+I
    )(Types.ROW_NAMED(Array(“id”, “price”, “count”),Types.STRING,Types.DOUBLE,Types.INT))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“id”, DataTypes.STRING().notNull())//主键非空
    .column(“price”, DataTypes.DOUBLE())
    .column(“count”, DataTypes.INT())
    .primaryKey(“id”)//指定主键
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS merge_engine_aggregation (
    | id STRING,
    | price DOUBLE,
    | count INT,
    | PRIMARY KEY (id) NOT ENFORCED
    |) WITH (
    | ‘changelog-producer’ = ‘lookup’,-- 注意:aggregation表引擎需要和lookup或者full-compaction一起使用时才支持流读
    | ‘merge-engine’ = ‘aggregation’,
    | ‘fields.price.aggregate-function’ = ‘max’,
    | ‘fields.count.aggregate-function’ = ‘sum’,
    | ‘fields.price.ignore-retract’ = ‘true’
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO merge_engine_aggregation
    |SELECT id,price,count FROM t1
    |“”".stripMargin)
    }

}


注意:count字段需要加反引号转义,否则SQL会报错。


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多条`+I`类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForAggregation`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.mergeengine.aggregation

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForAggregation {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM merge_engine_aggregation – 此时默认只能查到数据的最新值
    |-- /*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来首先运行`FlinkDataStreamWriteToPaimonForAggregation`向Paimon表中写入一条+I类型的数据。


再运行`FlinkDataStreamReadFromPaimonForAggregation`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±-------------------------------±------------+
| op | id | price | count |
±—±-------------------------------±-------------------------------±------------+
| +I | 1 | 3.4 | 10 |


修改`FlinkDataStreamWriteToPaimonForAggregation`中的代码,继续执行,向Paimon表中写入第2条`+I`类型的数据。  
 注意:这条数据的主键和前面的数据是相同的。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “1”, Double.box(3.4), Int.box(10))//+I
Row.ofKind(RowKind.INSERT, “1”, Double.box(2.12), Int.box(15))//+I
)(Types.ROW_NAMED(Array(“id”, “price”, “count”),Types.STRING,Types.DOUBLE,Types.INT))


此时可以在`FlinkDataStreamReadFromPaimonForPartialupdate`的控制台看到如下结果:



| -U | 1 | 3.4 | 10 |
| +U | 1 | 3.4 | 25 |


此时可以发现,price还是3.4,因为price字段是取的最大值;count字段的值变成了25,因为count字段取的是和。


通过Aggregation这个表引擎,可以实现数据写入的时候直接预聚合,可以简化数据聚合统计这块的操作。


##### 3.2.1.4 Sequence Field


默认情况下,Paimon中的主键表会根据数据的输入顺序确定数据的合并顺序,最后输入的数据会在最后进行合并,但是在分布式计算程序中,肯定会存在数据乱序问题的,这样可能会导致数据合并的结果并不是我们期望的。


在Flink中,针对数据乱序问题是通过watermark解决的。


在Paimon的主键表中,可以通过Sequence Field(序列字段)来解决。


针对咱们前面讲到的使用Partial Update表引擎构建宽表的案例,如果数据的写入顺序出现了错乱,肯定会导致结果异常的。


针对这个需求,这3条数据是这样的,默认情况下这3条数据是按照时间先后顺序产生的



1:<jack, 10, 175, null>
2:<jack, null, null, ‘beijing’>
3:<jack, 11, null, null>


如果他们按照1、2、3的顺序写入Paimon主键表中,那么可以得到我们期望的结果:`<jack, 11, 175, 'beijing'>`


如果先写入了第3条数据,再写入1、2条数据,那么结果就是这样的了:`<jack, 10, 175, 'beijing'>`,这样就不是我们期望看到的结果了。


所以,针对这种问题,可以使用`Sequence Field`来解决。  
 我们可以在建表语句中通过参数`sequence.field`来指定序列字段,一般建议使用时间字段作为序列字段。


这样就算顺序乱了,也不影响最终合并的结果,因为底层在合并数据的时候会把最大值的数据作为最后合并的结果。


下面我们来具体演示一下:


创建package:`tech.xuwei.paimon.sequencefield`


创建object:`FlinkDataStreamWriteToPaimonForSequencefield`


代码中需要用到这几条数据的时间戳:



2023-10-01 10:01:00 1696125660000
2023-10-01 10:01:01 1696125661000
2023-10-01 10:01:02 1696125662000


这个Object负责向Paimon表中模拟写入数据。  
 代码如下:



package tech.xuwei.paimon.sequencefield

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 使用Flink DataStream API向Paimon表中写入数据

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForSequencefield {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(11),null,null,Long.box(1696125662000L))//+I
    //Row.ofKind(RowKind.INSERT, “jack”, Int.box(10),Int.box(175),null,Long.box(1696125660000L))//+I
    //Row.ofKind(RowKind.INSERT, “jack”, null,null,“beijing”,Long.box(1696125661000L))//+I
    )(Types.ROW_NAMED(Array(“name”, “age”, “height”, “city”,“ts_millis”),Types.STRING,Types.INT,Types.INT,Types.STRING,Types.LONG))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING().notNull())//主键非空
    .column(“age”, DataTypes.INT())
    .column(“height”, DataTypes.INT())
    .column(“city”, DataTypes.STRING())
    .column(“ts_millis”, DataTypes.BIGINT())
    .primaryKey(“name”)//指定主键
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS sequence_field (
    | name STRING,
    | age INT,
    | height INT,
    | city STRING,
    | ts_millis BIGINT,
    | PRIMARY KEY (name) NOT ENFORCED
    |) WITH (
    | ‘changelog-producer’ = ‘lookup’,-- 注意:partial-update表引擎需要和lookup或者full-compaction一起使用时才支持流读
    | ‘merge-engine’ = ‘partial-update’,
    | ‘partial-update.ignore-delete’ = ‘true’,
    | ‘sequence.field’ = ‘ts_millis’,
    | ‘sequence.auto-padding’ = ‘millis-to-micro’ – 将序列字段的精度补足到微妙
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO sequence_field
    |SELECT name,age,height,city,ts_millis FROM t1
    |“”".stripMargin)
    }

}


注意:在执行代码的时候通过修改`env.fromElements(...)`中的注释来实现实时产生多条`+I`类型数据的效果。


接下来创建Object:`FlinkDataStreamReadFromPaimonForSequencefield`


这个Object负责从Paimon表中实时读取数据。


代码如下:



package tech.xuwei.paimon.sequencefield

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**

  • 使用Flink DataStream API从Paimon表中读取数据

  • Created by xuwei
    */
    object FlinkDataStreamReadFromPaimonForSequencefield {
    def main(args: Array[String]): Unit = {
    val conf = new Configuration()
    //指定WebUI界面的访问端口,默认就是8081
    conf.setString(RestOptions.BIND_PORT,“8081”)
    //为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
    val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)

    //禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
    env.disableOperatorChaining()

    val tEnv = StreamTableEnvironment.create(env)

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //执行SQL查询,打印输出结果
    val execSql =
    “”"
    |SELECT * FROM sequence_field – 此时默认只能查到数据的最新值
    |-- /*+ OPTIONS(‘scan.mode’=‘from-snapshot’,‘scan.snapshot-id’ = ‘1’) */ – 通过动态表选项来指定数据读取(扫描)模式,以及从哪里开始读取
    |“”".stripMargin
    val table = tEnv.sqlQuery(execSql)
    table.execute().print()

}

}


接下来首先运行`FlinkDataStreamWriteToPaimonForSequencefield`向Paimon表中写入一条`+I`类型的数据。


再运行`FlinkDataStreamReadFromPaimonForSequencefield`负责读取数据。  
 此时可以看到控制台输出如下结果:



±—±-------------------------------±------------±------------±-------------------------------±---------------------+
| op | name | age | height | city | ts_millis |
±—±-------------------------------±------------±------------±-------------------------------±---------------------+
| +I | jack | 11 | | | 1696125662000 |


修改`FlinkDataStreamWriteToPaimonForSequencefield`中的代码,继续执行,向Paimon表中写入第2条`+I`类型的数据。  
 注意:这条数据的主键和前面的数据是相同的。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(11),null,null,Long.box(1696125662000L))//+I
Row.ofKind(RowKind.INSERT, “jack”, Int.box(10),Int.box(175),null,Long.box(1696125660000L))//+I
//Row.ofKind(RowKind.INSERT, “jack”, null,null,“beijing”,Long.box(1696125661000L))//+I
)(Types.ROW_NAMED(Array(“name”, “age”, “height”, “city”,“ts_millis”),Types.STRING,Types.INT,Types.INT,Types.STRING,Types.LONG))


此时可以在`FlinkDataStreamReadFromPaimonForSequencefield`的控制台看到如下结果:



| -U | jack | 11 | | | 1696125662000 |
| +U | jack | 11 | 175 | | 1696125662000 |


注意:此时age字段的值没有被更新,因为这一条数据的时间没有上一条数据的时间大,因为我们指定了序列字段是`ts_millis`,所以`ts_millis`时间最大值的数据将是最后合并的结果。


其他为`null`的字段的值是可以被覆盖更新的。


修改`FlinkDataStreamWriteToPaimonForSequencefield`中的代码,继续执行,向Paimon表中写入第3条`+I`类型的数据。  
 注意:这条数据的主键和前面的数据是相同的。



//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(11),null,null,Long.box(1696125662000L))//+I
//Row.ofKind(RowKind.INSERT, “jack”, Int.box(10),Int.box(175),null,Long.box(1696125660000L))//+I
Row.ofKind(RowKind.INSERT, “jack”, null,null,“beijing”,Long.box(1696125661000L))//+I
)(Types.ROW_NAMED(Array(“name”, “age”, “height”, “city”,“ts_millis”),Types.STRING,Types.INT,Types.INT,Types.STRING,Types.LONG))


此时可以在`FlinkDataStreamReadFromPaimonForSequencefield`的控制台看到如下结果:



| -U | jack | 11 | 175 | | 1696125662000 |
| +U | jack | 11 | 175 | beijing | 1696125662000 |


这样最终得到的结果就是我们期望的了,这就是`Sequence Field`的典型应用场景了。


#### 3.3 Append Only表(仅追加表)


仅追加表很好理解,只要没有定义主键的表就是仅追加表。


仅追加表采用追加写入的方式,只能支持新增数据,不能更新和删除。


想要创建仅追加表很简单,我们只需要在建表语句中指定这个参数即可: `write-mode = append-only`


仅追加表主要用于无需更新数据的场景,例如数据仓库中ODS层的数据,不需要进行修改,保留数据原始的样子即可,此时推荐采用Paimon中的仅追加表。


仅追加表可以自动压缩表中的小文件,并且提供有序流式读取,我们也可以通过它来替代消息队列。


在实际工作中,除了数据库中的Binlog这种数据,还有大量的日志数据。  
 日志数据其实就属于Append Only数据,这种数据的数据量会非常大,一般情况下,我们会把这些日志数据存储在HDFS这种分布式文件系统中。


当我们在Paimon中使用仅追加表来存储这种数据的时候,数据可以实时写入,并且还可以实时读取;而且对实时资源的消耗也比较低,完全可以替代部分消息队列的场景。


最后,我们还需要注意一点,由于仅追加表没有主键,所以建议在使用的时候指定`bucket-key`,其实就是指定数据分桶(Bucket)的字段。  
 因为Bucket的范围是由数据中一个或多个列的哈希值确定的,我们可以通过`bucket-key`参数来指定分桶字段。  
 如果没有指定分桶字段,则默认会使用整行数据作为分桶字段,这样效率会比较低。


注意:如果是主键表,就算我们没有指定`bucket-key`,也不会使用整行数据作为分桶字段,因为主键表中有主键字段,默认会使用主键字段作为分桶字段。


下面我们来通过一个案例感受一下仅追加表的使用:


创建package:`tech.xuwei.paimon.appendonlytable`


创建object:`FlinkDataStreamWriteToPaimonForAppendonly`


代码如下:



package tech.xuwei.paimon.appendonlytable

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}

/**

  • 仅追加表

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForAppendonly {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(10)),//+I
    Row.ofKind(RowKind.INSERT, “tom”, Int.box(10)),//+I
    )(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING())
    .column(“age”, DataTypes.INT())
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.insertOnly())//仅追加类型的数据

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS append_only (
    | name STRING,
    | age INT
    |) WITH (
    | ‘write-mode’ = ‘append-only’,
    | ‘bucket’ = ‘2’
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO append_only
    |SELECT name,age FROM t1
    |“”".stripMargin)
    }

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

/**

  • 仅追加表

  • Created by xuwei
    */
    object FlinkDataStreamWriteToPaimonForAppendonly {
    def main(args: Array[String]): Unit = {
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
    val tEnv = StreamTableEnvironment.create(env)

    //手工构造一个Changelog DataStream 数据流
    val dataStream = env.fromElements(
    Row.ofKind(RowKind.INSERT, “jack”, Int.box(10)),//+I
    Row.ofKind(RowKind.INSERT, “tom”, Int.box(10)),//+I
    )(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))

    //将DataStream转换为Table
    val schema = Schema.newBuilder()
    .column(“name”, DataTypes.STRING())
    .column(“age”, DataTypes.INT())
    .build()
    val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.insertOnly())//仅追加类型的数据

    //创建Paimon类型的Catalog
    tEnv.executeSql(
    “”"
    |CREATE CATALOG paimon_catalog WITH (
    | ‘type’=‘paimon’,
    | ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
    |)
    |“”".stripMargin)
    tEnv.executeSql(“USE CATALOG paimon_catalog”)

    //注册临时表
    tEnv.createTemporaryView(“t1”,table)

    //创建Paimon类型的表
    tEnv.executeSql(
    “”"
    |-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
    |CREATE TABLE IF NOT EXISTS append_only (
    | name STRING,
    | age INT
    |) WITH (
    | ‘write-mode’ = ‘append-only’,
    | ‘bucket’ = ‘2’
    |)
    |“”".stripMargin)

    //向Paimon表中写入数据
    tEnv.executeSql(
    “”"
    |INSERT INTO append_only
    |SELECT name,age FROM t1
    |“”".stripMargin)
    }

[外链图片转存中…(img-4Azn93JE-1714146208185)]
[外链图片转存中…(img-fcrdEhXk-1714146208186)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值