GIE Gremlin 使用常见问题解答

与TinkerPop的兼容性

GIE支持Apache TinkerPop定义的属性图模型和Gremlin遍历语言,并提供了一个兼容TinkerPop 3.4版本的Gremlin WebSockets服务器。除了原生Gremlin查询外,我们还引入了一些语法糖来实现更简洁的表达。但由于分布式特性和实际考量,需要注意我们对Gremlin实现的以下限制。

  • 功能

    • 图数据变更。

    • Lambda和Groovy表达式及函数,例如.map{}.by{}.filter{}函数,以及System.currentTimeMillis()等。此外,我们还提供了expr() 语法糖来处理复杂表达式。

    • Gremlin遍历策略。

    • 事务。

    • 目前不支持二级索引。主键将自动建立索引。

  • Gremlin步骤:查看这里获取完整的Gremlin支持/不支持列表。

属性图约束

当前版本的GIE支持两种图存储:一种利用Vineyard为不可变图数据提供内存存储,另一种名为groot,基于RocksDB开发,通过snapshot isolation还支持实时写入和数据一致性。两种存储都支持图数据跨多台服务器分区。根据设计,引入了以下约束条件(适用于两种存储):

  • 每个图都有一个模式(schema),由其中使用的边标签(edge labels)、属性键(property keys)和顶点标签(vertex labels)组成。

  • 每种顶点类型或标签都有一个由用户定义的主键(属性)。系统会自动为每个顶点和边生成一个字符串类型的唯一标识符,该标识符既编码了标签信息,也包含了用户定义的主键(针对顶点)。

  • 每个顶点或边属性可以是以下数据类型之一:intlongfloatdoubleStringListListList

Inner ID 和 Property ID 有什么区别?

Inner ID与Property ID的主要区别在于,Inner ID是图引擎内部使用的系统分配标识符,用于高效的数据存储和检索,而Property ID是特定实体类型中用户自定义的属性。

例如,在LDBC(Linked Data Benchmark Council)模式中,我们有一个名为'PERSON'的实体类型,它拥有自己的属性列表,包括'id'、'name'和'birthday'。在实际存储中,我们为每个'PERSON'实体类型的实例维护键值对,并在内部维护一个唯一ID来区分每个这样的实例。此上下文中的唯一ID被称为内部ID,而属性列表中的'id'则是属性ID。

GIE Gremlin 提供了多种方法来通过内部ID或属性ID查询顶点实例,类似于:

// by its inner id
g.V(123456)
g.V().hasId(123456)

// by its property id
g.V().has('id', 1)

在上述情况下,顶点可能具有一个属性id,其值为1,该属性被映射到一个全局唯一的内部ID123456

对于边(edges),我们目前没有提供基于内部ID(Inner ID)的查询方法,原因有以下两点:

  • 首先,Inner ID由系统内部维护,默认情况下不应向用户暴露。

  • 其次,仅靠内部ID可能无法唯一标识一条边实例,通常需要像这样的三元组。

如何在GIE Gremlin中使用路径扩展功能?

通过path_expand,用户可以明确定义所需的路径模式,并基于该路径进一步定义相应特征。例如,如果一个类型为'PERSON'的实体想要查找可通过3跳到达的实例,在传统Gremlin中只能表示为g.V().hasLabel('PERSON').both().both().both()。而使用path_expand,可以更简洁地表示为g.V().hasLabel('PERSON').both('3..4').endV(),其中both('3..4')表示路径模式,'3..4'指定跳数范围为[3,4)。

我们可以通过path_expand进一步定义路径模式的特征。例如,如果一个类型为'PERSON'的实体想要查找可通过3跳到达的实例,同时确保路径是简单路径(没有重复的顶点或边),则可以表示为:

g.V().hasLabel('PERSON').both('3..4').with('PATH_OPT', 'SIMPLE').endV()

关于path_expand的更多示例和用法,可以参考PathExpand

如何在GIE Gremlin中像SQL一样过滤数据?

