Realización de actualizaciones o inserciones de Gremlin eficientes con fold()/coalesce()/unfold() - Amazon Neptune

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Realización de actualizaciones o inserciones de Gremlin eficientes con fold()/coalesce()/unfold()

Una actualización o inserción (o inserción condicional) reutiliza un vértice o un borde si ya existe, o lo crea si no existe. Las actualizaciones o inserciones eficientes pueden marcar una diferencia significativa en el rendimiento de las consultas de Gremlin.

En esta página se muestra cómo usar el patrón de Gremlin fold()/coalesce()/unfold() para crear actualizaciones o inserciones eficientes. Sin embargo, con el lanzamiento de la TinkerPop versión 3.6.x introducido en Neptune en la versión 1.2.1.0 del motor, en la mayoría de los casos son preferibles lo nuevo mergeV() y los mergeE() escalones. El patrón fold()/coalesce()/unfold() descrito aquí podría seguir siendo útil en algunas situaciones complejas, pero en general, utilice mergeV() y mergeE() si es posible, como se describe en Realización de actualizaciones o inserciones eficientes con los pasos mergeV() y mergeE() de Gremlin.

Las actualizaciones o inserciones permiten escribir operaciones de inserción de idempotencia; independientemente del número de veces que se ejecute una operación de este tipo, el resultado general es el mismo. Esto resulta útil en situaciones de escritura muy simultáneas, en las que las modificaciones simultáneas en la misma parte del gráfico pueden obligar a una o más transacciones a revertirse con una ConcurrentModificationException, por lo que es necesario volver a intentarlas.

Por ejemplo, la siguiente consulta realiza actualizaciones o inserciones en un vértice. Para ello, busca primero el vértice especificado en el conjunto de datos y, a continuación, pliega los resultados en una lista. En el primer recorrido proporcionado al paso coalesce(), la consulta despliega esta lista. Si la lista desplegada no está vacía, los resultados se emiten desde coalesce(). Sin embargo, si unfold() devuelve una colección vacía porque el vértice no existe actualmente, coalesce() pasa a evaluar el segundo recorrido con el que se ha proporcionado y, en este segundo recorrido , la consulta crea el vértice que falta.

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

Utilice una forma optimizada de coalesce() para las actualizaciones o inserciones

Neptune puede optimizar la expresión fold().coalesce(unfold(), ...) para realizar actualizaciones de alto rendimiento, pero esta optimización solo funciona si ambas partes de coalesce() devuelven un vértice o un borde, pero nada más. Si intenta devolver algo diferente, como una propiedad, desde cualquier parte del coalesce(), no se produce la optimización de Neptune. Es posible que la consulta se realice correctamente, pero no funcionará tan bien como en una versión optimizada, especialmente en conjuntos de datos de gran tamaño.

Dado que las consultas de actualización o inserción no optimizadas aumentan los tiempos de ejecución y reducen el rendimiento, vale la pena utilizar el punto de conexión explain de Gremlin para determinar si una consulta de actualización o inserción está totalmente optimizada. Al revisar los planes explain, busque líneas que comiencen por + not converted into Neptune steps y WARNING: >>. Por ejemplo:

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

Estas advertencias pueden ayudarle a identificar las partes de una consulta que impiden que se optimice por completo.

A veces, no es posible optimizar una consulta por completo. En estas situaciones, debe intentar colocar los pasos que no se pueden optimizar al final de la consulta, lo que permitirá que el motor optimice tantos pasos como sea posible. Esta técnica se utiliza en algunos de los ejemplos de actualizaciones o inserciones por lotes, en los que todas las actualizaciones o inserciones optimizadas para un conjunto de vértices o bordes se realizan antes de aplicar cualquier modificación adicional, potencialmente no optimizada, a los mismos vértices o bordes.

Realización de actualizaciones o inserciones por lotes para mejorar el rendimiento

En escenarios de escritura de alto rendimiento, puede encadenar pasos de actualizaciones o inserciones para realizar actualizaciones o inserciones en vértices y bordes por lotes. El procesamiento por lotes reduce la sobrecarga transaccional que supone realizar actualizaciones o inserciones en un gran número de vértices y bordes. A continuación, puede mejorar aún más el rendimiento realizando actualizaciones o inserciones en solicitudes por lotes en paralelo con varios clientes.

