Gremlin mergeV() 및 mergeE() 단계를 사용하여 효율적인 업서트 생성 - Amazon Neptune

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

Gremlin mergeV()mergeE() 단계를 사용하여 효율적인 업서트 생성

업서트(또는 조건부 삽입)는 버텍스나 엣지가 이미 있는 경우 이를 다시 사용하고 존재하지 않는 경우 이를 생성합니다. 효율적인 업서트는 Gremlin 쿼리의 성능에 상당한 차이를 만들 수 있습니다.

업서트를 사용하면 멱등식 삽입 연산을 작성할 수 있습니다. 이러한 연산을 몇 번 실행해도 전체 결과는 동일합니다. 이는 그래프의 동일한 부분을 동시에 수정하면 하나 이상의 트랜잭션이 ConcurrentModificationException와 함께 롤백되어 재시도가 필요한 동시 쓰기 시나리오에서 유용합니다.

예를 들어, 다음 쿼리는 제공된 Map을 사용하여 "v-1"T.id이 있는 버텍스를 먼저 찾도록 하여 버텍스를 업서트합니다. 해당 버텍스를 찾으면 버텍스가 반환됩니다. 찾을 수 없는 경우 해당 id 및 속성을 가진 버텍스가 onCreate 절을 통해 생성됩니다.

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

일괄 처리 업서트로 처리량 향상

쓰기 처리량이 높은 시나리오의 경우 mergeV()mergeE() 단계를 조합하여 버텍스와 엣지를 일괄적으로 업서트할 수 있습니다. 일괄 처리를 사용하면 많은 수의 버텍스와 엣지를 업서트할 때 발생하는 트랜잭션 오버헤드가 줄어듭니다. 그런 다음 여러 클라이언트를 통해 배치 요청을 병렬로 업서트하여 처리량을 더욱 개선할 수 있습니다.

일반적으로 배치 요청당 약 200개의 레코드를 업서트하는 것이 좋습니다. 레코드는 단일 버텍스, 엣지 레이블 또는 속성입니다. 예를 들어, 레이블이 하나이고 속성이 4개인 버텍스는 5개의 레코드를 생성합니다. 레이블과 단일 속성이 있는 엣지는 레코드 2개를 생성합니다. 각각 레이블이 하나이고 속성이 4개인 버텍스 배치를 업서트하려면 200 / (1 + 4) = 40이므로 배치 크기를 40으로 시작해야 합니다.

배치 크기를 시험해 볼 수 있습니다. 처음에는 배치당 200개의 레코드가 좋지만, 워크로드에 따라 배치 크기가 더 크거나 작을 수 있습니다. 하지만 Neptune에서 요청당 전체 Gremlin 단계 수를 제한할 수 있다는 점에 유의하세요. 이 제한은 문서화되어 있지 않지만, 안전을 위해 요청에 포함된 Gremlin 단계가 1,500개를 넘지 않도록 하는 것이 좋습니다. Neptune은 1,500단계가 넘는 대규모 배치 요청을 거부할 수 있습니다.

처리량을 늘리려면 여러 클라이언트를 사용하여 배치를 병렬로 업서트할 수 있습니다(효율적인 멀티스레드 Gremlin 쓰기 생성 참조). 클라이언트 수는 Neptune 라이터 인스턴스의 작업자 스레드 수와 같아야 합니다. 이 수는 일반적으로 서버 vCPU 수의 2배입니다. 예를 들어, r5.8xlarge 인스턴스에는 32개의 vCPU와 64개의 작업자 스레드가 있습니다. r5.8xlarge를 사용하는 처리량이 높은 쓰기 시나리오의 경우 64개의 클라이언트가 Neptune에 배치 업서트를 병렬로 작성하는 것이 좋습니다.

각 클라이언트는 배치 요청을 제출하고 요청이 완료될 때까지 기다렸다가 다른 요청을 제출해야 합니다. 여러 클라이언트가 병렬로 실행되더라도 각 개별 클라이언트는 순차적으로 요청을 제출합니다. 이렇게 하면 서버 측 요청 대기열에 과부하가 걸리지 않고 모든 작업자 스레드를 점유하는 요청 스트림이 서버에 꾸준히 공급됩니다(Neptune DB 클러스터의 DB 인스턴스 크기 조정 참조).

Traverser가 여러 개 생성되는 단계 피하기

Gremlin 단계가 실행되면 들어오는 Traverser를 받아 하나 이상의 출력 Traverser를 내보냅니다. 한 단계에서 내보내는 Traverser 수에 따라 다음 단계가 실행되는 횟수가 결정됩니다.

