子图复用优化是为了找到SQL执行计划中重复的节点,将其复用,避免这部分重复计算的逻辑。先回顾SQL执行的主要流程 parser -> validate -> logical optimize -> physical optimize -> translateToExecNode。

而子图复用的逻辑就是在这个阶段进行的

private[flink] def translateToExecNodeGraph(
optimizedRelNodes: Seq[RelNode],
isCompiled: Boolean): ExecNodeGraph = {
val nonPhysicalRel = optimizedRelNodes.filterNot(_.isInstanceOf[FlinkPhysicalRel])
if (nonPhysicalRel.nonEmpty) {
throw new TableException(
"The expected optimized plan is FlinkPhysicalRel plan, " +
s"actual plan is ${nonPhysicalRel.head.getClass.getSimpleName} plan.")
} require(optimizedRelNodes.forall(_.isInstanceOf[FlinkPhysicalRel]))
// Rewrite same rel object to different rel objects
// in order to get the correct dag (dag reuse is based on object not digest)
val shuttle = new SameRelObjectShuttle()
val relsWithoutSameObj = optimizedRelNodes.map(_.accept(shuttle))
// reuse subplan
val reusedPlan = SubplanReuser.reuseDuplicatedSubplan(relsWithoutSameObj, tableConfig)
// convert FlinkPhysicalRel DAG to ExecNodeGraph
val generator = new ExecNodeGraphGenerator()
val execGraph = generator.generate(reusedPlan.map(_.asInstanceOf[FlinkPhysicalRel]), isCompiled) // process the graph
val context = new ProcessorContext(this)
val processors = getExecNodeGraphProcessors
processors.foldLeft(execGraph)((graph, processor) => processor.process(graph, context))
}

可以看到这里首先会校验relNodes都是FlinkPhysicalRel 物理执行计划的节点

require(optimizedRelNodes.forall(_.isInstanceOf[FlinkPhysicalRel]))

SameRelObjectShuttle

/**
* Rewrite same rel object to different rel objects.
*
* <p>e.g.
* {{{
* Join Join
* / \ / \
* Filter1 Filter2 => Filter1 Filter2
* \ / | |
* Scan Scan1 Scan2
* }}}
* After rewrote, Scan1 and Scan2 are different object but have same digest.
*/
class SameRelObjectShuttle extends DefaultRelShuttle {
private val visitedNodes = Sets.newIdentityHashSet[RelNode]() override def visit(node: RelNode): RelNode = {
val visited = !visitedNodes.add(node)
var change = false
val newInputs = node.getInputs.map {
input =>
val newInput = input.accept(this)
change = change || (input ne newInput)
newInput
}
if (change || visited) {
node.copy(node.getTraitSet, newInputs)
} else {
node
}
}
}

然后进行rel节点重写,RelShuttle的作用就是提供visit的模式根据实现的逻辑来替换树中的某些节点。可以看到这个实现中会将 同一个objec(注意这里保存visitedNodes使用的是identity hash set) 第二次访问时 copy成一个新的对象,但是有相同的digest,这一步的目的是什么呢?

我们往下面看在后续生成ExecNode时, 会创建一个IdentityHashMap 来保存访问过的Rels,所以意思就是真正生成ExecNode时,是和Rels对象一一对应的。

private final Map<FlinkPhysicalRel, ExecNode<?>> visitedRels = new IdentityHashMap();
private ExecNode<?> generate(FlinkPhysicalRel rel, boolean isCompiled) {
ExecNode<?> execNode = visitedRels.get(rel);
if (execNode != null) {
return execNode;
} if (rel instanceof CommonIntermediateTableScan) {
throw new TableException("Intermediate RelNode can't be converted to ExecNode.");
} List<ExecNode<?>> inputNodes = new ArrayList<>();
for (RelNode input : rel.getInputs()) {
inputNodes.add(generate((FlinkPhysicalRel) input, isCompiled));
} execNode = rel.translateToExecNode(isCompiled);
// connects the input nodes
List<ExecEdge> inputEdges = new ArrayList<>(inputNodes.size());
for (ExecNode<?> inputNode : inputNodes) {
inputEdges.add(ExecEdge.builder().source(inputNode).target(execNode).build());
}
execNode.setInputEdges(inputEdges); visitedRels.put(rel, execNode);
return execNode;
}