Como regla general, recomendamos modificar aproximadamente 200 registros por solicitud en lote. Un registro es una propiedad o etiqueta de un solo vértice o borde. Un vértice con una sola etiqueta y 4 propiedades, por ejemplo, crea 5 registros. Un borde con una etiqueta y una sola propiedad crea 2 registros. Si desea realizar actualizaciones o inserciones en lotes de vértices, cada uno con una sola etiqueta y 4 propiedades, debería empezar con un tamaño de lote de 40, ya que 200 / (1 + 4) = 40.

Puede experimentar con el tamaño del lote. Un buen punto de partida son 200 registros por lote, pero el tamaño de lote ideal puede ser mayor o menor en función de la carga de trabajo. Sin embargo, tenga en cuenta que Neptune puede limitar el número total de pasos de Gremlin por solicitud. Este límite no está documentado, pero por si acaso, intente asegurarse de que sus solicitudes no contengan más de 1500 pasos de Gremlin. Neptune puede rechazar solicitudes por lotes grandes con más de 1500 pasos.

Para aumentar el rendimiento, puede realizar actualizaciones o inserciones en los lotes en paralelo utilizando varios clientes (consulte Creación de escrituras de Gremlin eficientes de múltiples subprocesos). El número de clientes debe ser el mismo que el número de subprocesos de trabajo de la instancia de Neptune Writer, que normalmente es 2 veces el número de subprocesos de trabajo del vCPUs servidor. Por ejemplo, una r5.8xlarge instancia tiene 32 vCPUs y 64 subprocesos de trabajo. En escenarios de escritura de alto rendimiento en los que se usa un r5.8xlarge, utilizaría 64 clientes que escriben actualizaciones o inserciones por lotes en Neptune en paralelo.

Cada cliente debe enviar una solicitud por lotes y esperar a que se complete antes de enviar otra. Aunque los múltiples clientes se ejecutan en paralelo, cada cliente individual envía las solicitudes en serie. Esto garantiza que el servidor reciba un flujo constante de solicitudes que ocupen todos los subprocesos de trabajadores sin saturar la cola de solicitudes del lado del servidor (consulte Dimensionamiento de las instancias de base de datos en un clúster de base de datos de Neptune).

Intente evitar pasos que generen múltiples recorridos

Cuando se ejecuta un paso de Gremlin, toma un recorrido entrante y emite uno o más recorridos de salida. El número de entrante que emite un paso determina el número de veces que se ejecuta el siguiente paso.

Por lo general, cuando se realizan operaciones por lotes, se desea que cada operación, como el vértice de actualización o inserción A, se ejecute una vez, de modo que la secuencia de operaciones tenga el siguiente aspecto: vértice de actualización o inserción A, vértice de actualización o inserción B, vértice de actualización o inserción C, etc. Mientras que un paso cree o modifique solo un elemento, solo emite un recorrido y los pasos que representan la siguiente operación se ejecutan solo una vez. Si, por otro lado, una operación crea o modifica más de un elemento, emite varios recorridos, lo que a su vez provoca que los pasos siguientes se ejecuten varias veces, una vez por recorrido emitido. Esto puede provocar que la base de datos realice un trabajo adicional innecesario y, en algunos casos, que se creen vértices, bordes o valores de propiedad adicionales no deseados.

Un ejemplo de cómo esto puede ir mal es con una consulta como g.V().addV(). Esta sencilla consulta añade un vértice por cada vértice que se encuentre en el gráfico, ya que V() emite un recorrido para cada vértice del gráfico y cada uno de esos recorridos activa una llamada a addV().

Consulte en Combinación de actualizaciones o inserciones e inserciones las formas de gestionar las operaciones que pueden emitir varios recorridos.

Actualizaciones o inserciones en vértices

Puede usar un identificador de vértice para determinar si existe un vértice correspondiente. Este es el enfoque preferido, ya que Neptune optimiza los upserts para casos de uso altamente concurrentes. IDs Por ejemplo, la siguiente consulta crea un vértice con un identificador de vértice determinado si aún no existe, o lo reutiliza si ya existe:

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

