从真实案例出发,全方位解读 NebulaGraph 中的执行计划

本文整理自 NebulaGraph 核心开发 Yee 在直播《聊聊执行计划这件事》中的主题分享。分享视频参见 B站:https://www.bilibili.com/video/BV1Cu4y1h7gn/

一条 Query 的一生

在开始正式地解读执行计划之前,我们先来了解在 NebulaGraph 中,一条查询语句(Query)是如何被校验、生成语法树,到最后被转为逻辑 / 物理的执行计划。而这个 Query 生命周期,无论是 NebulaGraph 原生查询语言 nGQL 或者是从 v2.x 开始兼容的 openCypher,都会经历从字符串到执行计划的过程。这个过程对应到编程语言,是一个编译的过程。

查询语句的生命周期大概有以下几个阶段:

同大多数的数据库类似,一个 Query 的字符串会经过 Parser(词法语法解析器),转成一个 AST(抽象语法树)。而抽象语法树中,不同的查询语句会转成不同的抽象语法树节点。再经过一道 Validator(校验器),进行语句的合法性校验。校验阶段,主要是根据创建点边元素的 Schema 信息(元数据信息)。由于 NebulaGraph 查询语言设计之初是 schema-based 的设定,因此在语句查询时可(基于语句上下文关系)事先进行相关推导,比如某个查询会用到哪些(Schema)类型。

这里插个知识点,在 NebulaGraph 中需要注意两个关键类型:EdgeType(Edge)和 Tag(Vertex),像每条 Edge 对应的边类型 EdgeType,其包含的边类型属性都是有具体的 Data Type。此外,便是每个点 Vertex 的属性,每个点的属性都会归类放到对应的 Tag 中,变成其对应的点类型属性 Tag property。类比关系型数据库的话,Tag 相当于是一张表。在 NebulaGraph 中,点和边的属性虽然受到 Schema 约束,但是对某个边的两端点对应的类型是不作约束。因此,插入点或者是修改点,是通过 ID(VID)进行相关操作,与点类型无关。

回到上面的 Query 生命周期,在进行语句校验之后,将之前的词法转成 NebulaGraph 中包含特定算子的执行计划 Execution Planner。随后,执行计划会经过一道优化,通过优化器 Optimizer 变成更优的执行计划,再交由调度器去执行。在传统的数据库中,执行计划分为逻辑计划和物理计划,但在 NebulaGraph 中目前只存在一种计划,便是物理计划,像上面流程图中,Optimzer 处理的计划便是一个物理计划。因此,经过 Optimizer 优化过的 optimized plan,其实是一个可直接执行的物理计划。

而执行计划的运行,主要是由上图的 Executor(其他数据库可能叫 Operator)完成。在执行这层,会涉及到接口交互,例如:同 storage 层“打交道”,会调用 rpc 接口;同 graph 层交互,会调用一些计算算子,像上图加粗的 Project、Filter 这种纯计算算子。除此之外,执行还会涉及到执行模型,比如多线程、Runtime 等问题。社区中大家遇到的一些执行性能问题,可能就同它们有关。

执行计划示例

GO 3 STEPS FROM "player100" OVER follow WHERE $$.player.age > 34 YIELD DISTINCT $$.player.name AS name, $$.player.age AS age | ORDER BY $-.age ASC, $-.name DESC;

示例出处:《nGQL 简明教程 vol.02 执行计划详解与调优》 网址:https://discuss.nebula-graph.com.cn/t/topic/12010

这是一个典型的 nGQL 查询语句,通过 GO 子句遍历,再通过 WHERE 子句进行过滤,最后经由 ORDER BY 排序输出。

下图是上面查询语句生成的执行计划:

go_n_steps

如上所示,这个执行计划还是相对比较复杂的。它同大家见惯的关系型数据库的执行计划不大一样,像右下角的 Loop,左上角单依赖的 LeftJoin 可能同大家之前接触的都不大一样。

