Criar upserts eficientes do Gremlin com fold()/coalesce()/unfold() - Amazon Neptune

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Criar upserts eficientes do Gremlin com fold()/coalesce()/unfold()

Um upsert (ou inserção condicional) reutilizará um vértice ou uma borda se já existir ou criará um desses elementos se não existir. Upserts eficientes podem fazer uma diferença significativa no desempenho das consultas do Gremlin.

Esta página mostra como usar o padrão fold()/coalesce()/unfold() do Gremlin para criar upserts eficientes. No entanto, com o lançamento da TinkerPop versão 3.6.x introduzida no Neptune na versão 1.2.1.0 do motor, as etapas novas mergeV() e são preferíveis na maioria dos casos. mergeE() O padrão fold()/coalesce()/unfold() descrito aqui ainda pode ser útil em algumas situações complexas, mas, em geral, use mergeV() e mergeE() se possível, conforme descrito em Criar surtos eficientes com as etapas mergeV() e mergeE() do Gremlin..

Os upserts permitem que você escreva operações de inserção idempotentes: não importa quantas vezes você execute essa operação, o resultado geral é o mesmo. Isso é útil em cenários de gravação altamente simultâneos em que modificações simultâneas na mesma parte do grafo podem forçar a reversão de uma ou mais transações com ConcurrentModificationException, exigindo uma nova tentativa.

Por exemplo, a consulta a seguir aplica upserts a um vértice procurando primeiro pelo vértice especificado no conjunto de dados e, depois, dobrando os resultados em uma lista. No primeiro percurso fornecido para a etapa coalesce(), a consulta então desdobra essa lista. Se a lista desdobrada não estiver vazia, os resultados serão emitidos pelo coalesce(). Se, no entanto, unfold() gerar uma coleção vazia porque o vértice não existe no momento, coalesce() avaliará o segundo percurso com o qual foi fornecida e, nesse segundo percurso, a consulta criará o vértice ausente.

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

Usar uma forma otimizada de coalesce() para upserts

O Neptune pode otimizar a expressão fold().coalesce(unfold(), ...) para fazer atualizações de throughput, mas essa otimização só funcionará se as duas partes do coalesce() gerarem um vértice ou uma borda, nada mais. Se você tentar gerar algo diferente, como uma propriedade, de qualquer parte do coalesce(), a otimização do Neptune não ocorrerá. A consulta poderá ser bem-sucedida, mas não funcionará tão bem quanto uma versão otimizada, principalmente em grandes conjuntos de dados.

Como as consultas de upsert não otimizadas aumentam os tempos de execução e reduzem o throughput, vale a pena usar o endpoint explain do Gremlin para determinar se uma consulta de upsert está totalmente otimizada. Ao revisar os planos explain, procure linhas que comecem com + not converted into Neptune steps e WARNING: >>. Por exemplo:

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

Esses avisos podem ajudar você a identificar as partes de uma consulta que estão impedindo que ela seja totalmente otimizada.

Às vezes, não é possível otimizar totalmente uma consulta. Nessas situações, você deve tentar colocar as etapas que não podem ser otimizadas no final da consulta, permitindo que o mecanismo otimize o máximo de etapas possível. Essa técnica é usada em alguns exemplos de upserts em lote, em que todos os upserts otimizados para um conjunto de vértices ou bordas são executados antes que qualquer modificação adicional, possivelmente não otimizada, seja aplicada aos mesmos vértices ou bordas.

Agrupar upserts em lote para melhorar o throughput

Para cenários de gravação de throughput, é possível encadear as etapas para aplicar upserts a vértices e bordas em lote. O agrupamento em lote reduz a sobrecarga transacional de aplicar upserts a um grande número de vértices e bordas. Depois, é possível melhorar ainda mais o throughput aplicando upserts às solicitações em lote paralelamente usando vários clientes.

Como regra, recomendamos aplicar upserts a cerca de duzentos registros por solicitação em lote. Registro é um único rótulo ou propriedade de vértice ou borda. Um vértice com um único rótulo e quatro propriedades, por exemplo, cria cinco registros. Uma borda com um rótulo e uma única propriedade cria dois registros. Se você quiser aplicar upserts a lotes de vértices, cada um com um único rótulo e quatro propriedades, deverá começar com um tamanho de lote de quarenta, porque 200 / (1 + 4) = 40.

É possível experimentar o tamanho do lote. Um valor de duzentos registros por lote é um bom ponto de partida, mas o tamanho ideal pode ser maior ou menor, dependendo da workload. Observe, no entanto, que o Neptune pode limitar o número total de etapas do Gremlin por solicitação. Esse limite não está documentado, mas, por segurança, tente garantir que suas solicitações não contenham mais de 1.500 etapas do Gremlin. O Neptune pode rejeitar grandes solicitações em lote com mais de 1.500 etapas.

