使用 openCypher 和 Bolt 的 Neptune 最佳实践 - Amazon Neptune
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

使用 openCypher 和 Bolt 的 Neptune 最佳实践

将 openCypher 查询语言和 Bolt 协议与 Neptune 结合使用时,请遵循以下最佳实践。有关在 Neptune 中使用 openCypher 的信息,请参阅使用 openCypher 访问 Neptune 图形

在查询中首选定向边缘而非双向边缘

当 Neptune 执行查询优化时,双向边缘会使创建最佳查询计划变得困难。次优计划要求引擎执行不必要的工作,从而导致性能降低。

因此,请尽可能使用定向边缘而不是双向边缘。例如,使用:

MATCH p=(:airport {code: 'ANC'})-[:route]->(d) RETURN p)

而不是:

MATCH p=(:airport {code: 'ANC'})-[:route]-(d) RETURN p)

大多数数据模型实际上不需要在两个方向上遍历边缘,因此,通过切换到使用定向边缘,查询可以显著提高性能。

如果您的数据模型确实需要遍历双向边缘,请将 MATCH 模式中的第一个节点(左侧)设置为筛选限制最严的节点。

例如,“为我找到往返 ANC 机场的所有 routes”。编写这个查询以从 ANC 机场开始,如下所示:

MATCH p=(src:airport {code: 'ANC'})-[:route]-(d) RETURN p

引擎可以执行最少的工作量来满足查询,因为受限制最严的节点放置为模式中的第一个节点(左侧)。然后,引擎可以优化查询。

这比在模式末尾筛选 ANC 机场要好得多,如下所示:

MATCH p=(d)-[:route]-(src:airport {code: 'ANC'}) RETURN p

当受限制最严的节点没有放在模式中的首位时,引擎必须执行额外的工作,因为它无法优化查询,必须执行额外的查找才能得出结果。

Neptune 不支持在一个事务中进行多个并发查询

尽管 Bolt 驱动程序本身允许在事务中进行并发查询,但 Neptune 不支持在一个事务中并发运行多个查询。相反,Neptune 要求一个事务中的多个查询按顺序运行,并且在启动下一个查询之前完全消耗掉每个查询的结果。

以下示例显示了如何使用 Bolt 在一个事务中按顺序运行多个查询,以便在下一个查询开始之前完全消耗掉每个查询的结果:

final String query = "MATCH (n) RETURN n"; try (Driver driver = getDriver(HOST_BOLT, getDefaultConfig())) { try (Session session = driver.session(readSessionConfig)) { try (Transaction trx = session.beginTransaction()) { final Result res_1 = trx.run(query); Assert.assertEquals(10000, res_1.list().size()); final Result res_2 = trx.run(query); Assert.assertEquals(10000, res_2.list().size()); } } }

在失效转移后创建新连接

在失效转移的情况下,Bolt 驱动程序可以继续连接到旧的写入器实例,而不是新的活动写入器实例,因为 DNS 名称已解析为特定的 IP 地址。

为防止出现这种情况,请在进行任何失效转移后关闭 Driver 对象,然后重新连接该对象。

长寿命应用程序的连接处理

在构建长寿命的应用程序(例如,在容器内或 Amazon EC2 实例上运行的应用程序)时,只需实例化 Driver 对象一次,然后在应用程序的生命周期内重用该对象。Driver 对象是线程安全的,并且将其初始化的开销非常大。

的连接处理 Amazon Lambda

不建议在 Amazon Lambda 功能中使用螺栓驱动器,因为它们具有连接开销和管理要求。请改用 HTTPS 端点

完成后关闭驱动程序对象

在完成对客户端的操作后,务必关闭客户端,以便服务器关闭 Bolt 连接并释放与连接关联的所有资源。如果您使用 driver.close() 关闭驱动程序,则会自动发生这种情况。

如果驱动程序未正确关闭,Neptune 会在 20 分钟后终止所有空闲的 Bolt 连接,或者,如果您使用的是 IAM 身份验证,则会在 10 天后终止所有空闲的 Bolt 连接。

