属性索引和tag/edge索引的使用限制

nebula 版本

部署方式:单机

安装方式: Docker

是否为线上版本:N

在官方文档对索引的描述中:

实际测试中,当我对tag做索引并使用属性过滤时,索引却可以生效,例如我只对ip tag建了索引
但是如下查询却可以生效

match (n:ip{last_time:1678175040070}) with n,size(()-[]->(n)) as s order by s return n,s limit 2

相当于match语句中i_T的索引代替了i_TA的属性查找

另外,当我对tag的某个属性建了索引之后,其他属性的筛选也可以生效,例如当我只对last_time做了索引,如下查询却可以生效,我并没有对ip tag的ip属性做索引

match (n:ip{ip:167910745}) with n,size(()-[]->(n)) as s order by s return n,s  limit 2

不要用nightly,换3.4.0.
另外,每个发行的小版本,行为都有区别。
索引这块,经常出现一些难以理解的行为出现。
特别是加上limit上之后。。。。

这。。。

但大体的思路是:只要为tag或者某个属性建索引,一般就能查。
如果不建索引,能不能查,这个。。。我也搞不清楚,边界情况太多

所以并没有什么明确的约束是吗,感觉全凭感觉 :joy:如果发现查询语句不行就建个索引试试

不,如果你没有办法知道开始遍历的vid,你就应该建一个索引。只给出属性不行,必须要有个索引才能找到vid,然后图遍历就可以继续了。

我知道,我想说的是,我并不能准确的知道某一个语句需要什么样的索引或者需不需要再加一个新的索引,就像上面的例子来说,如果没有实验得出,那我很可能会再建一个属性索引。

试了下,3.4.0也是一样的表现

能不能跑出结果和走没有某个索引是两件不同的事情,虽然有关联。有可能某个索引并没有走,但是通过其它手段,这个 query 还是可以跑。

举个例子,nba 这个 graph 里面 bachelor 这个 tag 上没有给属性建索引,player 上有属性索引,如下:

(root@nebula) [nba]> show tag indexes
+---------------------+------------+----------+
| Index Name          | By Tag     | Columns  |
+---------------------+------------+----------+
| "bachelor_index"    | "bachelor" | []       |
| "player_age_index"  | "player"   | ["age"]  |
| "player_name_index" | "player"   | ["name"] |
| "team_name_index"   | "team"     | ["name"] |
+---------------------+------------+----------+

如果使用 player 上的属性做 where 条件,它的执行计划中明确显示 IndexScan 中使用了 “Tim Duncan” 去查询,如下。

explain match (v:player) where v.player.name == "Tim Duncan" return v
-----+----------------+--------------+----------------+-------------------------------------------
|  7 | IndexScan      | 2            |                | outputVar: {                             |
|    |                |              |                |   "colNames": [                          |
|    |                |              |                |     "_vid"                               |
|    |                |              |                |   ],                                     |
|    |                |              |                |   "type": "DATASET",                     |
|    |                |              |                |   "name": "__IndexScan_1"                |
|    |                |              |                | }                                        |
|    |                |              |                | inputVar:                                |
|    |                |              |                | space: 2                                 |
|    |                |              |                | dedup: false                             |
|    |                |              |                | limit: 9223372036854775807               |
|    |                |              |                | filter:                                  |
|    |                |              |                | orderBy: []                              |
|    |                |              |                | schemaId: 3                              |
|    |                |              |                | isEdge: false                            |
|    |                |              |                | returnCols: [                            |
|    |                |              |                |   "_vid"                                 |
|    |                |              |                | ]                                        |
|    |                |              |                | indexCtx: [                              |
|    |                |              |                |   {                                      |
|    |                |              |                |     "columnHints": [                     |
|    |                |              |                |       {                                  |
|    |                |              |                |         "includeEnd": false,             |
|    |                |              |                |         "endValue": "__EMPTY__",         |
|    |                |              |                |         "includeBegin": true,            |
|    |                |              |                |         "beginValue": "Tim Duncan",      |
|    |                |              |                |         "column": "name",                |
|    |                |              |                |         "scanType": "PREFIX"             |
|    |                |              |                |       }                                  |
|    |                |              |                |     ],                                   |
|    |                |              |                |     "filter": "",                        |
|    |                |              |                |     "index_id": 9                        |
|    |                |              |                |   }                                      |
|    |                |              |                | ]                                        |
-----+----------------+--------------+----------------+-------------------------------------------

