Impala4.x源码阅读笔记(二)——Impala如何高效读取Iceberg表

前言

本文为笔者个人阅读Apache Impala源码时的笔记,仅代表我个人对代码的理解,个人水平有限,文章可能存在理解错误、遗漏或者过时之处。如果有任何错误或者有更好的见解,欢迎指正。

Iceberg表是一种用于存储大规模结构化数据的开源表格式,旨在提供高效的数据存储和丰富的查询能力。不同于Parquet,Orc等文件格式定义了数据如何在文件中存储和索引,Iceberg作为一种表格式定义的是数据文件如何组织,换句话说就是一系列的数据文件如何构成一张表以及我们如何从大量数据文件中找到我们需要的。Iceberg表支持事务性写入和多版本并发控制,这使得它适合于需要大规模数据存储和高效查询的数据湖或数据仓库环境。Iceberg表还提供了对数据架构变化的良好支持,使得数据架构的演进变得更加容易。

Iceberg表的基本结构包括数据文件和元数据文件两部分,这些文件被组织在目录结构中,其中数据文件是实际存储数据的地方,支持Parquet、Orc等业界主流数据文件格式。元数据文件除了记录表的结构信息和演进记录外,还有一种Manifest文件用于索引数据文件,支持Iceberg使用快照(Snapshot)的概念来维护表的版本信息,可以快速定位某个版本的表包括了哪些数据文件,这使得我们可以方便地在Iceberg表上进行时间旅行查询和回滚操作。另外在Iceberg V2格式的表中,还在之前V1格式表的基础之上增加了数据的行级更新与删除能力,通过写入专门的Delete File和MOR(Merge On Read)技术,可以在不重写现有数据文件的前提下实现行级别的删除。

在impala步入4.x的大版本后,对Iceberg表的支持一直是社区关注的重点。在社区的重点投入下,截止Apache Impala 4.3.0版本,Impala对Iceberg表的支持度已经相当高了,除了建表删表修改字段等常规表支持的操作外,对Iceberg特有的时间旅行、版本回滚、清理快照也进行了支持,另外还提供了从Hive表迁移到Iceberg的功能。社区目前正在大力推进对于Iceberg V2表的支持,目前已经完整支持了对Iceberg表的行级Delete和MOR读取。Impala在实现MOR时没有使用Iceberg API提供的读取方法,而是使用了自身由C++实现的执行引擎进行读取。得益于Impala本身对于HDFS+Parquet表的长期优化,使得Impala在Iceberg的MOR读取性能方面也具有优势。

本文主要根据源码就Impala如何高效读取地Iceberg表进行分析,这里的读取包括了同时包括了对Iceberg表的时间旅行和MOR的支持。分析的过程中着重于扫描Iceberg表的执行计划如何制定,以及期间做了哪些优化。

准备工作

为了分析Impala如何读取Iceberg表,我们可以从一个简单但是功能覆盖充分的例子入手。首先我们使用Impala创建一张Iceberg V2表,并进行一些数据写入和删除操作:

-- 创建一张V2格式的Iceberg表
CREATE TABLE ice_v2 (id int, name string) STORED BY ICEBERG TBLPROPERTIES('format-version'='2');
-- 插入最初的两条数据
INSERT INTO ice_v2 VALUES (1, 'a'), (2, 'b');
-- 删除id为2的一条数据
DELETE FROM ice_v2 WHERE id = 2;
-- 再插入一条数据
INSERT INTO ice_v2 VALUES (3, 'c');

现在我们就准备好了一张包含Delete数据的Iceberg表了,因为我们总共进行了三次DML操作,现在这张Iceberg表就有3个快照了。在Impala中可以通过执行DESCRIBE HISTORY语句查看Iceberg表历史版本:

在这里插入图片描述

也可以看见数据文件中有两个Insert文件和一个Delete文件,都使用Parquet格式储存:

在这里插入图片描述

为了后续一些概念的理解,我们再看下这三个文件中的数据:

在这里插入图片描述

可以发现Insert文件中的数据和我们执行的Insert语句是对应的,而Delete文件中的数据这是另外的Schema,记录的是删除的行所在的文件和位置。在有了这些信息之后,我们就可以开始分析Impala是如何读取一张Iceberg表的了。

执行计划

首先我们先看一下扫描Iceberg表的执行计划长什么样,执行如下SQL:

-- Result: (3, 'c'), (1, 'a')
SELECT * FROM ice_v2 FOR SYSTEM_VERSION AS OF 5109113003992490801

其中FOR SYSTEM_VERSION AS OF子句就是对Iceberg表进行时间旅行查询使用的,5109113003992490801DESCRIBE HISTORY中显示的Iceberg表最新的快照ID。这样我们可以指定一个Iceberg快照版本进行查询,当然不指定时Impala会默认查询最新的快照,因此实际上这个查询子句加与不加结果是一样的,只是为了后面触发时间旅行查询相关的逻辑。我们也可以用FOR SYSTEM_TIME AS OF子句来指定一个时间点来查询这个时间点时这张Iceberg表的数据,就像进行了时间旅行穿越回过去直接查询这张表一样,也就是所谓的时间旅行查询了。虽然这只是一个简单的SELECT *查询,但是它在Impala中的执行计划却不简单:

在这里插入图片描述

从SQL角度来说,一条SQL在Impala中的处理过程逻辑上大体可以分为四个阶段,解析、分析、计划与执行。其中解析就是解析SQL,通过SQL Parser将SQL字符串转换为语句类StatementBase的子类对象,比如说上文中的查询会被转换为SelectStmt,其中又包含SelectListFromClause等子句部分,FromClause又包括了一个TableRef的列表表示FROM子句后面的各个表或者类似于表的对象,在TableRef中又包括一个TimeTravelSpec对象对应了我们的时间旅行子句。这些语句对象都实现了自己的analyze方法,会在后面的分析过程中调用。比如说SelectStmtanalyze会调用FromClauseanalyze,同时自己还会进行星号展开、注册Slot等工作。FromClauseanalyze则会分析路径、建立别名并调用各个TableRefanalyze方法。在TableRefanalyze方法中除了分析Join、Hints之外也会调用TimeTravelSpecanalyze方法。TimeTravelSpecanalyze方法则会计算AS OF后的表达式得到目标时间旅行的版本或者时间,为后续索引数据文件做准备。

经过了复杂的分析之后就可以进入计划阶段了,计划阶段的任务就是根据语句类对象和先前的分析结果给查询制定一个执行计划。执行计划是一个由各种PlanNode的子类对象构成的一个树状结构PlanTree,它会指导查询如何进行执行,具体包括执行需要哪些结点参与,结点间的数据流向等等。执行计划会被转为Thrift结构体传递给Impala的C++执行引擎进行执行,执行时PlanNode会转变为对应ExecNode的子类对象,每个结点都负责各自对应的工作,比如说ScanNode负责一张表的扫描任务,是树中的叶子结点。而JoinNodeUnionNode分别负责连接任务与合并任务,都有多个输入和一个输出。数据在结点间以行批的形式传递,最终汇聚到根结点并返回给客户端完成查询执行。

计划阶段又可以大体分为两部分,首先是制定单点执行计划,给出一颗完整的执行计划树。但是单点执行计划还不足以指导查询执行,Impala作为一个MPP架构的执行引擎可以在分布式集群的多个执行者上并发执行查询,因此还需要将单点执行计划转变为分布式执行计划。分布式执行计划实际上就是将单点执行计划切分为多个片段Fragment,并在其间插入一些交换节点ExchangeNode用于在Fragment之间传递数据。在上面的执行计划树图中我们可以看到结点间有虚线和实线两种连接方式,实线连接的结点就是同属于一个Fragment的,而虚线连接的是不同的FragmentFragment是查询执行阶段可以调度的最小执行单元,调度器可以根据Fragment的性质将其调度到一个或多个执行者上创建执行实例Instance并开始实际执行。比如说对于只有一个文件的扫描结点的Fragment,我们只需要将其调度到一个执行者上执行就够了,而那些扫描文件很多或者需要高并发执行的Fragment,我们则需要将其调度到多个执行者并发加速执行。

在Impala的执行计划中一般都是一个ScanNode对应负责一张表的扫描任务,而从图中可以发现,我们为了扫描这张Iceberg表却出动了三个ScanNode,甚至还有一个JoinNode和一个UnionNode,这主要是因为Iceberg表中有Delete文件出现了。我们可以对比看一下没有Delete文件时的Iceberg表扫描查询计划是怎样的,执行SQL:

-- Result: (1, 'a'), (2, 'b')
SELECT * FROM ice_v2 FOR SYSTEM_TIME AS OF '2023-12-07 16:00:05.5';

这次我们使用时间旅行回到表进行了第一次插入但是还没有进行Delete的时间点进行查询,执行计划如下:

在这里插入图片描述

这回就是简简单单一个ScanNode了。Impala究竟是如何为Iceberg表制定扫描计划的?我们可以从代码中找到答案。

代码分析

本文要分析的Iceberg表的扫描计划的制定是单点执行计划的制定的一部分,完整的执行计划制定代码是十分庞大和复杂的,幸运的是我们只需要关注其中的一小部分就能得到需要的答案。首先我们先快速深入的目标代码的位置,调用路径如下:

getPlannedExecRequest() -> createExecRequest() -> createPlans() -> createPlanFragments()
-> createSingleNodePlan() -> createQueryPlan() -> createSelectPlan() -> createTableRefsPlan()
-> createTableRefNode() -> createScanNode() {// 如果TableRef是实际的表就会调用createScanNode()方法为其创建扫描结点...FeTable table = tblRef.getTable();// 通过表的类型选择对应的方法创建ScanNode// Iceberg表是FeIcebergTable同时也实现了FeFsTable接口,同时也是一种文件系统表(Hdfs表)if (table instanceof FeFsTable) {if (table instanceof FeIcebergTable) {// 对于Iceberg表有专门的IcebergScanPlanner创建扫描结点// IcebergScanPlanner构造时需要传入分析信息、查询上下文、表引用、连词和聚合信息// 同时还会在构造方法中完成谓词抽取,即将conjuncts中可以下推到Iceberg API中的谓词抽取出来并转换为Iceberg的谓词对象IcebergScanPlanner icebergPlanner = new IcebergScanPlanner(analyzer, ctx_, tblRef,conjuncts, aggInfo);return icebergPlanner.createIcebergScanPlan();}return createHdfsScanPlan(tblRef, aggInfo, conjuncts, analyzer);} else if (table instanceof FeDataSourceTable) {...
}

接下来就进入正题了,我们看下IcebergScanPlanner::createIcebergScanPlan()是如何给Iceberg表创建扫描计划的:

public PlanNode createIcebergScanPlan() throws ImpalaException {// 首先是一个重要的判断,决定是否需要调用Iceberg的planFiles() API来列出待扫描文件// needIcebergForPlanning()等价于 !impalaIcebergPredicateMapping_.isEmpty() || tblRef_.getTimeTravelSpec() != null;// 即如果有谓词下推或者有时间旅行子句的话,我们是需要调用planFiles()来列出待扫描文件的// 因为这种情况下要扫描的文件往往不是最新快照的全部数据文件,而是其的一个子集或者是其他快照版本包含的数据文件if (!needIcebergForPlanning()) {// 为连词中引用到的字段进行槽位物化,槽位物化可以理解为在数据行的内存定义中保留一块内存存放对应字段的数据// 因为谓词评估需要使用这些字段进行计算,所以即使select list中没有它们,我们必须在数据行定义中给它们预留位置analyzer_.materializeSlots(conjuncts_);// 对于不需要planFiles()的扫描,我们可以直接调用setFileDescriptorsBasedOnFileStore()方法// 使用缓存的最新快照的文件集合FileStore来设置文件描述符FileDescriptors,用于之后的扫描计划创建setFileDescriptorsBasedOnFileStore();// 然后调用createIcebergScanPlanImpl()进行具体的扫描计划创建return createIcebergScanPlanImpl();}// 对于进行了谓词下推或包含时间旅行的查询,我们无法根据缓存的最新快照的文件集合来得到需要扫描的FileDescriptors// 需要调用filterFileDescriptors()来根据谓词或时间旅行子句来过滤得到需要FileDescriptorsfilterFileDescriptors();// 调用filterConjuncts()来过滤谓词,因为在filterFileDescriptors()中我们可能已经将部分谓词下推到Iceberg中了// 这部分谓词不需要在执行时再次计算,因为Iceberg API已经在文件层面将不符合谓词的数据文件都过滤了// 剩余需要扫描的文件中的数据肯定符合这些已经下推的谓词,因此我们需要将其过滤掉,避免扫描时多余的谓词计算// 只将那些没有被下推成功的谓词保留,并传递给扫描结点,由Impala的执行引擎负责计算和过滤数据filterConjuncts();analyzer_.materializeSlots(conjuncts_);return createIcebergScanPlanImpl();
}