Neptune 支持的并发 Bolt 连接不超过 1000 个。如果您在使用完连接后没有显式关闭连接,并且实时连接的数量达到了 1000 的限制,则任何新的连接尝试都会失败。

使用显式事务模式进行读写

在将事务与 Neptune 和 Bolt 驱动程序结合使用时,最好将读取和写入事务的访问模式显式设置为正确的设置。

只读事务

对于只读事务,如果您在构建会话时没有传入适当的访问模式配置,则使用默认的隔离级别,即突变查询隔离。因此,对于只读事务来说,将访问模式显式设置为 read 非常重要。

自动提交读取事务示例:

SessionConfig sessionConfig = SessionConfig .builder() .withFetchSize(1000) .withDefaultAccessMode(AccessMode.READ) .build(); Session session = driver.session(sessionConfig); try { (Add your application code here) } catch (final Exception e) { throw e; } finally { driver.close() }

读取事务示例:

Driver driver = GraphDatabase.driver(url, auth, config); SessionConfig sessionConfig = SessionConfig .builder() .withDefaultAccessMode(AccessMode.READ) .build(); driver.session(sessionConfig).readTransaction( new TransactionWork<List<String>>() { @Override public List<String> execute(org.neo4j.driver.Transaction tx) { (Add your application code here) } } );

在这两种情况下,都使用 Neptune 只读事务语义实现 SNAPSHOT 隔离

由于只读副本仅接受只读查询,因此提交到只读副本的任何查询都在 SNAPSHOT 隔离语义下运行。

只读事务没有脏读或不可重复读取。

只读事务

对于突变查询,有三种不同的机制可以创建写入事务,每种机制如下所示:

隐式写入事务示例:

Driver driver = GraphDatabase.driver(url, auth, config); SessionConfig sessionConfig = SessionConfig .builder() .withDefaultAccessMode(AccessMode.WRITE) .build(); driver.session(sessionConfig).writeTransaction( new TransactionWork<List<String>>() { @Override public List<String> execute(org.neo4j.driver.Transaction tx) { (Add your application code here) } } );

自动提交写入事务示例:

SessionConfig sessionConfig = SessionConfig .builder() .withFetchSize(1000) .withDefaultAccessMode(AccessMode.Write) .build(); Session session = driver.session(sessionConfig); try { (Add your application code here) } catch (final Exception e) { throw e; } finally { driver.close() }

显式写入事务示例:

Driver driver = GraphDatabase.driver(url, auth, config); SessionConfig sessionConfig = SessionConfig .builder() .withFetchSize(1000) .withDefaultAccessMode(AccessMode.WRITE) .build(); Transaction beginWriteTransaction = driver.session(sessionConfig).beginTransaction(); (Add your application code here) beginWriteTransaction.commit(); driver.close();
写入事务的隔离级别
  • 作为突变查询的一部分进行的读取是在 READ COMMITTED 事务隔离下运行的。

  • 对于作为突变查询一部分进行的读取,没有脏读。

  • 在突变查询中读取时,记录和记录范围会被锁定。

  • 当突变事务已读取索引范围时,可以强力保证在读取结束之前,任何并发事务都不会修改该范围。

突变查询不是线程安全的。

有关冲突,请参阅使用锁定等待超时解决冲突

突变查询失败时不会自动重试。

异常的重试逻辑

对于所有允许重试的异常,通常最好使用指数回退和重试策略,在两次重试之间提供逐渐延长的等待时间,以便更好地处理诸如 ConcurrentModificationException 错误等临时问题。下面显示指数回退和重试模式的示例:

public static void main() { try (Driver driver = getDriver(HOST_BOLT, getDefaultConfig())) { retriableOperation(driver, "CREATE (n {prop:'1'})") .withRetries(5) .withExponentialBackoff(true) .maxWaitTimeInMilliSec(500) .call(); } } protected RetryableWrapper retriableOperation(final Driver driver, final String query){ return new RetryableWrapper<Void>() { @Override public Void submit() { log.info("Performing graph Operation in a retry manner......"); try (Session session = driver.session(writeSessionConfig)) { try (Transaction trx = session.beginTransaction()) { trx.run(query).consume(); trx.commit(); } } return null; } @Override public boolean isRetryable(Exception e) { if (isCME(e)) { log.debug("Retrying on exception.... {}", e); return true; } return false; } private boolean isCME(Exception ex) { return ex.getMessage().contains("Operation failed due to conflicting concurrent operations"); } }; } /** * Wrapper which can retry on certain condition. Client can retry operation using this class. */ @Log4j2 @Getter public abstract class RetryableWrapper<T> { private long retries = 5; private long maxWaitTimeInSec = 1; private boolean exponentialBackoff = true; /** * Override the method with custom implementation, which will be called in retryable block. */ public abstract T submit() throws Exception; /** * Override with custom logic, on which exception to retry with. */ public abstract boolean isRetryable(final Exception e); /** * Define the number of retries. * * @param retries -no of retries. */ public RetryableWrapper<T> withRetries(final long retries) { this.retries = retries; return this; } /** * Max wait time before making the next call. * * @param time - max polling interval. */ public RetryableWrapper<T> maxWaitTimeInMilliSec(final long time) { this.maxWaitTimeInSec = time; return this; } /** * ExponentialBackoff coefficient. */ public RetryableWrapper<T> withExponentialBackoff(final boolean expo) { this.exponentialBackoff = expo; return this; } /** * Call client method which is wrapped in submit method. */ public T call() throws Exception { int count = 0; Exception exceptionForMitigationPurpose = null; do { final long waitTime = exponentialBackoff ? Math.min(getWaitTimeExp(retries), maxWaitTimeInSec) : 0; try { return submit(); } catch (Exception e) { exceptionForMitigationPurpose = e; if (isRetryable(e) && count < retries) { Thread.sleep(waitTime); log.debug("Retrying on exception attempt - {} on exception cause - {}", count, e.getMessage()); } else if (!isRetryable(e)) { log.error(e.getMessage()); throw new RuntimeException(e); } } } while (++count < retries); throw new IOException(String.format( "Retry was unsuccessful.... attempts %d. Hence throwing exception " + "back to the caller...", count), exceptionForMitigationPurpose); } /* * Returns the next wait interval, in milliseconds, using an exponential backoff * algorithm. */ private long getWaitTimeExp(final long retryCount) { if (0 == retryCount) { return 0; } return ((long) Math.pow(2, retryCount) * 100L); } }

使用单个 SET 子句一次设置多个属性

与其使用多个 SET 子句来设置单个属性,不如使用映射一次为一个实体设置多个属性。

您可以使用:

MATCH (n:SomeLabel {`~id`: 'id1'}) SET n += {property1 : 'value1', property2 : 'value2', property3 = 'value3'}

而不是:

MATCH (n:SomeLabel {`~id`: 'id1'}) SET n.property1 = 'value1' SET n.property2 = 'value2' SET n.property3 = 'value3'

SET 子句接受单个属性或映射。如果在单个实体上更新多个属性,则使用带有映射的单个 SET 子句允许在单个操作中执行更新,而不是在多个操作中执行更新,这样可以更有效地执行多个操作。

使用 SET 子句一次删除多个属性

使用 OpenCypher 语言时,REMOVE 用于从实体中移除属性。在 Neptune 中,要删除的每个属性都需要单独的操作,这增加了查询延迟。相反,您可以将 SET 与地图一起使用,将所有属性值设置为null,在 Neptune 中,这等同于删除属性。当需要移除单个实体上的多个属性时,Neptune 的性能将得到提高。

使用:

WITH {prop1: null, prop2: null, prop3: null} as propertiesToRemove MATCH (n) SET n += propertiesToRemove

而不是:

MATCH (n) REMOVE n.prop1, n.prop2, n.prop3

使用参数化查询

