Réalisation d'upserts efficaces avec les étapes Gremlin mergeV() et mergeE() - Amazon Neptune

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Réalisation d'upserts efficaces avec les étapes Gremlin mergeV() et mergeE()

Une insertion conditionnelle (également appelée « upsert ») réutilise un sommet ou une arête qui existe déjà, ou crée l'objet nécessaire dans le cas contraire. Des upserts efficaces peuvent faire une différence significative dans les performances des requêtes Gremlin.

Les upserts vous permettent d'écrire des opérations d'insertion idempotentes : quel que soit le nombre de fois que vous exécutez cette opération, le résultat global est le même. Cela est utile dans les scénarios d'écriture hautement simultanés où les modifications simultanées apportées à la même partie du graphe peuvent forcer une ou plusieurs transactions à revenir en arrière avec une exception ConcurrentModificationException, nécessitant ainsi de nouvelles tentatives.

Par exemple, la requête suivante insère un sommet en utilisant l'élément Map fourni pour essayer d'abord de trouver un sommet avec le T.id "v-1". Si ce sommet est trouvé, il est renvoyé. Dans le cas contraire, un sommet contenant cet id et cette propriété est créé par le biais de la clause onCreate.

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

Exécution d'upserts par lots pour améliorer le débit

Pour les scénarios d'écriture à haut débit, vous pouvez enchaîner les étapes mergeV() et mergeE() pour effectuer l'upsert en bloc des sommets et des arêtes. Le traitement par lots réduit la charge transactionnelle liée à l'insertion par upsert d'un grand nombre de sommets et d'arêtes. Vous pouvez ainsi améliorer davantage le débit en augmentant les demandes d'upserts par lots en parallèle à l'aide de plusieurs clients.

En règle générale, nous recommandons d'insérer par upsert environ 200 enregistrements par demande par lots. Un enregistrement correspond à une étiquette ou propriété individuelle de sommet ou d'arête. Par exemple, un sommet doté d'une seule étiquette et de quatre propriétés génère cinq enregistrements. Une arête dotée d'une étiquette et d'une seule propriété génère deux enregistrements. Si vous souhaitez insérer par upsert des lots de sommets, chacun avec une seule étiquette et quatre propriétés, vous devez commencer par une taille de lot de 40, car 200 / (1 + 4) = 40.

Vous pouvez tester différentes tailles de lots. 200 enregistrements par lot constituent un bon point de départ, mais la taille de lot idéale peut être supérieure ou inférieure en fonction de votre charge de travail. Notez toutefois que Neptune peut limiter le nombre total d'étapes Gremlin par demande. Cette limite n'est pas documentée, mais par mesure de sécurité, essayez de faire en sorte que les demandes ne contiennent pas plus de 1 500 étapes Gremlin. Neptune peut rejeter des demandes en bloc volumineuses comportant plus de 1 500 étapes.

Pour augmenter le débit, vous pouvez insérer par upsert des lots en parallèle à l'aide de plusieurs clients (voir Création d'écritures Gremlin multithreads efficaces). Le nombre de clients doit être identique au nombre de threads de travail de l'instance d'enregistreur Neptune, qui correspond généralement au nombre de vCPU sur le serveur, multiplié par deux. Par exemple, une instance r5.8xlarge possède 32 vCPU et 64 threads de travail. Pour les scénarios d'écriture à haut débit utilisant une instance r5.8xlarge, vous devez utiliser 64 clients écrivant des upserts par lots sur Neptune en parallèle.

Chaque client doit soumettre une demande par lots et attendre qu'elle soit terminée avant de soumettre une autre demande. Bien que les différents clients fonctionnent en parallèle, chacun d'eux soumet des demandes en série. Cela garantit que le serveur reçoit un flux constant de demandes qui occupent tous les threads de travail sans encombrer la file d'attente des demandes côté serveur (voir Dimensionnement des instances de base de données dans un cluster de bases de données Neptune).

Essayer d'éviter les étapes qui génèrent plusieurs traverseurs

