

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

# 使用 openCypher 和 Bolt 的 Neptune 最佳实践
<a name="best-practices-opencypher"></a>

将 openCypher 查询语言和 Bolt 协议与 Neptune 结合使用时，请遵循以下最佳实践。有关在 Neptune 中使用 openCypher 的信息，请参阅[使用 openCypher 访问 Neptune 图形](access-graph-opencypher.md)。

**Topics**
+ [在失效转移后创建新连接](#best-practices-opencypher-renew-connection)
+ [长寿命应用程序的连接处理](#best-practices-opencypher-long-connections)
+ [的连接处理 AWS Lambda](#best-practices-opencypher-lambda-connections)
+ [在查询中首选定向边缘而非双向边缘](best-practices-opencypher-directed-edges.md)
+ [Neptune 不支持在一个事务中进行多个并发查询](best-practices-opencypher-multiple-queries.md)
+ [完成后关闭驱动程序对象](best-practices-opencypher-close-driver.md)
+ [使用显式事务模式进行读写](best-practices-opencypher-use-explicit-txs.md)
+ [异常的重试逻辑](best-practices-opencypher-retry-logic.md)
+ [使用单个 SET 子句一次设置多个属性](best-practices-content-0.md)
+ [使用参数化查询](best-practices-content-2.md)
+ [在 UNWIND 子句中使用扁平化映射而非嵌套映射](best-practices-content-3.md)
+ [在可变长度路径（VLP）表达式中将限制性更强的节点放在左侧](best-practices-content-4.md)
+ [使用精细的关系名称避免冗余节点标签检查](best-practices-content-5.md)
+ [尽可能指定边缘标签](best-practices-content-6.md)
+ [尽可能避免使用 WITH 子句](best-practices-content-7.md)
+ [尽早在查询中放置限制性筛选条件](best-practices-content-8.md)
+ [显式检查属性是否存在](best-practices-content-9.md)
+ [不要使用命名路径（除非必需）](best-practices-content-10.md)
+ [避免使用 COLLECT(DISTINCT())](best-practices-content-11.md)
+ [检索所有属性值时，最好使用 properties 函数而不是单个属性查找](best-practices-content-12.md)
+ [在查询之外执行静态计算](best-practices-content-13.md)
+ [对批量输入使用 UNWIND（而非单个语句）](best-practices-content-14.md)
+ [最好 IDs 对节点/关系使用自定义](best-practices-content-15.md)
+ [避免在查询中进行 \$1id 计算](best-practices-content-16.md)
+ [更新/合并多个节点](best-practices-merge-multiple-nodes.md)

## 在失效转移后创建新连接
<a name="best-practices-opencypher-renew-connection"></a>

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

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

## 长寿命应用程序的连接处理
<a name="best-practices-opencypher-long-connections"></a>

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

## 的连接处理 AWS Lambda
<a name="best-practices-opencypher-lambda-connections"></a>

不建议在 AWS Lambda 功能中使用螺栓驱动器，因为它们具有连接开销和管理要求。请改用 [HTTPS 端点](access-graph-opencypher-queries.md)。

# 在查询中首选定向边缘而非双向边缘
<a name="best-practices-opencypher-directed-edges"></a>

当 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 不支持在一个事务中进行多个并发查询
<a name="best-practices-opencypher-multiple-queries"></a>

尽管 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());
    }
  }
}
```

# 完成后关闭驱动程序对象
<a name="best-practices-opencypher-close-driver"></a>

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

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

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

# 使用显式事务模式进行读写
<a name="best-practices-opencypher-use-explicit-txs"></a>

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

## 只读事务
<a name="best-practices-opencypher-read-txs"></a>

对于只读事务，如果您在构建会话时没有传入适当的访问模式配置，则使用默认的隔离级别，即突变查询隔离。因此，对于只读事务来说，将访问模式显式设置为 `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 只读事务语义](transactions-neptune.md#transactions-neptune-read-only)实现 [`SNAPSHOT` 隔离](transactions-isolation-levels.md)。

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

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

## 突变事务
<a name="best-practices-opencypher-mutation-txs"></a>

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

**隐式写入事务示例：**

```
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` 事务隔离下运行的。
+ 对于作为突变查询一部分进行的读取，没有脏读。
+ 在突变查询中读取时，记录和记录范围会被锁定。
+ 当突变事务已读取索引范围时，可以强力保证在读取结束之前，任何并发事务都不会修改该范围。

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

有关冲突，请参阅[使用锁定等待超时解决冲突](transactions-neptune.md#transactions-neptune-conflicts)。

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

# 异常的重试逻辑
<a name="best-practices-opencypher-retry-logic"></a>

对于所有允许重试的异常，通常最好使用[指数回退和重试策略](https://docs.aws.amazon.com/general/latest/gr/api-retries.html)，在两次重试之间提供逐渐延长的等待时间，以便更好地处理诸如 `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 子句一次设置多个属性
<a name="best-practices-content-0"></a>

 使用映射一次为一个实体设置多个属性，而非使用多个 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 子句一次删除多个属性
<a name="best-practices-content-1"></a>

 使用 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
```

# 使用参数化查询
<a name="best-practices-content-2"></a>

 建议在使用 openCypher 进行查询时始终使用参数化查询。查询引擎可以利用重复的参数化查询来实现查询计划缓存等功能，在这些功能中，重复调用具有不同参数的相同参数化结构可以利用缓存的计划。为参数化查询生成的查询计划只有在 100 ms 内完成且参数类型为 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 子句中使用扁平化映射而非嵌套映射
<a name="best-practices-content-3"></a>

 深层嵌套结构可能会限制查询引擎生成最佳查询计划的能力。为了部分缓解此问题，以下定义的模式将针对以下场景创建最佳计划：
+  场景 1：使用包含 NUMBER、STRING 和 BOOLEAN 的 Cypher 字面量列表的 UNWIND 子句。
+  场景 2：使用扁平化映射列表的 UNWIND 子句，其中仅包含 Cypher 字面量（NUMBER、STRING、BOOLEAN）作为值。

 在编写包含 UNWIND 子句的查询时，请遵循上述建议来提高性能。

场景 1 示例：

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

参数为：

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

 场景 2 的一个示例是生成要执行 CREATE 或 MERGE 操作的节点的列表。使用以下模式（而非发出多个语句）将属性定义为一组扁平化映射：

```
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）表达式中将限制性更强的节点放在左侧
<a name="best-practices-content-4"></a>

 在可变长度路径（VLP）查询中，查询引擎通过选择在表达式的左侧或右侧开始遍历来优化评估。该决定基于左侧和右侧模式的基数。基数是与指定模式匹配的节点数。
+  如果右侧模式的基数为 1，则右侧将是起点。
+  如果左侧和右侧的基数均为 1，则检查两边的扩展并从较小的扩展侧开始。扩展是 VLP 表达式中左侧节点和右侧节点的传出或传入边缘的数量。仅当 VLP 关系为单向关系且提供了关系类型时，才使用优化的这一部分。
+  否则，左侧将是起点。

 对于 VLP 表达式链，此优化只能应用于第一个表达式。另一个 VLPs 从左侧开始评估。例如，假设 (a) 和 (b) 的基数为 1，(c) 的基数大于 1。
+  `(a)-[*1..]->(c)`：评价从 (a) 开始。
+  `(c)-[*1..]->(a)`：评价从 (a) 开始。
+  `(a)-[*1..]-(c)`：评价从 (a) 开始。
+  `(c)-[*1..]-(a)`：评价从 (a) 开始。

 现在让 (a) 的传入边缘数为 2，(a) 的传出边缘数为 3，(b) 的传入边缘数为 4，(b) 的传出边缘数为 5。
+  `(a)-[*1..]->(b)`：评估从 (a) 开始，因为 (a) 的传出边缘数小于 (b) 的传入边缘数。
+  `(a)<-[*1..]-(b)`：评估从 (a) 开始，因为 (a) 的传入边缘数小于 (b) 的传出边缘数。

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

# 使用精细的关系名称避免冗余节点标签检查
<a name="best-practices-content-5"></a>

 在优化性能时，使用节点模式专有的关系标签可以避免对节点进行标签筛选。考虑一个图表模型，其中关系 `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`），则这两个版本的查询结果将有所不同。

# 尽可能指定边缘标签
<a name="best-practices-content-6"></a>

 在模式中指定边缘时，建议尽可能提供边缘标签。考虑以下查询示例，该查询用于将居住在某个城市中的所有人与访问过该城市的所有人链接起来。

```
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 子句
<a name="best-practices-content-7"></a>

 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 子句将模式 `(city)-[:part_of]->(country {name: 'India'})`（限制性更强）的位置限制在 `(person)-[:lives_in]->(city)` 之前。这使得该计划不太理想。对此查询的优化是删除 WITH 子句，让计划程序计算出最佳计划。

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

# 尽早在查询中放置限制性筛选条件
<a name="best-practices-content-8"></a>

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

 以下示例可帮助您了解这些影响。假设您编写了一个查询来返回居住在 `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
```

 一般规则是在引入变量后尽快放置筛选条件。

# 显式检查属性是否存在
<a name="best-practices-content-9"></a>

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

 考虑一个图形模型，其中类型 `person` 的节点始终具有属性 `name`。不应进行如下查询：

```
MATCH (n:person)
RETURN n.name
```

 而应在查询中通过 IS NOT NULL 检查显式验证该属性是否存在：

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

# 不要使用命名路径（除非必需）
<a name="best-practices-content-10"></a>

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

```
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())
<a name="best-practices-content-11"></a>

**注意**  
从引擎版本 [1.4.7.0 开始](engine-releases-1.4.7.0.md)，不再需要此建议的重写。

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

 请考虑以下查询：

```
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 函数而不是单个属性查找
<a name="best-practices-content-12"></a>

 `properties()` 函数用于返回包含实体所有属性的映射，并且比单独返回属性要高效。

 假设您的 `Person` 节点包含 5 个属性（`firstName`、`lastName`、`age`、`dept` 和 `company`），则首选以下查询：

```
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
```

# 在查询之外执行静态计算
<a name="best-practices-content-13"></a>

 建议在客户端解析静态计算（简单 mathematical/string 操作）。例如，您想查找所有比作者大一岁或小于一岁的人：

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

 在这里，`$age` 通过参数注入到查询中，然后加上固定值。之后该值与 `p.age` 进行比较。然而，更好的方法是在客户端进行加法计算，并将计算出的值作为参数 \$1ageplusone 传递。这有助于查询引擎创建优化的计划，并避免对每个传入的行进行静态计算。根据这些准则，得出的更有效的查询版本是：

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

# 对批量输入使用 UNWIND（而非单个语句）
<a name="best-practices-content-14"></a>

 当需要对不同的输入执行相同查询时，对批量输入运行一个查询比对每个输入执行一个查询更高效。

 如果您想在一组节点上合并，可以选择对每个输入运行一个合并查询：

```
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'}...]}
```

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

# 最好 IDs 对节点/关系使用自定义
<a name="best-practices-content-15"></a>

 Neptune 允许用户对节点和关系 IDs 进行显式分配。ID 在数据集中必须是全局唯一的，并且必须是确定性的，才有用。确定性 ID 可以像属性一样用作查找或筛选机制；但是，从查询执行的角度来看，使用 ID 比使用属性更优化。使用自定义有几个好处 IDs - 
+  现有实体的属性可以为空，但是 ID 必须存在。这让查询引擎在执行期间可以使用优化的联接。
+  当执行并发突变查询时，当用于访问节点时，出现[并发修改异常](https://docs.aws.amazon.com//neptune/latest/userguide/transactions-exceptions.html) (CMEs) 的可能性会 IDs 大大降低，因为由于其强制唯一性，所使用的锁 IDs 比属性少。
+  使用 IDs 可以避免创建重复数据的机会，因为 Neptune 会强制使用唯一性 IDs，这与属性不同。

 以下查询示例使用自定义 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` 且名为 `john` 的节点。在这种情况下，您现在将有两个 `id=1` 的节点，每个节点都有不同的名称（alice 和 john）。

# 避免在查询中进行 \$1id 计算
<a name="best-practices-content-16"></a>

 在查询 IDs 中使用 custom 时，请务必在查询之外执行静态计算，并在参数中提供这些值。如果提供静态值，引擎便能更好地优化查找，避免扫描和筛选这些值。

 如果要在数据库中存在的节点之间创建边缘，则可以选择使用以下查询：

```
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'}]}
```

 在上述查询中，section 的 `id` 是在查询中计算的。由于计算是动态的，因此引擎无法静态内联 ID，最终会扫描所有 section 节点。然后，引擎对所需的节点执行后筛选。如果数据库中有许多 section 节点，这样做成本可能会很高。

 避免扫描和筛选所有 section 节点的更好方法是，在传递到数据库的 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'}]}
```

# 更新/合并多个节点
<a name="best-practices-merge-multiple-nodes"></a>

 在多个节点上运行`MERGE`或`CREATE`查询时，建议将与单个 MERGE/CREATE 子句结合使用，而不是为每个节点使用 MERGE/CREATE 子句。`UNWIND`对一个节点使用一个子句的查询会导致执行计划效率低下，因为每行都需要优化。这导致查询的大部分执行时间都花在静态处理上，而不是花在实际更新上。

 每个节点一个子句并不是最优方法，因为它不能随着节点数量的增加而扩展：

```
MERGE (p1:Person {name: 'NameA'})
ON CREATE SET p1 += {prop1: 'prop1V1', prop2: 'prop2V1'}
MERGE (p2:Person {name: 'NameB'})
ON CREATE SET p2 += {prop1: 'prop1V2', prop2: 'prop2V2'}
MERGE (p3:Person {name: 'NameC'})
ON CREATE SET p3 += {prop1: 'prop1V3', prop2: 'prop1V3'}
```

 结合使用一个`UNWIND` MERGE/CREATE 子句可以实现相同的行为，但可以获得更优的执行计划。考虑到这一点，更改后的查询将如下所示：

```
## If not using custom id for nodes/relationship
UNWIND [{name: 'NameA', prop1: 'prop1V1', prop2: 'prop2V1'}, {name: 'NameB', prop1: 'prop1V2', prop2: 'prop2V2'}, {name: 'NameC', prop1: 'prop1V3', prop2: 'prop1V3'}] AS props
MERGE (p:Person {name: props.name})
ON CREATE SET p = props

## If using custom id for nodes/relationship
UNWIND [{`~id`: '1', 'name': 'NameA', 'prop1: 'prop1V1', prop2: 'prop2V1'}, {`~id`: '2', name: 'NameB', prop1: 'prop1V2', prop2: 'prop2V2'}, {`~id`: '3', name: 'NameC', prop1: 'prop1V3', prop2: 'prop1V3'}] AS props
MERGE (p:Person {`~id`: props.id})
ON CREATE SET p = removeKeyFromMap(props, '~id')
```