Tenga en cuenta que esta consulta termina con un paso id(). Si bien no es estrictamente necesario para realizar una actualización o inserción en el vértice, añadir un paso id() hasta el final de una consulta de actualización o inserción garantiza que el servidor no vuelva a serializar todas las propiedades del vértice para el cliente, lo que ayuda a reducir el costo de bloqueo de la consulta.

También puede utilizar una propiedad de vértice para determinar si el vértice existe:

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

Si es posible, utilice los vértices proporcionados por el usuario IDs para crear vértices y utilícelos para determinar si existe un vértice durante una IDs operación de upsert. Esto permite a Neptune optimizar las perturbaciones alrededor del. IDs Una actualización o inserción basada en identificador puede ser considerablemente más eficiente que una actualización o inserción basada en propiedades en escenarios con una gran simultaneidad en las modificaciones.

Encadenamiento de actualizaciones o inserciones de vértices

Puede encadenar actualizaciones o inserciones de vértices para insertarlos en un lote:

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

Actualizaciones o inserciones de bordes

Puede usar el borde IDs para alterar los bordes de la misma manera que para desviar los vértices con un vértice personalizado. IDs De nuevo, este es el enfoque preferido porque permite a Neptune optimizar la consulta. Por ejemplo, la siguiente consulta crea un borde en función de su identificador de borde si aún no existe, o lo reutiliza si ya existe. La consulta también utiliza los IDs to vértices from y si necesita crear una arista nueva.

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

Muchas aplicaciones utilizan un vértice personalizadoIDs, pero dejan que Neptune genere el borde. IDs Si no conoce el identificador de una arista, pero sí conoce el to vértice from yIDs, puede utilizar esta formulación para descomponer una arista:

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

Tenga en cuenta que el paso de vértice de la cláusula where() debería ser inV() (o outV() si ha utilizado inE() para buscar el borde), no otherV(). No utilice otherV() aquí o la consulta no se optimizará y el rendimiento se verá afectado. Por ejemplo, Neptune no optimizaría la siguiente consulta:

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

Si no conoce la arista o el vértice de la parte delantera, puede colocarlos IDs en posición vertical utilizando las propiedades del vértice:

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

Al igual que con los upserts de vértices, es preferible utilizar los upserts de borde basados en ID utilizando un ID de borde o from un to vérticeIDs, en lugar de los upserts basados en propiedades, de modo que Neptune pueda optimizar completamente el upsert.

Comprobación de la existencia de vértices from y to

Observe la construcción de los pasos que permiten crear un nuevo borde: addE().from().to(). Esta construcción garantiza que la consulta compruebe la existencia tanto del vértice from como to. Si alguna de estas opciones no existe, la consulta devuelve el siguiente error:

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

Si es posible que el vértice from o to no existan, debería intentar realizar actualizaciones o inserciones en ellos antes de realizar actualizaciones o inserciones en el borde entre ellos. Consulte Combinación de actualizaciones o inserciones de vértices y bordes.

Existe una construcción alternativa para crear un borde que no debería usar: V().addE().to(). Solo añade un borde si existe el vértice from. Si el vértice to no existe, la consulta genera un error, como se ha descrito anteriormente, pero si el vértice from no existe, falla de forma silenciosa al insertar un borde, sin generar ningún error. Por ejemplo, si el vértice from no existe, la siguiente actualización o inserción se completa sin realizar actualizaciones o inserciones en un borde:

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

Encadenamiento de actualizaciones o inserciones de bordes

Si desea encadenar las direcciones verticales de arista para crear una solicitud por lotes, debe comenzar cada posición vertical con una búsqueda de vértices, incluso si ya conoce la arista. IDs

Si ya conoce IDs las aristas que desea intercalar y los to vértices from y, puede IDs utilizar esta formulación:

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

Quizás el escenario más común de rotación de aristas por lotes sea que conozca el to vértice from yIDs, pero no sepa cuáles son las aristas que desea IDs intercalar. En ese caso, utilice la siguiente fórmula:

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