如果换到 bachelor 上,这个查询还是可以跑,但是你看它的 IndexScan 的内容,就没有使用(并不存在的)属性上的索引。

 explain match (v:bachelor) where v.bachelor.name == "Tim Duncan" return v
-----+----------------+--------------+----------------+---------------------------------------------
|  7 | IndexScan      | 2            |                | outputVar: {                               |
|    |                |              |                |   "colNames": [                            |
|    |                |              |                |     "_vid"                                 |
|    |                |              |                |   ],                                       |
|    |                |              |                |   "type": "DATASET",                       |
|    |                |              |                |   "name": "__IndexScan_1"                  |
|    |                |              |                | }                                          |
|    |                |              |                | inputVar:                                  |
|    |                |              |                | space: 2                                   |
|    |                |              |                | dedup: false                               |
|    |                |              |                | limit: 9223372036854775807                 |
|    |                |              |                | filter:                                    |
|    |                |              |                | orderBy: []                                |
|    |                |              |                | schemaId: 5                                |
|    |                |              |                | isEdge: false                              |
|    |                |              |                | returnCols: [                              |
|    |                |              |                |   "_vid"                                   |
|    |                |              |                | ]                                          |
|    |                |              |                | indexCtx: [                                |
|    |                |              |                |   {                                        |
|    |                |              |                |     "columnHints": [],                     |
|    |                |              |                |     "index_id": 12,                        |
|    |                |              |                |     "filter": ""                           |
|    |                |              |                |   }                                        |
|    |                |              |                | ]                                          |
-----+----------------+--------------+----------------+---------------------------------------------
1 个赞

可是现在 能不能跑出结果和走没有某个索引是两件不同的事情 这个界限并不明确
如果没有走索引也能跑出结果的话,那不建索引为什么要报错呢?

另外就是,现在岂不是薛定谔的索引了,有的语句我能跑出结果但是不用走索引,有的却是我必须有索引才能跑出结果。

我觉得这是一个很严重的行为不一致问题,甚至感觉都不是生产级别数据库应该出现的问题

另外,在确切一点来说,应该怎么判断什么时候没有对应的索引也能跑出结果和什么时候必须要有索引否则报错呢?

不是薛定谔,看下执行计划可以看明白的,有一丢丢学习成本吧。

@may11544 感谢能帮我们发现并反馈问题。

首先,nebula 的查询语句实现确实对索引有一定的依赖,造成了用户的使用难度,我们也意识到了这个问题并且在之后的版本会有相应的改进。先说下 3.x 版本这样设计的背景和原因:
关系型数据库中通过指定表名来约束查询的数据域(比如: select teacher.age from teacher),但图数据库中逻辑表的概念被淡化,以一个典型的图查询语句为例: MATCH (v) RETURN v.age,这条语句的查询语义是捞取所有的点数据,大数据量场景下这样的查询会占用很多资源甚至对服务进程是有害的,我们希望避免这样的图查询场景,于是采用了如下两种方案:

  1. 限制查询的数据域。类似 select teacher.age from teacher,查询语句需限制查询范围,比如 MATCH (v:teacher) RETURN v.age,而类似 MATCH (v) RETURN v.age 这样的语句被限制执行(报错)。由于一些实现层的原因,用户需要对 teacher 创建索引以加速查询,至于是 tag 索引还是属性索引对查询本身是否输出正确结果并无影响。当前查询语句所有可用的索引经由索引选取模块挑选出最优索引来生成执行计划,用户不需要过多关注该模块的索引选取逻辑,当然 DBA 出于性能调优的目的需要对执行计划有一定的了解,这和传统的关系型数据库并无太大差别。
  2. 限制查询结果数量。MATCH (v) RETURN v.age LIMIT 3 通过指定 limit 来完成查询,这种场景下并不需要用户创建索引。
    不知上述两点是否解答了你所说"薛定谔的索引"的困惑。至于官方文档的描述我也不是很能看懂,希望能有所改善。

