Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.
Creazione di upsert Gremlin efficienti con fold()/coalesce()/unfold()
Un upsert (o inserimento condizionale) riutilizza un vertice o uno arco se già esiste oppure lo crea se non esiste. Upsert efficienti possono fare una differenza significativa nelle prestazioni delle query Gremlin.
Questa pagina mostra come utilizzare il modello Gremlin fold()/coalesce()/unfold()
per creare upsert efficienti. Tuttavia, con il rilascio della TinkerPop versione 3.6.x introdotta in Neptune nella versione 1.2.1.0 del motore, nella maggior parte dei casi sono preferibili le nuove mergeV()
fasi. mergeE()
Il modello fold()/coalesce()/unfold()
qui descritto può essere ancora utile in alcune situazioni complesse, ma in generale, se possibile, utilizzare mergeV()
e mergeE()
, come descritto in Creazione di upsert efficienti con i passaggi mergeV() e mergeE() di Gremlin.
Gli upsert consentono di scrivere operazioni di inserimento idempotenti: indipendentemente dal numero di volte in cui si esegue un'operazione di questo tipo, il risultato complessivo è lo stesso. Ciò è utile in scenari di scrittura altamente simultanei in cui modifiche simultanee alla stessa parte del grafo possono forzare il rollback di una o più transazioni con un'eccezione ConcurrentModificationException
, rendendo quindi necessari un nuovo tentativo.
Ad esempio, la seguente query esegue l'upsert di un vertice cercando prima il vertice specificato nel set di dati e quindi raggruppando i risultati in un elenco. Nel primo attraversamento fornito al passaggio coalesce()
, la query espande quindi questo elenco. Se l'elenco espanso non è vuoto, i risultati vengono emessi da coalesce()
. Se, tuttavia, unfold()
restituisce una raccolta vuota perché il vertice attualmente non esiste, coalesce()
passa a valutare il secondo attraversamento che gli è stato fornito e in questo secondo attraversamento la query crea il vertice mancante.
g.V('v-1').fold() .coalesce( unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') )
Utilizzo di una forma ottimizzata di coalesce()
per gli upsert
Neptune può ottimizzare l'idioma fold().coalesce(unfold(), ...)
per apportare aggiornamenti con velocità di trasmissione effettiva elevata, ma questa ottimizzazione funziona solo se entrambe le parti di coalesce()
restituiscono un vertice o un arco e nient'altro. Se si tenta di restituire qualcosa di diverso, ad esempio una proprietà, da qualsiasi parte di coalesce()
, l'ottimizzazione di Neptune non si verifica. La query può avere esito positivo, ma non avrà le stesse prestazioni di una versione ottimizzata, in particolare su set di dati di grandi dimensioni.
Poiché le query upsert non ottimizzate aumentano i tempi di esecuzione e riducono la velocità di trasmissione effettiva, vale la pena utilizzare l'endpoint Gremlin explain
per determinare se una query upsert è completamente ottimizzata. Quando si esaminano i piani explain
, cercare le righe che iniziano con + not converted into Neptune steps
e WARNING: >>
. Per esempio:
+ not converted into Neptune steps: [FoldStep, CoalesceStep([[UnfoldStep], [AddEdgeSte
...
WARNING: >> FoldStep << is not supported natively yet
Questi avvisi consentono di identificare le parti di una query che ne impediscono l'ottimizzazione completa.
A volte non è possibile ottimizzare completamente una query. In queste situazioni è consigliabile provare a inserire i passaggi che non possono essere ottimizzati alla fine della query, in modo da consentire al motore di ottimizzare il maggior numero possibile di passaggi. Questa tecnica viene utilizzata in alcuni esempi di upsert in batch, in cui tutti gli upsert ottimizzati per un insieme di vertici o archi vengono eseguiti prima di applicare eventuali modifiche aggiuntive, potenzialmente non ottimizzate, agli stessi vertici o archi.
Upsert in batch per migliorare la velocità di trasmissione effettiva
Per scenari di scrittura con velocità di trasmissione effettiva elevata, è possibile concatenare i passaggi per eseguire l'upsert di vertici e archi in batch. Il batching riduce il sovraccarico transazionale dell'upsert di un gran numero di vertici e archi. È quindi possibile migliorare ulteriormente la velocità di trasmissione effettiva eseguendo l'upsert di richieste batch in parallelo utilizzando più client.
Come regola generale, è consigliabile eseguire l'upsert di circa 200 record per richiesta batch. Un record è una singola etichetta o proprietà di un vertice o un arco. Un vertice con una singola etichetta e 4 proprietà, ad esempio, crea 5 record. Un arco con un'etichetta e una singola proprietà crea 2 record. Se si vuole eseguire l'upsert di batch di vertici, ciascuno con una singola etichetta e 4 proprietà, è necessario iniziare con una dimensione di batch di 40, perché 200 / (1 + 4)
= 40
.
È possibile sperimentare con la dimensione del batch. 200 record per batch sono un buon punto di partenza, ma la dimensione ideale del batch può essere maggiore o minore a seconda del carico di lavoro. Tenere presente, tuttavia, che Neptune può limitare il numero complessivo di passaggi Gremlin per richiesta. Questo limite non è documentato, ma per sicurezza, cercare di assicurarsi che le richieste non contengano più di 1.500 passaggi Gremlin. Neptune può rifiutare richieste batch di grandi dimensioni con più di 1.500 passaggi.
Per aumentare la velocità di trasmissione effettiva, è possibile eseguire l'upsert di batch in parallelo utilizzando più client (vedi Creazione di scritture multithread Gremlin efficienti). Il numero di client deve essere uguale al numero di thread di lavoro sull'istanza di Neptune writer, che in genere è 2 volte il numero vCPUs di thread presenti sul server. Ad esempio, un'r5.8xlarge
istanza ha 32 vCPUs e 64 thread di lavoro. Per scenari di scrittura con velocità di trasmissione effettiva elevata che utilizzano un'istanza r5.8xlarge
, è necessario utilizzare 64 client che scrivono upsert in batch su Neptune in parallelo.
Ogni client deve inviare una richiesta batch e attendere il completamento della richiesta prima di inviarne un'altra. Sebbene più client vengano eseguiti in parallelo, ogni singolo client invia le richieste in modo seriale. Ciò garantisce che il server riceva un flusso costante di richieste che occupano tutti i thread di lavoro senza sovraccaricare la coda delle richieste lato server (vedi Dimensionamento delle istanze database in un cluster database Neptune).
Cercare di evitare passaggi che generano più traverser
Quando viene eseguito un passaggio Gremlin, accetta un traverser in entrata ed emette uno o più traverser in uscita. Il numero di traverser emessi da un passaggio determina il numero di volte in cui viene eseguito il passaggio successivo.
In genere, quando si eseguono operazioni in batch, si desidera che ogni operazione, ad esempio l'upsert del vertice A, venga eseguita una sola volta, in modo che la sequenza di operazioni sia la seguente: upsert del vertice A, quindi upsert del vertice B, quindi upsert del vertice C e così via. Finché un passaggio crea o modifica un solo elemento, emette un solo traverser e i passaggi che rappresentano l'operazione successiva vengono eseguiti una sola volta. Se, invece, un'operazione crea o modifica più di un elemento, emette più traverser, che a loro volta fanno sì che i passaggi successivi vengano eseguiti più volte, una volta per ogni traverser emesso. Ciò può comportare l'esecuzione di operazioni aggiuntive non necessarie nel database e, in alcuni casi, la creazione di vertici, archi o valori di proprietà aggiuntivi indesiderati.
Un esempio di come le cose possano andare male è con una query come g.V().addV()
. Questa semplice query aggiunge un vertice per ogni vertice trovato nel grafo, perché V()
emette un traverser per ogni vertice del grafo e ognuno di questi traverser attiva una chiamata a addV()
.
Consulta Combinazione di upsert e inserimenti per informazioni su come gestire le operazioni che possono emettere più traverser.
Upsert dei vertici
È possibile utilizzare un ID vertice per determinare se esiste un vertice corrispondente. Questo è l'approccio preferito, perché Neptune ottimizza gli upsert per casi d'uso altamente simultanei. IDs Ad esempio, la seguente query crea un vertice con un determinato ID vertice se non esiste già o lo riutilizza se esiste:
g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .id()
Tenere presente che questa query termina con un passaggio id()
Sebbene non sia strettamente necessario ai fini dell'upsert del vertice, l'aggiunta di un passaggio id()
alla fine di una query di upsert assicura che il server non serializzi tutte le proprietà del vertice sul client, riducendo il costo di blocco della query.
In alternativa, è possibile utilizzare una proprietà del vertice per determinare se il vertice esiste:
g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .fold() .coalesce(unfold(), addV('Person').property('email', 'person-1@example.org')) .id()
Se possibile, utilizzate i vertici forniti dall'utente IDs per creare vertici e utilizzateli per determinare se esiste un vertice durante un'operazione IDs di ribaltamento. Ciò consente a Neptune di ottimizzare i turbamenti intorno al. IDs Un upsert basato su ID può essere significativamente più efficiente di un upsert basato su proprietà in scenari di modifica altamente simultanei.
Concatenamento degli upsert dei vertici
È possibile concatenare gli upsert dei vertici per inserirli in un batch:
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 degli archi
È possibile utilizzare il bordo IDs per ribaltare i bordi nello stesso modo in cui si ribaltano i vertici utilizzando un vertice personalizzato. IDs Anche in questo caso, si tratta dell'approccio preferito perché consente a Neptune di ottimizzare la query. Ad esempio, la seguente query crea un arco in base al relativo ID arco se non esiste già oppure lo riutilizza se esiste. L'interrogazione utilizza anche i to
vertici IDs del from
e se deve creare un nuovo spigolo.
g.E('e-1') .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2')) .property(id, 'e-1')) .id()
Molte applicazioni utilizzano vertici personalizzatiIDs, ma lasciano che Neptune generi l'edge. IDs Se non conoscete l'ID di uno spigolo, ma conoscete il to
vertice from
andIDs, potete usare questa formulazione per capovolgere uno spigolo:
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()
Notare che il passaggio del vertice nella clausola where()
deve essere inV()
(o outV()
se è stato usato inE()
per trovare l'arco), non otherV()
. Non utilizzare otherV()
in questo caso, altrimenti la query non verrà ottimizzata e le prestazioni ne risentiranno. Ad esempio, Neptune non ottimizzerà la seguente query:
// 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 non conosci lo spigolo o il vertice in primo piano, puoi IDs capovolgerlo usando le proprietà del vertice:
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()
Come per i ribaltamenti dei vertici, è preferibile utilizzare i ribaltamenti degli spigoli basati su ID utilizzando un ID del bordo o from
un to
verticeIDs, piuttosto che gli upsert basati sulle proprietà, in modo che Neptune possa ottimizzare completamente l'upsert.
Verifica dell'esistenza dei vertici from
e to
Notare la costruzione dei passaggi che creano un nuovo arco: addE().from().to()
. Questa costruzione assicura che la query verifichi l'esistenza sia del vertice from
che del vertice to
. Se uno dei due non esiste, la query restituisce un errore come segue:
{ "detailedMessage": "Encountered a traverser that does not map to a value for child
...
"code": "IllegalArgumentException", "requestId": "..." }
Se è possibile che il vertice from
o il vertice to
non esista, è consigliabile provare a eseguirne l'upsert prima di eseguire l'upsert dell'arco tra di loro. Per informazioni, consulta Combinazione di upsert di vertici e archi.
Esiste una costruzione alternativa per la creazione di un arco che non è consigliabile usare: V().addE().to()
. Aggiunge uno arco solo se esiste il vertice from
. Se il vertice to
non esiste, la query genera un errore, come descritto in precedenza, ma se il vertice from
non esiste, l'inserimento di un arco non riesce ma non viene generato alcun errore. Ad esempio, il seguente upsert viene completato senza eseguire l'upsert di un arco se il vertice from
non esiste:
// 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()
Concatenamento degli upsert degli archi
Se desiderate concatenare gli upsert degli spigoli per creare una richiesta batch, dovete iniziare ogni upsert con una ricerca dei vertici, anche se conoscete già lo spigolo. IDs
Se conosci già gli IDs spigoli che vuoi ribaltare e i vertici e, puoi usare questa IDs formulazionefrom
: to
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()
Forse lo scenario di ribaltamento degli spigoli in batch più comune è che si conosce il to
vertice from
andIDs, ma non si conoscono gli spigoli che si desidera IDs ribaltare. In tal caso, utilizzare la seguente formulazione:
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 IDs conoscete gli spigoli che volete ribaltare, ma non conoscete i IDs to
vertici from
e (questo è insolito), potete usare questa formulazione:
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()
Combinazione di upsert di vertici e archi
A volte si può desiderare di eseguire l'upsert sia dei vertici che degli archi che li collegano. È possibile combinare gli esempi batch illustrati qui. L'esempio seguente esegue l'upsert di 3 vertici e 2 archi:
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()
Combinazione di upsert e inserimenti
A volte si può desiderare di eseguire l'upsert sia dei vertici che degli archi che li collegano. È possibile combinare gli esempi batch illustrati qui. L'esempio seguente esegue l'upsert di 3 vertici e 2 archi:
Gli upsert in genere procedono un elemento alla volta. Se ci si attiene ai modelli di upsert qui presentati, ogni operazione di upsert emette un singolo traverser, che fa sì che l'operazione successiva venga eseguita una sola volta.
Tuttavia, a volte si può voler combinare gli upsert con gli inserimenti. Questo può essere il caso, ad esempio, se si usano gli archi per rappresentare istanze di azioni o eventi. Una richiesta potrebbe utilizzare gli upsert per assicurarsi che esistano tutti i vertici necessari e quindi utilizzare gli inserimenti per aggiungere gli archi. Con richieste di questo tipo, occorre prestare attenzione al numero potenziale di traverser emessi da ciascuna operazione.
Considerare l'esempio seguente, che combina upsert e inserimenti per aggiungere archi che rappresentano gli eventi nel 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()
L'interrogazione dovrebbe inserire 5 spigoli: 2 spigoli e 3 FOLLOWED spigoli. VISITED Tuttavia, la query così come è scritta inserisce 8 spigoli: 2 FOLLOWED e 6VISITED. La ragione di ciò è che l'operazione che inserisce FOLLOWED i 2 spigoli emette 2 trasversali, facendo sì che la successiva operazione di inserimento, che inserisce 3 spigoli, venga eseguita due volte.
La soluzione consiste nell'aggiungere un passaggio fold()
dopo ogni operazione che può potenzialmente emettere più di un traverser:
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()
Qui abbiamo inserito un fold()
passaggio dopo l'operazione che inserisce gli spigoli. FOLLOWED In questo modo si ottiene un singolo traverser, che fa sì che l'operazione successiva venga eseguita una sola volta.
Lo svantaggio di questo approccio è che la query ora non è completamente ottimizzata, perché fold()
non è ottimizzato. L'operazione di inserimento che segue fold()
ora non sarà ottimizzata.
Se è necessario usare fold()
per ridurre il numero di traverser per i passaggi successivi, provare a ordinare le operazioni in modo che quelle meno costose occupino la parte non ottimizzata della query.
Upsert che modificano vertici e archi esistenti
A volte si desidera creare un vertice o un arco se non esiste e quindi aggiungere o aggiornare una proprietà, indipendentemente dal fatto che si tratti di un vertice o di un arco nuovo o esistente.
Per aggiungere o modificare una proprietà, utilizzare il passaggio property()
. Utilizzare questo passaggio all'esterno del passaggio coalesce()
. Se si prova a modificare la proprietà di un vertice o di un arco esistente all'interno del passaggio coalesce()
, la query potrebbe non essere ottimizzata dal motore di query Neptune.
La seguente query aggiunge o aggiorna la proprietà counter su ogni vertice di cui è stato eseguito l'upsert. Ogni passaggio property()
ha una cardinalità singola per garantire che i nuovi valori sostituiscano quelli esistenti, anziché essere aggiunti a un insieme di valori esistenti.
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 si dispone di un valore di proprietà, ad esempio il valore di timestamp lastUpdated
, che si applica a tutti gli elementi di cui è stato eseguito l'upsert, è possibile aggiungerlo o aggiornarlo alla fine della query:
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 esistono condizioni aggiuntive che determinano se un vertice o un arco debba essere ulteriormente modificato, è possibile utilizzare un passaggio has()
per filtrare gli elementi a cui verrà applicata una modifica. L'esempio seguente utilizza un passaggio has()
per filtrare i vertici di cui è stato eseguito l'upsert in base al valore della relativa proprietà version
. La query quindi aggiorna a 3 il valore version
di qualsiasi vertice il cui valore version
sia inferiore 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()