Effiziente Upserts mit mergeV()- und mergeE()-Schritten in Gremlin - Amazon Neptune

Die vorliegende Übersetzung wurde maschinell erstellt. Im Falle eines Konflikts oder eines Widerspruchs zwischen dieser übersetzten Fassung und der englischen Fassung (einschließlich infolge von Verzögerungen bei der Übersetzung) ist die englische Fassung maßgeblich.

Effiziente Upserts mit mergeV()- und mergeE()-Schritten in Gremlin

Ein Upsert (bzw. eine bedingte Einfügung) verwendet Eckpunkte oder Kanten, wenn bereits vorhanden, oder erstellt sie andernfalls. Effiziente Upserts können die Leistung von Gremlin-Abfragen deutlich verbessern.

Mit Upserts können Sie idempotente Einfügeoperationen schreiben: Das Ergebnis bleibt gleich, unabhängig davon, wie häufig eine Operation ausgeführt wird. Dies ist in hoch gleichzeitigen Schreibszenarien nützlich, in denen gleichzeitige Änderungen für denselben Teil eines Diagramms zumden Rollback einer oder mehrerer Transaktionen mit einer ConcurrentModificationException erzwingen können, was Wiederholungen erforderlich macht.

Die folgende Abfrage führt beispielsweise einen Upsert für einen Eckpunkt mittels der bereitgestellten Map aus, um zunächst zu versuchen, einen Eckpunkt mit der T.id "v-1" zu finden. Wenn dieser Eckpunkt gefunden wird, wird er zurückgegeben. Wenn er nicht gefunden wird, wird durch die onCreate-Klausel ein Eckpunkt mit dieser id und der Eigenschaft erstellt.

g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org'])

Batching von Upserts zur Verbesserung des Durchsatzes

In Schreibszenarien mit einem hohen Durchsatz können Sie mergeV()- und mergeE()-Schritte verketten, um Upserts für Eckpunkte und Kanten batchweise auszuführen. Batching reduziert den Transaktionsaufwand, der entsteht, wenn für eine große Anzahl von Eckpunkten und Kanten Upserts ausgeführt werden. Sie können den Durchsatz weiter verbessern, indem Sie Batch-Anforderungen parallel über mehrere Clients ausführen.

Als Faustregel gilt, dass pro Batch-Anforderung Upserts für ungefähr 200 Datensätze ausgeführt werden sollten. Ein Datensatz ist eine einzelne Eckpunkt- oder Kantenbezeichnung oder -eigenschaft. Ein Eckpunkt mit einer einzelnen Bezeichnung und 4 Eigenschaften generiert beispielsweise 5 Datensätze. Eine Kante mit einer Bezeichnung und einer einzelnen Eigenschaft generiert 2 Datensätze. Wenn Sie Upserts für Batches von Eckpunkten ausführen möchten, die jeweils eine einzelne Bezeichnung und 4 Eigenschaften besitzen, sollten Sie mit einer Batchgröße von 40 beginnen, da 200 / (1 + 4) = 40.

Sie können mit der Batchgröße experimentieren. 200 Datensätze pro Batch sind ein guter Ausgangspunkt. Die ideale Batchgröße kann je nach Workload jedoch höher oder niedriger sein. Beachten Sie jedoch, dass Neptune die Gesamtzahl der Gremlin-Schritte pro Anforderung einschränken kann. Diese Einschränkung ist nicht dokumentiert. Um jedoch auf der sicheren Seite zu sein, sollten Ihre Anforderungen nicht mehr als 1 500 Gremlin-Schritte enthalten. Neptune lehnt große Batchanforderungen mit mehr als 1 500 Schritten möglicherweise ab.

Um den Durchsatz zu erhöhen, können Sie Upserts für Batches parallel über mehrere Clients ausführen (siehe Erstellen von effizienten Multi-Thread-Gremlin-Schreibvorgängen). Die Anzahl der Clients sollte der Anzahl der Worker-Threads auf Ihrer Neptune-Writer-Instance entsprechen, die normalerweise dem 2-fachen der Anzahl vCPUs auf dem Server entspricht. Eine r5.8xlarge Instance hat beispielsweise 32 vCPUs und 64 Worker-Threads. Für Schreibszenarien mit einem hohen Durchsatz über eine r5.8xlarge würden Sie daher 64 Clients verwenden, die Batch-Upserts parallel in Neptune schreiben.

Jeder Client sollte eine Batch-Anforderung einreichen und mit der Einreichung einer weiteren Anforderung bis zum Abschluss der Anforderung warten. Obwohl diese mehreren Clients parallel ausgeführt werden, senden die einzelnen Clients die Anforderungen seriell. Dies stellt sicher, dass der Server kontinuierlich Anforderungen erhält, die alle Worker-Threads auslasten, ohne dass die serverseitige Anforderungswarteschlange überlastet wird (siehe Dimensionieren von DB-Instances in einem Neptune-DB-Cluster).