看到这里上面将同一个object 拆成两个的目的就更不可理解了,因为本来是一个object的话在这里天然就复用了,但是拆成2个反而就不能复用了。

这里的目的是先将相同的object被重复引用的节点拆开,然后再根据digest相同以及内部规则来决定是否复用。这样就可以有Flink引擎来控制哪些节点是可以合并的。

SubplanReuseContext

在context中通过ReusableSubplanVisitor构造两组映射关系

// mapping a relNode to its digest
private val mapRelToDigest = Maps.newIdentityHashMap[RelNode, String]()
// mapping the digest to RelNodes
private val mapDigestToReusableNodes = new util.HashMap[String, util.List[RelNode]]()

中间的逻辑比较简单就是遍历整棵树,查找是否存在可reusable的节点,怎么判断可reusable呢?

  • 同一digest下,挂了多个RelNode节点,那么这一组RelNode是同一语义的,是可以复用的候选
  • 节点没有disable reusable
/** Returns true if the given node is reusable disabled */
private def isNodeReusableDisabled(node: RelNode): Boolean = {
node match {
// TableSourceScan node can not be reused if reuse TableSource disabled
case _: FlinkLogicalLegacyTableSourceScan | _: CommonPhysicalLegacyTableSourceScan |
_: FlinkLogicalTableSourceScan | _: CommonPhysicalTableSourceScan =>
!tableSourceReuseEnabled
// Exchange node can not be reused if its input is reusable disabled
case e: Exchange => isNodeReusableDisabled(e.getInput)
// TableFunctionScan and sink can not be reused
case _: TableFunctionScan | _: LegacySink | _: Sink => true
case _ => false
}
}

例如TableFunctionScan就不能被Reuse(这个原因还没理解),或者exchange只有input被reuse时,该节点才能复用

SubplanReuseShuttle

在以上的visit执行完之后以及知道哪些节点是可以复用的了,最后通过一个Shuttle来将可复用的节点进行替换

class SubplanReuseShuttle(context: SubplanReuseContext) extends DefaultRelShuttle {
private val mapDigestToNewNode = new util.HashMap[String, RelNode]() override def visit(rel: RelNode): RelNode = {
val canReuseOtherNode = context.reuseOtherNode(rel)
val digest = context.getRelDigest(rel)
if (canReuseOtherNode) {
val newNode = mapDigestToNewNode.get(digest)
if (newNode == null) {
throw new TableException("This should not happen")
}
newNode
} else {
val newNode = visitInputs(rel)
mapDigestToNewNode.put(digest, newNode)
newNode
}
}
}

实现的方式就是记录每个digest对应的newNode,当可以复用时,那么直接返回该复用digest对应的RelNode(替换了原先的digest相同,对象不同的RelNode),这样整棵树中可复用的节点又重新合并了。