从结构上看,同 Neo4j 的树 Tree 结构不同,NebulaGraph 中的执行计划,不仅有方向,还有环。为什么 NebulaGraph 的执行计划结构如此不同呢?

explain format="dot" {$a=go 2 steps from "Tim Duncan" over like yield like.likeness as c1;$b=go 1 steps from "Tony Parker" over like yield like.likeness as c1;$c=yield $a.c1 as c1 union yield $b.c1 as c1;
}

我们来看一下上面这个例子,这里定义了 a、b、c 变量。a 和 b 之间不存在任何关系,都是几跳遍历之后,输出结果。而 c 则是对这两个变量进行了 union 操作。我们来看看这三条语句对应的执行计划:

可以看到,第一条语句跟第二条语句,还有第三条语句,在执行依赖上是一个顺序依赖:以上图黄色圈为分界线,黄色圈以下为 a 的 plan,黄色圈以上、Union_11 以下为 b 的 plan,Union_11 及其以上为 c 的 plan。

而数据依赖可以看箭头部分,Project_9 的数据依赖输入为 a,它是由 Project_4 提供的,而非顺序上下方的 PassThrough_13,同理,Project_10 数据依赖是 Project_8 的输出变量 b。

这里需要花点时间区分的是:执行依赖和数据依赖。执行依赖就是上图黑色箭头的顺序依赖,而数据依赖则要看算子对应的 inputVar 来自哪里。因此,数据依赖可能同执行依赖是同一个逻辑顺序,也可能是不同的顺序。

造成上述情况的原因是,nGQL 这个查询语言本身是非常灵活的。不同于 SQL 将 a、b、c 分别生成执行计划,生成 3 个执行计划,在 NebulaGraph 中,像上面这种 a、b 没有数据依赖关系,c 依赖 a、b,在生成 plan 的时候,会将其当成拥有一定顺序 subsequential 的语句进行处理。一条执行计划便可完成多条语句的执行,当然非常灵活,但也变相地增加了语句调优的难度。

再来看一个 openCypher 的例子:

match (v:player)
with v.name as name
match (v:player)--(v2) where v2.name = name
with v2.age as age
match (v:player) where v.age>age
return v.name

在这个多 MATCH 的例子中,每个 MATCH 都可以作为一个 Pattern。像这条语句中第一个 MATCH 的输出结果传递给下面的第二个 MATCH 变成过滤输入,而第三个 MATCH 的过滤输入则来源于第二个 MATCH 的输出。每个 MATCH 通过 WITH 进行连接,它不同 nGQL,存在 Subquery 的概念,在 openCypher 这里,数据依赖关系是线性,非常自然地通过一个个 WITH 传递给下一个 Pattern 的。如果第一个 WITH 没有将结果带到第二个 MATCH,第三个 MATCH 便无法完成。

正是因为这种线性关系,Neo4j 的执行计划是树状的;而 nGQL 因为查询灵活性的缘故,生成执行计划中有向有环图。

执行计划的优化

目前,NebulaGraph 只做了 RBO 优化,即:Rule-Based Optimization 基于规则的优化。NebulaGraph 的 RBO 是基于业界流行的实现方法,有些许的差异化,用户可以基于之前的经验或者相关规则,进行 plan 的优化(变形)。

NebulaGraph 的 RBO 是以 memo + bottom up 方式进行的。

在 NebulaGraph 中,每个 plan 的 plan node 会放到一个小组 group 中,每个 group 中的 plan node 语义上等价,上一个 group node 连接到下一个 group,而下一个 group 中有多个 plan node 的话,便可变化出多个 plan。

整个优化的过程,其实是一个迭代的过程,优化器会找到每个 plan 中最叶子的 plan node,同规则集进行匹配,规则集可以查看这里:https://github.com/vesoft-inc/nebula/tree/master/src/graph/optimizer/rule。如果规则匹配上,则将两个 subplan 进行变形,生成新的 subplan,并插入到相关的 group 中。

