Paimon的部分更新以及DeleteVector实现

背景

本文基于 Paimon 0.9
出于对与Paimon内部的DeleteVctor的实现以及部分更新的实现进行的源码阅读。
关于 DeleteVector的介绍可以看这里

说明

对于Paimon来说无论是Spark中使用还是Flink使用,后面的逻辑都是一样的,所以我们以Spark为例来说。所以我们会参考类 org.apache.paimon.spark.SparkSource,
对于Flink可以参考org.apache.paimon.flink.FlinkTableFactory
如没特别说明,这里都是以主键表来进行说明。

paimon的部分字段更新

这里主要的场景更多的是多流或者多批写同一个表字段的场景,且每个流或批只更新某几个字段(同样的主键),具体的配置或说明参考Partial Update
这里涉及到的方法为 SparkTableWrite.write,最终会到MergeTreeWriter.write:

 @Override
    public void write(KeyValue kv) throws Exception {
        long sequenceNumber = newSequenceNumber();
        boolean success = writeBuffer.put(sequenceNumber, kv.valueKind(), kv.key(), kv.value());
        if (!success) {
            flushWriteBuffer(false, false);
            success = writeBuffer.put(sequenceNumber, kv.valueKind(), kv.key(), kv.value());
            if (!success) {
                throw new RuntimeException("Mem table is too small to hold a single element.");
            }
        }
    }
  • writeBuffer.put 主要是往buffer中写数据
    这里的writeBufferSortBufferWriteBuffer类实例。
    这里会 主键+sequenceNumber+valueKind + value 的形式写入数据
  • flushWriteBuffer 这里就会涉及到数据落盘以及部分更新的逻辑:
      writeBuffer.forEach(
                  keyComparator,
                  mergeFunction,
                  changelogWriter == null ? null : changelogWriter::write,
                  dataWriter::write);
    
    • mergeFunction 这里的函数就是会在MergeTreeWriter初始化,也就是会初始化为PartialUpdateMergeFunction
    • 对于forEach的实现会构建一个 MergeIterator,在这里面会调用 PartialUpdateMergeFunction.add方法
      这里就会涉及到部分更新的逻辑,主要就是:把按照 主键+sequenceNumber 排序好的数据传给PartialUpdateMergeFunction(这里sequenceNumber作用在FirstRowMergeFunction或者DeduplicateMergeFunction函数中的作用更加明显,通过sequenceNumber来保留最老或者最新的数据),
      这样PartialUpdateMergeFunction只需要判断前后两个的数据的主键是否一致来进行更新。
      具体的更新逻辑见: Partial Update
      new MergeIterator(
              awConsumer, buffer.sortedIterator(), keyComparator, mergeFunction);
      
      这里的buffer.sortedIterator主要看SortBufferWriteBuffer构造方法(也就是为什么会按照主键+sequenceNumber排序):
       public SortBufferWriteBuffer(
              RowType keyType,
              RowType valueType,
              @Nullable FieldsComparator userDefinedSeqComparator,
              MemorySegmentPool memoryPool,
              boolean spillable,
              MemorySize maxDiskSize,
              int sortMaxFan,
              CompressOptions compression,
              IOManager ioManager) {
          ...
          // key fields
          IntStream sortFields = IntStream.range(0, keyType.getFieldCount());
      
          // user define sequence fields
          if (userDefinedSeqComparator != null) {
              IntStream udsFields =
                      IntStream.of(userDefinedSeqComparator.compareFields())
                              .map(operand -> operand + keyType.getFieldCount() + 2);
              sortFields = IntStream.concat(sortFields, udsFields);
          }
      
          // sequence field
          sortFields = IntStream.concat(sortFields, IntStream.of(keyType.getFieldCount()));
      
          int[] sortFieldArray = sortFields.toArray();
      
          // row type
          List<DataType> fieldTypes = new ArrayList<>(keyType.getFieldTypes());
          fieldTypes.add(new BigIntType(false));
          fieldTypes.add(new TinyIntType(false));
          fieldTypes.addAll(valueType.getFieldTypes());
      
          NormalizedKeyComputer normalizedKeyComputer =
                  CodeGenUtils.newNormalizedKeyComputer(fieldTypes, sortFieldArray);
          RecordComparator keyComparator =
                  CodeGenUtils.newRecordComparator(fieldTypes, sortFieldArray);
          ...
          InternalRowSerializer serializer =
                  InternalSerializers.create(KeyValue.schema(keyType, valueType));
          BinaryInMemorySortBuffer inMemorySortBuffer =
                  BinaryInMemorySortBuffer.createBuffer(
                          normalizedKeyComputer, serializer, keyComparator, memoryPool);
      
      其中IntStream.concat(sortFields, IntStream.of(keyType.getFieldCount())) 就会会把sequenceNumber这个字段带入到排序中去,
      也就是在buffer.sortedIterato方法中调用。
      如果有定义sequence.field,那这里面的字段也会参与排序,见:udsFields 字段

DeleteVector的实现

关于deleteVector的实现,可以参考Introduce deletion vectors for primary key table
大概的思想是: 基于Compaction + lookup的机制产生 DeleteVector:

  • 当一个记录不属于 level0层的话,就不会产生DelectVector
  • 当一个记录只属于需要进行compaction的level的话,就不会产生DeleteVector
  • 当一个记录只属于 level0层的话,就要去查询不包含 Compaction的层的文件数据,从而产生DeleteVector
    注意: deleteVector只支持主键表, 是属于bucket级别的,一个bucket一个DeleteVector。

DeleteVector的写

按照以上的说法,只有在Compaction的时候,才会产生DeleteVector,所以 我们直接到达 MergeTreeWriter.flushWriteBuffer,这里涉及到DeleteVector的数据流如下:

compactManager.triggerCompaction(forcedFullCompaction)
       ||
       \/
submitCompaction
       ||
       \/
MergeTreeCompactTask.doCompact
       ||
       \/
    rewrite  
       ||
       \/
rewriteImpl 
       ||
       \/
LookupMergeTreeCompactRewriter.rewrite 
       ||
       \/
rewriteOrProduceChangelog
       ||
       \/
createMergeWrapper
       ||
       \/
iterator.next()
       ||
       \/
RecordReaderIterator.next()
       ||
       \/
advanceIfNeeded
       ||
       \/
currentIterator.next() 
       ||
       \/
SortMergeIterator.next()
       ||
       \/
LookupChangelogMergeFunctionWrapper.add(winner)
       ||
       \/
LookupChangelogMergeFunctionWrapper.getResult()


  • 这里MergeTreeCompactTask.doCompact写完之后,会有result.setDeletionFile(compactDfSupplier.get())
    compactDfSupplier 这里的源自submitCompaction方法中的compactDfSupplier构造:

     if (dvMaintainer != null) {
              compactDfSupplier =
                      lazyGenDeletionFile
                              ? () -> CompactDeletionFile.lazyGeneration(dvMaintainer)
                              : () -> CompactDeletionFile.generateFiles(dvMaintainer);
          }
    

    而这里的deleteVector的产生来自LookupChangelogMergeFunctionWrapper.getResult(),见以下说明

  • 这里的LookupMergeTreeCompactRewriter.rewriteLookupMergeTreeCompactRewriter实例是在创建MergeTreeWriter

     CompactManager compactManager =
                    createCompactManager(
                            partition, bucket, compactStrategy, compactExecutor, levels, dvMaintainer)
    

    这里会调用createRewriter方法创建LookupMergeTreeCompactRewriter实例,
    其中会根据lookupStrategy来创建该实例:

     public LookupStrategy lookupStrategy() {
            return LookupStrategy.from(
                    mergeEngine().equals(MergeEngine.FIRST_ROW),
                    changelogProducer().equals(ChangelogProducer.LOOKUP),
                    deletionVectorsEnabled(),
                    options.get(FORCE_LOOKUP));
    
  • 这里 currentIterator.next() 是 通过调用currentIterator = SortMergeReaderWithLoserTree.readBatch获取的,而SortMergeReaderWithLoserTree 是通过readerForMergeTree方法获取的

  • 这里LookupChangelogMergeFunctionWrapper.getResult()才是重点

     @Override
      public ChangelogResult getResult() {
          // 1. Compute the latest high level record and containLevel0 of candidates
          LinkedList<KeyValue> candidates = mergeFunction.candidates();
          Iterator<KeyValue> descending = candidates.descendingIterator();
          KeyValue highLevel = null;
          boolean containLevel0 = false;
          while (descending.hasNext()) {
              KeyValue kv = descending.next();
              if (kv.level() > 0) {
                  descending.remove();
                  if (highLevel == null) {
                      highLevel = kv;
                  }
              } else {
                  containLevel0 = true;
              }
          }
    
          // 2. Lookup if latest high level record is absent
          if (highLevel == null) {
              InternalRow lookupKey = candidates.get(0).key();
              T lookupResult = lookup.apply(lookupKey);
              if (lookupResult != null) {
                  if (lookupStrategy.deletionVector) {
                      PositionedKeyValue positionedKeyValue = (PositionedKeyValue) lookupResult;
                      highLevel = positionedKeyValue.keyValue();
                      deletionVectorsMaintainer.notifyNewDeletion(
                              positionedKeyValue.fileName(), positionedKeyValue.rowPosition());
                  } else {
                      highLevel = (KeyValue) lookupResult;
                  }
              }
          }
    
          // 3. Calculate result
          KeyValue result = calculateResult(candidates, highLevel);
    
          // 4. Set changelog when there's level-0 records
          reusedResult.reset();
          if (containLevel0 && lookupStrategy.produceChangelog) {
              setChangelog(highLevel, result);
          }
    
          return reusedResult.setResult(result);
      } 
    
  • 这里主要说明 lookup.apply的方法,其中 lookup的 构造是在createLookupChangelogMergeFunctionWrapper构造中:

       @Override
         public MergeFunctionWrapper<ChangelogResult> create(
                 MergeFunctionFactory<KeyValue> mfFactory,
                 int outputLevel,
                 LookupLevels<T> lookupLevels,
                 @Nullable DeletionVectorsMaintainer deletionVectorsMaintainer) {
             return new LookupChangelogMergeFunctionWrapper<>(
                     mfFactory,
                     key -> {
                         try {
                             return lookupLevels.lookup(key, outputLevel + 1);
                         } catch (IOException e) {
                             throw new UncheckedIOException(e);
                         }
                     },
                     valueEqualiser,
                     changelogRowDeduplicate,
                     lookupStrategy,
                     deletionVectorsMaintainer,
                     userDefinedSeqComparator);
         }
     }
    

    这里的lookupLevels.lookup 会最终调用createLookupFile 方法构造LookupFile 实例,
    其中会调用 valueProcessor.persistToDisk(kv, batch.returnedPosition()方法,持久化 行号到对应的文件,
    这样就能获取到对应的行号。

  • 获取到对应的结果 lookupResult 后
    调用 deletionVectorsMaintainer.notifyNewDeletion(positionedKeyValue.fileName(), positionedKeyValue.rowPosition()方法去构造
    DeletionVector.
    上面提到的result.setDeletionFile(compactDfSupplier.get())会调用 CompactDeletionFile.generateFiles(dvMaintainer) 方法
    从而调用maintainer.writeDeletionVectorsIndex方法,从而写如到DeleteVector文件中。

DeleteVector的读

DeleteVector的读取主要在以下方法中构造:PrimaryKeyFileStoreTable.newRead:
最终会调用RawFileSplitRead.createReader从而调用 ApplyDeletionVectorReader(fileRecordReader, deletionVector)方法构造ApplyDeletionVectorReader实例:

 public RecordIterator<InternalRow> readBatch() throws IOException {
        RecordIterator<InternalRow> batch = reader.readBatch();

        if (batch == null) {
            return null;
        }

        checkArgument(
                batch instanceof FileRecordIterator,
                "There is a bug, RecordIterator in ApplyDeletionVectorReader must be RecordWithPositionIterator");

        return new ApplyDeletionFileRecordIterator(
                (FileRecordIterator<InternalRow>) batch, deletionVector);
    }

该处的readBatch方法会构造一个ApplyDeletionFileRecordIterator迭代器,可见在next()方法会对每一个记录调用deletionVector.isDeleted是否删除的判断.

 @Override
    public InternalRow next() throws IOException {
        while (true) {
            InternalRow next = iterator.next();
            if (next == null) {
                return null;
            }
            if (!deletionVector.isDeleted(returnedPosition())) {
                return next;
            }
        }
    }

FAQ

写入文件的时候,怎么记录行号和主键的关系?

这里不会写入的时候记录行号,会在调用createLookupFile 在构建 LookupFile这个文件的时候(初始化),从parquet文件读取过来的时候,就会获取行号。

<think>我们正在讨论Paimon表的更新操作。根据引用内容,特别是引用[4]中提到了Paimon的MERGEINTO语法,这是实现更新操作的关键。引用[4]展示了Paimon的MERGEINTO语法,类似于SQL的MERGE语句,它可以根据源表的数据来更新目标表(Paimon表)的数据。语法结构如下:MERGEINTO target_tableUSING source_tableONconditionWHENMATCHED THENUPDATE SET...WHEN NOTMATCHED THENINSERT ...另外,引用[3]提到Paimon支持模式演化,修改的列会实时同步到Paimon表且不需要重启作业,这为更新操作提供了灵活性。根据引用[1]和引用[5],Paimon表需要处理变更数据(changelog),通过设置changelog-producer参数可以控制变更数据的产生方式。因此,实现Paimon表的更新操作有两种主要方式:1.使用MERGEINTO语句进行更新(适用于批量更新)2.通过实时同步数据源(如MySQLCDC)将更新操作同步到Paimon表(适用于实时更新)下面分别介绍这两种方式:方式一:使用MERGEINTO语句更新Paimon表步骤:1.准备一个源表(可以是Paimon表、外部表或内存中的临时表),其中包含需要更新的数据。2.使用MERGEINTO语句将源表的数据合并到目标Paimon表。示例(参考引用[4]):```sqlMERGEINTO target--目标表(Paimon表)USINGsource--源表(包含更新数据的表)ONsource.a= target.a--连接条件,例如根据主键匹配WHENMATCHED THENUPDATE SETa =source.a, b= source.b,c =source.c--当匹配时更新目标表的字段WHEN NOTMATCHED THENINSERT (a,b,c)values (source.a, source.b,source.c) --当不匹配时插入新行```注意:在更新时,通常我们只更新匹配到的记录,而插入新记录。也可以根据需求只写更新部分或只写插入部分。方式二:通过实时同步数据源实现更新步骤:1.配置一个CDC源(如MySQL),将变更数据(包括插入、更新、删除)实时同步到Paimon表。2.在创建Paimon表时,设置合适的changelog-producer(例如设置为'input',表示输入数据本身已经包含变更类型)和主键。引用[5]中给出了一个同步MySQL表到Paimon表的例子,该例子使用Flink作业进行同步。在同步过程中,MySQL中的更新操作会被捕获并同步到Paimon表。示例命令(来自引用[5]):```bashbin/flink run\/path/to/paimon-flink-action.jar\mysql-sync-table\--warehouse hdfs://node001:8020/paimon/hive\--databasedefault \--table order_detail_cdc \--primary-keys order_id \--mysql-confhostname=node003 \--mysql-confusername=root \--mysql-confpassword=123456\--mysql-conf database-name=test \--mysql-conftable-name='order_detail'\--catalog-confmetastore=hive\--catalog-confuri=thrift://node001:9083\--table-conf bucket=4\--table-conf changelog-producer=input \--table-confsink.parallelism=4```在这个例子中,MySQL表`order_detail`的更新操作会通过CDC同步到Paimon表`order_detail_cdc`。总结:-如果需要进行批量更新,可以使用MERGEINTO语句。-如果需要实时更新,可以通过CDC同步数据源(如MySQL)到Paimon表。注意:在实时同步方式中,Paimon表必须定义主键,因为更新操作需要根据主键来定位记录。另外,引用[3]还提到,Paimon支持从多个数据源同步数据,包括MySQL单表/多表、整个数据库,以及Kafka topic等。因此,根据具体场景选择合适的方式。相关问题:1.如何为Paimon表设置主键?2.在实时同步MySQL数据到Paimon表时,如何保证数据一致性?3.MERGEINTO语句在Paimon中执行时有哪些注意事项?4.Paimon表的changelog-producer参数有哪些可选值?分别代表什么含义?</think>### Paimon 更新操作的实现方式 Paimon更新操作主要通过以下两种核心机制实现: #### 1. **`MERGE INTO` 语法(批量更新)[^4]** ```sql MERGE INTO target_table -- 目标Paimon表 USING source_table -- 源数据表(可以是CDC数据流或临时表) ON source.key = target.key -- 主键匹配条件 WHEN MATCHED THEN UPDATE SET target.col1 = source.col1, target.col2 = source.col2 WHEN NOT MATCHED THEN INSERT (key, col1, col2) VALUES (source.key, source.col1, source.col2) ``` **实现原理**: - 基于主键匹配源表和目标表的记录 - 匹配时执行字段级更新(可更新部分字段) - 未匹配时插入新记录 - 支持原子性事务保证数据一致性 #### 2. **CDC实时同步更新(流式更新)[^1][^3][^5]** 通过配置`changelog-producer`参数,将数据源的变更实时同步到Paimon表: ```bash # 示例:MySQL CDC同步到Paimon表 bin/flink run paimon-flink-action.jar mysql-sync-table \ --table-conf changelog-producer=input \ # 关键配置 --primary-keys order_id \ # 主键(更新依据) --mysql-conf table-name='order_detail' # 源表 ``` **实现机制**: - **变更数据捕获**:将源库的`UPDATE/DELETE`操作转化为Paimon的变更日志 - **主键定位**:通过定义的主键定位需要更新的记录 - **模式演化**:支持动态添加/修改列(无需重启作业)[^3][^5] - **合并策略**:底层通过Compaction机制合并增量更新[^6] --- ### 关键配置说明 1. **`changelog-producer` 类型[^1]**: - `none`:不生成变更日志(仅追加) - `input`:直接使用输入数据中的变更记录(需CDC源) - `lookup`:通过查找表生成变更(适用非CDC源) 2. **主键必需性**: - 所有更新操作**必须定义主键**(`--primary-keys`参数) - 主键用于唯一标识记录和定位更新位置[^5] 3. **流批一体支持**: - 批量更新:通过`MERGE INTO` SQL执行 - 流式更新:通过Flink CDC连接器实现 - 统一存储:更新后的数据可直接用于批处理和流分析 --- ### 示例场景 **MySQL表结构变更同步到Paimon**[^5]: 1. 在MySQL源表新增字段`new_column` 2. 在Paimon表自动同步新增字段(模式演化) 3. 源表对该字段的更新操作实时同步到Paimon: ```sql -- MySQL执行 UPDATE orders SET new_column = 'value' WHERE id = 1001; ``` 4. Paimon通过`changelog-producer=input`捕获此更新 5. 定位主键`id=1001`的记录并更新对应字段值 --- ### 注意事项 1. **性能优化**: - 合理设置`bucket`分桶数(避免数据倾斜) - 调整`sink.parallelism`并行度(提升吞吐量)[^5] 2. **一致性保证**: - 启用`changelog-producer`时需确保源数据包含完整的变更日志 - 批量更新建议在低峰期执行 3. **删除操作**: - 通过`MERGE`+删除条件或CDC的`DELETE`记录实现 - 需启用`deletion-vectors`特性(标记删除而非物理删除)[^9] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值