Para aumentar o throughput, é possível inverter lotes em paralelo usando vários clientes (consulte Criação de gravações eficientes com multi-thread do Gremlin). O número de clientes deve ser igual ao número de threads de trabalho na sua instância do Neptune Writer, que normalmente é 2 vezes o número vCPUs do servidor. Por exemplo, uma r5.8xlarge instância tem 32 vCPUs e 64 threads de trabalho. Para cenários de gravação de throughput usando um r5.8xlarge, você usaria 64 clientes gravando upserts em lote no Neptune em paralelo.

Cada cliente deve enviar uma solicitação em lote e aguardar a conclusão da solicitação antes de enviar outra solicitação. Embora os vários clientes funcionem paralelamente, cada cliente individual envia solicitações em série. Isso garante que o servidor receba um fluxo constante de solicitações que ocupem todos os threads de operador sem inundar a fila de solicitações do lado do servidor (consulte Dimensionar instâncias de banco de dados em um cluster de banco de dados do Neptune).

Tentar evitar etapas que gerem vários percursos

Quando uma etapa do Gremlin é executada, ela pega um percurso de entrada e emite um ou mais percursos de saída. O número de percursos emitidos por uma etapa determina o número de vezes que a próxima etapa é executada.

Normalmente, ao realizar operações em lote, é recomendável que cada operação, como o vértice de upsert A, seja executada uma vez, para que a sequência de operações seja a seguinte: vértice de upsert A, depois vértice de upsert B, vértice de upsert C, etc. Desde que uma etapa crie ou modifique somente um elemento, ela emite somente um percurso, e as etapas que representam a próxima operação serão executadas somente uma vez. Se, por outro lado, uma operação criar ou modificar mais de um elemento, ela emitirá vários percursos o que, por sua vez, faz com que as etapas subsequentes sejam executadas várias vezes, uma vez por percurso emitido. Isso pode fazer com que o banco de dados execute trabalho adicional desnecessário e, em alguns casos, pode ocasionar a criação de vértices, bordas ou valores de propriedades adicionais indesejados.

Um exemplo de como as coisas podem dar errado é uma consulta como g.V().addV(). Essa consulta simples adiciona um vértice para cada vértice encontrado no grafo, porque V() emite um percurso para cada vértice no grafo e cada um desses percursos aciona uma chamada para addV().

Consulte Misturar upserts e inserções para saber como lidar com operações que podem emitir vários percursos.

Aplicar upserts a vértices

É possível usar um ID de vértice para determinar se existe um vértice correspondente. Essa é a abordagem preferida, porque o Neptune otimiza upserts para casos de uso altamente simultâneos. IDs Por exemplo, a seguinte consulta criará um vértice com um ID específico, se ele ainda não existir, ou o reutilizará se existir:

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

Observe que essa consulta termina com uma etapa id(). Embora não seja estritamente necessário para aplicar upserts ao vértice, adicionar uma etapa id() ao final de uma consulta de upsert garante que o servidor não serialize todas as propriedades do vértice de volta para o cliente, o que ajuda a reduzir o custo de bloqueio da consulta.

Você também pode usar uma propriedade de vértice para determinar se o 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()

Se possível, use seu próprio usuário fornecido IDs para criar vértices e use-os IDs para determinar se existe um vértice durante uma operação de upsert. Isso permite que Neptune otimize as alterações em torno do. IDs Um upsert baseado em ID pode ser significativamente mais eficiente do que um upsert baseado em propriedade em cenários de modificações altamente simultâneas.

Encadear upserts de vértice

É possível encadear upserts de vértice para inseri-los em um 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()

Aplicar upserts a bordas

Você pode usar IDs a borda para elevar as bordas da mesma forma que você eleva os vértices usando vértices personalizados. IDs Novamente, essa é a abordagem preferencial porque permite que o Neptune otimize a consulta. Por exemplo, a consulta a seguir cria uma borda com base em seu ID de borda, se ela ainda não existir, ou a reutiliza se existir. A consulta também usa os to vértices IDs dos from e se precisar criar uma nova aresta.

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

Muitos aplicativos usam vértices personalizadosIDs, mas deixam Neptune gerar borda. IDs Se você não sabe o ID de uma aresta, mas conhece o to vértice from eIDs, você pode usar essa formulação para inverter uma aresta:

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

Observe que a etapa de vértice na cláusula where() deve ser inV() (ou outV() se você já usou inE() para encontrar a borda), não otherV(). Não use otherV() aqui, ou a consulta não será otimizada e o desempenho será prejudicado. Por exemplo, o Neptune não otimizaria a seguinte 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()

Se você não conhece a borda ou o vértice na IDs frente, pode inverter usando as propriedades do 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()

Assim como acontece com os upserts de vértice, é preferível usar upserts de borda baseados em ID usando um ID de borda ou from um to vérticeIDs, em vez de upserts baseados em propriedades, para que Neptune possa otimizar totalmente o upsert.

Conferir a existência dos vértices from e to

Observe a construção das etapas que criam uma borda: addE().from().to(). Essa construção garante que a consulta confira a existência dos vértices from e to. Se alguma delas não existir, a consulta vai gerar um erro da seguinte forma:

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