比如上图左侧的 Filter 算子,它和 GetNeighbors 算子通过某个 rule 匹配上了,便会生成新的 GetNeighbors node,它带有 Filter。新的 GetNeighbors node 会插入到之前的 Filter 所在的 group,因为 memo 的原因,新的 GetNeighbors 便可同之前的 GetNeighbors 连接的 subplan Start 进行连接。

基于 RBO 默认如果匹配上规则,则匹配上规则之后生成的新的 plan 是优于之前的 plan,新的 plan 会直接替代旧的 plan。因此,这个执行计划最后会变成右侧这样。

几个执行计划调优的 flag

上文提到过,执行计划中会有个 Runtime 阶段,下面参数主要是影响 graph 这边 Runtime 阶段的执行效率。

  • --max_job_size:每个算子内部并发的最大 job 数;
  • --min_batch_size:算子内部并发执行时,切分任务时的最小 batch 行数;
  • --optimize_appendvertices:AppendVertices 算子内部的性能优化控制,当没有悬挂边时可以打开该项减少 RPC 调用。

max_job_size 主要控制 graph 部分算子的并发程度,同 min_batch_size 配合使用。这和 NebulaGraph 的物化模型有关,在 NebulaGraph 中每个算子在被执行完之后,其结果会被物化到内存中,在下一次迭代的时候去对应内存中捞取数据,而不是通过 Pipeline 的方式进行计算。max_job_sizemin_batch_size 这两个 flag 控制了迭代过程中每个线程可处理的 batch 数量,从而达到优化的效果。

optimize_appendvertices 参数主要是用来服务 MATCH 语句的,当我们使用 MATCH 时,可能会常遇到一个情况:用 MATCH 去做路径查找时,希望这个路径中是不存在悬挂边的。假如,你知道你的数据中不存在悬挂边,可以设置 optimize_appendverticestrue,这样就无需再同 storage 交互去验证是否这条边的起始点存在,从而会节约执行时间。

执行计划如何看?

上面说了些原理,下面可是实操下,教你看懂执行计划,以及如何去优化它。

PROFILE 或者是 EXPLAIN 加在对应的语句前面,即可得到相关的执行计划。像这样:

profile GET SUBGRAPH 5 STEPS FROM "Yao Ming" OUT like where like. likeness > 80 YIELD VERTICES AS nodes, EDGES AS relationshipis;
explain GET SUBGRAPH 5 STEPS FROM "Yao Ming" OUT Like where like. likeness > 80 YIELD VERTICES AS nodes, EDGES AS relationshipis;

如果你不知道数据量的情况下,用 EXPLAIN 可查看对应的执行计划构成,它不包括该语句各个算子的执行时间。

在社区中,常会到一类问题:我通过 SUBGRAPH 进行条件过滤时,是不是每一跳都会应用到边过滤。相信通过这个例子,你就能知道是不是每跳都会应用到条件过滤了。

