

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

# 最佳实践：充分利用 Neptune
<a name="best-practices"></a>

下面是有关使用 Amazon Neptune 的一些一般建议。使用此信息作为参考可以快速找到使用 Amazon Neptune 和最大程度地提高性能的建议。

**Contents**
+ [Amazon Neptune 基本操作指导](best-practices-general-basic.md)
  + [Amazon Neptune 安全最佳实践](best-practices-general-security.md)
  + [避免在集群中使用不同的实例类](best-practices-general-basic.md#best-practices-loader-heterogeneous-instances)
  + [避免在批量加载期间重复重启](best-practices-general-basic.md#best-practices-loader-repeated-restarts)
  + [如果您的谓词数量很多，则启用 OSGP 索引](best-practices-general-basic.md#best-practices-general-predicates)
  + [尽可能避免长时间运行的事务](best-practices-general-basic.md#best-practices-general-long-running-transactions)
  + [使用 Neptune 指标的最佳实践](best-practices-general-metrics.md)
  + [优化 Neptune 查询的最佳实践](best-practices-general-basic.md#best-practices-general-tuning)
  + [跨只读副本的负载均衡](best-practices-general-basic.md#best-practices-general-loadbalance)
  + [使用较大的临时实例加快加载速度](best-practices-general-basic.md#best-practices-loader-tempinstance)
  + [通过失效转移到只读副本来调整写入器实例的大小](best-practices-general-basic.md#best-practices-resize-instance)
  + [数据预提取任务中断错误后重试上传](best-practices-general-basic.md#load-api-reference-status-interrupted)
+ [将 Gremlin 与 Neptune 结合使用的一般最佳实践](best-practices-gremlin.md)
  + [Neptune 无服务器的心跳配置](best-practices-gremlin-heartbeat-serverless.md)
  + [构建更新插入查询以利用 DFE 引擎](best-practices-gremlin.md#best-practices-gremlin-upserts)
  + [在要部署 Gremlin 代码的上下文中对其进行测试](best-practices-gremlin-console-glv-differences.md)
  + [创建高效的多线程 Gremlin 写入](best-practices-gremlin-multithreaded-writes.md)
  + [使用创建时间属性修剪记录](best-practices-gremlin-prune.md)
  + [将 `datetime( )` 方法用于 Groovy 时间数据](best-practices-gremlin-datetime.md)
  + [将本机日期和时间用于 GLV 时间数据](best-practices-gremlin-datetime-glv.md)
+ [将 Gremlin Java 客户端与 Neptune 结合使用的最佳实践](best-practices-gremlin-java-client.md)
  + [跨多个线程重用客户端对象](best-practices-gremlin-java-reuse.md)
  + [为读取和写入端点创建单独的 Gremlin Java 客户端对象](best-practices-gremlin-java-separate.md)
  + [将多个只读副本端点添加到 Gremlin Java 连接池](best-practices-gremlin-java-multiple.md)
  + [关闭客户端以避免连接限制](best-practices-gremlin-java-close-connections.md)
  + [在失效转移后创建新连接](best-practices-gremlin-java-new-connection.md)
  + [将 `maxInProcessPerConnection` 和 `maxSimultaneousUsagePerConnection` 设置为相同值](best-practices-gremlin-java-maxes.md)
  + [将查询以字节码而不是字符串的格式发送到服务器](best-practices-gremlin-java-bytecode.md)
  + [始终完全消耗查询返回的 ResultSet 或迭代器](best-practices-gremlin-java-resultset.md)
  + [按批次批量添加顶点和边缘](best-practices-gremlin-java-batch-add.md)
  + [禁用 Java 虚拟机中的 DNS 缓存](best-practices-gremlin-java-disable-dns-caching.md)
  + [（可选）在每个查询级别设置超时](best-practices-gremlin-java-per-query-timeout.md)
  + [排除 `java.util.concurrent.TimeoutException` 问题](best-practices-gremlin-java-exceptions-TimeoutException.md)
+ [使用 openCypher 和 Bolt 的 Neptune 最佳实践](best-practices-opencypher.md)
  + [在失效转移后创建新连接](best-practices-opencypher.md#best-practices-opencypher-renew-connection)
  + [长寿命应用程序的连接处理](best-practices-opencypher.md#best-practices-opencypher-long-connections)
  + [的连接处理 AWS Lambda](best-practices-opencypher.md#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-use-explicit-txs.md#best-practices-opencypher-read-txs)
    + [突变事务](best-practices-opencypher-use-explicit-txs.md#best-practices-opencypher-mutation-txs)
  + [异常的重试逻辑](best-practices-opencypher-retry-logic.md)
  + [使用单个 SET 子句一次设置多个属性](best-practices-content-0.md)
    + [使用 SET 子句一次删除多个属性](best-practices-content-0.md#best-practices-content-1)
  + [使用参数化查询](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)
+ [使用 SPARQL 的 Neptune 最佳实践](best-practices-sparql.md)
  + [默认查询所有命名图形](best-practices-sparql-query.md)
  + [为加载指定命名图形](best-practices-sparql-graph.md)
  + [在查询的 FILTER、FILTER...IN 和 VALUES 之间进行选择](best-practices-sparql-batch.md)

# Amazon Neptune 基本操作指导
<a name="best-practices-general-basic"></a>

以下是使用 Neptune 时您应遵循的基本操作指导。
+ 了解 Neptune 数据库实例，以便您可以根据自己的性能和用例要求适当调整其大小。请参阅[Amazon Neptune 数据库集群和实例](feature-overview-db-clusters.md)。
+ 监控您的 CPU 和内存使用情况。这有助于您了解何时迁移到具有更大 CPU 或内存容量的数据库实例类，以实现您所需的查询性能。您可以将 Amazon 设置 CloudWatch 为在使用模式发生变化或接近部署容量时通知您。这样做可以帮助您维护系统性能和可用性。有关详细信息，请参阅 [监控实例](feature-overview-db-clusters.md#feature-overview-monitoring-instances) 和 [监控 Neptune](monitoring.md)。

  由于 Neptune 具有自己的内存管理器，即使 CPU 使用率较高，相对较低的内存使用率也是正常的。执行查询时遇到 out-of-memory异常是需要增加可用内存的最佳指标。
+ 启用自动备份并设置备份时段，使备份在便利的时段进行。
+ 测试您的数据库实例的故障转移，以了解对于您的使用案例而言，该过程需要多长时间。它还有助于确保访问您数据库实例的应用程序在故障转移之后可以自动连接到新数据库实例。
+ 如果可能，请在同一区域和 VPC 中运行您的客户端和 Neptune 集群，因为使用 VPC 对等连接的跨区域连接可能会导致查询响应时间的延迟。对于几个毫秒的查询响应，需要将客户端和 Neptune 集群保留在同一个区域和 VPC 中。
+ 在创建只读副本实例时，该实例至少应与主写入器实例一样大。这有助于阻止复制滞后，并避免副本重新启动。请参阅[避免在集群中使用不同的实例类](#best-practices-loader-heterogeneous-instances)。
+ 在升级到新的主要引擎版本之前，请务必在升级之前在其上测试您的应用程序。为此，您可以克隆数据库集群，使克隆集群运行新的引擎版本，然后在克隆上测试您的应用程序。
+ 为了便于进行故障转移，理想情况下，所有实例的大小应相同。

**Topics**
+ [Amazon Neptune 安全最佳实践](best-practices-general-security.md)
+ [避免在集群中使用不同的实例类](#best-practices-loader-heterogeneous-instances)
+ [避免在批量加载期间重复重启](#best-practices-loader-repeated-restarts)
+ [如果您的谓词数量很多，则启用 OSGP 索引](#best-practices-general-predicates)
+ [尽可能避免长时间运行的事务](#best-practices-general-long-running-transactions)
+ [使用 Neptune 指标的最佳实践](best-practices-general-metrics.md)
+ [优化 Neptune 查询的最佳实践](#best-practices-general-tuning)
+ [跨只读副本的负载均衡](#best-practices-general-loadbalance)
+ [使用较大的临时实例加快加载速度](#best-practices-loader-tempinstance)
+ [通过失效转移到只读副本来调整写入器实例的大小](#best-practices-resize-instance)
+ [数据预提取任务中断错误后重试上传](#load-api-reference-status-interrupted)

# Amazon Neptune 安全最佳实践
<a name="best-practices-general-security"></a>

使用 AWS Identity and Access Management (IAM) 账户控制对 Neptune API 操作的访问权限。控制创建、修改或删除 Neptune 资源（如数据库实例、安全组、选项组或参数组）的操作，以及执行常见管理操作（如备份和还原数据库实例）的操作。
+ 尽可能使用临时凭证而不是永久凭证。
+ 为每个管理 Amazon Relational Database Service (Amazon RDS) 资源的用户分配一个 IAM 账户。切勿使用 AWS 账户根用户来管理 Neptune 资源。为每个人（包括您自己）创建一个 IAM 用户。
+ 授予每位用户执行其职责所需的最小权限集。
+ 使用 IAM 组有效地管理适用于多个用户的权限。
+ 定期轮换您的 IAM 凭证。

有关使用 IAM 访问 Neptune 资源的更多信息，请参阅[确保 Amazon Neptune 数据库的安全](security.md)。有关使用 IAM 的一般信息，请参阅《IAM 用户指南》**中的 [AWS Identity and Access Management](https://docs.aws.amazon.com/IAM/latest/UserGuide/Welcome.html) 和 [IAM 最佳实践](https://docs.aws.amazon.com/IAM/latest/UserGuide/IAMBestPractices.html)。

## 避免在集群中使用不同的实例类
<a name="best-practices-loader-heterogeneous-instances"></a>

当您的数据库集群包含不同类的实例时，可能会随着时间推移而出现问题。最常见的问题是，由于复制滞后，小的读取器实例可能会进入重复重启循环。如果读取器节点的数据库实例类配置比写入器数据库实例的配置弱，则更改量可能太大，读取器无法跟上。

**重要**  
为避免复制滞后导致重复重启，请配置您的数据库集群，使所有实例具有相同的实例类（大小）。

您可以使用 Amazon 中的`ClusterReplicaLag`指标查看数据库集群中的写入器实例（主实例）和读取器之间的延迟 CloudWatch。`VolumeWriteIOPs` 指标还允许您检测集群中可能造成复制滞后的写入活动突发情况。

## 避免在批量加载期间重复重启
<a name="best-practices-loader-repeated-restarts"></a>

如果您在批量加载期间由于复制滞后而经历了重复重启只读副本的循环，则您的副本可能无法跟上数据库集群中写入器的速度。

要么将读取器扩展到比写入器大，要么在批量加载期间暂时将它们移除，然后在完成后重新创建。

## 如果您的谓词数量很多，则启用 OSGP 索引
<a name="best-practices-general-predicates"></a>

如果您的数据模型包含大量不同的谓词（大多数情况下超过 1000），则可能会面临性能降低和更高的运营成本。

在这种情况下，您可以通过启用 [OSGP 索引](feature-overview-storage-indexing.md#feature-overview-storage-indexing-osgp)来提高性能。请参阅[OSGP 索引](features-lab-mode.md#features-lab-mode-features-osgp-index)。

## 尽可能避免长时间运行的事务
<a name="best-practices-general-long-running-transactions"></a>

长时间运行的事务（无论是只读事务还是读写事务）可能导致以下类型的意外问题：

读取器实例或具有并发写入的写入器实例上长时间运行的事务可能会导致大量累积不同版本的数据。这可能会给筛选掉大部分结果的读取查询带来更高的延迟。

在某些情况下，数小时内累积的版本可能会导致新的写入受到节流。

如果实例重启，长时间运行且写入次数较多的读写事务也可能导致问题。如果实例因维护事件或崩溃而重启，则将回滚所有未提交的写入操作。此类撤消操作通常在后台运行，不会阻止实例恢复正常，但是任何与正在回滚的操作冲突的新写入操作都会失败。

例如，如果在上一次运行中断开连接后重试同一查询，则实例重启时查询可能会失败。

撤消操作所需的时间与所涉及更改的大小成正比。

# 使用 Neptune 指标的最佳实践
<a name="best-practices-general-metrics"></a>

要确定因资源不足和其它常见瓶颈导致的性能问题，您可以监控可用于 Neptune 数据库集群的指标。

定期监控性能指标以收集有关各种时间范围内的平均值、最大值和最小值数据。这可帮助确定性能下降的时间。使用这些数据，您可以为特定的指标阈值设置 Amazon CloudWatch 警报，以便在达到这些阈值时收到提醒。

设置新的数据库集群并让它在典型工作负载下运行时，尝试按一些不同的间隔（例如，一小时、二十四小时、一周、两周）来捕获所有性能指标的平均值、最大值和最小值。这将使您能够了解运行状况。这有助于将操作的峰值时间与非峰值时间进行比较。您随后可以利用这些信息确定性能何时降到标准水平以下，然后相应设置警报。

有关如何查看 Neptune 指标的信息，请参阅 [使用亚马逊监控 Neptune CloudWatch](cloudwatch.md)。

以下是首先要查看的最重要指标：
+ **BufferCacheHitRatio**— 由缓冲区缓存处理的请求的百分比。缓存未命中会给查询执行带来显著的延迟。如果缓存命中率低于 99.9%，并且您的应用程序存在延迟问题，请考虑升级实例类型以在内存中缓存更多数据。
+ **CPU 利用率** – 使用的计算机处理容量的百分比。根据您的查询性能目标，高 CPU 消耗值可能是正常情况。
+ **可释放内存** – 数据库实例上可用的 RAM 量（以 MB 为单位）。Neptune 有自己的内存管理器，因此该指标可能低于您的预期。如果查询经常抛出 out-of-memory异常，则应考虑将实例类升级到具有更多 RAM 的实例类，这是一个好兆头。

对于 CPU 和内存指标，**监控**选项卡中的红线指标标记为 75%。如果实例内存消耗频繁越过红线，请检查您的工作负载并考虑升级实例以改进查询性能。

## 优化 Neptune 查询的最佳实践
<a name="best-practices-general-tuning"></a>

 提高 Neptune 性能的最佳方式之一是优化最常使用和占用资源最多的查询，以降低其运行成本。

有关如何调整 Gremlin 查询的信息，请参阅[Gremlin 查询提示](gremlin-query-hints.md)和[调整 Gremlin 查询](gremlin-traversal-tuning.md)。有关如何调整 SPARQL 查询的信息，请参阅 [SPARQL 查询提示](sparql-query-hints.md)。

## 跨只读副本的负载均衡
<a name="best-practices-general-loadbalance"></a>

读取器终端节点轮询路由的运行方式是更改 DNS 条目指向的主机。客户端必须创建新连接并解析 DNS 记录才能连接到新的只读副本，因为 WebSocket 连接通常会长时间保持活动状态。

要为连续请求获取不同的只读副本，请确保您的客户端在每次连接时都会解析 DNS 条目。这可能需要关闭连接并重新连接到读取器终端节点。

您还可以通过显式连接到实例终端节点来跨只读副本对请求进行负载均载。

## 使用较大的临时实例加快加载速度
<a name="best-practices-loader-tempinstance"></a>

实例的大小越大，您的加载性能越高。如果您没有使用较大的实例类型，但希望提高加载速度，则可以先使用较大的实例进行加载然后删除它。

**注意**  
以下过程是针对新集群的。如果您目前已经有一个集群，则可以添加一个新的较大实例，然后将其提升为新的主数据库实例。

**使用较大的实例大小加载数据**

1.  使用单个 `r5.12xlarge` 实例创建集群。此实例是主数据库实例。

1. 创建大小相同 (`r5.12xlarge`) 的一个或多个只读副本。

   您可以创建大小较小的只读副本，但如果它们的大小不足以跟上主实例的写入速度，则可能必须频繁地重启。由此产生的停机时间会大大降低性能。

1. 在批量加载程序命令中，加入 `“parallelism” : “OVERSUBSCRIBE”` 以告知 Neptune 使用所有可用的 CPU 资源进行加载（请参阅[Neptune 加载程序请求参数](load-api-reference-load.md#load-api-reference-load-parameters)）。然后，加载操作将尽可能 I/O 快地进行，这通常需要 60-70% 的 CPU 资源。

1. 使用 Neptune 加载程序加载您的数据。加载任务在主数据库实例上运行。

1. 数据完成加载后，请务必将集群中的所有实例缩减为相同的实例类型，以避免额外费用和重复重启问题（请参阅[避免使用不同的实例大小](#best-practices-loader-heterogeneous-instances)）。

## 通过失效转移到只读副本来调整写入器实例的大小
<a name="best-practices-resize-instance"></a>

调整数据库集群中实例（包括写入器实例）大小的最佳方法是创建或修改只读副本实例，使其具有所需的大小，然后故意失效转移到该只读副本。您的应用程序看到的停机时间只是更改写入器的 IP 地址所需的时间，大约为 3 到 5 秒。

您用来故意将当前写入器实例失效转移到只读副本实例的 Neptune 管理 API 是 [FailoverDBCluster](api-clusters.md#FailoverDBCluster)。如果您使用的是 Gremlin Java 客户端，则可能需要在失效转移后创建一个新的客户端对象来获取新的 IP 地址，如[此处](best-practices-gremlin-java-new-connection.md)所述。

请务必将所有实例更改为相同的大小，这样可以避免重复重启的循环，如下所述。

## 数据预提取任务中断错误后重试上传
<a name="load-api-reference-status-interrupted"></a>

当您使用批量加载程序将数据加载到 Neptune 时，有时可能会出现 `LOAD_FAILED` 状态，在对某个请求的响应中报告 `PARSING_ERROR` 和 `Data prefetch task interrupted` 消息，以要求提供详细信息，如下所示：

```
"errorLogs" : [
  {
    "errorCode" : "PARSING_ERROR",
    "errorMessage" : "Data prefetch task interrupted: Data prefetch task for 11467 failed",
    "fileName" : "s3://amzn-s3-demo-bucket/some-source-file",
    "recordNum" : 0
  }
]
```

如果遇到此错误，只需重试批量上传请求。

在发生临时中断时会出现此错误，这种中断一般不是由您的请求或数据导致的，并且通常可以通过再次运行批量上传请求加以解决。

如果您使用的是默认设置，即 `"mode":"AUTO"` 和 `"failOnError":"TRUE"`，则在发生中断时，加载程序会跳过已经成功加载的文件，并继续加载尚未加载的文件。

# 将 Gremlin 与 Neptune 结合使用的一般最佳实践
<a name="best-practices-gremlin"></a>

在将 Gremlin 图形遍历语言与 Neptune 一起使用时，请遵循以下建议。有关将 Gremlin 与 Neptune 一起使用的信息，请参阅[使用 Gremlin 访问 Neptune 图形](access-graph-gremlin.md)。

**重要**  
3.4.11 TinkerPop 版本中进行了更改，提高了查询处理方式的正确性，但目前有时会严重影响查询性能。  
例如，这种查询的运行速度可能会慢得多：  

```
g.V().hasLabel('airport').
  order().
    by(out().count(),desc).
  limit(10).
  out()
```
由于 3.4.11 的更改，极限步骤之后的顶点现在以非最佳方式获取。 TinkerPop 为避免这种情况，您可以通过在 `order().by()` 之后的任何点添加 barrier() 步骤来修改查询。例如：  

```
g.V().hasLabel('airport').
  order().
    by(out().count(),desc).
  limit(10).
  barrier().
  out()
```
TinkerPop [3.4.11 已在 Neptune 引擎版本 1.0.5.0 中启用。](engine-releases-1.0.5.0.md)

**Topics**
+ [Neptune 无服务器的心跳配置](best-practices-gremlin-heartbeat-serverless.md)
+ [构建更新插入查询以利用 DFE 引擎](#best-practices-gremlin-upserts)
+ [在要部署 Gremlin 代码的上下文中对其进行测试](best-practices-gremlin-console-glv-differences.md)
+ [创建高效的多线程 Gremlin 写入](best-practices-gremlin-multithreaded-writes.md)
+ [使用创建时间属性修剪记录](best-practices-gremlin-prune.md)
+ [将 `datetime( )` 方法用于 Groovy 时间数据](best-practices-gremlin-datetime.md)
+ [将本机日期和时间用于 GLV 时间数据](best-practices-gremlin-datetime-glv.md)

# Neptune 无服务器的心跳配置
<a name="best-practices-gremlin-heartbeat-serverless"></a>

在 Neptune Serverless 中使用 Gremlin WebSocket 客户端时，你需要适当配置客户端的 ping 间隔，以便在扩展事件期间保持稳定的连接。Gremlin 客户端使用 WebSocket 连接并定期发送 ping 来验证连接是否处于活动状态。客户端期望服务器在 ping 间隔时间范围内做出响应。如果服务器没有响应，客户端会自动关闭连接。

**对于 Neptune **预配置的**实例，我们建议将 ping 间隔设置为 5 秒。**对于 Neptune **Serverless 集群**，我们建议将 ping 间隔设置为至少 **20 秒**，以适应扩展操作期间可能出现的延迟。此参数控制客户端在向服务器写入数据之间等待多长时间，然后再发送 ping 以验证连接是否仍处于活动状态。

此参数的配置因客户端实现而异：

**Java 客户端配置**

对于 Java TinkerPop Gremlin 客户端，请配置以下参数：`keepAliveInterval`

```
Cluster.Builder builder = Cluster.build()
    .addContactPoint(endpoint)
    .keepAliveInterval(20000); // Configure ping interval in milliseconds
```

有关 Java 驱动程序配置的更多详细信息，请参阅 [Java TinkerPop 文档](https://tinkerpop.apache.org/docs/current/reference/#gremlin-java-configuration)。

**Go 客户端配置**

对于 Gremlin Go 客户端，请配置以下参数：`KeepAliveInterval`

```
rc, err := driver.NewDriverRemoteConnection(endpoint,
    func(settings *driver.DriverRemoteConnectionSettings) {
        settings.TraversalSource = "g"
        settings.AuthInfo = auth
        settings.KeepAliveInterval = 20 * time.Second // Configure ping interval
        ...
    })
```

有关 Go 驱动程序配置的更多详细信息，请参阅 [Go TinkerPop 文档](https://tinkerpop.apache.org/docs/current/reference/#gremlin-go-configuration)。

**JavaScript/Node.js 客户端配置**

对于 JavaScript /Node.js Gremlin 客户端，请配置以下参数：`pingInterval`

```
const gremlin = require('gremlin');
const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;

const connection = new DriverRemoteConnection(endpoint, {
    traversalSource: 'g',
    pingInterval: 20000  // Configure ping interval in milliseconds
});
```

有关 JavaScript 驱动程序配置的更多详细信息，请参阅[JavaScript TinkerPop 文档](https://tinkerpop.apache.org/docs/current/reference/#gremlin-javascript-configuration)。

**Python 客户端配置**

对于 Python Gremlin 客户端，ping 间隔通常在传输层进行管理。有关配置选项，请参阅特定的传输实现文档：

```
from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection

g = traversal().with_remote(
    DriverRemoteConnection('wss://your-neptune-endpoint:your-neptune-port/gremlin','g',
        transport_factory=lambda: AiohttpTransport(read_timeout=60,
                                                    write_timeout=20,
                                                    heartbeat=20, // Configure heartbeat
                                                    call_from_event_loop=True,
                                                    max_content_length=100*1024*1024,
                                                    ssl_options=ssl.create_default_context(Purpose.CLIENT_AUTH))))
```

有关 Python 驱动程序配置的更多详细信息，请参阅 [Python TinkerPop 文档](https://tinkerpop.apache.org/docs/current/reference/#gremlin-python-configuration)。

此配置可确保您的客户端在 Neptune Serverless 扩展事件期间保持连接稳定性，从而防止不必要的连接关闭并提高应用程序的可靠性。

## 构建更新插入查询以利用 DFE 引擎
<a name="best-practices-gremlin-upserts"></a>

[使用 Gremlin `mergeV()` 和 `mergeE()` 步骤进行高效的更新插入](gremlin-efficient-upserts.md)解释了如何构建更新插入查询以尽可能有效地使用 DFE 引擎。

# 在要部署 Gremlin 代码的上下文中对其进行测试
<a name="best-practices-gremlin-console-glv-differences"></a>

在 Gremlin 中，客户端可以通过多种方式向服务器提交查询：使用 WebSocket、或 Bytecode GLV，或者使用基于字符串的脚本通过 Gremlin 控制台。

务必认识到，根据您提交查询的方式，Gremlin 查询执行可能会有所不同。如果在字节码模式下提交，返回空结果的查询可能会视为成功，但如果在脚本模式下提交，则视为失败。例如，如果您包含`next()`在脚本模式查询中，`next()`则会将发送到服务器，但使用 ByteCode 客户端通常会自行处理。`next()`在第一种情况下，如果未找到任何结果，查询就会失败；但在第二种情况下，无论结果集是否为空，查询都会成功。

如果您在一个上下文中开发和测试代码（例如，通常以文本形式提交查询的 Gremlin 控制台），但随后在不同的上下文中部署代码（例如，通过使用字节码的 Java 驱动程序），则可能会遇到问题，即代码在生产环境中的行为与在开发环境中的行为不同。

**重要**  
请务必在将要部署 Gremlin 代码的 GLV 环境中测试该代码，以免出现意外结果。

# 创建高效的多线程 Gremlin 写入
<a name="best-practices-gremlin-multithreaded-writes"></a>

使用 Gremlin 将数据通过多线程加载到 Neptune 时，有一些指导方针可供遵循。

如果有可能，向每个线程提供一组可以插入或修改而不造成冲突的顶点或边缘。例如，线程 1 地址 ID 范围是 1–50000，线程 2 地址 ID 范围是 50001–100000，以此类推。这会减少造成 `ConcurrentModificationException` 的可能性。为安全起见，围绕所有写入放置一个 `try/catch` 块。如果出现任何失败，您可在短暂延迟后重试它们。

以介于 50 到 100（顶点或边缘）之间的批大小进行的批量写入通常可以很好地工作。如果您要为每个顶点添加大量属性，其数量接近 50，那么 100 是比较好的选择。进行一些试验是值得的。因此，对于批量写入，您可以使用类似于下文的方法：

```
g.addV(‘test’).property(id,’1’).as(‘a’).
  addV(‘test’).property(id,’2’).
  addE(‘friend’).to(‘a’).
```

然后，在每个批量操作中重复此方法。

相比在 Gremlin 与服务器的每次往返通信中添加一个顶点或边缘，使用批处理可大幅提升效率。

如果您使用 Gremlin 语言变体 (GLV) 客户端，则可以先创建遍历以便用编程方式创建批处理。然后在其中进行添加，最后对其进行迭代，例如：

```
  t.addV(‘test’).property(id,’1’).as(‘a’)
  t.addV(‘test’).property(id,’2’)
  t.addE(‘friend’).to(‘a’)
  t.iterate()
```

如果可能，最好是使用 Gremlin 语言变体客户端。不过您可以使用其他客户端执行类似的操作，通过将字符串连接起来构建一个批次，以文本字符串的格式提交查询。

如果您使用的是 Gremlin 客户端库之一而不是基本 HTTP 进行查询，线程应该全部共享同一个客户端、集群或连接池。您可能需要调整设置来实现尽可能最佳的吞吐量，诸如 Gremlin 客户端使用的连接池大小和工作线程数等设置。

# 使用创建时间属性修剪记录
<a name="best-practices-gremlin-prune"></a>

您可以通过将创建时间作为属性存储在顶点上并定期删除它们来修剪过时的记录。

如果需要将数据存储特定的生命周期，然后从图形中删除它（顶点生存时间），则可以在创建顶点时存储时间戳属性。您随后可以定期发出对在特定时间之前创建的所有顶点的 `drop()` 查询；例如：

```
g.V().has(“timestamp”, lt(datetime('2018-10-11')))
```

# 将 `datetime( )` 方法用于 Groovy 时间数据
<a name="best-practices-gremlin-datetime"></a>

Neptune 提供了 `datetime` 方法，用于为 Gremlin **Groovy** 变体中发送的查询指定日期和时间。这包括 Gremlin 控制台、使用 HTTP REST API 的文本字符串以及使用 Groovy 的任何其他序列化。

**重要**  
这*仅* 适用于您将 Gremlin 查询作为*文本字符串* 发送的方法。如果您使用的是 Gremlin 语言变体，则必须使用该语言的本机日期类和函数。有关更多信息，请参阅下一节：[将本机日期和时间用于 GLV 时间数据](best-practices-gremlin-datetime-glv.md)。  
以 TinkerPop `3.5.2`（在 [Neptune 引擎版本 1.1.1.0](engine-releases-1.1.1.0.md) 中引入）开头，`datetime`是不可或缺的一部分。 TinkerPop

您可以使用 `datetime` 方法来存储和比较日期：

```
g.V('3').property('date',datetime('2001-02-08'))
```

```
g.V().has('date',gt(datetime('2000-01-01')))
```

# 将本机日期和时间用于 GLV 时间数据
<a name="best-practices-gremlin-datetime-glv"></a>

如果您使用的是 Gremlin 语言变体 (GLV)，则必须对 Gremlin 时间数据使用由编程语言提供的原生日期和时间类以及函数。

官方 TinkerPop 图书馆都是 Gremlin 语言变体库。
+  [Go](https://tinkerpop.apache.org/docs/current/reference/#gremlin-go) 
+  [Java](https://tinkerpop.apache.org/docs/current/reference/#gremlin-java) 
+  [Javascript](https://tinkerpop.apache.org/docs/current/reference/#gremlin-javascript) 
+  [.NET](https://tinkerpop.apache.org/docs/current/reference/#gremlin-dotnet) 
+  [Python](https://tinkerpop.apache.org/docs/current/reference/#gremlin-python) 

**重要**  
 本页面仅适用于 Gremlin 语言变体（GLV）库。如果您使用了将 Gremlin 查询作为文本字符串发送的方法，则必须使用 Gremlin 的 datetime() 函数。这包括 Gremlin 控制台、使用 HTTP REST API 的文本字符串或通过驱动程序直接提交的 Gremlin 字符串。



**Go**  
 下面是采用 Go 的示例的一部分，该示例为 ID 是“3”的顶点创建名为“日期”的单个属性。它将该值设置为使用 Go time.Now() 函数生成的日期。

```
import ( "time" )

g.V('3').property('date', time.Now()).next();
```

有关使用 Go 连接到 Neptune 的完整示例，请参阅[使用 Go 客户端连接到 Neptune 数据库实例](https://docs.aws.amazon.com//neptune/latest/userguide/access-graph-gremlin-go.html)。

**Java**  
下面是采用 Java 的示例的一部分，该示例为 ID 为“`3`”的顶点创建名为“`date`”的单个属性。它将值设置为使用 Java `Date()` 构造函数生成的日期。

```
import java.util.date

g.V('3').property('date', new Date()).next();
```

有关使用 Java 连接到 Neptune 的完整示例，请参阅[使用 Java 客户端连接到 Neptune 数据库实例](access-graph-gremlin-java.md)。

**Node.js (JavaScript)**  
以下是中的部分示例 JavaScript ，它为标识为 “`date`” 的顶点创建了一个名为 “`3`” 的单个属性。它将值设置为使用 Node.js `Date()` 构造函数生成的日期。

```
g.V('3').property('date', new Date()).next()
```

有关使用 Node.js 连接到 Neptune 的完整示例，请参阅[使用 Node.js 连接到 Neptune 数据库实例](access-graph-gremlin-node-js.md)。

**.NET (C\$1)**  
下面是采用 C\$1 的示例的一部分，该示例为 ID 为“`3`”的顶点创建名为“`date`”的单个属性。它将值设置为使用 .NET `DateTime.UtcNow` 属性生成的日期。

```
Using System;

g.V('3').property('date', DateTime.UtcNow).next()
```

有关使用 C\$1 连接到 Neptune 的完整示例，请参阅[使用 .NET 连接到 Neptune 数据库实例](access-graph-gremlin-dotnet.md)。

**Python**  
下面是采用 Python 的示例的一部分，该示例为 ID 是“`3`”的顶点创建名为“`date`”的单个属性。它将值设置为使用 Python `datetime.now()` 方法生成的日期。

```
import datetime

g.V('3').property('date',datetime.datetime.now()).next()
```

有关使用 Python 连接到 Neptune 的完整示例，请参阅[使用 Python 连接到 Neptune 数据库实例](access-graph-gremlin-python.md)。

# 将 Gremlin Java 客户端与 Neptune 结合使用的最佳实践
<a name="best-practices-gremlin-java-client"></a>

在 Neptune 上使用 Gremlin Java 客户端时，请遵循以下建议。这些最佳实践可帮助您在使用 Java 驱动程序时优化性能、有效管理连接并避免常见的陷阱。

有关为 Neptune Serverless 配置心跳间隔的信息，请参阅。[Neptune 无服务器的心跳配置](best-practices-gremlin-heartbeat-serverless.md)

**Topics**
+ [跨多个线程重用客户端对象](best-practices-gremlin-java-reuse.md)
+ [为读取和写入端点创建单独的 Gremlin Java 客户端对象](best-practices-gremlin-java-separate.md)
+ [将多个只读副本端点添加到 Gremlin Java 连接池](best-practices-gremlin-java-multiple.md)
+ [关闭客户端以避免连接限制](best-practices-gremlin-java-close-connections.md)
+ [在失效转移后创建新连接](best-practices-gremlin-java-new-connection.md)
+ [将 `maxInProcessPerConnection` 和 `maxSimultaneousUsagePerConnection` 设置为相同值](best-practices-gremlin-java-maxes.md)
+ [将查询以字节码而不是字符串的格式发送到服务器](best-practices-gremlin-java-bytecode.md)
+ [始终完全消耗查询返回的 ResultSet 或迭代器](best-practices-gremlin-java-resultset.md)
+ [按批次批量添加顶点和边缘](best-practices-gremlin-java-batch-add.md)
+ [禁用 Java 虚拟机中的 DNS 缓存](best-practices-gremlin-java-disable-dns-caching.md)
+ [（可选）在每个查询级别设置超时](best-practices-gremlin-java-per-query-timeout.md)
+ [排除 `java.util.concurrent.TimeoutException` 问题](best-practices-gremlin-java-exceptions-TimeoutException.md)

# 跨多个线程重用客户端对象
<a name="best-practices-gremlin-java-reuse"></a>

在多个线程之间重用同一个客户端（或 `GraphTraversalSource`）对象 即，在应用程序中创建 `org.apache.tinkerpop.gremlin.driver.Client` 类的共享实例，而不是在每个线程中这样做。`Client` 对象是线程安全的，并且将其初始化的开销非常大。

在内部创建 `Client` 对象的 `GraphTraversalSource` 也是这种情况。例如，下面的代码会导致实例化新的 `Client` 对象：

```
import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal; 

  /////

GraphTraversalSource traversal = traversal()
                                   .withRemote(DriverRemoteConnection.using(cluster));
```

# 为读取和写入端点创建单独的 Gremlin Java 客户端对象
<a name="best-practices-gremlin-java-separate"></a>

您可以通过仅在写入器终端节点上执行写入并从一个或多个只读终端节点进行读取来提高性能。

```
Client readerClient = Cluster.build("https://reader-endpoint")
          ...
          .connect()

Client writerClient = Cluster.build("https://writer-endpoint")
          ...
          .connect()
```

# 将多个只读副本端点添加到 Gremlin Java 连接池
<a name="best-practices-gremlin-java-multiple"></a>

在创建 Gremlin Java `Cluster` 对象时，您可以使用 `.addContactPoint()` 方法将多个只读副本实例添加到连接池的联系点。

```
Cluster.Builder readerBuilder = Cluster.build()
          .port(8182)
          .minConnectionPoolSize(…)
          .maxConnectionPoolSize(…)
          ………
          .addContactPoint("reader-endpoint-1")
          .addContactPoint("reader-endpoint-2")
```

# 关闭客户端以避免连接限制
<a name="best-practices-gremlin-java-close-connections"></a>

使用完客户端后，请务必关闭客户端，以确保服务器关闭 WebSocket 连接并释放与连接关联的所有资源。如果您使用 `Cluster.close( )` 关闭集群，则客户端会自动关闭，因为随后在内部调用了 `client.close( )`。

如果客户端未正确关闭，Neptune 会在 20 到 25 分钟后终止所有空闲 WebSocket连接。但是，如果您在使用完连接后没有明确关闭 WebSocket连接，并且实时连接的数量达到[WebSocket 并发连接限制](limits.md#limits-websockets)，则会拒绝其他连接，并显示 HTTP `429` 错误代码。此时，您必须重启 Neptune 实例来关闭连接。

调用 `cluster.close()` 的建议不适用于 Java AWS Lambda 函数。有关详细信息，请参阅 [在 AWS Lambda 函数中管理 Gremlin WebSocket 连接](lambda-functions-websocket-connections.md)。

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

在故障转移的情况下，Gremlin 驱动程序可能会继续连接到旧写入器，因为集群 DNS 名称解析为 IP 地址。如果出现这种情况，您可以在故障转移后创建新的 `Client` 对象。

# 将 `maxInProcessPerConnection` 和 `maxSimultaneousUsagePerConnection` 设置为相同值
<a name="best-practices-gremlin-java-maxes"></a>

`maxInProcessPerConnection`和`maxSimultaneousUsagePerConnection`参数都与您在单个 WebSocket连接上可以同时提交的最大查询数有关。在内部，这些参数相互关联，如果修改其中之一而不修改其他参数，可能会导致客户端在尝试从客户端连接池提取连接时收到超时错误。

我们建议您保留默认的最小处理中值和同时使用值，并将 `maxInProcessPerConnection` 和 `maxSimultaneousUsagePerConnection` 设置为相同值。

为这些参数设置的值取决于查询复杂性和数据模型。在查询返回大量数据的使用案例中，每个查询会需要更多带宽，因此应使用较低的参数值和较高的 `maxConnectionPoolSize` 值。

相比之下，在查询返回数据量较少的情况下，`maxInProcessPerConnection` 和 `maxSimultaneousUsagePerConnection` 应设置为比 `maxConnectionPoolSize` 更高的值。

# 将查询以字节码而不是字符串的格式发送到服务器
<a name="best-practices-gremlin-java-bytecode"></a>

在提交查询时，使用字节码而不是字符串具有以下好处：
+ **及早捕获无效查询语法：**使用字节码变量让您可以在编译阶段检测到无效的查询语法。如果您使用基于字符串的变量，则在将查询提交到服务器并返回错误之前，您无法发现无效语法。
+ **避免基于字符串的性能损失：**[任何基于字符串的查询提交，无论是使用 WebSockets 还是 HTTP，都会导致顶点分离，这意味着 Vertex 对象由 ID、Label 和与顶点关联的所有属性组成（请参阅元素的属性）。](http://tinkerpop.apache.org/docs/current/reference/#_properties_of_elements)

  这会导致当无需属性时在服务器上产生不必要的计算。例如，在客户希望使用查询 `g.V("hakuna#1")` 获得 ID 为“hakuna\$11”的顶点时。如果查询是以基于字符串的提交形式发送，服务器将花费时间在 ID、标签以及此顶点的所有属性中检索。如果查询是以字节码提交的形式发送，服务器仅花时间检索顶点的 ID 和标签。

换而言之，不要提交类似于下文的查询：

```
  final Cluster cluster = Cluster.build("localhost")
                                 .port(8182)
                                 .maxInProcessPerConnection(32)
                                 .maxSimultaneousUsagePerConnection(32)
                                 .serializer(Serializers.GRAPHBINARY_V1D0)
                                 .create();

  try {
      final Client client = cluster.connect();
      List<Result> results = client.submit("g.V().has('name','pumba').out('friendOf').id()").all().get();
      System.out.println(verticesWithNamePumba);
  } finally {
      cluster.close();
  }
```

而是使用字节码提交查询，如下所示：

```
  final Cluster cluster = Cluster.build("localhost")
                                 .port(8182)
                                 .maxInProcessPerConnection(32)
                                 .maxSimultaneousUsagePerConnection(32)
                                 .serializer(Serializers.GRAPHBINARY_V1D0)
                                 .create();

  try {
      final GraphTraversalSource g = traversal().withRemote(DriverRemoteConnection.using(cluster));
      List<Object> verticesWithNamePumba = g.V().has("name", "pumba").out("friendOf").id().toList();
      System.out.println(verticesWithNamePumba);
  } finally {
      cluster.close();
  }
```

# 始终完全消耗查询返回的 ResultSet 或迭代器
<a name="best-practices-gremlin-java-resultset"></a>

客户端对象应始终完全使用 `ResultSet`（在基于字符串的提交时）或 `GraphTraversal` 返回的迭代器。如果未完全使用查询结果，服务器会挂起，等待客户端使用完它们。

如果您的应用程序只需一部分结果，您可以在查询中使用 `limit(X)` 步骤来限制服务器生成的结果数。

# 按批次批量添加顶点和边缘
<a name="best-practices-gremlin-java-batch-add"></a>

对 Neptune 数据库的每个查询都在单个事务的范围内运行，除非您使用会话。这意味着，如果您需要使用 gremlin 查询插入大量数据，请将它们以 50-100 个的批大小打包到一起，通过减少为每个负载创建的事务数来改善性能。

例如，如下所示添加 5 个顶点到数据库：

```
// Create a GraphTraversalSource for the remote connection
final GraphTraversalSource g = traversal().withRemote(DriverRemoteConnection.using(cluster));
// Add 5 vertices in a single query
g.addV("Person").property(T.id, "P1")
 .addV("Person").property(T.id, "P2")
 .addV("Person").property(T.id, "P3")
 .addV("Person").property(T.id, "P4")
 .addV("Person").property(T.id, "P5").iterate();
```

同样，您可以使用批量添加边缘。`addE`用于引`V()`用现有顶点作为每条边的源和目标：

```
// Add edges in a single batched query
g.V("P1").addE("knows").to(V("P2"))
 .V("P2").addE("knows").to(V("P3"))
 .V("P3").addE("knows").to(V("P4"))
 .V("P4").addE("knows").to(V("P5")).iterate();
```

您也可以将顶点和边的创建合并到一个批次中。`as()`用于标注新创建的顶点，以便在同一遍历中添加边时可以引用它们：

```
// Add vertices and edges together in a single query
g.addV("Person").property(T.id, "P1").as("p1")
 .addV("Person").property(T.id, "P2").as("p2")
 .addV("Person").property(T.id, "P3").as("p3")
 .addE("knows").from("p1").to("p2")
 .addE("knows").from("p2").to("p3").iterate();
```

# 禁用 Java 虚拟机中的 DNS 缓存
<a name="best-practices-gremlin-java-disable-dns-caching"></a>

[在您想要在多个只读副本之间平衡请求的环境中，您需要在 Java 虚拟机 (JVM) 中禁用 DNS 缓存，并在创建集群对象时提供 Neptune 的读取器终端节点。](https://tinkerpop.apache.org/javadocs/current/core/org/apache/tinkerpop/gremlin/driver/Cluster.html)禁用 JVM DNS 缓存可以确保为每个新连接重新解析 DNS，以在所有只读副本之间分配连接请求。可以在应用程序的初始化代码中使用以下行执行此操作：

```
java.security.Security.setProperty("networkaddress.cache.ttl", "0");
```

但是，上面的 A [mazon Gremlin Java](https://github.com/awslabs/amazon-neptune-tools/tree/master/neptune-gremlin-client) 客户端代码提供了更完整、更强大的负载平衡解决方案。 GitHubAmazon Java Gremlin 客户端知道您的集群拓扑，并在您的 Neptune 集群中的一组实例之间公平地分配连接和请求。有关使用该客户端的 Java Lambda 函数示例，请参阅[此博客文章](https://aws.amazon.com/blogs/database/load-balance-graph-queries-using-the-amazon-neptune-gremlin-client/)。

# （可选）在每个查询级别设置超时
<a name="best-practices-gremlin-java-per-query-timeout"></a>

Neptune 向您提供了使用参数组选项 `neptune_query_timeout` 为查询设置超时的功能（请参阅[参数](parameters.md)）。您也可以用如下代码覆盖全局超时：

```
  final Cluster cluster = Cluster.build("localhost")
                                 .port(8182)
                                 .maxInProcessPerConnection(32)
                                 .maxSimultaneousUsagePerConnection(32)
                                 .serializer(Serializers.GRAPHBINARY_V1D0)
                                 .create();

  try {
      final GraphTraversalSource g = traversal().withRemote(DriverRemoteConnection.using(cluster));
      List<Object> verticesWithNamePumba = g.with(ARGS_EVAL_TIMEOUT, 500L).V().has("name", "pumba").out("friendOf").id().toList();
      System.out.println(verticesWithNamePumba);
  } finally {
      cluster.close();
  }
```

或者，对于基于字符串的查询提交，代码如下所示：

```
  RequestOptions options = RequestOptions.build().timeout(500).create();
  List<Result> result = client.submit("g.V()", options).all().get();
```

**注意**  
如果您将查询超时值设置得过高，可能会产生意想不到的成本，尤其是在无服务器实例上。如果没有合理的超时设置，查询保持运行的时间可能会比预期长得多，从而产生您意想不到的成本。在运行查询时可以纵向扩展到昂贵的大型实例类型的无服务器实例上尤其如此。  
您可以通过使用查询超时值来避免此类意外开支，该值可以适应您预期的运行时间，并且只会导致异常长的运行超时。  
 从 Neptune 引擎版本 1.3.2.0 开始，Neptune 支持一个新的 neptune\$1lab\$1mode 参数作为 `StrictTimeoutValidation`。当该参数的值设置为 `Enabled` 时，通过请求选项或查询提示指定的单查询超时时间，不得超过参数组中设置的全局超时值。若超出限制，Neptune 会抛出 `InvalidParameterException`。  
 当值为时，可以在 “/status” 端点的响应中确认此`Disabled`设置。在引擎版本中`1.3.2.0`，此参数的默认值为`Disabled`。从引擎版本开始`1.4.0.0`，`StrictTimeoutValidation``Enabled`参数默认为。  
 有关配置多个超时设置时如何确定超时优先级的更多信息，请参阅 nept [une\$1query\$1](parameters.md#parameters-db-cluster-parameters-neptune_query_timeout) timeout 参数文档。

# 排除 `java.util.concurrent.TimeoutException` 问题
<a name="best-practices-gremlin-java-exceptions-TimeoutException"></a>

当 Gremlin 请求在等待其中一个`java.util.concurrent.TimeoutException`连接中的插槽可用时，客户端本身超时，Gremlin Java 客户端会抛出。 WebSocket 此超时持续时间由 `maxWaitForConnection` 客户端可配置参数控制。

**注意**  
由于在客户端上超时的请求从不会发送到服务器，因此它们不会反映在服务器上捕获的任何指标中，例如 `GremlinRequestsPerSec`。

这种超时通常通过以下两种方式之一引起：
+ **服务器实际上已达到最大容量。**如果是这样的话，服务器上的队列就会被填满，你可以通过监控[MainRequestQueuePendingRequests](cw-metrics.md#cw-metrics-available) CloudWatch 指标来检测到这一点。服务器可以处理的并行查询数量取决于其实例大小。

  如果 `MainRequestQueuePendingRequests` 指标未显示服务器上积累了待处理的请求，则服务器可以处理更多请求，而超时是由客户端节流造成的。
+ **客户端节流请求。**通常可以通过更改客户端配置设置来解决这个问题。

  可通过以下方式粗略估计客户端可以发送的最大并行请求数：

  ```
  maxParallelQueries = maxConnectionPoolSize * Max( maxSimultaneousUsagePerConnection, maxInProcessPerConnection )
  ```

  向客户端发送的查询超过 `maxParallelQueries` 会导致 `java.util.concurrent.TimeoutException` 异常。您可以通过多种方式解决这个问题：
  + *增加连接超时时间。*如果延迟对您的应用程序不重要，请增加客户端的 `maxWaitForConnection` 设置。然后，客户端会等待更长的时间才会超时，这反过来又会增加延迟。
  + *增加每个连接的最大请求数。*这允许使用同一个 WebSocket 连接发送更多请求。通过增加客户端的 `maxSimultaneousUsagePerConnection` 和 `maxInProcessPerConnection` 设置来实现此目的。这些设置通常应具有相同的值。
  + *增加连接池中的连接数。*通过增加客户端的 `maxConnectionPoolSize` 设置来实现此目的。代价是资源消耗增加，因为每个连接都使用内存和操作系统文件描述符，并且在初始化期间需要 SSL 和 WebSocket握手。

# 使用 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')
```

# 使用 SPARQL 的 Neptune 最佳实践
<a name="best-practices-sparql"></a>

当您将 SPARQL 查询语言与 Neptune 一起使用时，请遵循以下最佳实践。有关在 Neptune 中使用 SPARQL 的信息，请参阅[使用 SPARQL 访问 Neptune 图形](access-graph-sparql.md)。

**Topics**
+ [默认查询所有命名图形](best-practices-sparql-query.md)
+ [为加载指定命名图形](best-practices-sparql-graph.md)
+ [在查询的 FILTER、FILTER...IN 和 VALUES 之间进行选择](best-practices-sparql-batch.md)

# 默认查询所有命名图形
<a name="best-practices-sparql-query"></a>

Amazon Neptune 将每个三元组与一个命名图形相关联。默认图形定义为所有命名图形的并集。

如果提交 SPARQL 查询而未通过 `GRAPH` 关键字或 `FROM NAMED` 之类的构造明确指定图形，Neptune 将始终考虑数据库实例中的所有三元组。例如，以下查询从 Neptune SPARQL 端点返回所有三元组：

```
SELECT * WHERE { ?s ?p ?o }
```

显示在多个图形中的三元组将仅返回一次。

有关默认图形规范的信息，请参阅 SPARQL 1.1 查询语言规范的 [RDF 数据集](https://www.w3.org/TR/sparql11-query/#rdfDataset)一节。

# 为加载指定命名图形
<a name="best-practices-sparql-graph"></a>

Amazon Neptune 将每个三元组与一个命名图形相关联。如果加载、插入或更新三元组时未指定命名图形，Neptune 使用 URI `http://aws.amazon.com/neptune/vocab/v01/DefaultNamedGraph` 定义的回退命名图形。

如果您使用的是 Neptune 批量加载程序，则可以使用 `parserConfiguration: namedGraphUri` 参数指定要用于所有三元组（或第四个位置空白的四元组）的命名图形。有关 Neptune 加载程序 `Load` 命令语法的信息，请参阅[Neptune 加载程序命令](load-api-reference-load.md)。

# 在查询的 FILTER、FILTER...IN 和 VALUES 之间进行选择
<a name="best-practices-sparql-batch"></a>

有三种方法在 SPARQL 查询中注入值：`FILTER`、`FILTER...IN` 和 `VALUES`。

例如，假设您要在单个查询内查找多个人员的好友。您可以使用 `FILTER` 构建查询，如下所示：

```
  PREFIX ex: <https://www.example.com/>
  PREFIX foaf : <http://xmlns.com/foaf/0.1/>

  SELECT ?s ?o
  WHERE {?s foaf:knows ?o. FILTER (?s = ex:person1 || ?s = ex:person2)}
```

这将返回图形中 `?s` 绑定到 `ex:person1` 或 `ex:person2` 并且传出边缘标记为 `foaf:knows` 的所有三元组。

您也可以创建一个使用 `FILTER...IN` 的查询，该查询返回等同的结果：

```
  PREFIX ex: <https://www.example.com/>
  PREFIX foaf : <http://xmlns.com/foaf/0.1/>

  SELECT ?s ?o
  WHERE {?s foaf:knows ?o. FILTER (?s IN (ex:person1, ex:person2))}
```

您也可以使用 `VALUES` 创建查询，在此例中也会返回等同结果：

```
  PREFIX ex: <https://www.example.com/>
  PREFIX foaf : <http://xmlns.com/foaf/0.1/>

  SELECT ?s ?o
  WHERE {?s foaf:knows ?o. VALUES ?s {ex:person1 ex:person2}}
```

尽管在许多情况下，这些查询在语义上是等效的，但在某些情况下，两个 `FILTER` 变体不同于 `VALUES` 变体：
+ 第一种情况是在您注入重复值时，例如将同一个人注入两次。在这种情况下，`VALUES` 查询会在结果中包含重复项。您可通过将 `DISTINCT` 添加到 `SELECT` 子句来显式消除此类重复。但是，在某些情况下，您可能希望在查询结果中包含重复项用于冗余值注入。

  而 `FILTER` 和 `FILTER...IN` 在相同值多次显示时仅提取该值一次。
+ 第二种情况涉及 `VALUES` 始终执行完全匹配，而 `FILTER` 在某些情况下可能会应用类型提升并执行模糊匹配。

  例如，当您在 values 子句中包含类似于 `"2.0"^^xsd:float` 的文本时，`VALUES` 查询与此文本完全匹配，包括文本值和数据类型。

  相比之下，`FILTER` 会为这些数字文本生成模糊匹配。该匹配会包括值相同但数字数据类型（例如 `xsd:double`）不同的文本。
**注意**  
枚举字符串文本或 URI 时，`FILTER` 与 `VALUES` 的行为没有区别。

`FILTER` 与 `VALUES` 之间的区别会影响优化以及所造成的查询计算策略。除非您的用例需要模糊匹配，否则我们建议您使用 `VALUES`，因为它会避免查找与类型转换相关的特殊情况。因此，`VALUES` 通常会生成更高效的查询，运行速度更快，而且成本更低。