建议在使用 OpenCypher 进行查询时始终使用参数化查询。查询引擎可以利用重复的参数化查询来实现查询计划缓存等功能,在这些功能中,重复调用具有不同参数的相同参数化结构可以利用缓存的计划。为参数化查询生成的查询计划只有在 100 毫秒内完成且参数类型为 NUMBER、BOOLEAN 或 STRING 时才会被缓存和重复使用。

使用:

MATCH (n:foo) WHERE id(n) = $id RETURN n

带参数:

parameters={"id": "first"} parameters={"id": "second"} parameters={"id": "third"}

而不是:

MATCH (n:foo) WHERE id(n) = "first" RETURN n MATCH (n:foo) WHERE id(n) = "second" RETURN n MATCH (n:foo) WHERE id(n) = "third" RETURN n

在 UNWIND 子句中使用扁平化地图而不是嵌套地图

深层嵌套结构可能会限制查询引擎生成最佳查询计划的能力。为了部分缓解此问题,以下定义的模式将为以下情况创建最佳计划:

  • 场景 1:使用包含数字、字符串和布尔值的密码字面值列表进行解压。

  • 场景 2:使用扁平化地图列表进行展开,其中仅包含密码文字(数字、字符串、布尔值)作为值。

在编写包含 UNWIND 子句的查询时,请使用上述建议来提高性能。

场景 1 示例:

UNWIND $ids as x MATCH(t:ticket {`~id`: x})

带参数:

parameters={ "ids": [1, 2, 3] }

场景 2 的一个示例是生成要创建或合并的节点列表。与其发出多个语句,不如使用以下模式将属性定义为一组扁平化地图:

UNWIND $props as p CREATE(t:ticket {title: p.title, severity:p.severity})

带参数:

parameters={ "props": [ {"title": "food poisoning", "severity": "2"}, {"title": "Simone is in office", "severity": "3"} ] }

而不是嵌套的节点对象,比如:

UNWIND $nodes as n CREATE(t:ticket n.properties)

带参数:

parameters={ "nodes": [ {"id": "ticket1", "properties": {"title": "food poisoning", "severity": "2"}}, {"id": "ticket2", "properties": {"title": "Simone is in office", "severity": "3"}} ] }

在可变长度路径 (VLP) 表达式中将限制性更强的节点放在左侧

在可变长度路径 (VLP) 查询中,查询引擎通过选择在表达式的左侧或右侧开始遍历来优化评估。该决定基于左侧和右侧模式的基数。基数是与指定模式匹配的节点数。

  • 如果右侧模式的基数为 1,则右侧将是起点。

  • 如果左侧和右侧的基数均为 1,则检查两侧的扩展并从较小的扩展侧开始。扩展是 VLP 表达式中左侧节点和右侧节点的传出或传入边的数量。仅当 VLP 关系为单向关系且提供了关系类型时,才使用优化的这一部分。

  • 否则,左侧将是起点。

对于 VLP 表达式链,此优化只能应用于第一个表达式。其他 VLP 从左侧开始评估。例如,假设 (a)、(b) 的基数为一,(c) 的基数大于一。

  • (a)-[*1..]->(c):评价以 (a) 开头。

  • (c)-[*1..]->(a):评价以 (a) 开头。

  • (a)-[*1..]-(c):评价以 (a) 开头。

  • (c)-[*1..]-(a):评价以 (a) 开头。

现在让 (a) 的传入边为二,(a) 的出边为三,(b) 的传入边为四,(b) 的出边为五。

  • (a)-[*1..]->(b): 评估从 (a) 开始,因为 (a) 的传出边缘小于 (b) 的传入边缘。

  • (a)<-[*1..]-(b): 评估从 (a) 开始,因为 (a) 的传入边缘小于 (b) 的传出边缘。

通常,将限制性更强的模式放在 VLP 表达式的左侧。

使用精细的关系名称避免冗余的节点标签检查