Execution Plan (optimize time 41 us)-----+-------------+--------------+----------------+----------------------------------
| id | name        | dependencies | profiling data | operator info                   |
-----+-------------+--------------+----------------+----------------------------------
|  2 | DataCollect | 1            |                | outputVar: {                    |
|    |             |              |                |   "colNames": [                 |
|    |             |              |                |     "nodes",                    |
|    |             |              |                |     "relationshipis"            |
|    |             |              |                |   ],                            |
|    |             |              |                |   "type": "DATASET",            |
|    |             |              |                |   "name": "__DataCollect_2"     |
|    |             |              |                | }                               |
|    |             |              |                | inputVar: [                     |
|    |             |              |                |   {                             |
|    |             |              |                |     "colNames": [],             |
|    |             |              |                |     "type": "DATASET",          |
|    |             |              |                |     "name": "__Subgraph_1"      |
|    |             |              |                |   }                             |
|    |             |              |                | ]                               |
|    |             |              |                | distinct: false                 |
|    |             |              |                | kind: SUBGRAPH                  |
-----+-------------+--------------+----------------+----------------------------------
|  1 | Subgraph    | 0            |                | outputVar: {                    |
|    |             |              |                |   "colNames": [],               |
|    |             |              |                |   "type": "DATASET",            |
|    |             |              |                |   "name": "__Subgraph_1"        |
|    |             |              |                | }                               |
|    |             |              |                | inputVar: __VAR_0               |
|    |             |              |                | src: COLUMN[0]                  |
|    |             |              |                | tag_filter:                     |
|    |             |              |                | edge_filter: (like.likeness>80) |
|    |             |              |                | filter: (like.likeness>80)      |
|    |             |              |                | vertexProps: [                  |
|    |             |              |                |   {                             |
|    |             |              |                |     "props": [                  |
|    |             |              |                |       "_tag"                    |
|    |             |              |                |     ],                          |
|    |             |              |                |     "tagId": 2                  |
|    |             |              |                |   },                            |
|    |             |              |                |   {                             |
|    |             |              |                |     "props": [                  |
|    |             |              |                |       "_tag"                    |
|    |             |              |                |     ],                          |
|    |             |              |                |     "tagId": 4                  |
|    |             |              |                |   },                            |
|    |             |              |                |   {                             |
|    |             |              |                |     "props": [                  |
|    |             |              |                |       "_tag"                    |
|    |             |              |                |     ],                          |
|    |             |              |                |     "tagId": 3                  |
|    |             |              |                |   }                             |
|    |             |              |                | ]                               |
|    |             |              |                | edgeProps: [                    |
|    |             |              |                |   {                             |
|    |             |              |                |     "props": [                  |
|    |             |              |                |       "_dst",                   |
|    |             |              |                |       "_rank",                  |
|    |             |              |                |       "_type",                  |
|    |             |              |                |       "likeness"                |
|    |             |              |                |     ],                          |
|    |             |              |                |     "type": 5                   |
|    |             |              |                |   }                             |
|    |             |              |                | ]                               |
|    |             |              |                | steps: 5                        |
-----+-------------+--------------+----------------+----------------------------------
|  0 | Start       |              |                | outputVar: {                    |
|    |             |              |                |   "colNames": [],               |
|    |             |              |                |   "type": "DATASET",            |
|    |             |              |                |   "name": "__Start_0"           |
|    |             |              |                | }                               |
-----+-------------+--------------+----------------+----------------------------------Tue, 17 Oct 2023 17:35:12 CST

生成在终端的这个执行计划是表结构的,通过 id 和 dependence 可查看到对应的依赖关系,从而解决之前提到过的执行计划中有环这种问题。

先看看当中 subgraph 算子信息,我们可以 edge_filter 中带有表达式,如果 edge_filter / tag_filter 中带有表达式,则表示该表达式被计算下推了。

下面是通过 PROFILE 查看到的更具体的执行计划:

DataCollect 算子有这些参数:

  • execTime:graphd 的处理时间;
  • rows:返回的数据条数;
  • totalTime:从算子起始到到算子退出时间;
  • version:像前面章节提到过的 LOOP,其实 LOOP 里面有一个 LOOP body,对应也是一个 subplan。而这个 subplan 则是会被重复执行,且数据放在同一个变量当中,而这个变量则会有多个版本,即 version。假如算子不在 LOOP body 内,则只有一个 verison。默认为 0;

Subgraph 算子有这些参数(同上面已讲解参数去重):

  • resp[]:每个 storage 节点执行的结果
  • exec:storaged 的处理时间,同上面的 graphd 处理时间的 execTime
  • storage_detail:storage 信息,由于 subgraph 需要同 storage 交互,因此由这个字段用来记录 storage 这边 plan node 的执行时间;
  • total:storage client 接收到 graphd 请求到 storage client 发送请求的时间,即 storaged 本身的处理时间加上序列化和反序列化的时间;
  • vertices:该语句涉及的点的数据;
  • total_rpc_time:graphd 调用 storage client 发出请求到接收到请求时间;

