使用 fold()/coalesce()/unfold() 進行有效的 Gremlin upsert - Amazon Neptune

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

使用 fold()/coalesce()/unfold() 進行有效的 Gremlin upsert

upsert (或條件式插入) 會重複使用頂點或邊緣 (如果它已經存在),或者如果不存在,則建立它。有效的 upsert 可以在 Gremlin 查詢的效能上產生顯著的差異。

本頁面說明如何使用 fold()/coalesce()/unfold() Grimlin 模式進行有效的 upsert。但是,隨著引擎 TinkerPop 版本 1.2.1.0 中 Neptune 中引入的 3.6.x 版本,在大多數情況下,新的mergeV()mergeE()步驟更可取。此處描述的 fold()/coalesce()/unfold() 模式在某些複雜的情況下仍然很有用,但通常會使用 mergeV()mergeE() (如果可以的話),如 使用 Gremlin mergeV() 和 mergeE() 步驟進行有效的 upsert 中所述。

Upsert 可讓您撰寫等冪性插入操作:無論您執行多少次這類操作,整體結果都是一樣的。這在高度並行寫入案例中非常有用,其中對圖形的相同部分進行並行修改可以強制一或多個交易使用 ConcurrentModificationException 進行復原,從而需要重試。

例如,以下查詢會 upsert 頂點,方法是首先尋找資料集中的指定頂點,然後將結果摺疊成清單。在提供給 coalesce() 步驟的第一個周遊中,查詢接著會展開此清單。如果展開的清單不是空的,則會從 coalesce() 發出結果。不過,如果 unfold() 由於頂點目前不存在而傳回空集合,則 coalesce() 繼續評估與其一起提供的第二個周遊,並在此第二個周遊中,查詢會建立缺少的頂點。