在优化性能时,使用节点模式专有的关系标签可以删除节点上的标签筛选。考虑一个图表模型,其中关系likes仅用于定义两个person节点之间的关系。我们可以编写以下查询来找到这种模式:

MATCH (n:person)-[:likes]->(m:person) RETURN n, m

n 和 m 上的person标签检查是多余的,因为我们将关系定义为只有当两者都属于该类型时才会出现person。为了优化性能,我们可以按如下方式编写查询:

MATCH (n)-[:likes]->(m) RETURN n, m

当属性仅限于单个节点标签时,也可以应用此模式。假设只有person节点具有该属性email,因此验证节点标签是否匹配person是多余的。将此查询写成:

MATCH (n:person) WHERE n.email = 'xxx@gmail.com' RETURN n

比将此查询写成以下方式效率要低:

MATCH (n) WHERE n.email = 'xxx@gmail.com' RETURN n

只有在性能很重要并且在建模过程中要进行检查以确保这些边缘标签不会被重复用于涉及其他节点标签的模式时,才应采用这种模式。如果您稍后在另一个节点标签上引入一个email属性(例如)company,则这两个版本的查询结果将有所不同。

尽可能指定边缘标签

在图案中指定边缘时,建议尽可能提供边缘标签。以以下示例查询为例,该查询用于将居住在城市中的所有人与访问过该城市的所有人联系起来。

MATCH (person)-->(city {country: "US"})-->(anotherPerson) RETURN person, anotherPerson

如果您的图表模型使用多个边缘标签将人们链接到城市以外的节点,则由于不指定结束标签,Neptune 将需要评估其他路径,这些路径稍后将被丢弃。在上面的查询中,由于没有给出边缘标签,因此引擎会先做更多工作,然后筛选出值以获得正确的结果。上述查询的更好版本可能是:

MATCH (person)-[:livesIn]->(city {country: "US"})-[:visitedBy]->(anotherPerson) RETURN person, anotherPerson

这不仅有助于评估,而且使查询计划器能够创建更好的计划。您甚至可以将此最佳实践与冗余节点标签检查相结合,以删除城市标签检查并将查询写成:

MATCH (person)-[:livesIn]->({country: "US"})-[:visitedBy]->(anotherPerson) RETURN person, anotherPerson

尽可能避免使用 WITH 子句

OpenCypher 中的 WITH 子句充当边界,在此之前的所有内容都执行完毕,然后将结果值传递给查询的其余部分。当你需要临时聚合或想要限制结果数量时,需要使用 WITH 子句,但除此之外,你应该尽量避免使用 WITH 子句。一般指导是删除这些简单的 WITH 子句(不包括聚合、排序依据或限制),以使查询计划器能够处理整个查询,从而创建全局最优计划。举个例子,假设你写了一个查询来返回居住在India以下地区的所有人:

MATCH (person)-[:lives_in]->(city) WITH person, city MATCH (city)-[:part_of]->(country {name: 'India'}) RETURN collect(person) AS result

在上述版本中,WITH 子句限制了之前(person)-[:lives_in]->(city)模式的位置(city)-[:part_of]->(country {name: 'India'})(限制性更强)。这使得该计划不太理想。对此查询的优化是删除 WITH 子句,让计划者计算出最佳计划。

MATCH (person)-[:lives_in]->(city) MATCH (city)-[:part_of]->(country {name: 'India'}) RETURN collect(person) AS result

尽早在查询中放置限制性筛选器

在所有情况下,在查询中尽早放置筛选器有助于减少查询计划必须考虑的中间解决方案。这意味着执行查询所需的内存和计算资源更少。

以下示例可帮助您了解这些影响。假设你写了一个查询来返回所有居住在里面的人India。查询的一个版本可能是:

MATCH (n)-[:lives_in]->(city)-[:part_of]->(country) WITH country, collect(n.firstName + " " + n.lastName) AS result WHERE country.name = 'India' RETURN result

上述版本的查询并不是实现此用例的最佳方式。筛选器稍后country.name = 'India'会出现在查询模式中。它将首先收集所有人员及其居住地,然后按国家对他们进行分组,然后仅筛选该群组country.name = India。仅查询居住在里面的人India然后执行收集聚合的最佳方式。

