本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
使用 Gremlin 制作高效的 upserts
高效的 upserts 可以对 Gremlin 查询的性能产生重大影响。
upsert(或有条件插入)会重复使用顶点或边缘(如果已存在),但如果不存在,则创建它。
Upserts 允许你编写幂等的插入操作:无论你运行多少次这样的操作,总体结果都是相同的。这在高度并发的写入场景中非常有用,在这种情况下,对图表的同一部分进行并发修改可能会强制使用ConcurrentModifcationException
, 因此需要重试.
例如,以下查询首先在数据集中查找指定的顶点,然后将结果折叠到列表中,从而使顶点变得更新。在提供给coalesce()
步骤,然后查询将展开此列表。如果展开的列表不为空,则结果将从coalesce()
. 但是,如果unfold()
返回一个空集合,因为顶点当前不存在,coalesce()
继续评估提供了它的第二次遍历,然后在第二次遍历中,查询将创建缺失的顶点。
g.V('v-1').fold() .coalesce( unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') )
使用优化的形式coalesce()
为了提示
Neptune 可以优化fold().coalesce(unfold(), ...)
成语可以进行高吞吐量更新,但是这种优化只有在coalesce()
返回顶点或边缘但没有其他任何东西。如果你试图从其中的任何部分返回不同的东西,例如财产coalesce()
,不会发生 Neptune 优化。查询可能会成功,但它不会像优化版本一样执行,特别是对于大型数据集。
由于未优化的 upsert 查询会增加执行时间并降低吞吐量,因此值得使用 Gremlinexplain
终端节点,以确定 upsert 查询是否已完全优化。在审核时explain
计划,寻找以开头的线+ not converted into Neptune steps
和WARNING: >>
. 例如:
+ not converted into Neptune steps: [FoldStep, CoalesceStep([[UnfoldStep], [AddEdgeSte
...
WARNING: >> FoldStep << is not supported natively yet
这些警告可以帮助您识别查询中阻止其完全优化的部分。
有时不可能完全优化查询。在这些情况下,你应该尝试将无法优化的步骤放在查询结束时,从而允许引擎优化尽可能多的步骤。在一些批处理 upsert 示例中使用了此技术,其中,在将任何其他可能未优化的修改应用于同一顶点或边之前,都会执行一组顶点或边的所有优化 upsert。
批处理 upserts 以提高吞吐量
对于高吞吐量写入场景,您可以将 upsert 步骤链接在一起,以批量打乱顶点和边缘。批处理减少了打乱大量顶点和边缘的事务开销。然后,您可以通过使用多个客户端 parallel 处理批处理请求来进一步提高吞吐量。
作为一项经验法则,我们建议每个批处理请求刷新大约 200 条记录。记录是单个顶点或边标签或属性。例如,具有单个标签和 4 个属性的顶点会创建 5 条记录。带有标签和单个属性的边缘可创建 2 条记录。如果你想加快批次顶点(每个顶点都有一个标签和 4 个属性),你应该从批次大小为 40 开始,因为200 / (1 + 4)
= 40
.
您可以尝试批量大小。每批 200 条记录是一个很好的起点,但理想的批量大小可能会更高或更低,具体取决于您的工作负载。但是请注意,Neptune 可能会限制每个请求的 Gremlin 步骤的总数。这个限制没有记录在案,但是为了安全起见,请尽量确保您的请求包含不超过 1500 个 Gremlin 步骤。Neptune 可能会拒绝超过 1500 个步骤的大批量请求。
为了提高吞吐量,您可以使用多个客户端 parallel 加快批处理。客户端数应与 Neptune 写入器实例上的工作线程数相同,通常是服务器上 vCPUs 数的 2 倍。例如,r5.8xlarge
实例有 32 个 vCPUs 和 64 个工作线程。对于高吞吐量写入场景,请使用r5.8xlarge
,你将 parallel 时使用 64 个客户端向 Neptune 写批量不高兴。
每个客户都应提交批处理请求,然后等待请求完成,然后再提交另一个请求。尽管多个客户端并行运行,但每个客户端都以连续方式提交请求。这可以确保为服务器提供稳定的请求流,这些请求流占用所有工作线程,而不会淹没服务器端请求队列。
尽量避免生成多个遍历器的步骤
当 Gremlin 步骤执行时,它需要一个传入的遍历器,并发出一个或多个输出遍历器。一个步骤发出的遍历器的数量决定了下一步执行的次数。
通常,在执行批处理操作时,您希望每个操作(例如 upsert 顶点 A)执行一次,以便操作序列如下所示:upsert 顶点 A,然后是 upsert 顶点 B,然后是 upsert 顶点 C,依此类推。只要一个步骤只创建或修改一个元素,它就只发出一个遍历器,而表示下一个操作的步骤只执行一次。另一方面,如果操作创建或修改了多个元素,则会发出多个遍历器,这反过来导致后续步骤多次执行,每个发射的遍历器一次。这可能会导致数据库执行不必要的额外工作,在某些情况下可能会导致创建不必要的额外顶点、边缘或属性值。
请参阅混合 upserts 和插入了解如何处理可以发出多个遍历器的操作。
不高兴顶点
您可以使用顶点 ID 来确定是否存在相应的顶点。这是首选的方法,因为 Neptune 针对 ID 周围的高度并发使用案例优化 upserts。例如,以下查询会创建一个具有给定顶点 ID 的顶点(如果不存在),如果它存在,则会重复使用它:
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .id()
请注意,此查询以id()
步骤。虽然不是为了使顶点加息的目的绝对必要,但是添加id()
步骤到 upsert 查询的末尾可确保服务器不会将所有顶点属性序列化回客户端,这有助于降低查询的锁定成本。
或者,您可以使用顶点属性来确定顶点是否存在:
g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .fold() .coalesce(unfold(), addV('Person').property('email', 'person-1@example.org')) .id(
如果可能,请使用自己的用户提供的 ID 来创建顶点,然后使用这些 ID 来确定在 upsert 操作期间是否存在顶点。这可以让 Neptune 优化身份证周围的不高潮。在高度并发的修改场景中,基于 ID 的 upsert 可能比基于属性的 upsert 高效得多。
串联顶点不安
你可以将顶点 upserts 链接在一起以批量插入它们:
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .id()
使边缘变得更加紧张
您可以像使用自定义顶点 ID 打开顶点一样,使用边缘 ID 来打开边缘。同样,这是首选的方法,因为它允许 Neptune 优化查询。例如,以下查询根据边缘 ID 创建边缘(如果尚不存在),或者如果它存在,则重复使用它。该查询还使用from
和to
如果需要创建新边缘,则会有顶点。
g.E('e-1') .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2')) .property(id, 'e-1')) .id()
许多应用程序使用自定义顶点 ID,但是留下 Neptune 来生成边缘 ID。如果您不知道边缘的 ID,但您确实知道from
和to
顶点 ID,你可以使用这个公式来设置边缘:
g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2'))) .id()
请注意,中的顶点步骤where()
子句应该是inV()
(或者outV()
如果你用过inE()
找到边缘),而不是otherV()
. 请勿使用otherV()
,在这里,否则查询将无法优化并且性能将受到影响。例如,Neptune 不会优化以下查询:
// Unoptimized upsert, because of otherV() g.V('v-1') .outE('KNOWS') .where(otherV().hasId('v-2')) .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2'))) .id()
如果你不知道边缘或顶点 ID,你可以使用顶点属性 upsert:
g.V() .hasLabel('Person') .has('name', 'person-1') .outE('LIVES_IN') .where(inV().hasLabel('City').has('name', 'city-1')) .fold() .coalesce(unfold(), addE('LIVES_IN').from(V().hasLabel('Person') .has('name', 'person-1')) .to(V().hasLabel('City') .has('name', 'city-1'))) .id()
与顶点 upserts 一样,最好使用边缘 ID 或者使用基于 ID 的边缘 upsertsfrom
和to
顶点 ID,而不是基于属性的 upserts,以便 Neptune 可以完全优化 upsert。
检查from
和to
顶点存在
请注意创建新边缘的步骤的构建:addE().from().to()
. 这种构造可确保查询检查是否存在from
和to
顶点。如果其中任何一个不存在,查询将返回如下所示的错误:
{ "detailedMessage": "Encountered a traverser that does not map to a value for child
...
"code": "IllegalArgumentException", "requestId": "..." }
如果可能的话from
或者to
顶点不存在,你应该尝试在突破它们之间的边缘之前打乱它们。请参阅 组合顶点和边缘 upsert。
还有一种替代结构可以创建一个你不应该使用的边缘:V().addE().to()
. 只有在from
顶点存在。如果to
顶点不存在,如前所述,查询会生成错误,但是如果from
顶点不存在,它默默地无法插入边缘,而不会产生任何错误。例如,如果from
顶点不存在:
// Will not insert edge if from vertex does not exist g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2'))) .id()
串联边缘
如果您想将边缘 upserts 链接在一起以创建批处理请求,则必须以顶点查找开始每个 upsert,即使您已经知道边缘 ID。
如果你已经知道你想打乱的边缘的 ID,以及from
和to
顶点,你可以使用这个配方:
g.V('v-1') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2')) .property(id, 'e-1')) .V('v-3') .outE('KNOWS') .hasId('e-2').fold() .coalesce(unfold(), V('v-3').addE('KNOWS') .to(V('v-4')) .property(id, 'e-2')) .V('v-5') .outE('KNOWS') .hasId('e-3') .fold() .coalesce(unfold(), V('v-5').addE('KNOWS') .to(V('v-6')) .property(id, 'e-3')) .id()
也许最常见的批处理边缘 upsert 场景是你知道from
和to
顶点 ID,但不知道你想打乱的边缘的 ID。在这种情况下,请使用以下公式:
g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2'))) .V('v-3') .outE('KNOWS') .where(inV().hasId('v-4')) .fold() .coalesce(unfold(), V('v-3').addE('KNOWS') .to(V('v-4'))) .V('v-5') .outE('KNOWS') .where(inV().hasId('v-6')) .fold() .coalesce(unfold(), V('v-5').addE('KNOWS').to(V('v-6'))) .id()
如果你知道你想打乱的边缘的 ID,但不知道from
和to
顶点(这很不寻常),你可以使用这个配方:
g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-1@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-2@example.org')) .property(id, 'e-1')) .V() .hasLabel('Person') .has('email', 'person-3@example.org') .outE('KNOWS') .hasId('e-2') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-3@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-4@example.org')) .property(id, 'e-2')) .V() .hasLabel('Person') .has('email', 'person-5@example.org') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-5@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-6@example.org')) .property(id, 'e-3')) .id()
组合顶点和边缘 upsert
有时候你可能想要使顶点和连接它们的边缘都不高兴。你可以混合这里提供的批处理示例。下面的示例打开 3 个顶点和 2 条边:
g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2') .property('name', 'person-2@example.org')) .V('c-1') .fold() .coalesce(unfold(), addV('City').property(id, 'c-1') .property('name', 'city-1')) .V('p-1') .outE('LIVES_IN') .where(inV().hasId('c-1')) .fold() .coalesce(unfold(), V('p-1').addE('LIVES_IN') .to(V('c-1'))) .V('p-2') .outE('LIVES_IN') .where(inV().hasId('c-1')) .fold() .coalesce(unfold(), V('p-2').addE('LIVES_IN') .to(V('c-1'))) .id()
混合 upserts 和插入
有时候你可能想要使顶点和连接它们的边缘都不高兴。你可以混合这里提供的批处理示例。下面的示例打开 3 个顶点和 2 条边:
Upserts 通常一次执行一个元素。如果你坚持这里显示的 upsert 模式,每个 upsert 操作都会发出一个遍历器,这会导致后续的操作只执行一次。
但是,有时候你可能想将不高兴与插入物混合起来。例如,如果您使用边缘来表示操作或事件的实例,则可能会出现这种情况。请求可能会使用 upserts 来确保所有必要的顶点都存在,然后使用插入来添加边。对于这种请求,请注意每个操作可能发出的遍历者数量。
考虑以下示例,它混合了 upserts 和插入内容,以在图中添加表示事件的边缘:
// Fully optimized, but inserts too many edges g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2') .property('name', 'person-2@example.org')) .V('p-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-3') .property('name', 'person-3@example.org')) .V('c-1') .fold() .coalesce(unfold(), addV('City').property(id, 'c-1') .property('name', 'city-1')) .V('p-1', 'p-2') .addE('FOLLOWED') .to(V('p-1')) .V('p-1', 'p-2', 'p-3') .addE('VISITED') .to(V('c-1')) .id()
查询应该插入 5 条边缘:2 个后面的边缘和 3 个访问的边缘。但是,写入的查询会插入 8 个边缘:接下来 2 次,6 次访问过。其原因是,插入 2 个 FOUNDER 边的操作会发出 2 个遍历器,导致后续插入 3 条边的插入操作执行两次。
解决办法是添加fold()
在可能发出多个遍历器的每个操作之后一步:
g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2'). .property('name', 'person-2@example.org')) .V('p-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-3'). .property('name', 'person-3@example.org')) .V('c-1') .fold(). .coalesce(unfold(), addV('City').property(id, 'c-1'). .property('name', 'city-1')) .V('p-1', 'p-2') .addE('FOLLOWED') .to(V('p-1')) .fold() .V('p-1', 'p-2', 'p-3') .addE('VISITED') .to(V('c-1')). .id()
在这里我们插入了fold()
在插入跟随边的操作之后一步。这会导致单个遍历器,然后导致后续操作只执行一次。
这种方法的缺点是查询现在没有完全优化,因为fold()
未优化。接下来的插入操作fold()
现在不会进行优化。
如果您需要使用fold()
为了减少代表后续步骤的遍历数量,请尝试对操作进行排序,以便最便宜的操作占用查询中未优化的部分。
修改现有顶点和边缘的 Upserts
有时候你想创建一个顶点或边(如果不存在),然后向它添加或更新属性,无论它是新的还是现有的顶点或边缘。
要添加或修改属性,请使用property()
步骤。在外面使用此步骤coalesce()
步骤。如果您尝试修改现有顶点或边的属性coalesce()
步骤中,Neptune 查询引擎可能无法对查询进行优化。
以下查询添加或更新每个升级顶点上的计数器属性。EAREproperty()
step 具有单个基数来确保新值替换任何现有值,而不是添加到一组现有值中。
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .property(single, 'counter', 1) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .property(single, 'counter', 2) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .property(single, 'counter', 3) .id()
如果你有属性值,例如lastUpdated
时间戳值,适用于所有高潮元素,您可以在查询结束时添加或更新它:
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2'). .fold(). .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .V('v-1', 'v-2', 'v-3') .property(single, 'lastUpdated', datetime('2020-02-08')) .id()
如果还有其他条件决定是否应进一步修改顶点或边,则可以使用has()
步骤来过滤要应用修改的元素。下面的示例使用了has()
根据顶点的值筛选不满的顶点version
财产。然后,查询将更新为 3version
的任何顶点version
小于 3:
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') .property('version', 3)) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org') .property('version', 3)) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org') .property('version', 3)) .V('v-1', 'v-2', 'v-3') .has('version', lt(3)) .property(single, 'version', 3) .id()
使用sideEffect()
在一个不高兴
如果要删除属性作为 upsert 的一部分,必须使用sideEffect()
步骤,但是,目前 Neptune 查询引擎尚未对其进行优化。这意味着sideEffect()
step,查询的所有后续部分都将使用 TinkerPop 而不是 Neptune 的步骤。
以下查询打乱了 3 个顶点,删除score
使用每个顶点的属性sideEffect()
直接跟随每个顶点 upsert:
// Optimizes only the first upsert g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1'). .property('email', 'person-1@example.org')) .sideEffect(properties('score').drop()) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .sideEffect(properties('score').drop()) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .sideEffect(properties('score').drop()) .id()
由于sideEffect()
未优化,第一个之后的所有内容sideEffect()
将使用未优化 TinkerPop 步骤。换句话说,只有第一个顶点 upsert 才会被优化。为了提高查询的性能,你可以移动所有sideEffect()
到底的行为:
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2'). .fold(). .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .sideEffect(V('v-1', 'v-2', 'v-3').properties('score').drop()) .id()
现在所有 upsert 步骤都将进行优化。这些区域有:sideEffect()
这里的步骤仍未优化,但它对查询性能的影响已最小化。