Lorsqu'une étape Gremlin s'exécute, elle utilise un traverseur entrant et émet un ou plusieurs traverseurs de sortie. Le nombre de traverseurs émis par une étape détermine le nombre de fois que l'étape suivante sera exécutée.

Généralement, lorsque vous effectuez des opérations par lots, vous souhaitez que chaque opération, telle que l'upsert du sommet A, soit exécutée une seule fois, de sorte que la séquence des opérations ressemble à ceci : upsert du sommet A, puis upsert du sommet B, puis upsert du sommet C, etc. Tant qu'une étape ne crée ou ne modifie qu'un seul élément, elle n'émet qu'un seul traverseur, et les étapes représentant l'opération suivante ne sont exécutées qu'une seule fois. Si, en revanche, une opération crée ou modifie plusieurs éléments, elle émet plusieurs traverseurs, ce qui entraîne l'exécution des étapes suivantes plusieurs fois, une fois par traverseur émis. Cela peut obliger la base de données à effectuer des tâches supplémentaires inutiles et, dans certains cas, à créer des sommets, des arêtes ou des valeurs de propriétés supplémentaires superflus.

La requête g.V().addV() est un bon exemple de cas où la situation peut dégénérer. Cette requête simple ajoute un sommet pour chaque sommet du graphe, car V() émet un traverseur pour chaque sommet du graphe et chacun de ces traverseurs déclenche un appel à addV().

Consultez Combinaison d'upserts et d'insertions pour découvrir comment gérer les opérations qui peuvent émettre plusieurs traverseurs.

Insertion de sommets par upsert

L'étape mergeV() est spécialement conçue pour l'upsert de sommets. Elle utilise comme argument un objet Map qui représente les éléments correspondant aux sommets existants dans le graphe. Si aucun élément n'est trouvé, elle utilise cet objet Map pour créer un sommet. Cette étape vous permet également de modifier le comportement en cas de création ou de correspondance. Le modulateur option() peut alors être associé à des jetons Merge.onCreate et Merge.onMatch pour contrôler ces comportements respectifs. Consultez la documentation de TinkerPop référence pour plus d'informations sur l'utilisation de cette étape.

Vous pouvez utiliser un ID de sommet pour déterminer si un sommet spécifique existe. Il s'agit de l'approche préférée, car Neptune optimise les upserts pour les cas d'utilisation hautement simultanés liés aux ID. Par exemple, la requête suivante crée un sommet avec un ID de sommet donné s'il n'existe pas déjà, ou le réutilise s'il existe déjà :

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

Notez que cette requête se termine par une étape id(). Bien que cela ne soit pas strictement nécessaire pour réaliser l'upsert du sommet, une étape id() à la fin d'une requête d'upsert garantit que le serveur ne sérialise pas toutes les propriétés du sommet vers le client, ce qui contribue à réduire le coût de verrouillage de la requête.

Vous pouvez également utiliser une propriété de sommet pour identifier un sommet :

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

Si possible, utilisez vos propres ID fournis par l'utilisateur pour créer des sommets, et servez-vous de ces ID pour déterminer si un sommet existe lors d'une opération d'upsert. Cela permet à Neptune d'optimiser les upserts. Un upsert basé sur un ID peut être nettement plus efficace qu'un upsert basé sur des propriétés lorsque des modifications simultanées sont courantes.

Enchaînement d'upserts de sommets

Vous pouvez enchaîner des upserts de sommets pour les insérer dans un lot :

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

Vous pouvez également utiliser cette syntaxe mergeV() :

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'])

Cependant, comme cette forme de requête inclut des éléments dans les critères de recherche qui sont superflus par rapport à la recherche de base par id, elle n'est pas aussi efficace que la requête précédente.

Exécution d'upserts d'arêtes

L'étape mergeE() est spécialement conçue pour l'upsert d'arêtes. Elle utilise comme argument un objet Map qui représente les éléments correspondant aux arêtes existantes dans le graphe. Si aucun élément n'est trouvé, elle utilise cet objet Map pour créer une arête. Cette étape vous permet également de modifier le comportement en cas de création ou de correspondance. Le modulateur option() peut alors être associé à des jetons Merge.onCreate et Merge.onMatch pour contrôler ces comportements respectifs. Consultez la documentation de TinkerPop référence pour plus d'informations sur l'utilisation de cette étape.