不知道所谓 “难以理解的行为” 具体指的是什么,能给一些语句的例子来说明吗?

除了我上述提到的两个约束之外并无其他规则,所以约束是明确的: 要么指定索引要么指定 limit。

比如 MATCH (v:teacher) where v.age>40 RETURN v.age,该查询可以跑出来结果的约束是需要 teacher 上有至少一个可用索引,如果用户知道 age 属性上的 age>40 选择度比较好,可以在 age 上创建属性索引来达到更优的查询性能,这些是用户需要感知的。

同上所述,不存在严重的行为不一致问题,“什么时候没有对应的索引也能跑出结果和什么时候必须要有索引否则报错” 也是明确的。

2 个赞

for example:

我个人观点,不符合设计的非预期语句行为都可以定义为 bug,你提到的问题应该已经有 issue 在跟,具体可以问一下参与相关讨论的同事。

推荐阅读一下

简单说,索引的依赖在于起点 vid 线索是否提供,如果没有的话,那样的查询如果想要可控、高效,就需要人为明确创建索引(nebula 的设计理念目前是默认没有允许全扫描的,在分布式、大数量级的前提下,优先考虑可控超过可查询)

我们也在考虑能够可配置地允许知道自己在做什么的用户去全扫描,不过现在还没做。

另外,在索引不命中情况下(不命中,则索引全扫描),3.x的版本做过策略更改,最近的版本是不命中也会全扫索引满足查询。

总体来说在理清楚(确实稍微有点绕)之后其实是很明确的,只不过随着版本变化,有些策略有了调整,有些优化也慢慢加进来了(比如3.4.0引入了 vertexIndexScan limit 下推),现在来说,想要根据自己的访问模式去精准获得想要的性能平衡是能做到的,不过需要知道的背景知识还是有一些的,我们会一点点变得更好,不过这些调优知识在任何数据库上都绕不过哈。

1 个赞

我指的薛定谔是用户层面,不是技术层面

我觉得调优知识在任何数据库上都绕不过这个不是理由。调优的前提是行为一致,结果可预期.
但是目前这两点貌似都不完备。
我指的行为一致是有一个统一的可预期的结果,比如:什么样的查询必须加索引,否则报错。什么样的查询在没有具体索引也没有利用到索引但是只有加索引才能不报错,可以出结果(没办法,就是这么绕)。什么样的查询建立索引之后能成功的利用到索引。
举个简单的例子:现在的索引机制就好比,我想根据mysql的A字段查询,但是报错显示我必须加索引,然后我对mysql的B字段加了索引,因为加了B索引导致A字段查询可以运行并返回结果了,但是却并没有用到B索引。对B施加的操作影响到基于A的查询的可用性,我觉得是不能接受的。

OK 是bug还是bydesign,有没有designdoc?

2 个赞

哈哈,如果你感受到我在”防御“,非常抱歉哈,我不想辩解,这里确实有好几个地方容易让人困惑,我们一直在把它变得更好,也感谢你能来花精力告诉我们你的感受:heart:,针对你具体的问题,我解释一下哈~~~

这个(索引需要超出被索引范围,不符合左匹配的索引条件等情况下)本来不允许、后来又允许的情况其实是3.2 左右的一个策略变更,如上边我提到的,原来需要命中才能做的查询,现在可以退而求其次全扫描查询了,这个文档的信息没有跟进,需要改一下 cc @abby-cyber-doc

背后的逻辑是能让用户可以更宽松的做更多的查询。

具体查询的 effort 可以通过 profile 看到(比如索引查询的行数、index context 里的 column hint)

但是背后查询的时候,思路还是这个旧的策略下的这段描述,如果我们的业务形态里能够明确知道(这当然不是所有的情况下都能足够幸运去知道的,所以才有了这个策略的这次变更,变得不像以前那么严格了)的情况下,去精准匹配你的属性过滤条件的起点查询,还是能够获得最可控、精准的性能的哈。

2 个赞

非常感谢耐心解答,加油。我会一直关注,哈哈。

1 个赞