生成格式的执行计划

在刚开始的时候展示的执行计划和实操时展示的执行计划格式并不相同,profile format="" 来完成格式指定,默认为表结构,可通过格式指定,指定为 .dot 格式,在终端复制出来执行计划,前往在线网站:https://dreampuf.github.io/GraphvizOnline/#digraph 进行格式渲染。

以上,便是一个全方位的执行计划讲解。当然你可以通过阅读思为的《nGQL 简明教程 vol.02 执行计划详解与调优》,了解更多的例子从而掌握执行计划。

推荐阅读

  • 《nGQL 简明教程 vol.02 执行计划详解与调优》:https://discuss.nebula-graph.com.cn/t/topic/12010

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

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

相关文章

【工艺库】SMIC数字后端工艺库

工艺库文件 Calibredigital文件夹apollolefprimetimesynopsys TD系列文件夹 本来是想找一个工艺库,想要其包含逻辑综合和SPICE Model相关的库文件,但是找了很久也没有直接找到想要的,主要原因还是自己对工艺库文件的构成不是很清楚&#xff0…

五年制专转本备考中如何进行有效的自我管理

时间管理 0 1 一天中的4个记忆黄金时间 清晨起床后,适合学习难以记忆的内容;8:00—10:00,适宜学习需要周密思考、分析判断的内容,是攻克难题的最佳时间;18:00后的两个小时&#x…

spring boot中使用Bean Validation做优雅的参数校验

一、Bean Validation简介 Bean Validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),目前最新稳定版2.0.2(201…

计算机二级Office真题解析 excel减免税,订单,成绩

第一题 1.将“Excel 减免税.xlsx”文件另存为 excel.xlsx,最后提交该文件(1 分)。 2.将“对应代码.xlsx”文件中的 sheet1 工作表插入到 excel.xlsx 中,工作 表名重命名为“代码”(3 分)。 3.在"序号&…

2024年度“阳江市惠民保”正式发布!阳江市专属补充医疗保险全新升级

11月14日,2024年度“阳江市惠民保”暨百场义诊活动发布会在阳江市华邑酒店顺利举行。2024年度“阳江市惠民保”一年保费最低只要59元,最高可获得400万元的医疗保障。 阳江市人民政府、阳江市医疗保障局、阳江市农业农村局、阳江市金融工作局、国家金融监…

碳交易机制下考虑需求响应的综合能源系统优化运行(附带Matlab程序)

碳交易机制下考虑需求响应的综合能源系统优化运行(附带Matlab程序) 仿真平台:MATLABCPLEX 使用的是yalmipcplex求解器完成求解 资源地址: 碳交易机制下考虑需求响应的综合能源系统优化运行(附带Matlab程序&#xff09…

Go常见数据结构的实现原理——map

(一)基础操作 版本:Go SDK 1.20.6 1、初始化 map分别支持字面量初始化和内置函数make()初始化。 字面量初始化: m : map[string] int {"apple": 2,"banana": 3,}使用内置函数make()初始化: m …

Spark SQL 每年的1月1日算当年的第一个自然周, 给出日期,计算是本年的第几周

一、问题 按每年的1月1日算当年的第一个自然周 (遇到跨年也不管,如果1月1日是周三,那么到1月5号(周日)算是本年的第一个自然周, 如果按周一是一周的第一天) 计算是本年的第几周,那么 spark sql 如何写 ? 二、分析 …

kubernetes集群编排——etcd

备份 从镜像中拷贝etcdctl二进制命令 [rootk8s1 ~]# docker run -it --rm reg.westos.org/k8s/etcd:3.5.6-0 sh 输入ctrlpq快捷键,把容器打入后台 获取容器id [rootk8s1 ~]# docker ps 从容器拷贝命令到本机 docker container cp c7e28b381f07:/usr/local/bin/etcdc…

cadence virtuoso layout drc error

问题: The BORDER layer must enclose all chip layout patterns, which all chip layout patterns include seal ring if seal ring has been added by designers. This rule checking includes the layers of DNW,AA,NW,NC,PC,MVN, MVP,DG,GT,SN,SP,SAB,CT,M1,V1…

C语言——分割单向链表

本文的内容是使用C语言分割单向链表,给出一个链表和一个值,要求链表中小于给定值的节点全都位于大于或等于给定值的节点之前,打印原始链表的所有元素和经此操作之后链表的所有元素。 分析:本题只是单向链表的分割,不涉…

年薪百万的人怎么做好工作复盘和总结

我们在为谁工作? 在大山宏泰《我们为什么工作》一书中有提到过: 70%左右的人认为工作只是维持生计的存在; 20%左右的人认为工作是个人价值的体现; 不到10%的人才会认为工作是幸福的。 人类的终极幸福有四重:被爱&…

Poly风格模型的创建与使用_unity基础开发教程

Poly风格模型的创建与使用 安装Poly相关组件Poly模型的创建Poly模型编辑 安装Poly相关组件 打开资源包管理器Package Manager 在弹出的窗口左上角Packages选择Unity Registry 搜索框搜索 Poly 搜索结果点击Polybrush 点击右下角 Install 同时也别忘了导入一下模型示例&#…

openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144) - 验证

文章目录 openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144) - 验证概述笔记重复数字IO的问题想法手工实现程序实现确定要摘掉的数字重合线自动化测试的问题测试程序的场景测试程序的运行效果测试程序实现备注END openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-14…