Vermeiden von Schritten, die mehrere Traverser generieren

Wenn ein Gremlin-Step ausgeführt wird, nimmt er einen eingehenden Traverser auf und gibt einen oder mehrere Ausgabe-Traverser aus. Die Anzahl der von einem Schritt ausgegebenen Traverser bestimmt, wie häufig der nächste Schritt ausgeführt wird.

In der Regel soll jede Batchoperation, z. B. ein Upsert von Eckpunkt A, nur einmal ausgeführt werden, sodass die Reihenfolge der Operationen wie folgt aussieht: Upsert von Eckpunkt A, dann Upsert von Eckpunkt B, dann Upsert von Eckpunkt C und so weiter. Solange ein Schritt nur ein Element erstellt oder ändert, gibt er nur einen Traverser aus, und die Schritte, die die nächste Operation darstellen, werden nur einmal ausgeführt. Wenn eine Operation hingegen mehr als ein Element erstellt oder ändert, gibt sie mehrere Traverser aus, sodass die nachfolgenden Schritte mehrfach ausgeführt werden, jeweils einmal pro ausgegebenem Traverser. Dies kann dazu führen, dass die Datenbank unnötige zusätzliche Arbeit leistet. In einigen Fällen kann es zur Erstellung unerwünschter zusätzlicher Eckpunkte, Kanten oder Eigenschaftswerte kommen.

Ein Beispiel für Fehler dieser Art ist eine Abfrage wie g.V().addV(). Diese einfache Abfrage fügt für jeden im Diagramm gefundenen Eckpunkt einen Eckpunkt hinzu, da V() für jeden Eckpunkt im Diagramm einen Traverser ausgibt und jeder Traverser den Aufruf von addV() auslöst.

Informationen zur Behandlung von Operationen, die mehrere Traverser auslösen können, finden Sie unter Kombinieren von Upserts und Einfügungen.

Upserts für Eckpunkte

Der mergeV()-Schritt ist speziell für Upserts von Eckpunkten vorgesehen. Er verwendet Map als Argument. Dieses Argument repräsentiert Elemente, die mit vorhandenen Eckpunkten im Diagramm abgeglichen werden sollen. Wenn ein Element nicht gefunden wird, wird diese Map zur Erstellung eines neuen Eckpunkts verwendet. Mit diesem Schritt können Sie auch das Verhalten bei einer Erstellung oder Übereinstimmung ändern, da der Modulator option() zusammen mit den Token Merge.onCreate und Merge.onMatch zur Steuerung des Verhaltens verwendet werden kann. Weitere Informationen zur Verwendung dieses Schritts finden Sie in der TinkerPop Referenzdokumentation.

Sie können eine Eckpunkt-ID verwenden, um festzustellen, ob ein bestimmter Eckpunkt vorhanden ist. Dies ist der bevorzugte Ansatz, da Neptune Upserts für stark parallele Anwendungsfälle optimiert. IDs Die folgende Abfrage erstellt beispielsweise einen Eckpunkt mit einer bestimmten Eckpunkt-ID, wenn noch nicht vorhanden, oder verwendet ihn erneut, wenn vorhanden:

g.mergeV([(T.id): 'v-1']). option(onCreate, [(T.label): 'PERSON', email: 'person-1@example.org', age: 21]). option(onMatch, [age: 22]). id()

Beachten Sie, dass diese Abfrage mit dem id()-Schritt endet. Der id()-Schritt am Ende einer Upsert-Abfrage ist nicht unbedingt erforderlich, stellt jedoch sicher, dass der Server nicht alle Eckpunkteigenschaften zurück zum Client serialisiert, was den Sperraufwand der Abfrage reduziert.

Alternativ können Sie zur Identifizierung eines Eckpunkts auch eine Eckpunkteigenschaft verwenden:

g.mergeV([email: 'person-1@example.org']). option(onCreate, [(T.label): 'PERSON', age: 21]). option(onMatch, [age: 22]). id()

Verwenden Sie IDs nach Möglichkeit Ihre eigenen, vom Benutzer bereitgestellten Scheitelpunkte und bestimmen Sie anhand dieserIDs, ob während eines Upsert-Vorgangs ein Scheitelpunkt vorhanden ist. So kann Neptune die Upserts optimieren. ID-basierte Upserts können deutlich effizienter als eigenschaftsbasierte Upserts sein, wenn gleichzeitige Änderungen häufig sind.

Verketten von Eckpunkt-Upserts

Sie können Eckpunkt-Upserts verketten, um sie in ein Batch einzufügen:

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