Flink SQL 子图复用逻辑分析的更多相关文章

  1. [源码分析] 带你梳理 Flink SQL / Table API内部执行流程

    [源码分析] 带你梳理 Flink SQL / Table API内部执行流程 目录 [源码分析] 带你梳理 Flink SQL / Table API内部执行流程 0x00 摘要 0x01 Apac ...

  2. 从"UDF不应有状态" 切入来剖析Flink SQL代码生成

    从"UDF不应有状态" 切入来剖析Flink SQL代码生成 目录 从"UDF不应有状态" 切入来剖析Flink SQL代码生成 0x00 摘要 0x01 概述 ...

  3. [源码分析]从"UDF不应有状态" 切入来剖析Flink SQL代码生成 (修订版)

    [源码分析]从"UDF不应有状态" 切入来剖析Flink SQL代码生成 (修订版) 目录 [源码分析]从"UDF不应有状态" 切入来剖析Flink SQL代码 ...

  4. KSQL和Flink SQL的比较

    Confluent公司于2017年11月宣布KSQL进化到1.0版本,标志着KSQL已经可以被正式用于生产环境.自那时起,整个Kafka发展的重心都偏向于KSQL——这一点可以从Confluent官方 ...

  5. MyBatis 别名标签 & sql的复用

    1.MyBatis 别名标签 如果在映射文件中,大量使用类名比较长,可以在sqlMapConfig.xml声明别名, 在映射文件中可以使用别名缩短配置,注意此配置要放在最前面 sqlMapConfig ...

  6. Flink SQL与 SQL Parser ,calcite

    http://vinoyang.com/2017/06/12/flink-table-sql-source/ Flink Table&Sql 如何结合Apache Calcite http:/ ...

  7. 使用flink Table &Sql api来构建批量和流式应用(3)Flink Sql 使用

    从flink的官方文档,我们知道flink的编程模型分为四层,sql层是最高层的api,Table api是中间层,DataStream/DataSet Api 是核心,stateful Stream ...

  8. Apache Flink SQL

    本篇核心目标是让大家概要了解一个完整的 Apache Flink SQL Job 的组成部分,以及 Apache Flink SQL 所提供的核心算子的语义,最后会应用 TumbleWindow 编写 ...

  9. OPPO数据中台之基石:基于Flink SQL构建实数据仓库

    小结: 1. OPPO数据中台之基石:基于Flink SQL构建实数据仓库 https://mp.weixin.qq.com/s/JsoMgIW6bKEFDGvq_KI6hg 作者 | 张俊编辑 | ...

随机推荐

  1. mysql中innodb和myisam区别

    前言 InnoDB和MyISAM是很多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,5.7之后就不一样了. 1.事务和外键 ● InnoDB具有事务,支持4个事务隔离级别,回滚,崩溃修 ...

  2. NC224938 加减

    NC224938 加减 题目 题目描述 小红拿到了一个长度为 \(n\) 的数组.她每次操作可以让某个数加 \(1\) 或者某个数减 \(1\) . 小红最多能进行 \(k\) 次操作.她希望操作结束 ...

  3. Android 功耗测试

    <head> <title>Evernote Export</title> <basefont face="微软雅黑" size=&quo ...

  4. 攻防世界MISC—进阶区1-10

    1.something_in_image zip中的文件用010 Editor打开后直接搜索flag,即可找到flag 2.wireshark-1 zip内是pcap文件,打开后根据题目知道要寻找登录 ...

  5. 记录一下第一次在CodeForces供题的事(未完待续)

    3月11日 因为想出题而开始打比赛上分 (Rating 1727) (期间最低掉到 1669) 6月4日凌晨 上分,有了权限 (Rating 2141) 6月4-6日 出了七道题 6月8-12日 又出 ...

  6. .net webapi 实现 接口版本控制并打通swagger支持

    我们在开发 webapi 项目时如果遇到 api 接口需要同时支持多个版本的时候,比如接口修改了入参之后但是又希望支持老版本的前端(这里的前端可能是网页,可能是app,小程序 等等)进行调用,这种情况 ...

  7. Solution -「Luogu 5170」类欧几里得算法

    推柿子大赛了属于是. 题目要求三个柿子,不妨分别记为: \[\begin {align} f (a, b, c, n) &= \sum \limits _{i = 0} ^{n} \lfloo ...

  8. NOI / 1.2编程基础之变量定义、赋值及转换全题详解(5063字)

    目录 01:整型数据类型存储空间大小 02:浮点型数据类型存储空间大小

  9. python打开文件、文件夹窗口、终端窗口

    简介 在一些项目中,我们会需要在生成完文件后打开某些文件或者文件夹窗口,这就需要使用到内置的文件打开方式了. 打开文件或文件夹 Windows import os import subprocess ...

  10. odoo14 button 事件调用python方法如何传递参数

    1 <field name="user_ids" 2 mode="kanban" 3 nolabel="1" 4 options=&q ...