Jenkins的一些其他操作

Jenkins的一些其他操作 1、代码仓库Gogs的搭建与配置 Gogs 是一款极易搭建的自助 Git 服务,它的目标在于打造一个最简单、快速和轻松的方式搭建 Git 服务。使用 Go 语言开发的它能够通过独立的二进制进行分发,支持了 Go 语言支持的所有平台&#xff0…

find和grep命令的简单使用

find和grep命令的简单使用 一、find例子--不同条件查找 二、grep正则表达式的简单说明例子--简单文本查找例子--结合管道进行查找 一、find find 命令在指定的目录下查找对应的文件。 find [path] [expression]● path 是要查找的目录路径,可以是一个目录或文件名…

asp.net core mvc 之 依赖注入

一、视图中使用依赖注入 1、core目录下添加 LogHelperService.cs 类 public class LogHelperService{public void Add(){}public string Read(){return "日志读取";}} 2、Startup.cs 文件中 注入依赖注入 3、Views目录中 _ViewImports.cshtml 添加引用 4、视图使用…

软文推广中媒体矩阵的优势在哪儿

咱们日常生活中是不是经常听到一句俗语,不要把鸡蛋放在同一个篮子里,其实在广告界这句话也同样适用,媒介矩阵是指企业在策划广告活动时,有目的、有计划的利用多种媒体进行广告传播,触达目标用户。今天媒介盒子就来和大…

Hbase 迁移小结:从实践中总结出的最佳迁移策略

在数据存储和处理领域,HBase作为一种分布式、可扩展的NoSQL数据库,被广泛应用于大规模数据的存储和分析。然而,随着业务需求的变化和技术发展的进步,有时候我们需要将现有的HBase数据迁移到其他环境或存储系统。HBase数据迁移是一…

缓存穿透、缓存击穿、缓存雪崩

目录 一、缓存的概念 1.为什么需要把用户的权限放入redis缓存 2.为什么减低了数据库的压力呢? 3.那么什么情况下用redis,什么情况下用mysql呢? 4.关于权限存入redis的逻辑? 二、使用缓存出现的三大情况 1.缓存穿透 1.1概念 1.2出现原…