Si sabes cuáles son IDs las aristas que quieres moldear, pero no conoces los IDs to vértices from y (esto es inusual), puedes usar esta fórmula:

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

Combinación de actualizaciones o inserciones de vértices y bordes

A veces, es posible que desee realizar actualizaciones o inserciones en ambos vértices y los bordes que los conectan. Puede combinar los ejemplos de lotes que se presentan aquí. En el siguiente ejemplo, se realizan actualizaciones o inserciones de 3 vértices y 2 bordes:

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

Combinación de actualizaciones o inserciones e inserciones

A veces, es posible que desee realizar actualizaciones o inserciones en ambos vértices y los bordes que los conectan. Puede combinar los ejemplos de lotes que se presentan aquí. En el siguiente ejemplo, se realizan actualizaciones o inserciones de 3 vértices y 2 bordes:

Por lo general, las actualizaciones o inserciones se realizan con un elemento a la vez. Si sigue los patrones de actualización o inserción que se presentan aquí, cada operación de actualización o inserción emite un único recorrido, lo que hace que la siguiente operación se ejecute solo una vez.

Sin embargo, a veces es posible que desee mezclar actualizaciones o inserciones con inserciones. Podría ser así, por ejemplo, si utiliza bordes para representar instancias de acciones o eventos. Una solicitud puede usar actualizaciones o inserciones para garantizar que existan todos los vértices necesarios y, a continuación, usar inserciones para añadir bordes. En el caso de solicitudes de este tipo, preste atención a la cantidad potencial de recorridos que emite cada operación.

Examine el siguiente ejemplo, en el que se combinan actualizaciones o inserciones e inserciones para añadir bordes que representen eventos en el gráfico:

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

La consulta debe insertar 5 aristas: 2 aristas y 3 FOLLOWED aristas. VISITED Sin embargo, la consulta tal como está escrita inserta 8 bordes: 2 FOLLOWED y 6VISITED. Esto se debe a que la operación que inserta las dos FOLLOWED aristas emite dos travesaños, lo que provoca que la siguiente operación de inserción, en la que se insertan 3 aristas, se ejecute dos veces.

La solución consiste en añadir un paso fold() después de cada operación que pueda emitir más de un recorrido:

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

Aquí hemos insertado un fold() paso después de la operación que inserta las aristas. FOLLOWED Esto da como resultado un único recorrido, lo que hace que la siguiente operación se ejecute solo una vez.

La desventaja de este enfoque es que la consulta ahora no se optimiza por completo, porque fold() no se optimiza. La operación de inserción que sigue a fold() no estará optimizada.

Si necesita usar fold() para reducir el número de recorridos para los pasos posteriores, intente ordenar las operaciones de manera que las menos costosas ocupen la parte no optimizada de la consulta.

Actualizaciones o inserciones que modifican vértices y bordes existentes

A veces, desea crear un vértice o un borde si no existe y, a continuación, añadir o actualizar una propiedad, independientemente de si se trata de un vértice o un borde nuevo o existente.

Para añadir o modificar una propiedad, utilice el paso property(). Utilice este paso fuera del paso coalesce(). Si intenta modificar la propiedad de un vértice o borde existente dentro del paso coalesce(), es posible que el motor de consultas de Neptune no optimice la consulta.

La siguiente consulta añade o actualiza una propiedad de contador en cada vértice en el que se ha realizado una actualización o inserción. Cada paso property() tiene una cardinalidad única para garantizar que los nuevos valores sustituyan a los valores existentes, en lugar de añadirlos a un conjunto de valores existentes.

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

Si tiene un valor de propiedad, como un valor de marca temporal lastUpdated, que se aplica a todos los elementos en los que se ha realizado una actualización o inserción, puede añadirlo o actualizarlo al final de la consulta:

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

Si hay condiciones adicionales que determinan si se debe seguir modificando un vértice o un borde, puede utilizar un paso has() para filtrar los elementos a los que se aplicará la modificación. En el siguiente ejemplo, se utiliza un paso has() para filtrar los vértices en los que se han realizado actualizaciones o inserciones en función del valor de su propiedad version. A continuación, la consulta actualiza a 3 la version de cualquier vértice cuyo version es menor que 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()