일반적으로 배치 작업을 수행할 때는 버텍스 A 업서트와 같은 각 작업을 한 번 실행하여 버텍스 A 업서트, 버텍스 B 업서트, 버텍스 C 업서트 식의 작업 순서를 이루도록 하려고 합니다. 단계가 한 요소만 생성하거나 수정하는 경우 하나의 Traverser만 내보내고 다음 작업을 나타내는 단계는 한 번만 실행됩니다. 반면, 작업에서 2개 이상의 요소를 만들거나 수정하면 여러 Traverser가 생성되어 후속 단계가 생성된 Traverser당 한 번씩 여러 번 실행됩니다. 이로 인해 데이터베이스에서 불필요한 추가 작업이 수행될 수 있으며, 경우에 따라 원하지 않는 추가 버텍스, 엣지 또는 속성값이 생성될 수 있습니다.

문제가 발생할 수 있는 예로는 g.V().addV()와 같은 쿼리를 들 수 있습니다. 이 간단한 쿼리는 그래프에 있는 모든 버텍스에 버텍스를 추가합니다. V()는 그래프의 각 버텍스에 대해 Traverser를 내보내고 각 Traverser가 addV()에 대한 호출을 트리거하기 때문입니다.

여러 Traverser를 내보낼 수 있는 연산을 처리하는 방법은 업서트와 삽입 혼합 섹션을 참조하세요.

버텍스 업서트

mergeV() 단계는 버텍스 업서트를 위해 특별히 설계되었습니다. 그래프의 기존 버텍스와 일치하는 요소를 나타내는 Map을 인수로 사용하고 요소를 찾을 수 없는 경우 해당 Map을 사용하여 새 버텍스를 만듭니다. 또한 이 단계를 통해 생성 또는 일치 시 동작을 변경할 수 있으며, option() 복조기를 Merge.onCreateMerge.onMatch 토큰과 함께 적용하여 각각의 동작을 제어할 수 있습니다. 이 단계를 사용하는 방법에 대한 자세한 내용은 TinkerPop 참조 설명서를 참조하십시오.

버텍스 ID를 사용하여 특정 버텍스가 존재하는지 확인할 수 있습니다. Neptune은 ID와 관련된 동시 사용 사례가 많은 경우 업서트를 최적화하므로, 이 접근 방식은 선호됩니다. 예를 들어, 다음 쿼리는 지정된 버텍스 ID가 없는 경우 버텍스를 생성하고 존재하는 경우 이를 재사용합니다.

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

참고로 이 쿼리는 id() 단계로 끝납니다. 버텍스 업서트를 위해 반드시 필요하지는 않지만, id() 단계는 업서트 쿼리의 끝까지 서버가 모든 버텍스 속성을 클라이언트로 다시 직렬화하지 않도록 보장하므로 쿼리의 잠금 비용을 줄이는 데 도움이 됩니다.

또는 버텍스 속성을 사용하여 버텍스를 식별할 수 있습니다.

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

가능하면 사용자가 제공한 자체 ID를 사용하여 버텍스를 만들고 이 ID를 사용하여 업서트 작업 중에 버텍스가 존재하는지 확인하세요. 이를 통해 Neptune은 업서트를 최적화할 수 있습니다. 동시 수정이 흔한 경우 ID 기반 업서트가 속성 기반 업서트보다 훨씬 더 효율적일 수 있습니다.

버텍스 업서트 연결

버텍스 업서트를 체인으로 연결하여 일괄적으로 삽입할 수 있습니다.

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

또는 다음 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'])

하지만 이 형식의 쿼리에는 id별 기본 조회에 불필요한 요소가 검색 기준에 포함되므로, 이전 쿼리만큼 효율적이지 않습니다.

엣지 업서트

mergeE() 단계는 엣지 업서트를 위해 특별히 설계되었습니다. 그래프의 기존 엣지와 일치하는 요소를 나타내는 Map을 인수로 사용하고 요소를 찾을 수 없는 경우 해당 Map을 사용하여 새 엣지를 만듭니다. 또한 이 단계를 통해 생성 또는 일치 시 동작을 변경할 수 있으며, option() 복조기를 Merge.onCreateMerge.onMatch 토큰과 함께 적용하여 각각의 동작을 제어할 수 있습니다. 이 단계를 사용하는 방법에 대한 자세한 내용은 TinkerPop 참조 설명서를 참조하십시오.

사용자 지정 버텍스 ID를 사용하여 버텍스를 업서트하는 것과 같은 방식으로 엣지 ID를 사용하여 엣지를 업서트할 수 있습니다. 다시 말하지만, 이 방법을 사용하면 Neptune에서 쿼리를 최적화할 수 있어 선호됩니다. 예를 들어, 다음 쿼리는 엣지가 아직 없으면 엣지 ID를 기반으로 엣지를 생성하고 존재하면 재사용합니다. 또한 쿼리는 새 엣지를 만들어야 하는 경우 Direction.fromDirection.to 버텍스의 ID를 사용합니다.

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

참고로 이 쿼리는 id() 단계로 끝납니다. 엣지 업서트를 위해 반드시 필요하지는 않지만, 업서트 쿼리의 끝에 id() 단계를 추가하면 서버가 모든 엣지 속성을 클라이언트로 다시 직렬화하지 않도록 보장하므로 쿼리의 잠금 비용을 줄이는 데 도움이 됩니다.