g.V('v-1').fold() .coalesce( unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') )

使用最佳化形式的 coalesce() 進行 upsert

Neptune 可以最佳化 fold().coalesce(unfold(), ...) 慣用語以進行高輸送量更新,但只有在 coalesce() 的這兩個部分都傳回頂點或邊緣,但未傳回其他項目時,此最佳化才有效。如果您嘗試從 coalesce() 的任何部分傳回不同的項目 (例如屬性),則 Neptune 最佳化不會發生。查詢可能會成功,但它的執行效能不會與最佳化的版本一樣好,特別是針對大型資料庫。

因為未最佳化的 upsert 查詢會增加執行時間並減少輸送量,所以值得您使用 Gremlin explain 端點來判斷 upsert 查詢是否已完全最佳化。檢閱 explain 計劃時,請找出哪些行以 WARNING: >>+ not converted into Neptune steps 開頭。例如:

+ not converted into Neptune steps: [FoldStep, CoalesceStep([[UnfoldStep], [AddEdgeSte... WARNING: >> FoldStep << is not supported natively yet

這些警告可協助您識別查詢中阻止其完全最佳化的部分。

有時候不可能完全最佳化查詢。在這些情況下,您應該嘗試在查詢結尾處放置無法最佳化的步驟,從而允許引擎最佳化盡可能多的步驟。此技術用於某些批次 upsert 範例,其中會先執行一組頂點或邊緣的所有最佳化 upsert,然後再將任何其他可能未最佳化的修改套用至相同的頂點或邊緣。

批次 upsert 以改善輸送量

對於高輸送量寫入案例,您可以將 upsert 步驟鏈結在一起,以批次方式 upsert 頂點和邊緣。批次處理可減少 upsert 大量頂點和邊緣的交易負荷。然後,您可以使用多個用戶端平行 upsert 批次請求,進一步改善輸送量。

根據經驗法則,我們建議每個批次請求 upsert 大約 200 筆記錄。記錄是單一頂點或邊緣標籤或屬性。例如,具有單一標籤和 4 個屬性的頂點會建立 5 筆記錄。具有一個標籤和單一屬性的邊緣會建立 2 筆記錄。如果您想要 upsert 頂點批次,每個頂點都有單一標籤和 4 個屬性,您應該從 40 的批次大小開始,因為 200 / (1 + 4) = 40

您可以嘗試批次大小。每個批次 200 筆記錄是一個很好的起點,但理想的批次大小可能會更高或更低,取決於您的工作負載。不過,請注意,Neptune 可能會限制每個請求的 Girmlin 步驟總數。此限制沒有明文記載,但為了安全起見,請嘗試確保您的請求包含不超過 1500 個 Gemlin 步驟。Neptune 可能會拒絕超過 1500 個步驟的大型批次請求。

若要增加輸送量,您可以使用多個用戶端平行 upsert 批次 (請參閱 建立有效率的多執行緒 Gremlin 寫入)。用戶端的數目應該與 Neptune 寫入器執行個體上的工作者執行緒數目相同,通常是伺服器 vCPUs 上的 2 倍。例如,執行個r5.8xlarge體有 32 vCPUs 和 64 個工作者執行緒。對於使用 r5.8xlarge 的高輸送量寫入案例,您將會使用 64 個將批次 upsert 平行寫入 Neptune 的用戶端。

每個用戶端都應提交批次請求,並等待請求完成,然後再提交另一個請求。儘管多個用戶端平行執行,但每個個別的用戶端以都序列方式提交請求。這可確保為伺服器提供穩定的請求串流,這些請求會佔用所有工作者執行緒,而不會大量湧入伺服器端要求佇列 (請參閱 調整 Neptune 資料庫叢集中資料庫執行個體的大小)。

嘗試避免產生多個周遊器的步驟

當一個 Gemlin 步驟執行時,它需要一個傳入的周遊器,並發出一個或多個輸出周遊器。由一個步驟發出的周遊器數目會確定下一個步驟的執行次數。

通常,在執行批次操作時,您想要每個操作 (例如 upsert 頂點 A) 執行一次,以便操作序列看起來像這樣:upsert 頂點 A、接著 upsert 頂點 B,然後 upsert 頂點 C,依此類推。只要一個步驟僅建立或修改一個元素,它就只會發出一個周遊器,而代表下一個操作的步驟只會執行一次。另一方面,如果一個操作建立或修改多個元素,它會發出多個周遊器,這又會導致後續步驟執行多次,每個發出的周遊器一次。這可能會導致資料庫執行不必要的額外工作,並且在某些情況下可能會導致建立不需要的其他頂點、邊緣或屬性值。

一個可能出錯的範例是類似 g.V().addV() 的查詢。這個簡單的查詢會為圖形中找到的每個頂點新增一個頂點,因為 V() 會為圖形中的每個頂點發出一個周遊器,而且其中每個周遊器都會觸發對 addV() 的呼叫。

如需處理可以發出多個周遊器之操作的方法,請參閱 混合 upsert 和插入

Upsert 頂點

您可以使用頂點 ID 來判斷對應頂點是否存在。這是首選的方法,因為 Neptune 針對周圍的高度並發用例優化了 upserts。IDs舉例來說,以下查詢會建立具有給定頂點 ID 的頂點 (如果尚未存在),或重複使用它 (如果存在):

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .id()

請注意,此查詢以 id() 步驟結尾。雖然基於 upsert 頂點的目的並不是絕對必要的,但將 id() 步驟新增至查詢結尾可確保伺服器不會將所有頂點屬性序列化回用戶端,這有助於降低查詢的鎖定成本。

或者,您可以使用頂點屬性來判斷頂點是否存在:

g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .fold() .coalesce(unfold(), addV('Person').property('email', 'person-1@example.org')) .id()

如果可能,請使用您自己的使用者提供的頂點IDs來建立頂點,並使用這些頂點IDs來判斷 upsert 作業期間是否存在頂點。這可以讓 Neptune 優化周圍的. IDs 在高度並行修改案例中,ID 型 upsert 可能明顯比屬性型 upsert 更有效率。

鏈結頂點 upsert

您可以將頂點 upsert 鏈結在一起,以批次方式插入它們:

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()

Upsert 邊緣

您可以使用邊IDs來提升邊線,方法與使用自訂頂點提升頂點的方式相同。IDs同樣地,這是偏好的方法,因為它允許 Neptune 最佳化查詢。例如,以下查詢會根據邊緣 ID 建立邊緣 (如果不存在),或者如果存在,則會重複使用它。如果需要建立新邊緣,則查詢也會使用fromto頂點IDs的。

g.E('e-1') .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2')) .property(id, 'e-1')) .id()

許多應用程式使用自訂頂點IDs,但離開 Neptune 產生邊緣IDs。如果您不知道邊緣的 ID,但您確實知道fromto頂點IDs,則可以使用此公式來提升邊緣:

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() (或者,如果您曾經使用 inE() 來尋找邊緣,則為 outV()),而不是 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()

如果您不知道前面的邊緣或頂點,則可以使用頂點屬性IDs來 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()

與頂點升頻器一樣,最好使用以 ID 為基礎的邊緣上升器,使用邊緣 ID 或fromto頂點IDs,而不是基於屬性的上升器,以便 Neptune 可以完全優化 upsert。

檢查 fromto 頂點是否存在

請注意,建立新邊緣之步驟的建構方式:addE().from().to()。這種建構方式可確保查詢檢查 fromto 頂點是否存在。如果其中一個不存在,則查詢會傳回錯誤,如下所示:

{ "detailedMessage": "Encountered a traverser that does not map to a value for child... "code": "IllegalArgumentException", "requestId": "..." }

如果 fromto 頂點可能不存在,則您應在它們之間 upsert 邊緣之前嘗試 upsert 它們。請參閱 結合頂點和邊緣 upsert

有一個替代的建構方式,可以建立您不應該使用的邊緣:V().addE().to()。如果 from 頂點存在,它只會新增一個邊緣。如果 to 頂點不存在,則查詢會產生錯誤,如先前所述,但是如果 from 頂點不存在,則它會無訊息地無法插入邊緣,而不會產生任何錯誤。例如,如果 from 頂點不存在,則下列 upsert 會完成,而不會 upsert 邊緣:

// 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()

鏈結邊緣 upsert

如果您想要將邊緣 upsert 鏈結在一起以建立批次請求,則必須以頂點查找開始每個 upsert,即使您已經知道邊緣也是如此。IDs

如果您已經知道要 upsert IDs 的邊緣以及fromto頂點IDs的邊緣,則可以使用以下公式:

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 場景是你知道fromto頂點IDs,但不知道你想要 upsert IDs 的邊緣。在此情況下,請使用以下公式:

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()

如果你知道IDs你想要 upsert 的邊緣,但不知道fromto頂點(這是不尋常IDs的),你可以使用這個公式:

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

有時您可能想要 upsert 頂點和連線它們的邊緣。您可以混合此處呈現的批次範例。以下範例會 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()

混合 upsert 和插入

有時您可能想要 upsert 頂點和連線它們的邊緣。您可以混合此處呈現的批次範例。以下範例會 upsert 3 個頂點和 2 個邊緣:

Upsert 通常一次處理一個元素。如果您堅持使用此處呈現的 upsert 模式,則每個 upsert 操作都會發出單一周遊器,這會導致後續操作僅執行一次。

不過,有時您可能想要混合 upsert 與插入。例如,如果您使用邊緣來代表動作或事件的執行個體,則可能會發生這種情況。請求可能會使用 upsert 來確保所有必要的頂點都存在,然後使用插入來新增邊緣。對於這種請求,請注意從每個操作發出的潛在周遊器數目。

考慮以下範例,它混合了 upsert 和插入,以將代表事件的邊緣新增至圖形:

// 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 個FOLLOWED邊緣和 3 個VISITED邊緣。然而,寫入查詢插入 8 個邊緣:2 FOLLOWED 和 6 VISITED。原因是插入 2 個FOLLOWED邊緣的操作會發出 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()

在這裡,我們插入插入FOLLOWED邊緣的操作後的fold()步驟。這會產生單一周遊器,然後導致後續操作僅執行一次。

這種方法的缺點是查詢現在未完全最佳化,因為 fold() 未最佳化。接在 fold() 後面的插入操作現在不會最佳化。

如果您需要使用 fold(),代表後續步驟減少周遊器的數目,請嘗試排序您的操作,以便最便宜的操作佔用查詢的非最佳化部分。

修改現有頂點和邊緣的 Upsert

有時,您想要建立頂點或邊緣 (如果不存在),然後將屬性新增至其中或更新其屬性,而不管它是新的還是現有的頂點或邊緣。

若要新增或修改屬性,請使用 property() 步驟。在 coalesce() 步驟之外使用此步驟。如果您嘗試修改 coalesce() 步驟內現有頂點或邊緣的屬性,Neptune 查詢引擎可能不會最佳化查詢。

以下查詢會在每個 upsert 的頂點上新增或更新計數器屬性。每個 property() 步驟都有單一基數,以確保新值會取代任何現有值,而不是新增至一組現有值。

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()

如果您有適用於所有 upsert 元素的屬性值 (例如 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 屬性的值篩選 upsert 的頂點。然後,查詢會將其 version 小於 3 的任何頂點的 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()