Se for possível que os vértices from ou to não existam, você deverá tentar aplicar upsert a eles antes de aplicar upsert à borda entre eles. Consulte Combinar upserts de vértice e borda.

Existe uma construção alternativa para criar uma borda que você não deve usar: V().addE().to(). Ela só adicionará uma borda se o vértice from existir. Se o vértice to não existir, a consulta gerará um erro, conforme descrito anteriormente, mas se o vértice from não existir, ele falhará silenciosamente ao inserir uma borda, sem gerar nenhum erro. Por exemplo, o seguinte upsert será concluído sem aplicar upsert a uma borda se o vértice from não existir:

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

Encadear upserts de borda

Se você quiser encadear upserts de borda para criar uma solicitação em lote, você deve começar cada upsert com uma pesquisa de vértice, mesmo que você já conheça a borda. IDs

Se você já conhece as IDs arestas que deseja inverter e os to vértices e, você pode usar esta formulação: IDs from

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

Talvez o cenário mais comum de aumento de bordas em lote seja que você conheça o to vértice from eIDs, mas não saiba quais bordas deseja inverter. IDs Nesse caso, use a seguinte formulação:

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

Se você conhece IDs as arestas que deseja inverter, mas não conhece os IDs to vértices from e (isso é incomum), você pode usar esta formulação:

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

Combinar upserts de vértice e borda

Às vezes, convém aplicar upserts aos vértices e às bordas que os conectam. Você pode misturar os exemplos de lote apresentados aqui. O seguinte exemplo aplica upserts a três vértices e duas bordas:

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

Misturar upserts e inserções

Às vezes, convém aplicar upserts aos vértices e às bordas que os conectam. Você pode misturar os exemplos de lote apresentados aqui. O seguinte exemplo aplica upserts a três vértices e duas bordas:

Os upserts normalmente avançam um elemento por vez. Se você seguir os padrões de upsert apresentados aqui, cada operação de upsert emitirá um único percurso, o que faz com que a operação subsequente seja executada apenas uma vez.

No entanto, às vezes convém misturar upserts com inserções. Esse pode ser o caso, por exemplo, caso você use bordas para representar instâncias de ações ou eventos. Uma solicitação pode usar upserts para garantir que todos os vértices necessários existam e, depois, usar inserções para adicionar bordas. Com solicitações desse tipo, preste atenção ao número potencial de percursos emitidos por cada operação.

Considere o seguinte exemplo, que mistura upserts e inserções para adicionar bordas que representam eventos no grafo:

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

A consulta deve inserir 5 arestas: 2 FOLLOWED arestas e 3 VISITED arestas. No entanto, a consulta conforme escrita insere 8 bordas: 2 FOLLOWED e 6VISITED. A razão para isso é que a operação que insere FOLLOWED as 2 arestas emite 2 travessas, fazendo com que a operação de inserção subsequente, que insere 3 bordas, seja executada duas vezes.

A correção é adicionar uma etapa fold() após cada operação que possa emitir mais de um percurso:

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

Aqui, inserimos uma fold() etapa após a operação que insere FOLLOWED bordas. Isso ocasiona um único percurso, o que faz com que a operação subsequente seja executada apenas uma vez.

A desvantagem dessa abordagem é que a consulta agora não está totalmente otimizada, porque fold() não está otimizado. A operação de inserção após fold() agora não será otimizada.

Se você precisar usar fold() para reduzir o número de percursos em nome das etapas subsequentes, tente ordenar suas operações de forma que as mais baratas ocupem a parte não otimizada da consulta.

Upserts que modificam vértices e bordas existentes

Às vezes, você deseja criar um vértice ou uma borda, se não existirem, e depois adicionar ou atualizar uma propriedade, independentemente de ser uma borda ou um vértice novo ou existente.

Para adicionar ou modificar uma propriedade, use a etapa property(). Use essa etapa fora da etapa coalesce(). Se você tentar modificar a propriedade de um vértice ou uma borda existente dentro da etapa coalesce(), a consulta poderá não ser otimizada pelo mecanismo de consulta do Neptune.

A consulta a seguir adiciona ou atualiza uma propriedade do contador em cada vértice com upserts aplicados. Cada etapa property() tem uma cardinalidade única para garantir que os novos valores substituam todos os valores existentes, em vez de serem adicionados a um 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()

Se você tiver um valor de propriedade, como um valor de carimbo de data e hora lastUpdated, que se aplique a todos os elementos alterados, poderá adicioná-lo ou atualizá-lo no final da 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()

Se houver condições adicionais que determinem se uma borda ou um vértice deve ou não ser modificado posteriormente, você poderá usar uma etapa has() para filtrar os elementos aos quais uma modificação será aplicada. O exemplo a seguir usa uma etapa has() para filtrar vértices com upserts aplicados com base no valor da propriedade version. Depois, a consulta é atualizada para a version 3 de qualquer vértice cuja version seja inferior a 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()