Alternativ können Sie auch diese mergeV()-Syntax verwenden:

g.mergeV([(T.id): 'v-1', (T.label): 'PERSON', email: 'person-1@example.org']). mergeV([(T.id): 'v-2', (T.label): 'PERSON', email: 'person-2@example.org']). mergeV([(T.id): 'v-3', (T.label): 'PERSON', email: 'person-3@example.org'])

Da dieses Abfrageformat jedoch Elemente in den Suchkriterien enthält, die für die einfache Suche nach id nicht benötigt werden, ist sie nicht so effizient wie die vorherige Abfrage.

Upserts für Kanten

Der mergeE()-Schritt ist speziell für Upserts von Kanten vorgesehen. Er verwendet Map als Argument. Dieses Argument repräsentiert Elemente, die mit vorhandenen Kanten im Diagramm abgeglichen werden sollen. Wenn ein Element nicht gefunden wird, wird diese Map zur Erstellung einer neuen Kante verwendet. Mit diesem Schritt können Sie auch das Verhalten bei einer Erstellung oder Übereinstimmung ändern, da der Modulator option() zusammen mit den Token Merge.onCreate und Merge.onMatch zur Steuerung des Verhaltens verwendet werden kann. Weitere Informationen zur Verwendung dieses Schritts finden Sie in der TinkerPop Referenzdokumentation.

Sie können Kanten IDs auf die gleiche Weise wie Scheitelpunkte mithilfe eines benutzerdefinierten Scheitelpunkts nach oben verschieben. IDs Dies ist auch hier der bevorzugte Ansatz, da Neptune so die Abfrage optimieren kann. Die folgende Abfrage erstellt beispielsweise eine Kante auf Grundlage der Kanten-ID, wenn noch nicht vorhanden, oder verwendet sie erneut, wenn vorhanden. Die Abfrage verwendet auch die IDs Direction.to Eckpunkte Direction.from und, wenn eine neue Kante erstellt werden muss:

g.mergeE([(T.id): 'e-1']). option(onCreate, [(from): 'v-1', (to): 'v-2', weight: 1.0]). option(onMatch, [weight: 0.5]). id()

Beachten Sie, dass diese Abfrage mit dem id()-Schritt endet. Der id()-Schritt am Ende der Upsert-Abfrage für die Kante ist nicht unbedingt erforderlich, stellt jedoch sicher, dass der Server nicht alle Kanteneigenschaften zurück zum Client serialisiert, was den Sperraufwand der Abfrage reduziert.

Viele Anwendungen verwenden benutzerdefinierte ScheitelpunkteIDs, überlassen es aber Neptune, um Kanten zu erzeugen. IDs Wenn Sie die ID einer Kante nicht kennen, aber Sie kennen den from und den to ScheitelpunktIDs, können Sie diese Art von Abfrage verwenden, um eine Kante zu verändern:

g.mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). id()

Alle Eckpunkte, auf die mergeE() verweist, müssen vorhanden sein, damit der Schritt die Kante erstellen kann.

Verketten von Kanten-Upserts

Wie bei Eckpunkt-Upserts können auch hier mergeE()-Schritte für Batch-Anforderungen verkettet werden:

g.mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). mergeE([(from): 'v-2', (to): 'v-3', (T.label): 'KNOWS']). mergeE([(from): 'v-3', (to): 'v-4', (T.label): 'KNOWS']). id()

Kombinieren von Eckpunkt- und Kanten-Upserts

Manchmal möchten Sie vielleicht Upserts für Eckpunkte und die Kanten ausführen, die sie verbinden. Sie können die hier gezeigten Batch-Beispiele kombinieren. Das folgende Beispiel zeigt ein Upsert für 3 Eckpunkte und 2 Kanten:

g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org']). mergeV([(id):'v-2']). option(onCreate, [(label): 'PERSON', 'email': 'person-2@example.org']). mergeV([(id):'v-3']). option(onCreate, [(label): 'PERSON', 'email': 'person-3@example.org']). mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). mergeE([(from): 'v-2', (to): 'v-3', (T.label): 'KNOWS']). id()

Kombinieren von Upserts und Einfügungen

Manchmal möchten Sie vielleicht Upserts für Eckpunkte und die Kanten ausführen, die sie verbinden. Sie können die hier gezeigten Batch-Beispiele kombinieren. Das folgende Beispiel zeigt ein Upsert für 3 Eckpunkte und 2 Kanten:

In der Regel werden Upserts Element für Element ausgeführt. Wenn Sie die hier gezeigten Upsert-Muster befolgen, gibt jede Upsert-Operation einen einzelnen Traverser aus, sodass die nachfolgende Operation jeweils nur einmal ausgeführt wird.