通过expr,我们可以在GIE Gremlin中支持类SQL表达式。例如,如果我们想查找所有名称为'marko'或年龄为'27'的'PERSON'实例,可以表示如下:

g.V().hasLabel('PERSON').where(expr('@.name=\"marko\" || @.age = 27'))

在传统的Gremlin中,它只能表示如下:

g.V().hasLabel('PERSON').has('name', 'marko').or().has('age', 27)

这等同于以下类SQL表达式:

SELECT *
FROM PERSON
WHERE name = 'marko' OR age = 27;

传统的Gremlin使用HasStep操作符来支持过滤查询,与SQL中的Where操作符相比存在一些局限性:

  • HasStep 只能表达基于当前顶点或边及其属性的查询过滤器,无法跨越多个顶点或边。

  • 另一方面,Gremlin中的HasStep对于复杂表达式可能不如SQL中那么直观。

We have addressed the limitations and shortcomings of Gremlin in filter expression by using expr, for more usage, please refer to Expression.

如何在GIE Gremlin中像SQL一样聚合数据?

我们进一步扩展了Gremlin中的group操作符,以支持类似SQL的多列分组操作。

按多个键分组

g.V().hasLabel('PERSON').groupCount().by('name', 'age')

等同于:

SELECT
  PERSON.name,
  PERSON.age,
  COUNT(*)
FROM
  PERSON
GROUP BY
  PERSON.name,
  PERSON.age

按多个值分组:

g.V()
  .hasLabel('PERSON')
  .group()
    .by('name')
    .by(count('age').as('age_cnt'), sum('age').as('age_sum'))

等同于:

SELECT
  PERSON.name,
  COUNT(age) AS age_cnt,
  SUM(age) AS age_sum
FROM
  PERSON
GROUP BY
  name

更多用法请参考Aggregate

如何在GIE中优化Gremlin查询性能?

使用适当的索引

GIE支持多种索引选项,例如顶点标签索引、主键索引和边标签索引。正确定义和使用索引可以显著提升查询性能。

例如,在LDBC模式中,我们将属性ID定义为实体类型'PERSON'的主键,并在存储中维护相应的主键索引。这使我们能够直接使用索引特定的'PERSON'实例,而无需扫描所有顶点并根据属性键值进行过滤。这可以通过以下Gremlin查询来表达:

g.V().hasLabel('PERSON').has('id', propertyIdValue)

其中'id'是属性ID,'propertyIdValue'是属性键的值。

此外,我们支持within操作符来查询同一属性键的多个值,这也可以通过主键索引进行优化。例如:

g.V().hasLabel('PERSON').has('id', within(propertyIdValue1, propertyIdValue2))

通过直接使用主键索引,可以显著提升查询性能,避免全表扫描和属性值过滤,从而优化查询效率。

如何在GIE Gremlin中使用子图?

GIE中的子图是基于边诱导的,这意味着它通过从更大的图中选择边的子集及其关联顶点来形成。

因此,您只能在执行像E()、outE()、inE()或bothE()这样的边输出操作符之后进行子图操作。以下是一个示例:

g.V().outE().limit(10).subgraph('sub_graph').count()

更多用法请参考Subgraph

关于查询并行度设置的建议

我们支持通过g.with('pegasus.worker.num', $worker_num)语法为每个查询设置并行度。最大并行度受限于机器核心数量。在我们的引擎调度中,采用先到先服务的策略。

如果您的查询工作负载是高QPS的,包含大量小型查询(例如从单个源顶点查询属性或邻居点),我们建议将worker_num降低至1。此调整使引擎能够分配足够的工作线程来并发处理这些多样化的小型查询。

反之,如果您处理的是从大量源顶点开始且涉及复杂操作的高成本大型查询,我们建议增加worker_num(例如16或32)。这样,引擎可以增强查询并行性,提高每个查询的整体效率。

在小型查询和大型查询并发运行的场景中,建议为大型查询分配合理数量的工作线程(确保它们不会占用所有可用工作线程)。这种方法有助于防止小型查询被大型查询阻塞,因为后者可能独占所有工作线程,导致小型查询出现高延迟。