MATCH (n)-[:lives_in]->(city)-[:part_of]->(country) WHERE country.name = 'India' RETURN collect(n.firstName + " " + n.lastName) AS result

一般规则是在引入变量后尽快放置过滤器。

明确检查属性是否存在

根据 OpenCypher 语义,当访问属性时,它等同于可选联接,即使该属性不存在,也必须保留所有行。如果您根据图表架构知道该实体将始终存在特定属性,则显式检查该属性的存在可以让查询引擎创建最佳计划并提高性能。

考虑一个图形模型,其中类型的节点person始终具有属性name。与其这样做,不如这样做:

MATCH (n:person) RETURN n.name

使用 IS NOT NOT NULL 检查明确验证查询中是否存在属性:

MATCH (n:person) WHERE n.name IS NOT NULL RETURN n.name

不要使用命名路径(除非是必需的)

查询中的命名路径总是会产生额外的成本,这可能会增加延迟和内存使用量的损失。请考虑以下查询:

MATCH p = (n)-[:commentedOn]->(m) WITH p, m, n, n.score + m.score as total WHERE total > 100 MATCH (m)-[:commentedON]->(o) WITH p, m, n, distinct(o) as o1 RETURN p, m.name, n.name, o1.name

在上面的查询中,假设我们只想知道节点的属性,那么就没有必要使用路径 “p”。通过将命名路径指定为变量,使用 DISTINCT 的聚合操作在时间和内存使用方面都将变得昂贵。上述查询的更优化的版本可能是:

MATCH (n)-[:commentedOn]->(m) WITH m, n, n.score + m.score as total WHERE total > 100 MATCH (m)-[:commentedON]->(o) WITH m, n, distinct(o) as o1 RETURN m.name, n.name, o1.name

避免收集 (不同 ())

每当要形成包含不同值的列表时,都会使用 COLLECT (DISTINCT ())。COLLECT 是一个聚合函数,分组是根据同一语句中投影的其他键来完成的。当使用 distinct 时,输入会被分成多个块,其中每个区块表示一个要减少的组。随着小组数量的增加,性能将受到影响。在 Neptune 中,在实际收集/形成列表之前执行 DISTINCT 要有效得多。这允许直接在整个区块的分组键上进行分组。

请考虑以下查询:

MATCH (n:Person)-[:commented_on]->(p:Post) WITH n, collect(distinct(p.post_id)) as post_list RETURN n, post_list

编写此查询的更优方法是:

MATCH (n:Person)-[:commented_on]->(p:Post) WITH DISTINCT n, p.post_id as postId WITH n, collect(postId) as post_list RETURN n, post_list

检索所有属性值时,最好使用属性函数而不是单个属性查找

properties()函数用于返回包含实体所有属性的地图,并且比单独返回属性要有效得多。

假设您的Person节点包含 5 个属性firstNamelastNameage、、deptcompany、和,则首选以下查询:

MATCH (n:Person) WHERE n.dept = 'AWS' RETURN properties(n) as personDetails

而不是使用:

MATCH (n:Person) WHERE n.dept = 'AWS' RETURN n.firstName, n.lastName, n.age, n.dept, n.company === OR === MATCH (n:Person) WHERE n.dept = 'AWS' RETURN {firstName: n.firstName, lastName: n.lastName, age: n.age, department: n.dept, company: n.company} as personDetails

在查询之外执行静态计算

建议在客户端解析静态计算(简单的数学/字符串运算)。以这个例子为例,你想查找所有比作者大一岁或小于一岁的人:

MATCH (m:Message)-[:HAS_CREATOR]->(p:person) WHERE p.age <= ($age + 1) RETURN m

在这里$age,通过参数注入到查询中,然后将其添加到固定值中。然后将该值与进行比较p.age。相反,更好的方法是在客户端进行加法,并将计算出的值作为参数 $ageplusone 传递。这有助于查询引擎创建优化的计划,并避免对每个传入的行进行静态计算。遵循这些指导原则,更有效的查询版本是:

MATCH (m:Message)-[:HAS_CREATOR]->(p:person) WHERE p.age <= $ageplusone RETURN m

使用 UNWIND 而不是单个语句进行批量输入

每当需要对不同的输入执行相同的查询时,与其为每个输入执行一个查询,不如为一批输入运行一个查询,性能会更高。

如果你想在一组节点上合并,一个选择是为每个输入运行一个合并查询:

MERGE (n:Person {`~id`: $id}) SET n.name = $name, n.age = $age, n.employer = $employer

带参数:

params = {id: '1', name: 'john', age: 25, employer: 'Amazon'}

需要对每个输入执行上述查询。虽然这种方法行得通,但可能需要为大量输入执行许多查询。在这种情况下,批处理可能有助于减少在服务器上执行的查询数量,并提高整体吞吐量。

请使用以下模式:

UNWIND $persons as person MERGE (n:Person {`~id`: person.id}) SET n += person

带参数:

params = {persons: [{id: '1', name: 'john', age: 25, employer: 'Amazon'}, {id: '2', name: 'jack', age: 28, employer: 'Amazon'}, {id: '3', name: 'alice', age: 24, employer: 'Amazon'}...]}

建议尝试不同的批次大小,以确定哪种批次最适合您的工作负载。

最好为节点/关系使用自定义 ID

Neptune 允许用户在节点和关系上显式分配 ID。ID 在数据集中必须是全局唯一且具有确定性才有用。确定性 ID 可以像属性一样用作查找或筛选机制;但是,从查询执行的角度来看,使用 ID 比使用属性要优化得多。使用自定义 ID 有几个好处-

  • 现有实体的属性可以为空,但是 ID 必须存在。这允许查询引擎在执行期间使用优化的联接。

  • 当执行并发突变查询时,当使用 ID 访问节点时,出现并发修改异常 (CME) 的可能性会大大降低,因为由于其强制唯一性,对于 ID 的锁比对属性的锁要少。

  • 使用 ID 可以避免创建重复数据的机会,因为 Neptune 强制执行 ID 的唯一性,这与属性不同。

以下查询示例使用自定义 ID:

注意

该属性~id用于指定 ID,而id仅作为任何其他属性存储。

CREATE (n:Person {`~id`: '1', name: 'alice'})

不使用自定义 ID:

CREATE (n:Person {id: '1', name: 'alice'})

如果使用后一种机制,则不会强制执行唯一性,您可以稍后执行查询:

CREATE (n:Person {id: '1', name: 'john'})

这将创建第二个id=1名为 named 的节点john。在这种情况下,你现在将有两个节点id=1,每个节点都有不同的名称-(alice 和 john)。

避免在查询中进行~id计算

在查询中使用自定义 ID 时,请务必在查询之外执行静态计算,并在参数中提供这些值。当提供静态值时,引擎能够更好地优化查找并避免扫描和筛选这些值。

如果要在数据库中存在的节点之间创建边,可以选择以下方法:

UNWIND $sections as section MATCH (s:Section {`~id`: 'Sec-' + section.id}) MERGE (s)-[:IS_PART_OF]->(g:Group {`~id`: 'g1'})

带参数:

parameters={sections: [{id: '1'}, {id: '2'}]}

在上面的查询中id,正在查询中计算该节的。由于计算是动态的,因此引擎无法静态内联 ID,最终会扫描所有节点。然后,引擎对所需的节点执行后筛选。如果数据库中有许多节点,这可能会很昂贵。

实现这一目标的更好方法是在传递到数据库的 ID 中Sec-加上前缀:

UNWIND $sections as section MATCH (s:Section {`~id`: section.id}) MERGE (s)-[:IS_PART_OF]->(g:Group {`~id`: 'g1'})

带参数:

parameters={sections: [{id: 'Sec-1'}, {id: 'Sec-2'}]}