Manchmal möchten Sie jedoch vielleicht Upserts und Einfügungen kombinieren. Dies kann beispielsweise der Fall sein, wenn Sie Kanten verwenden, um Instances von Aktionen oder Ereignissen darzustellen. Möglicherweise verwendet eine Anforderung Upserts, um sicherzustellen, dass alle notwendigen Eckpunkte vorhanden sind, und dann Einfügungen, um Kanten hinzuzufügen. Achten Sie bei Anforderungen dieser Art auf die potenzielle Anzahl von Traversern, die bei jeder Operation ausgegeben werden.

Im folgenden Beispiel werden Upserts und Einfügungen kombiniert, um dem Diagramm Kanten hinzuzufügen, die Ereignisse repräsentieren:

// Fully optimized, but inserts too many edges g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org']). mergeV([(id):'v-2']). option(onCreate, [(label): 'PERSON', 'email': 'person-2@example.org']). mergeV([(id):'v-3']). option(onCreate, [(label): 'PERSON', 'email': 'person-3@example.org']). mergeV([(T.id): 'c-1', (T.label): 'CITY', 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()

Die Abfrage sollte 5 Kanten einfügen: 2 FOLLOWED Kanten und 3 VISITED Kanten. Die geschriebene Abfrage fügt jedoch 8 Kanten ein: 2 FOLLOWED und VISITED 6. Der Grund dafür ist, dass bei der Operation, bei der die beiden FOLLOWED Kanten eingefügt werden, 2 Traverser ausgegeben werden, wodurch die nachfolgende Einfügeoperation, bei der 3 Kanten eingefügt werden, zweimal ausgeführt wird.

Die Lösung besteht darin, nach jeder Operation, die mehr als einen Traverser ausgeben könnte, einen fold()-Schritt hinzuzufügen:

g.mergeV([(T.id): 'v-1', (T.label): 'PERSON', email: 'person-1@example.org']). mergeV([(T.id): 'v-2', (T.label): 'PERSON', email: 'person-2@example.org']). mergeV([(T.id): 'v-3', (T.label): 'PERSON', email: 'person-3@example.org']). mergeV([(T.id): 'c-1', (T.label): 'CITY', 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()

Hier haben wir nach der Operation, die Kanten einfügt, einen fold() Schritt eingefügt. FOLLOWED Das Ergebnis ist ein einzelner Traverser, sodass die nachfolgende Operation nur einmal ausgeführt wird.

Dieser Ansatz hat den Nachteil, dass die Abfrage jetzt nicht vollständig optimiert ist, da fold() nicht optimiert ist. Die Einfügeoperation nach fold() wird jetzt ebenfalls nicht optimiert.

Wenn Sie die fold() verwenden müssen, um die Zahl der Traverser für nachfolgende Schritte zu reduzieren, ordnen Sie die Operationen so an, dass die kostengünstigsten Operationen den nicht optimierten Teil der Abfrage einnehmen.

Kardinalität einstellen

Die Standardkardinalität für Scheitelpunkteigenschaften in Neptune ist festgelegt, was bedeutet, dass bei Verwendung von mergeV () allen in der Map angegebenen Werten diese Kardinalität zugewiesen wird. Um die einfache Kardinalität zu verwenden, müssen Sie ihre Verwendung explizit angeben. Ab TinkerPop Version 3.7.0 gibt es eine neue Syntax, die es ermöglicht, die Kardinalität als Teil der Map anzugeben, wie im folgenden Beispiel gezeigt:

g.mergeV([(T.id): 1234]). option(onMatch, ['age': single(20), 'name': single('alice'), 'city': set('miami')])

Alternativ können Sie die Kardinalität dafür wie folgt als Standard festlegen: option

// age and name are set to single cardinality by default g.mergeV([(T.id): 1234]). option(onMatch, ['age': 22, 'name': 'alice', 'city': set('boston')], single)

mergeV()Vor Version 3.7.0 gibt es weniger Optionen zum Einstellen der Kardinalität. Der allgemeine Ansatz besteht darin, auf den folgenden property() Schritt zurückzugreifen:

g.mergeV([(T.id): '1234']). option(onMatch, sideEffect(property(single,'age', 20). property(set,'city','miami')).constant([:]))
Anmerkung

Dieser Ansatz funktioniert nur, mergeV() wenn er mit einem Startschritt verwendet wird. Sie könnten daher keine Kette mergeV() innerhalb einer einzigen Traversierung erstellen, da der erste Schritt mergeV() nach dem Startschritt, der diese Syntax verwendet, einen Fehler erzeugt, falls es sich bei dem eingehenden Traverser um ein Graphelement handelt. In diesem Fall sollten Sie Ihre mergeV() Aufrufe in mehrere Anfragen aufteilen, von denen jede ein Startschritt sein kann.