Vous pouvez utiliser les ID d'arêtes pour insérer des arêtes par upsert tout comme vous exécutez des upserts de sommets à l'aide d'ID de sommet personnalisés. Là aussi, il s'agit de l'approche préférée, car elle permet à Neptune d'optimiser la requête. Par exemple, la requête suivante crée une arête en fonction de son ID d'arête si elle n'existe pas déjà, ou la réutilise si elle existe déjà. Cette requête utilise également les ID des sommets Direction.from et Direction.to si elle doit créer une arête :

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

Notez que cette requête se termine par une étape id(). Bien que cela ne soit pas strictement nécessaire pour réaliser l'upsert de l'arête, une étape id() à la fin d'une requête d'upsert garantit que le serveur ne sérialise pas toutes les propriétés de l'arête vers le client, ce qui contribue à réduire le coût de verrouillage de la requête.

De nombreuses applications utilisent des ID de sommet personnalisés, mais laissent à Neptune le soin de générer les ID d'arête. Si vous ne connaissez pas l'ID d'une arête, mais que vous connaissez les ID de sommet from et to, vous pouvez utiliser ce type de requête pour réaliser l'upsert d'une arête :

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

Tous les sommets référencés par mergeE() doivent exister pour que l'étape crée l'arête.

Enchaînement d'upserts d'arêtes

Comme pour les upserts de sommets, il est simple d'enchaîner les étapes mergeE() pour les demandes en bloc :

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

Combinaison d'upserts de sommets et d'arêtes

Parfois, il peut être utile d'insérer par upsert à la fois les sommets et les arêtes qui les relient. Vous pouvez combiner les exemples de lots présentés ici. L'exemple suivant insère par upsert trois sommets et deux arêtes :

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

Combinaison d'upserts et d'insertions

Parfois, il peut être utile d'insérer par upsert à la fois les sommets et les arêtes qui les relient. Vous pouvez combiner les exemples de lots présentés ici. L'exemple suivant insère par upsert trois sommets et deux arêtes :

Les upserts traitent généralement un élément à la fois. Si vous vous en tenez aux modèles d'upsert présentés ici, chaque opération d'upsert émet un seul traverseur, ce qui entraîne l'exécution de l'opération suivante une seule fois.

Cependant, il peut arriver que vous souhaitiez combiner des upserts avec des insertions. Cela peut notamment être le cas si vous utilisez des arêtes pour représenter des instances d'actions ou d'événements. Une demande peut utiliser des upserts pour s'assurer que tous les sommets nécessaires existent, puis utiliser des insertions pour ajouter des arêtes. Avec les demandes de ce type, soyez attentif au nombre potentiel de traverseurs émis par chaque opération.

Prenons l'exemple suivant, qui combine des upserts et des insertions pour ajouter des arêtes représentant des événements dans le graphe :

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

La requête doit insérer cinq arêtes : deux arêtes SUIVIES et trois arêtes VISITÉES. Cependant, la requête telle qu'elle est écrite insère huit arêtes : deux arêtes SUIVIES et six arêtes VISITÉES. Cela est dû au fait que l'opération qui insère les deux arêtes suivies émet deux traverseurs, ce qui entraîne l'exécution de l'opération suivante d'insertion de trois arêtes deux fois.

La solution consiste à ajouter une étape fold() après chaque opération susceptible d'émettre plusieurs traverseurs :

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

Nous avons inséré ici une étape fold() après l'opération qui insère les arêtes SUIVIES. Il en résulte un seul traverseur, et l'opération suivante n'est donc exécutée qu'une seule fois.

L'inconvénient de cette approche est que la requête n'est plus entièrement optimisée, car fold() n'est pas optimisé. L'opération d'insertion qui suit fold() ne sera maintenant pas optimisée non plus.

Si vous devez utiliser fold() pour réduire le nombre de traverseurs lors des étapes suivantes, essayez d'organiser les opérations de manière à ce que les moins coûteuses occupent la partie non optimisée de la requête.