对于上文中有时间旅行子句查询,没法进入createIcebergScanPlan()中的if分支,需要调用filterFileDescriptors()来得到应该扫描的文件,我们继续看下其是如何实现的:

private void filterFileDescriptors() throws ImpalaException {TimeTravelSpec timeTravelSpec = tblRef_.getTimeTravelSpec();// 调用IcebergUtil::planFiles()并传递Iceberg表、之前抽取并转换为Iceberg谓词的谓词列表和时间旅行描述对象// 其封装了Iceberg API的TableScan::planFiles()方法,可以将谓词和时间旅行版本传递给Iceberg API// 其返回的fileScanTasks就包含了所有我们需要扫描的数据文件,包括Delete文件try (CloseableIterable<FileScanTask> fileScanTasks =IcebergUtil.planFiles(getIceTable(),new ArrayList<>(impalaIcebergPredicateMapping_.keySet()), timeTravelSpec)) {long dataFilesCacheMisses = 0;for (FileScanTask fileScanTask : fileScanTasks) {// 遍历每个FileScanTask,首先处理残余表达式,也就是没有被下推到Iceberg的谓词// 这些谓词无法在文件级别应用,需要Impala的扫描结点在行级别应用,将其先全部加入到集合residualExpressions_中Expression residualExpr = fileScanTask.residual();if (residualExpr != null && !(residualExpr instanceof True)) {residualExpressions_.add(residualExpr);}// 调用getFileDescriptor()来获取文件描述符FileDescriptor和是否命中缓存的标志// getFileDescriptor()中封装了一层缓存文件描述符的逻辑,即使未命中缓存也会创建新的并添加到缓存Pair<FileDescriptor, Boolean> fileDesc = getFileDescriptor(fileScanTask.file());if (!fileDesc.second) ++dataFilesCacheMisses;// 如果这个文件没有对应的Delete文件,我们将其加入到无Delete文件的数据文件列表dataFilesWithoutDeletes_中if (fileScanTask.deletes().isEmpty()) {dataFilesWithoutDeletes_.add(fileDesc.first);} else {// 否则将其加入到有Delete文件的数据文件列表dataFilesWithDeletes_中dataFilesWithDeletes_.add(fileDesc.first);// 同时还需要遍历其所有的Delete文件,将其加入到Delete文件列表deleteFiles_中for (DeleteFile delFile : fileScanTask.deletes()) {// 截止本文撰写时,Impala还未支持等值删除EQUALITY_DELETES,只支持位置删除POSITION_DELETES// 如果发现目标表有等值删除文件,我们无法扫描,需要引发一个异常if (delFile.content() == FileContent.EQUALITY_DELETES) {throw new ImpalaRuntimeException(String.format("Iceberg table %s has EQUALITY delete file which is currently " +"not supported by Impala, for example: %s", getIceTable().getFullName(),delFile.path()));}Pair<FileDescriptor, Boolean> delFileDesc = getFileDescriptor(delFile);if (!delFileDesc.second) ++dataFilesCacheMisses;deleteFiles_.add(delFileDesc.first);}}}if (dataFilesCacheMisses > 0) {Preconditions.checkState(timeTravelSpec != null);LOG.info("File descriptors had to be loaded on demand during time travel: " +String.valueOf(dataFilesCacheMisses));}} catch (IOException | TableLoadingException e) {throw new ImpalaRuntimeException(String.format("Failed to load data files for Iceberg table: %s", getIceTable().getFullName()),e);}// 最后更新下Delete文件的统计信息,包括数据行数和行数据量,用于后续预估内存消耗等updateDeleteStatistics();
}

对谓词的处理并非本文的关注的重点,所以我们跳过filterConjuncts()方法,继续看关键的createIcebergScanPlanImpl()方法最后如何为Iceberg扫描创建扫描结点的:

private PlanNode createIcebergScanPlanImpl() throws ImpalaException {// 对于没有Delete文件的情况,我们只需要一个扫描结点来扫描数据文件就行了,也就是上文中第二个查询的情况if (deleteFiles_.isEmpty()) {Preconditions.checkState(dataFilesWithDeletes_.isEmpty());// 创建一个新的Iceberg扫描结点对象,并传递结点ID、表引用、连词列表、聚合信息、文件列表// 非IDENTITY列的连词列表和需要跳过连词列表,然后调用init()方法进行初始化// IcebergScanNode继承了HdfsScanNode,大部分逻辑也是复用的HdfsScanNode的// 所以很多Hdfs Scan的优化和功能也能在Iceberg扫描中使用PlanNode ret = new IcebergScanNode(ctx_.getNextNodeId(), tblRef_, conjuncts_,aggInfo_, dataFilesWithoutDeletes_, nonIdentityConjuncts_,getSkippedConjuncts());ret.init(analyzer_);return ret;}// 如果有Delete文件,那么一个扫描结点就不够用了,我们需要另外一个扫描结点专门负责扫描Delete文件// 然后还需要一个反连接结点将两者的数据进行ANTI JOIN,实现删除行的效果// 这部分逻辑由createPositionJoinNode()方法实现,我们后面分析,总之它会返回一个JoinNodePlanNode joinNode = createPositionJoinNode();// 如果先前的分析阶段发现这是可以进行优化的针对Iceberg V2表的count(*)查询// 则剩余的无Delete文件的数据文件的不需要实际扫描了,可从元数据中得到行数// 因此这里可以直接返回所有数据文件和所有删除文件之间的ANTI JOIN// 不需要处理后面的无Delete文件对应的数据文件,它们的行数会被一个ArithmeticExpr直接加到结果中if (ctx_.getQueryCtx().isOptimize_count_star_for_iceberg_v2()) return joinNode;// 当然,当所有数据文件都有相应的删除文件,即dataFilesWithoutDeletes_为空时// 我们也只需要返回所有数据文件和所有删除文件之间的ANTI JOINif (dataFilesWithoutDeletes_.isEmpty()) return joinNode;// 如果还有无Delete文件对应的数据文件的话,我们则还需要这些文件创建一个扫描结点IcebergScanNode dataScanNode = new IcebergScanNode(ctx_.getNextNodeId(), tblRef_, conjuncts_, aggInfo_, dataFilesWithoutDeletes_,nonIdentityConjuncts_, getSkippedConjuncts());dataScanNode.init(analyzer_);// 然后根据表引用的槽位描述符创建输出表达式,并依此创建一个合并结点UnionNode来合并数据List<Expr> outputExprs = tblRef_.getDesc().getSlots().stream().map(entry -> new SlotRef(entry)).collect(Collectors.toList());UnionNode unionNode = new UnionNode(ctx_.getNextNodeId(), tblRef_.getId(),outputExprs, false);// 将之前创建的负责无Delete文件的数据扫描结点和负责有Delete文件的连接结点作为UnionNode的输入// 并完成初始化后返回,这样这张Iceberg表的扫描计划就创建完成了// 根结点为一个UnionNode,也就对应了上文中第一个查询的情况unionNode.addChild(dataScanNode, outputExprs);unionNode.addChild(joinNode, outputExprs);unionNode.init(analyzer_);Preconditions.checkState(unionNode.getChildCount() == 2);Preconditions.checkState(unionNode.getFirstNonPassthroughChildIndex() == 2);return unionNode;
}