많은 애플리케이션이 사용자 지정 버텍스 ID를 사용하지만, Neptune은 자체적으로 엣지 ID를 생성합니다. 엣지의 ID는 몰라도 fromto 버텍스 ID는 알고 있다면 다음과 같은 쿼리를 사용하여 엣지를 업서트할 수 있습니다.

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

mergeE()에서 참조하는 모든 버텍스는 엣지를 만드는 단계에 존재해야 합니다.

엣지 업서트 연결

버텍스 업서트와 마찬가지로 배치 요청의 경우 mergeE() 단계를 서로 연결하는 것도 간단합니다.

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

버텍스 업서트와 엣지 업서트의 결합

버텍스와 버텍스를 연결하는 엣지를 모두 업서트해야 하는 경우가 있을 수 있습니다. 여기에 제시된 배치 예제를 혼합하여 사용할 수 있습니다. 다음 예제에서는 버텍스 3개와 엣지 2개를 업서트합니다.

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

업서트와 삽입 혼합

버텍스와 버텍스를 연결하는 엣지를 모두 업서트해야 하는 경우가 있을 수 있습니다. 여기에 제시된 배치 예제를 혼합하여 사용할 수 있습니다. 다음 예제에서는 버텍스 3개와 엣지 2개를 업서트합니다.

업서트는 일반적으로 한 번에 한 요소씩 진행됩니다. 여기에 제시된 업서트 패턴을 고수하면 각 업서트 작업에서 Traverser가 하나만 생성되어 후속 작업이 한 번만 실행됩니다.

하지만 업서트와 삽입을 섞어서 사용해야 하는 경우도 있습니다. 예를 들어, 엣지를 사용하여 동작이나 이벤트의 인스턴스를 나타내는 경우가 이에 해당합니다. 요청에서는 업서트를 사용하여 필요한 버텍스가 모두 존재하는지 확인한 다음, 삽입을 통해 엣지를 추가할 수 있습니다. 이런 요청 유형의 경우 각 작업에서 발생할 수 있는 잠재적 Traverser 수에 유의하세요.

업서트와 삽입을 혼합하여 이벤트를 나타내는 엣지를 그래프에 추가하는 다음 예제를 생각해 보세요.

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

쿼리는 FOLLOWED 엣지 2개와 VISITED 엣지 3개의 총 5개의 엣지를 삽입해야 합니다. 하지만 쿼리를 작성하면 8개(FOLLOWED 2개, VISITED 6개)가 삽입됩니다. 그 이유는 FOLLOWED 엣지 2개를 삽입하는 작업에서 2개의 Traverser가 발생하여 3개의 엣지를 삽입하는 후속 삽입 작업이 2번씩 실행되기 때문입니다.

잠재적으로 2개 이상의 Traverser를 내보낼 수 있는 각 작업 뒤에 fold() 단계를 추가하면 문제를 해결할 수 있습니다.

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

여기서는 FOLLOWED 엣지를 삽입하는 작업 뒤에 fold() 단계를 삽입했습니다. 그 결과 단일 Traverser가 생성되어 후속 작업이 한 번만 실행됩니다.

이 접근 방식의 단점은 fold()가 최적화되지 않아 쿼리가 완전히 최적화되지 않았다는 것입니다. fold() 이후 발생하는 삽입 작업도 이제 최적화되지 않습니다.

후속 단계를 대신하여 Traverser 수를 줄이는 데 fold()를 사용해야 하는 경우 가장 비용이 적게 드는 작업이 쿼리에서 최적화되지 않은 부분을 차지하도록 작업을 정렬해 보세요.

카디널리티 설정

Neptune의 꼭짓점 속성에 대한 기본 카디널리티가 설정되어 있습니다. 즉, merGev () 를 사용하면 맵에 제공된 값에 모두 해당 카디널리티가 지정됩니다. 단일 카디널리티를 사용하려면 사용법을 명시해야 합니다. TinkerPop 3.7.0부터 다음 예와 같이 카디널리티를 맵의 일부로 제공할 수 있는 새로운 구문이 생겼습니다.

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

또는 다음과 같이 카디널리티를 기본값으로 설정할 수도 있습니다. 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)

버전 3.7.0 mergeV() 이전 버전에서는 카디널리티 설정 옵션이 더 적습니다. 일반적인 접근 방식은 다음과 같은 단계로 돌아가는 것입니다. property()

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

이 접근 방식은 시작 단계와 함께 사용할 mergeV() 때만 사용할 수 있습니다. 따라서 이 구문을 사용하는 시작 단계 mergeV() 이후 첫 번째 단계에서 들어오는 트래버서가 그래프 요소인 경우 오류가 발생하므로 단일 순회 mergeV() 내에서 체인을 연결할 수 없습니다. 이 경우에는 mergeV() 호출을 각각 시작 단계가 될 수 있는 여러 요청으로 나누는 것이 좋습니다.