看完关键的createIcebergScanPlanImpl()方法后,Iceberg表的扫描计划是如何制定的就已经很清晰了,但是还有最后一块也是最核心的逻辑还需要再进一步分析,也就是期间我们调用的createPositionJoinNode()方法,它被用来创建实现Delete文件和数据文件之间数据行删除逻辑的ANTI JOIN结点,代码分析如下:

  private PlanNode createPositionJoinNode() throws ImpalaException {Preconditions.checkState(deletesRecordCount_ != 0);Preconditions.checkState(dataFilesWithDeletesSumPaths_ != 0);Preconditions.checkState(dataFilesWithDeletesMaxPath_ != 0);// 我们需要为数据文件和Delete文件分别创建一个扫描结点,不过在此之前还有些准备工作PlanNodeId dataScanNodeId = ctx_.getNextNodeId();PlanNodeId deleteScanNodeId = ctx_.getNextNodeId();// 首先需要为Delete文件先创建一张虚拟表,因为扫描结点都是以表为基础的,而Delete文件并不是实际存在的表IcebergPositionDeleteTable deleteTable = new IcebergPositionDeleteTable(getIceTable(),getIceTable().getName() + "-POSITION-DELETE-" + deleteScanNodeId.toString(),deleteFiles_, deletesRecordCount_, getFilePathStats());analyzer_.addVirtualTable(deleteTable);// 有了对应Delete文件的虚拟表IcebergPositionDeleteTable后,再为其创建表引用对象TableRefTableRef deleteDeltaRef = TableRef.newTableRef(analyzer_,Arrays.asList(deleteTable.getDb().getName(), deleteTable.getName()),tblRef_.getUniqueAlias() + "-position-delete");// 为了数据文件和Delete文件之间能够进行ANTI JOIN,我们还需要为数据文件的表添加相关的虚拟列// 这些虚拟列并不是实际存在于表中的列,而是为了作为Join Key存在的// addDataVirtualPositionSlots()方法会为数据表添加两个虚拟列,分别为INPUT__FILE__NAME和FILE__POSITION// Impala已经为Parquet和Orc文件实现了这两个虚拟列,其数据可以在扫描时填充,含义分别是文件名和行号addDataVirtualPositionSlots(tblRef_);// addDeletePositionSlots()方法会为Delete文件的虚拟表添加两个实际列,分别为file_path和pos// 这两个列是在Delete文件中实际存在的,可以看上文中解析Delete文件得到的结果addDeletePositionSlots(deleteDeltaRef);// 然后就可以为数据文件和Delete文件分别创建一个扫描结点并初始化了IcebergScanNode dataScanNode = new IcebergScanNode(dataScanNodeId, tblRef_, conjuncts_, aggInfo_, dataFilesWithDeletes_,nonIdentityConjuncts_, getSkippedConjuncts(), deleteScanNodeId);dataScanNode.init(analyzer_);IcebergScanNode deleteScanNode = new IcebergScanNode(deleteScanNodeId,deleteDeltaRef,Collections.emptyList(), /*conjuncts*/aggInfo_,Lists.newArrayList(deleteFiles_),Collections.emptyList(), /*nonIdentityConjuncts*/Collections.emptyList()); /*skippedConjuncts*/deleteScanNode.init(analyzer_);// 然后是ANTI JOIN结点的创建,首先需要调用createPositionJoinConjuncts()创建连接谓词// 它会生成两个EQ谓词,相当于INPUT__FILE__NAME = file_path AND FILE__POSITION = pos// 这样数据文件中的行和Delete文件中的行匹配上时就可以在ANTI JOIN中被剔除了,实现了删除的效果List<BinaryPredicate> positionJoinConjuncts = createPositionJoinConjuncts(analyzer_, tblRef_.getDesc(), deleteDeltaRef.getDesc());// Impala还实现了一种专门针对Iceberg Delete文件DELETE JOIN结点IcebergDeleteNode// 如果没设置query option disable_optimized_iceberg_v2_read的话// 就会使用IcebergDeleteNode代替LEFT_ANTI_JOIN的HashJoinNode来进行ANTI JOIN// IcebergDeleteNode继承了HashJoinNode,同时专门针对Iceberg Delete进行了优化TQueryOptions queryOpts = analyzer_.getQueryCtx().client_request.query_options;JoinNode joinNode = null;if (queryOpts.disable_optimized_iceberg_v2_read) {joinNode = new HashJoinNode(dataScanNode, deleteScanNode,/*straight_join=*/true, DistributionMode.NONE, JoinOperator.LEFT_ANTI_JOIN,positionJoinConjuncts, /*otherJoinConjuncts=*/Collections.emptyList());} else {joinNode =new IcebergDeleteNode(dataScanNode, deleteScanNode, positionJoinConjuncts);}// ANTI JOIN结点的创建好之后再进行一些初始化工作就可以返回了joinNode.setId(ctx_.getNextNodeId());joinNode.init(analyzer_);joinNode.setIsDeleteRowsJoin();return joinNode;}

至此,Iceberg表的扫描计划的制定就结束了,后续再完成一些别的必须的工作就可以下发到执行引擎中进行执行了。

总结

这篇文章主要是在执行计划的制定方面分析了Iceberg表在Impala中是如何扫描的,总的来说为了实现高性能的Iceberg的MOR功能,对于一张包含Delete数据的Iceberg表Impala会可能会使用多个扫描结点以及ANTI JOIN结点和UNION结点协同工作,配合完成任务,这样的设计和实现既能复用很多现有的Impala代码,也能利用起现有的很多针对HDFS表的扫描优化。但是文章篇幅有限,实际上代码中还有许多内容无法详细展开分析,对于扫描的实际执行也还未能涉猎,有机会的话会在后续的文章中继续介绍。有兴趣的读者也可以自行阅读,Impala的代码注释还是比较丰富的,代码风格规范也比较好,阅读难度不大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/218503.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

L1-040:最佳情侣身高差

题目描述 专家通过多组情侣研究数据发现&#xff0c;最佳的情侣身高差遵循着一个公式&#xff1a;&#xff08;女方的身高&#xff09;1.09 &#xff08;男方的身高&#xff09;。如果符合&#xff0c;你俩的身高差不管是牵手、拥抱、接吻&#xff0c;都是最和谐的差度。 下面就…

如何建立一套完善的销售管理体系?

如何建立一套完善的销售管理体系&#xff1f; 该提问下已有许多专业的回答&#xff0c;从多个角度为题主出谋划策&#xff1a;销售主管如何提升个人能力、销售团队如何管理、PDCA管理方法论、销售闭环……似乎都与硬性的个人能力挂钩&#xff0c;销售能力、管理能力等等。 或…

电商控制台系统前台注册登录后台审核的测试

电商项目&#xff08;前台&#xff09;&#xff1a; 登录接口 注册接口 后台&#xff1a; 注册审核&#xff1a;建一个线程类 注意程序中的一个问题。 这里是 5 条记录&#xff0c;2 条记录显示应该是 3 页&#xff0c;实际操作过程 有审核机制&#xff0c;出现了数据记录动态变…

如何使用Selenium进行Web自动化测试?一文6个步骤轻松玩转!

概述&#xff1a; Web自动化测试是现代软件开发过程中至关重要的一环。Selenium是一个强大的自动化测试工具&#xff0c;可以模拟用户在Web浏览器中的操作&#xff0c;实现自动化的测试流程。本文将介绍如何使用Selenium进行Web自动化测试&#xff0c;并附带代码示例&#xff0…

容器技术与操作系统

文章目录 容器技术 vs 虚拟机操作系统容器 Docker与操作系统 容器技术 vs 虚拟机 操作系统 操作系统是一个很重而且很笨的程序&#xff0c;简称笨重&#xff0c;有多笨重呢&#xff1f; 操作系统运行起来是需要占用很多资源的&#xff0c;大家对此肯定深有体会&#xff0c;刚…

基于模型驱动的可视化开发平台——JNPF

目录 一、模型驱动原理 二、低代码核心功能 1.业务建模&#xff1a; 2.表单建模&#xff1a; 3.页面建模&#xff1a; 4.流程建模&#xff1a; 5.报表建模&#xff1a; 6.门户建模&#xff1a; 7.大屏建模&#xff1a; 8.移动建模&#xff1a; 目前&#xff0c;国外内…

一个例子带你入门影刀编码版(二)

文章结构 摘要元素定位店铺所有宝贝单个宝贝详细信息相关链接 摘要 将通过一个电商业务场景下的真实需求&#xff0c;带领大家零基础入门影刀编码版&#xff0c;本系列将会分三步讲解&#xff0c;从接到需求到最后完成发版&#xff0c;整个过程中我们需要做些什么&#xff1f;…

BugKu-Web-Simple_SSTI_1Simple_SSTI_2(浅析SSTI模板注入!)

何为SSTI模块注入&#xff1f; SSTI即服务器端模板注入&#xff08;Server-Side Template Injection&#xff09;&#xff0c;是一种注入漏洞。 服务端接收了用户的恶意输入以后&#xff0c;未经任何处理就将其作为Web应用模板内容的一部分&#xff0c;模板引擎在进行目标编译渲…

深入理解TheadLocal的使用场景和注意事项

前言 在日常实际开发当中我们往往会看到项目中有使用 ThreadLocal 的场景&#xff0c;大多数人有时候可能涉及不到自己的业务则没有进行关注。通常我在看代码时对于一些未知的东西常常引起我的好奇&#xff0c;我往往会分析&#xff1a;为什么要这么做&#xff1f;好处是什么&…

fuxploide,一款针对文件上传的Fuzz检测工具

fuxploide,一款针对文件上传的Fuzz检测工具 1.工具概述2.安装3.参数解析4.使用案例1.工具概述 Fuxploider 是一种开源渗透测试工具,可自动检测和利用文件上传表单缺陷。该工具能够检测允许上传的文件类型,并能够检测哪种技术最适合在所需的 Web 服务器上上传 Web Shell 或任…

【ARM Trace32(劳特巴赫) 使用介绍 1.2 - ARM 系统调试中常见的挑战】

请阅读【Trace32 ARM 专栏导读】 文章目录 ARM 系统调试中常见的挑战ARM 系统调试接口简例DAP-Debug Access portDAP 状态检查多核调试虚拟/物理地址Cache 数据一致性问题系统异常系统复位系统死机PC 采样Memory 采样RAM/Core Dump 分析小概率问题ARM 系统调试中常见的挑战 调试…

交直流充电桩检测

随着电动汽车的普及&#xff0c;充电桩的需求也在不断增加。为了保证充电桩的正常运行和使用安全&#xff0c;对其进行定期检测是非常必要的。检查充电桩的外壳是否有破损、变形等现象&#xff0c;以及充电桩的标识是否清晰可见。同时&#xff0c;还要检查充电桩的接口是否完好…

Elasticsearch:使用 OpenAI 生成嵌入并进行向量搜索 - nodejs

在我之前的文章&#xff1a; Elasticsearch&#xff1a;使用 Open AI 和 Langchain 的 RAG - Retrieval Augmented Generation &#xff08;一&#xff09;&#xff08;二&#xff09;&#xff08;三&#xff09;&#xff08;四&#xff09;​​​​​ 我详细地描述了如何使用…

自然语言处理第2天:自然语言处理词语编码

​ ☁️主页 Nowl &#x1f525;专栏 《自然语言处理》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​​ 文章目录 一、自然语言处理介绍二、常见的词编码方式1.one-hot介绍缺点 2.词嵌入介绍说明 三、代码演示四、结语 一、自然语言处理介绍 自然语言处理&#xf…

5、Grounded Segement Anything

github sam安装与基本使用 stable diffusion安装与基本使用 安装GroundingDINO git clone https://github.com/IDEA-Research/GroundingDINO.git cd GroundingDINO pip install -e .pip install diffusers transformers accelerate scipy safetensors安装RAM&Tag2Text …

深度学习基本概念

1.全连接层 全连接层就是该层的所有节点与输入节点全部相连&#xff0c;如图所 示。假设输入节点为X1&#xff0c; X 2&#xff0c; X 3&#xff0c;输出节点为 Y 1&#xff0c; Y 2&#xff0c; Y 3&#xff0c; Y 4。令 矩阵 W 代表全连接层的权重&#xff0c; W 12也就代表 …

【C】⽂件操作

1. 为什么使⽤⽂件&#xff1f; 如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失了&#xff0c;等再次运⾏程序&#xff0c;是看不到上次程序的数据的&#xff0c;如果要将数据进⾏持久化…

世微 AP5199S 降压恒流车灯驱动IC 兼容HV9910

说明 AP5199S 是一款外围电路简单的多功能平均电流 型 LED 恒流驱动器&#xff0c;适用于宽电压范围的非隔离式 大功率恒流 LED 驱动领域。 芯片 PWM 端口支持超小占空比的 PWM 调光&#xff0c; 可响应最小 60ns 脉宽。芯片采用我司专利算法&#xff0c;为客 户提供最佳解决方…

AIGC时代,如何保障ai绘图的算力需求

AIGC是目前非常热门的技术领域&#xff0c;被广泛应用于各个行业和领域&#xff0c;但同时AIGC也面临着诸多的痛点&#xff0c;那么如何解决这些痛点问题呢&#xff1f;云时代&#xff0c;又是如何通过云电脑赋能AIGC行业的&#xff0c;那么一起来文章中了解一下吧。 AIGC是什…

Trie 树详解

Trie 树详解 Trie 树&#xff08;字典树&#xff09;是一种用于高效存储和搜索字符串集合的树状数据结构。它的主要特点是能够在O(N)时间内实现字符串的插入、删除和搜索操作&#xff0c;其中 N 是字符串的长度。Trie 树的结构适用于敏感词过滤、单词搜索、自动补全等场景。在…