

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

# openCypher와 Bolt를 사용한 Neptune 모범 사례
<a name="best-practices-opencypher"></a>

Neptune과 함께 openCypher 쿼리 언어 및 Bolt 프로토콜을 사용할 때는 다음 모범 사례를 따르세요. openCypher를 Neptune과 함께 사용하는 방법에 대한 자세한 내용은 [openCypher를 사용하여 Neptune 그래프에 액세스](access-graph-opencypher.md)를 참조하세요.

**Topics**
+ [장애 조치 후 새로운 연결 생성](#best-practices-opencypher-renew-connection)
+ [수명이 긴 애플리케이션의 연결 처리](#best-practices-opencypher-long-connections)
+ [에 대한 연결 처리 AWS Lambda](#best-practices-opencypher-lambda-connections)
+ [쿼리에서는 양방향 엣지보다 방향성 엣지 선호](best-practices-opencypher-directed-edges.md)
+ [Neptune은 트랜잭션에서 여러 개의 동시 쿼리를 지원하지 않음](best-practices-opencypher-multiple-queries.md)
+ [완료 후 드라이버 객체 닫기](best-practices-opencypher-close-driver.md)
+ [읽기 및 쓰기에 명시적 트랜잭션 모드 사용](best-practices-opencypher-use-explicit-txs.md)
+ [예외에 대한 재시도 로직](best-practices-opencypher-retry-logic.md)
+ [단일 SET 절을 사용하여 한 번에 여러 속성 설정](best-practices-content-0.md)
+ [파라미터화된 쿼리 사용](best-practices-content-2.md)
+ [UNWIND 절에서 중첩 맵 대신 평면화된 맵 사용](best-practices-content-3.md)
+ [가변 길이 경로(VLP) 표현식의 왼쪽에 더 제한적인 노드 배치](best-practices-content-4.md)
+ [세분화된 관계 이름을 사용하여 중복 노드 레이블 확인 방지](best-practices-content-5.md)
+ [가능한 경우 엣지 레이블 지정](best-practices-content-6.md)
+ [가능한 경우 WITH 절 사용 안 함](best-practices-content-7.md)
+ [쿼리에서 가능한 한 빨리 제한 필터를 배치합니다.](best-practices-content-8.md)
+ [속성이 존재하는지 명시적으로 확인](best-practices-content-9.md)
+ [명명된 경로를 사용하지 않음(필수가 아닌 경우).](best-practices-content-10.md)
+ [COLLECT(DISTINCT()) 사용 안 함](best-practices-content-11.md)
+ [모든 속성 값을 검색할 때 개별 속성 조회보다 속성 함수를 선호합니다.](best-practices-content-12.md)
+ [쿼리 외부에서 정적 계산 수행](best-practices-content-13.md)
+ [개별 문 대신 UNWIND를 사용한 배치 입력](best-practices-content-14.md)
+ [노드/관계에 사용자 정의 ID 사용 선호](best-practices-content-15.md)
+ [쿼리에서 \$1id 계산 수행 안 함](best-practices-content-16.md)
+ [여러 노드 업데이트/병합](best-practices-merge-multiple-nodes.md)

## 장애 조치 후 새로운 연결 생성
<a name="best-practices-opencypher-renew-connection"></a>

장애 조치 시 DNS 이름이 특정 IP 주소로 확인되므로 Bolt 드라이버는 새 활성 인스턴스 대신 이전 라이터 인스턴스에 계속 연결할 수 있습니다.

이를 방지하려면 장애 조치 후 `Driver` 객체를 닫았다가 다시 연결하면 됩니다.

## 수명이 긴 애플리케이션의 연결 처리
<a name="best-practices-opencypher-long-connections"></a>

컨테이너 내에서 실행되거나 Amazon EC2 인스턴스에서 실행되는 애플리케이션과 같이 수명이 긴 애플리케이션을 구축할 때는 `Driver` 객체를 한 번 인스턴스화한 다음, 해당 객체를 애플리케이션 수명 기간 동안 재사용합니다. `Driver` 객체는 스레드 세이프이고 그 초기화의 오버헤드가 상당합니다.

## 에 대한 연결 처리 AWS Lambda
<a name="best-practices-opencypher-lambda-connections"></a>

볼트 드라이버는 연결 오버헤드 및 관리 요구 사항으로 인해 AWS Lambda 함수 내에서 사용하지 않는 것이 좋습니다. 대신 [HTTPS 엔드포인트](access-graph-opencypher-queries.md)를 사용하세요.

# 쿼리에서는 양방향 엣지보다 방향성 엣지 선호
<a name="best-practices-opencypher-directed-edges"></a>

Neptune은 쿼리 최적화를 수행할 때 양방향 엣지로 인해 최적의 쿼리 계획을 만들기가 어렵습니다. 최적화되지 않은 계획에서는 엔진이 불필요한 작업을 수행해야 하므로, 성능이 저하됩니다.

따라서 가능하면 양방향 엣지 대신 방향이 지정된 엣지를 사용하는 게 좋습니다. 예를 들어, 다음을 사용합니다.

```
MATCH p=(:airport {code: 'ANC'})-[:route]->(d) RETURN p)
```

다음 사용을 삼갑니다.

```
MATCH p=(:airport {code: 'ANC'})-[:route]-(d) RETURN p)
```

대부분의 데이터 모델은 실제로 엣지를 양방향으로 순회할 필요가 없으므로, 방향성 엣지를 사용하도록 전환하면 쿼리의 성능이 크게 향상될 수 있습니다.

데이터 모델에서 양방향 엣지를 순회해야 하는 경우 `MATCH` 패턴의 첫 번째 노드(왼쪽)를 필터링이 가장 엄격한 노드로 만듭니다.

“`ANC` 공항을 오가는 모든 `routes`를 찾아줘”를 예로 들어 보겠습니다. 이 쿼리를 작성하여 `ANC` 공항에서 다음과 같이 시작하세요.

```
MATCH p=(src:airport {code: 'ANC'})-[:route]-(d) RETURN p
```

가장 제한된 노드가 패턴의 첫 번째 노드(왼쪽)로 배치되기 때문에 엔진은 쿼리를 충족하는 데 필요한 최소한의 작업만 수행할 수 있습니다. 그러면 엔진이 쿼리를 최적화할 수 있습니다.

다음과 같이 패턴 끝에서 `ANC` 공항을 필터링하는 것보다 이 방법이 훨씬 좋습니다.

```
MATCH p=(d)-[:route]-(src:airport {code: 'ANC'}) RETURN p
```

가장 제한된 노드를 패턴의 첫 번째로 배치하지 않으면 엔진에서 추가 작업을 수행해야 합니다. 쿼리를 최적화할 수 없고 결과에 도달하기 위해 별도로 검색을 수행해야 하기 때문입니다.

# Neptune은 트랜잭션에서 여러 개의 동시 쿼리를 지원하지 않음
<a name="best-practices-opencypher-multiple-queries"></a>

Bolt 드라이버 자체는 트랜잭션에서 동시 쿼리를 허용하지만, Neptune은 동시에 실행되는 트랜잭션에서 여러 개의 쿼리를 지원하지 않습니다. 대신 Neptune에서는 한 트랜잭션의 여러 쿼리를 순차적으로 실행하고 다음 쿼리가 시작되기 전에 각 쿼리의 결과를 완전히 소비해야 합니다.

아래 예제는 Bolt를 사용하여 트랜잭션에서 여러 쿼리를 순차적으로 실행하여 다음 쿼리가 시작되기 전에 각 쿼리의 결과가 완전히 소비되도록 하는 방법을 보여줍니다.

```
final String query = "MATCH (n) RETURN n";

try (Driver driver = getDriver(HOST_BOLT, getDefaultConfig())) {
  try (Session session = driver.session(readSessionConfig)) {
    try (Transaction trx = session.beginTransaction()) {
      final Result res_1 = trx.run(query);
      Assert.assertEquals(10000, res_1.list().size());
      final Result res_2 = trx.run(query);
      Assert.assertEquals(10000, res_2.list().size());
    }
  }
}
```

# 완료 후 드라이버 객체 닫기
<a name="best-practices-opencypher-close-driver"></a>

완료되면 서버가 Bolt 연결을 닫고 연결과 관련된 모든 리소스가 해제되도록 클라이언트를 닫아야 합니다. `driver.close()`를 사용하여 드라이버를 닫으면 자동으로 이루어집니다.

드라이버가 제대로 닫히지 않으면 Neptune은 20분 후 또는 IAM 인증을 사용하는 경우 10일 후에 모든 유휴 Bolt 연결을 종료합니다.

Neptune은 1,000개 이상의 동시 Bolt 연결을 지원하지 않습니다. 완료 후 연결을 명시적으로 닫지 않고 활성 연결 수가 1,000개 제한에 도달하면 새로운 연결 시도가 실패합니다.

# 읽기 및 쓰기에 명시적 트랜잭션 모드 사용
<a name="best-practices-opencypher-use-explicit-txs"></a>

Neptune 및 Bolt 드라이버와 함께 트랜잭션을 사용할 때는 읽기 및 쓰기 트랜잭션 모두에 대한 액세스 모드를 올바른 설정으로 명시적으로 지정하는 것이 가장 좋습니다.

## 읽기 전용 트랜잭션
<a name="best-practices-opencypher-read-txs"></a>

읽기 전용 트랜잭션의 경우 세션을 빌드할 때 적절한 액세스 모드 구성을 전달하지 않으면 기본 격리 수준인 변형 쿼리 격리가 사용됩니다. 따라서 읽기 전용 트랜잭션의 경우 액세스 모드를 `read`로 명시적으로 설정하는 것이 중요합니다.

**읽기 트랜잭션 자동 커밋 예제:**

```
SessionConfig sessionConfig = SessionConfig
  .builder()
  .withFetchSize(1000)
  .withDefaultAccessMode(AccessMode.READ)
  .build();
Session session = driver.session(sessionConfig);
try {
  (Add your application code here)
} catch (final Exception e) {
  throw e;
} finally {
  driver.close()
}
```

**읽기 트랜잭션 예제:**

```
Driver driver = GraphDatabase.driver(url, auth, config);
SessionConfig sessionConfig = SessionConfig
  .builder()
  .withDefaultAccessMode(AccessMode.READ)
  .build();
driver.session(sessionConfig).readTransaction(
  new TransactionWork<List<String>>() {
    @Override
    public List<String> execute(org.neo4j.driver.Transaction tx) {
      (Add your application code here)
    }
  }
);
```

두 경우 모두 [Neptune 읽기 전용 트랜잭션 체계](transactions-neptune.md#transactions-neptune-read-only)를 사용하여 [`SNAPSHOT` 격리](transactions-isolation-levels.md)가 이루어집니다.

읽기 전용 복제본은 읽기 전용 쿼리만 허용하므로, 읽기 전용 복제본에 제출된 모든 쿼리는 `SNAPSHOT` 격리 체계로 실행됩니다.

읽기 전용 트랜잭션에는 더티 읽기나 반복 불가능한 읽기가 없습니다.

## 변형 트랜잭션
<a name="best-practices-opencypher-mutation-txs"></a>

변형 쿼리의 경우 쓰기 트랜잭션을 생성하는 3가지 메커니즘이 있으며, 각 메커니즘은 다음과 같습니다.

**암시적 쓰기 트랜잭션 예제:**

```
Driver driver = GraphDatabase.driver(url, auth, config);
SessionConfig sessionConfig = SessionConfig
  .builder()
  .withDefaultAccessMode(AccessMode.WRITE)
  .build();
driver.session(sessionConfig).writeTransaction(
  new TransactionWork<List<String>>() {
    @Override
    public List<String> execute(org.neo4j.driver.Transaction tx) {
      (Add your application code here)
    }
  }
);
```

**자동 커밋 쓰기 트랜잭션 예제:**

```
SessionConfig sessionConfig = SessionConfig
  .builder()
  .withFetchSize(1000)
  .withDefaultAccessMode(AccessMode.Write)
  .build();
Session session = driver.session(sessionConfig);
try {
  (Add your application code here)
} catch (final Exception e) {
    throw e;
} finally {
    driver.close()
}
```

**명시적 쓰기 트랜잭션 예제:**

```
Driver driver = GraphDatabase.driver(url, auth, config);
SessionConfig sessionConfig = SessionConfig
  .builder()
  .withFetchSize(1000)
  .withDefaultAccessMode(AccessMode.WRITE)
  .build();
Transaction beginWriteTransaction = driver.session(sessionConfig).beginTransaction();
  (Add your application code here)
beginWriteTransaction.commit();
driver.close();
```

**쓰기 트랜잭션의 격리 수준**
+ 변형 쿼리의 일부로 이루어진 읽기는 `READ COMMITTED` 트랜잭션 격리 상태에서 실행됩니다.
+ 변형 쿼리의 일부로 이루어진 읽기에 대한 더티 읽기는 없습니다.
+ 변형 쿼리를 읽을 때 레코드와 레코드 범위가 잠깁니다.
+ 인덱스의 범위를 변형 쿼리에서 읽었을 때 읽기가 끝날 때까지 어떤 동시 트랜잭션도 이 범위를 수정하지 못하도록 강력하게 보장할 수 있습니다.

변형 쿼리는 스레드 세이프가 아닙니다.

충돌에 대해서는 [잠금-대기 제한 시간을 이용한 충돌 해결](transactions-neptune.md#transactions-neptune-conflicts)을 참조하세요.

변형 쿼리는 실패 시 자동으로 재시도되지 않습니다.

# 예외에 대한 재시도 로직
<a name="best-practices-opencypher-retry-logic"></a>

재시도를 허용하는 모든 예외에 대해서는 일반적으로 `ConcurrentModificationException` 오류와 같은 일시적인 문제를 더 잘 처리하기 위해 재시도 간 대기 시간을 점진적으로 늘리는 [지수 백오프 및 재시도 전략](https://docs.aws.amazon.com/general/latest/gr/api-retries.html)을 사용하는 것이 가장 좋습니다. 다음은 지수 백오프 및 재시도 패턴의 예제입니다.

```
public static void main() {
  try (Driver driver = getDriver(HOST_BOLT, getDefaultConfig())) {
    retriableOperation(driver, "CREATE (n {prop:'1'})")
        .withRetries(5)
        .withExponentialBackoff(true)
        .maxWaitTimeInMilliSec(500)
        .call();
  }
}

protected RetryableWrapper retriableOperation(final Driver driver, final String query){
  return new RetryableWrapper<Void>() {
    @Override
    public Void submit() {
      log.info("Performing graph Operation in a retry manner......");
      try (Session session = driver.session(writeSessionConfig)) {
        try (Transaction trx =  session.beginTransaction()) {
            trx.run(query).consume();
            trx.commit();
        }
      }
      return null;
    }

    @Override
    public boolean isRetryable(Exception e) {
      if (isCME(e)) {
        log.debug("Retrying on exception.... {}", e);
        return true;
      }
      return false;
    }

    private boolean isCME(Exception ex) {
      return ex.getMessage().contains("Operation failed due to conflicting concurrent operations");
    }
  };
}



/**
 * Wrapper which can retry on certain condition. Client can retry operation using this class.
 */
@Log4j2
@Getter
public abstract class RetryableWrapper<T> {

  private long retries = 5;
  private long maxWaitTimeInSec = 1;
  private boolean exponentialBackoff = true;

  /**
   * Override the method with custom implementation, which will be called in retryable block.
   */
  public abstract T submit() throws Exception;

  /**
   * Override with custom logic, on which exception to retry with.
   */
  public abstract boolean isRetryable(final Exception e);

  /**
   * Define the number of retries.
   *
   * @param retries -no of retries.
   */
  public RetryableWrapper<T> withRetries(final long retries) {
    this.retries = retries;
    return this;
  }

  /**
   * Max wait time before making the next call.
   *
   * @param time - max polling interval.
   */
  public RetryableWrapper<T> maxWaitTimeInMilliSec(final long time) {
    this.maxWaitTimeInSec = time;
    return this;
  }

  /**
   * ExponentialBackoff coefficient.
   */
  public RetryableWrapper<T> withExponentialBackoff(final boolean expo) {
    this.exponentialBackoff = expo;
    return this;
  }

  /**
   * Call client method which is wrapped in submit method.
   */
  public T call() throws Exception {
    int count = 0;
    Exception exceptionForMitigationPurpose = null;
    do {
      final long waitTime = exponentialBackoff ? Math.min(getWaitTimeExp(retries), maxWaitTimeInSec) : 0;
      try {
          return submit();
      } catch (Exception e) {
        exceptionForMitigationPurpose = e;
        if (isRetryable(e) && count < retries) {
          Thread.sleep(waitTime);
          log.debug("Retrying on exception attempt - {} on exception cause - {}", count, e.getMessage());
        } else if (!isRetryable(e)) {
          log.error(e.getMessage());
          throw new RuntimeException(e);
        }
      }
    } while (++count < retries);

    throw new IOException(String.format(
          "Retry was unsuccessful.... attempts %d. Hence throwing exception " + "back to the caller...", count),
          exceptionForMitigationPurpose);
  }

  /*
   * Returns the next wait interval, in milliseconds, using an exponential backoff
   * algorithm.
   */
  private long getWaitTimeExp(final long retryCount) {
    if (0 == retryCount) {
      return 0;
    }
    return ((long) Math.pow(2, retryCount) * 100L);
  }
}
```

# 단일 SET 절을 사용하여 한 번에 여러 속성 설정
<a name="best-practices-content-0"></a>

 여러 SET 절을 사용하여 개별 속성을 설정하는 대신 맵을 사용하여 개체에 대한 여러 속성을 한 번에 설정합니다.

 다음을 사용할 수 있습니다.

```
MATCH (n:SomeLabel {`~id`: 'id1'})
SET n += {property1 : 'value1',
property2 : 'value2',
property3 : 'value3'}
```

 다음 사용 안 함: 

```
MATCH (n:SomeLabel {`~id`: 'id1'})
SET n.property1 = 'value1'
SET n.property2 = 'value2'
SET n.property3 = 'value3'
```

 SET 절은 단일 속성 또는 맵을 허용합니다. 단일 개체에서 여러 속성을 업데이트하는 경우 맵과 함께 단일 SET 절을 사용하면 여러 작업 대신 단일 작업으로 업데이트를 수행할 수 있으며, 이를 보다 효율적으로 실행할 수 있습니다.

## SET 절을 사용하여 한 번에 여러 속성 제거
<a name="best-practices-content-1"></a>

 openCypher 언어를 사용하는 경우 REMOVE는 개체에서 속성을 제거하는 데 사용됩니다. Neptune에서 제거되는 각 속성에는 별도의 작업이 필요하므로 쿼리 지연 시간이 추가됩니다. 대신 맵과 함께 SET를 사용하여 모든 속성 값을 `null`로 설정할 수 있습니다. 이는 Neptune에서 속성을 제거하는 것과 같습니다. 단일 개체에서 여러 속성을 제거해야 하는 경우 Neptune의 성능이 향상됩니다.

다음 사용:

```
WITH {prop1: null, prop2: null, prop3: null} as propertiesToRemove 
MATCH (n) 
SET n += propertiesToRemove
```

다음 사용 안 함:

```
MATCH (n) 
REMOVE n.prop1, n.prop2, n.prop3
```

# 파라미터화된 쿼리 사용
<a name="best-practices-content-2"></a>

 openCypher를 사용하여 쿼리할 때는 항상 파라미터화된 쿼리를 사용하는 것이 좋습니다. 쿼리 엔진은 쿼리 계획 캐시와 같은 특성을 위해 반복되는 파라미터화된 쿼리를 활용할 수 있습니다. 즉, 서로 다른 파라미터로 동일한 파라미터화된 구조를 반복적으로 간접 호출할 때 캐시된 계획을 활용할 수 있습니다. 파라미터화된 쿼리에 대해 생성된 쿼리 계획은 100밀리초 이내에 완료되고 파라미터 유형이 NUMBER, BOOLEAN 또는 STRING인 경우에만 캐시되고 재사용됩니다.

다음 사용:

```
MATCH (n:foo) WHERE id(n) = $id RETURN n
```

파라미터 포함:

```
parameters={"id": "first"}
parameters={"id": "second"}
parameters={"id": "third"}
```

다음 사용 안 함:

```
MATCH (n:foo) WHERE id(n) = "first" RETURN n
MATCH (n:foo) WHERE id(n) = "second" RETURN n
MATCH (n:foo) WHERE id(n) = "third" RETURN n
```

# UNWIND 절에서 중첩 맵 대신 평면화된 맵 사용
<a name="best-practices-content-3"></a>

 심층 중첩 구조는 쿼리 엔진이 최적의 쿼리 계획을 생성하는 기능을 제한할 수 있습니다. 이 문제를 부분적으로 완화하기 위해 다음과 같이 정의된 패턴은 다음 시나리오에 대한 최적의 계획을 생성합니다.
+  시나리오 1: NUMBER, STRING 및 BOOLEAN을 포함하는 사이퍼 리터럴 목록으로 UNWIND.
+  시나리오 2: 사이퍼 리터럴(NUMBER, STRING, BOOLEAN)만 값으로 포함하는 평면화된 맵 목록으로 UNWIND.

 UNWIND 절이 포함된 쿼리를 작성할 때는 위의 권장 사항을 사용하여 성능을 개선합니다.

시나리오 1 예제:

```
UNWIND $ids as x
MATCH(t:ticket {`~id`: x})
```

파라미터 포함:

```
parameters={
  "ids": [1, 2, 3]
}
```

 시나리오 2의 예제는 CREATE 또는 MERGE할 노드 목록을 생성하는 것입니다. 여러 문을 발행하는 대신, 속성을 평탄화된 맵 집합으로 정의하는 다음 패턴을 사용하세요.

```
UNWIND $props as p
CREATE(t:ticket {title: p.title, severity:p.severity})
```

파라미터 포함:

```
parameters={
  "props": [
    {"title": "food poisoning", "severity": "2"},
    {"title": "Simone is in office", "severity": "3"}
  ]
}
```

다음과 같은 중첩 노드 객체 사용 안 함:

```
UNWIND $nodes as n
CREATE(t:ticket n.properties)
```

파라미터 포함:

```
parameters={
  "nodes": [
    {"id": "ticket1", "properties": {"title": "food poisoning", "severity": "2"}},
    {"id": "ticket2", "properties": {"title": "Simone is in office", "severity": "3"}}
  ]
}
```

# 가변 길이 경로(VLP) 표현식의 왼쪽에 더 제한적인 노드 배치
<a name="best-practices-content-4"></a>

 가변 길이 경로(VLP) 쿼리에서 쿼리 엔진은 표현식의 왼쪽 또는 오른쪽에서 순회를 시작하도록 선택하여 평가를 최적화합니다. 결정은 왼쪽과 오른쪽에 있는 패턴의 카디널리티를 기반으로 합니다. 카디널리티는 지정된 패턴과 일치하는 노드 수입니다.
+  오른쪽 패턴의 카디널리티가 1인 경우 오른쪽이 시작점이 됩니다.
+  왼쪽과 오른쪽의 카디널리티가 1인 경우 양쪽에서 확장이 확인되고 작은 확장으로 양쪽에서 시작됩니다. 확장은 VLP 표현식의 왼쪽에 있는 노드와 오른쪽에 있는 노드의 발신 또는 수신 엣지 수입니다. 최적화의 이 부분은 VLP 관계가 단방향이고 관계 유형이 제공된 경우에만 사용됩니다.
+  그렇지 않으면 왼쪽이 시작점이 됩니다.

 VLP 표현식 체인의 경우이 최적화는 첫 번째 표현식에만 적용할 수 있습니다. 다른 VLP는 왼쪽부터 평가를 시작합니다. 예를 들어 (a), (b)의 카디널리티가 1이고 (c)의 카디널리티가 1보다 크도록 합니다.
+  `(a)-[*1..]->(c)`: 평가는 (a)로 시작합니다.
+  `(c)-[*1..]->(a)`: 평가는 (a)로 시작합니다.
+  `(a)-[*1..]-(c)`: 평가는 (a)로 시작합니다.
+  `(c)-[*1..]-(a)`: 평가는 (a)로 시작합니다.

 이제 (a)의 수신 엣지가 2이고, (a)의 발신 엣지가 3이고, (b)의 수신 엣지가 4이고, (b)의 발신 엣지가 5가 되도록 합니다.
+  `(a)-[*1..]->(b)`: (a)의 발신 엣지가 (b)의 수신 엣지보다 작으면 평가가 (a)로 시작됩니다.
+  `(a)<-[*1..]-(b)`: (a)의 수신 엣지가 (b)의 발신 엣지보다 작으면 평가가 (a)로 시작됩니다.

 일반적으로 VLP 표현식의 왼쪽에 보다 제한적인 패턴을 배치합니다.

# 세분화된 관계 이름을 사용하여 중복 노드 레이블 확인 방지
<a name="best-practices-content-5"></a>

 성능 최적화를 위해 노드 패턴에 고유한 관계 레이블을 사용하면 노드에 대한 레이블 필터링을 제거할 수 있습니다. 그래프 모델에서 `likes` 관계가 두 `person` 노드 간의 관계 정의에만 사용된다고 가정해 보세요. 이 패턴을 찾기 위해 다음과 같은 쿼리를 작성할 수 있습니다.

```
MATCH (n:person)-[:likes]->(m:person)
RETURN n, m
```

 `person` 레이블 검사는 n과 m 모두 `person` 유형일 때만 관계가 나타나도록 정의했으므로 중복됩니다. 성능을 최적화하기 위해 다음과 같이 쿼리를 작성할 수 있습니다.

```
MATCH (n)-[:likes]->(m)
RETURN n, m
```

 이 패턴은 속성이 단일 노드 레이블에 독점적으로 적용될 때도 적용될 수 있습니다. `person` 노드만 `email` 속성을 가질 경우, 노드 레이블이 `person`과 일치하는지 확인하는 것은 중복됩니다. 이 쿼리를 다음과 같이 작성할 수 있습니다.

```
MATCH (n:person)
WHERE n.email = 'xxx@gmail.com'
RETURN n
```

 이 쿼리를 다음과 같이 작성하는 것보다 효율성이 떨어집니다.

```
MATCH (n)
WHERE n.email = 'xxx@gmail.com'
RETURN n
```

 성능이 중요하고 모델링 과정에서 이러한 엣지 레이블이 다른 노드 레이블 관련 패턴에 재사용되지 않도록 확인 절차가 마련된 경우에만 이 패턴을 채택해야 합니다. 이후 `company`와 같은 다른 노드 레이블에 `email` 속성을 추가하면 두 버전의 쿼리 결과는 달라질 것입니다.

# 가능한 경우 엣지 레이블 지정
<a name="best-practices-content-6"></a>

 패턴에서 엣지를 지정할 때 가능하면 엣지 레이블을 제공하는 것이 좋습니다. 도시에 거주하는 모든 사람을 해당 도시를 방문한 모든 사람과 연결하는 데 사용되는 다음 예제 쿼리를 생각해 보세요.

```
MATCH (person)-->(city {country: "US"})-->(anotherPerson)
RETURN person, anotherPerson
```

 그래프 모델이 여러 엣지 레이블을 사용하여 도시 이외의 노드에 사람을 연결하는 경우 엔드 레이블을 지정하지 않으면 Neptune은 나중에 폐기될 추가 경로를 평가해야 합니다. 위 쿼리에서 엣지 레이블이 지정되지 않았으므로 엔진은 먼저 더 많은 작업을 수행한 다음 값을 필터링하여 올바른 결과를 얻습니다. 위 쿼리의 더 나은 버전은 다음과 같습니다.

```
MATCH (person)-[:livesIn]->(city {country: "US"})-[:visitedBy]->(anotherPerson)
RETURN person, anotherPerson
```

 이렇게 하면 평가에 도움이 될 뿐만 아니라 쿼리 플래너가 더 나은 계획을 만들 수 있습니다. 이 모범 사례를 중복 노드 레이블 검사와 결합하여 도시 레이블 검사를 제거하고 쿼리를 다음과 같이 작성할 수도 있습니다.

```
MATCH (person)-[:livesIn]->({country: "US"})-[:visitedBy]->(anotherPerson)
RETURN person, anotherPerson
```

# 가능한 경우 WITH 절 사용 안 함
<a name="best-practices-content-7"></a>

 openCypher의 WITH 절은 모든 항목이 실행되기 전에 결과 값이 쿼리의 나머지 부분으로 전달되는 경계 역할을 합니다. WITH 절은 중간 집계가 필요하거나 결과 수를 제한하려는 경우에 필요하지만 WITH 절을 사용하지 않도록 해야 합니다. 일반적인 지침은 이러한 간단한 WITH 절(집계, 순서 또는 제한 없음)을 제거하여 쿼리 플래너가 전체 쿼리에서 작업하여 전역적으로 최적의 계획을 생성할 수 있도록 하는 것입니다. 예를 들어 `India`에 거주하는 모든 사람을 반환하는 쿼리를 작성했다고 가정해 보겠습니다.

```
MATCH (person)-[:lives_in]->(city)
WITH person, city
MATCH (city)-[:part_of]->(country {name: 'India'})
RETURN collect(person) AS result
```

 위 버전에서 WITH 절은 패턴 `(person)-[:lives_in]->(city)`(더 제한적인 조건)을 `(city)-[:part_of]->(country {name: 'India'})` 앞에 배치하도록 강제합니다. 이렇게 하면 계획이 최적화되지 않습니다. 이 쿼리를 최적화하려면 WITH 절을 제거하고 플래너가 최상의 실행 계획을 계산하도록 해야 합니다.

```
MATCH (person)-[:lives_in]->(city)
MATCH (city)-[:part_of]->(country {name: 'India'})
RETURN collect(person) AS result
```

# 쿼리에서 가능한 한 빨리 제한 필터를 배치합니다.
<a name="best-practices-content-8"></a>

 모든 시나리오에서 쿼리 내 필터의 조기 배치로 인해 쿼리 계획이 고려해야 하는 중간 해법을 줄일 수 있습니다. 즉, 쿼리를 실행하는 데 필요한 메모리와 컴퓨팅 리소스가 줄어듭니다.

 다음 예제는 이러한 영향을 이해하는 데 도움이 됩니다. `India`에 거주하는 모든 사람을 반환하는 쿼리를 작성한다고 가정해 보겠습니다. 쿼리의 한 가지 버전은 다음과 같습니다.

```
MATCH (n)-[:lives_in]->(city)-[:part_of]->(country)
WITH country, collect(n.firstName + " "  + n.lastName) AS result
WHERE country.name = 'India'
RETURN result
```

 위의 쿼리 버전은 이 사용 사례를 달성하는 가장 좋은 방법이 아닙니다. `country.name = 'India'` 필터는 나중에 쿼리 패턴에 나타납니다. 먼저 모든 사람과 거주 지역을 수집하여 국가별로 그룹화한 `country.name = India`의 그룹만 필터링합니다. `India`에 거주하는 사람만 쿼리한 다음 수집 집계를 수행하는 최적의 방법입니다.

```
MATCH (n)-[:lives_in]->(city)-[:part_of]->(country)
WHERE country.name = 'India'
RETURN collect(n.firstName + " "  + n.lastName) AS result
```

 일반적인 규칙은 변수가 도입된 후 가능한 한 빨리 필터를 배치하는 것입니다.

# 속성이 존재하는지 명시적으로 확인
<a name="best-practices-content-9"></a>

 openCypher 의미 체계를 기반으로 속성에 액세스할 때 이는 선택적 조인과 동일하며 속성이 없더라도 모든 행을 유지해야 합니다. 그래프 스키마를 기반으로 해당 개체에 대해 특정 속성이 항상 존재할 것임을 알고 있는 경우 해당 속성의 존재 여부를 명시적으로 확인하면 쿼리 엔진이 최적의 계획을 생성하고 성능을 개선할 수 있습니다.

 `person` 유형의 노드에 항상 `name` 속성이 있는 그래프 모델을 생각해 보세요. 다음 사용 안 함: 

```
MATCH (n:person)
RETURN n.name
```

 IS NOT NULL 검사를 사용하여 쿼리의 속성 존재를 명시적으로 확인합니다.

```
MATCH (n:person)
WHERE n.name IS NOT NULL
RETURN n.name
```

# 명명된 경로를 사용하지 않음(필수가 아닌 경우).
<a name="best-practices-content-10"></a>

 쿼리의 명명된 경로는 항상 추가 비용이 발생하므로 지연 시간 및 메모리 사용량이 높을수록 페널티가 부과될 수 있습니다. 다음과 같은 쿼리를 가정합니다.

```
MATCH p = (n)-[:commentedOn]->(m)
WITH p, m, n, n.score + m.score as total
WHERE total > 100 
MATCH (m)-[:commentedON]->(o)
WITH p, m, n, distinct(o) as o1
RETURN p, m.name, n.name, o1.name
```

 위의 쿼리에서는 노드의 속성만 알고 싶다고 가정할 때 경로 "p"를 사용할 필요가 없습니다. 명명된 경로를 변수로 지정하면 DISTINCT를 사용하는 집계 작업은 시간과 메모리 사용량 측면에서 비용이 많이 듭니다. 위 쿼리의 보다 최적화된 버전은 다음과 같을 수 있습니다.

```
MATCH (n)-[:commentedOn]->(m)
WITH m, n, n.score + m.score as total
WHERE total > 100 
MATCH (m)-[:commentedON]->(o)
WITH m, n, distinct(o) as o1
RETURN m.name, n.name, o1.name
```

# COLLECT(DISTINCT()) 사용 안 함
<a name="best-practices-content-11"></a>

**참고**  
엔진 버전 [ 1.4.7.0](engine-releases-1.4.7.0.md)부터이 권장 재작성은 더 이상 필요하지 않습니다.

 COLLECT(DISTINCT())는 고유한 값을 포함하는 목록을 구성할 때마다 사용됩니다. COLLECT는 집계 함수이며 동일한 문에 프로젝션되는 추가 키를 기반으로 그룹화가 수행됩니다. 고유를 사용하면 입력이 여러 청크로 분할되며, 각 청크는 축소할 그룹 하나를 나타냅니다. 그룹 수가 증가하면 성능에 영향을 미칩니다. Neptune에서는 목록을 실제로 수집/구성하기 전에 DISTINCT를 수행하는 것이 훨씬 더 효율적입니다. 이렇게 하면 전체 청크의 그룹화 키에서 직접 그룹화를 수행할 수 있습니다.

 다음과 같은 쿼리를 가정합니다.

```
MATCH (n:Person)-[:commented_on]->(p:Post)
WITH n, collect(distinct(p.post_id)) as post_list
RETURN n, post_list
```

 이 쿼리를 작성하는 보다 최적의 방법은 다음과 같습니다.

```
MATCH (n:Person)-[:commented_on]->(p:Post)
WITH DISTINCT n, p.post_id as postId
WITH n, collect(postId) as post_list
RETURN n, post_list
```

# 모든 속성 값을 검색할 때 개별 속성 조회보다 속성 함수를 선호합니다.
<a name="best-practices-content-12"></a>

 `properties()` 함수는 개체의 모든 속성이 포함된 맵을 반환하는 데 사용되며 속성을 개별적으로 반환하는 것보다 훨씬 효율적입니다.

 `Person` 노드에, `firstName`, `lastName`, `age`, `dept`, `company`의 5개 속성이 있다고 가정하면 다음 쿼리가 선호됩니다.

```
MATCH (n:Person)
WHERE n.dept = 'AWS'
RETURN properties(n) as personDetails
```

 다음 사용 안 함: 

```
MATCH (n:Person)
WHERE n.dept = 'AWS'
RETURN n.firstName, n.lastName, n.age, n.dept, n.company
    
=== OR ===
    
MATCH (n:Person)
WHERE n.dept = 'AWS'
RETURN {firstName: n.firstName, lastName: n.lastName, age: n.age, 
department: n.dept, company: n.company} as personDetails
```

# 쿼리 외부에서 정적 계산 수행
<a name="best-practices-content-13"></a>

 클라이언트 측에서 정적 계산(단순 수학/문자열 작업)을 해결하는 것이 좋습니다. 작성자보다 1세 이하인 모든 사람을 찾으려는 경우 이 예제를 고려해 보세요.

```
MATCH (m:Message)-[:HAS_CREATOR]->(p:person)
WHERE p.age <= ($age + 1)
RETURN m
```

 여기서 `$age`는 파라미터를 통해 쿼리에 주입된 다음 고정 값에 추가됩니다. 그런 다음 이 값을 `p.age`와 비교합니다. 대신 클라이언트 측에서 추가를 수행하고 계산된 값을 파라미터 \$1ageplusone으로 전달하는 것이 더 좋습니다. 이렇게 하면 쿼리 엔진이 최적화된 계획을 생성하고 들어오는 각 행에 대한 정적 계산을 방지할 수 있습니다. 이러한 지침에 따라 쿼리의 보다 효율적인 동사는 다음과 같습니다.

```
MATCH (m:Message)-[:HAS_CREATOR]->(p:person)
WHERE p.age <= $ageplusone
RETURN m
```

# 개별 문 대신 UNWIND를 사용한 배치 입력
<a name="best-practices-content-14"></a>

 다른 입력에 대해 동일한 쿼리를 실행해야 할 때마다 입력당 하나의 쿼리를 실행하는 대신 입력 배치에 대한 쿼리를 실행하는 것이 훨씬 더 좋습니다.

 노드 세트에서 병합하려는 경우 한 가지 옵션은 입력당 병합 쿼리를 실행하는 것입니다.

```
MERGE (n:Person {`~id`: $id})
SET n.name = $name, n.age = $age, n.employer = $employer
```

 파라미터 포함: 

```
params = {id: '1', name: 'john', age: 25, employer: 'Amazon'}
```

 위의 쿼리는 모든 입력에 대해 실행되어야 합니다. 이 접근 방식이 작동하는 동안 대규모 입력 세트에 대해 많은 쿼리를 실행해야 할 수 있습니다. 이 시나리오에서 일괄 처리는 서버에서 실행되는 쿼리 수를 줄이고 전체 처리량을 개선하는 데 도움이 될 수 있습니다.

 다음 패턴을 사용합니다.

```
UNWIND $persons as person
MERGE (n:Person {`~id`: person.id})
SET n += person
```

 파라미터 포함: 

```
params = {persons: [{id: '1', name: 'john', age: 25, employer: 'Amazon'}, 
{id: '2', name: 'jack', age: 28, employer: 'Amazon'},
{id: '3', name: 'alice', age: 24, employer: 'Amazon'}...]}
```

 워크로드에 가장 적합한 것을 결정하려면 다양한 배치 크기로 실험하는 것이 좋습니다.

# 노드/관계에 사용자 정의 ID 사용 선호
<a name="best-practices-content-15"></a>

 Neptune은 사용자가 노드와 관계에 ID를 명시적으로 할당할 수 있도록 합니다. ID는 데이터세트에서 전역적으로 고유해야 하며 유용하도록 결정적이어야 합니다. 결정적 ID는 속성과 마찬가지로 조회 또는 필터링 메커니즘으로 사용할 수 있지만, ID 사용은 속성을 사용하는 것보다 쿼리 실행 관점에서 훨씬 더 최적화되어 있습니다. 사용자 정의 ID를 사용하면 여러 가지 이점이 있습니다.
+  속성은 기존 개체에 대해 null일 수 있지만 ID가 있어야 합니다. 이렇게 하면 쿼리 엔진이 실행 중에 최적화된 조인을 사용할 수 있습니다.
+  동시 변형 쿼리를 실행하면 강제 고유성으로 인해 속성보다 IDs에 대한 잠금이 더 적기 때문에 ID를 사용하여 노드에 액세스할 때 [동시 수정 예외](https://docs.aws.amazon.com//neptune/latest/userguide/transactions-exceptions.html)(CMEs)가 발생할 가능성이 크게 줄어듭니다.
+  ID를 사용하면 중복 데이터 생성 가능성을 피할 수 있습니다. 왜냐하면 Neptune은 속성과 달리 ID에 대해 고유성을 강제하기 때문입니다.

 다음 쿼리 예제에서는 사용자 지정 ID를 사용합니다.

**참고**  
 `~id` 속성은 ID를 지정하는 데 사용되는 반면, `id`는 다른 속성과 마찬가지로 단순히 저장됩니다.

```
CREATE (n:Person {`~id`: '1', name: 'alice'})
```

 사용자 지정 ID를 사용하지 않는 경우: 

```
CREATE (n:Person {id: '1', name: 'alice'})
```

 후자의 메커니즘을 사용하는 경우 고유성 적용이 없으며 나중에 쿼리를 실행할 수 있습니다.

```
CREATE (n:Person {id: '1', name: 'john'})
```

 이로 인해 `id=1`이 `john`인 두 번째 노드가 생성됩니다. 이 시나리오에서는 이제 `id=1`을 가진 두 개의 노드가 존재하게 되며, 각각 다른 이름(alice와 john)을 가집니다.

# 쿼리에서 \$1id 계산 수행 안 함
<a name="best-practices-content-16"></a>

 쿼리에서 사용자 지정 ID를 사용하는 경우 항상 쿼리 외부에서 정적 계산을 수행하고 파라미터에 이러한 값을 제공합니다. 정적 값이 제공되면 엔진이 조회를 최적화하고 이러한 값을 스캔하고 필터링하지 않아도 됩니다.

 데이터베이스에 있는 노드 간에 엣지를 생성하려는 경우 한 가지 옵션은 다음과 같습니다.

```
UNWIND $sections as section
MATCH (s:Section {`~id`: 'Sec-' + section.id})
MERGE (s)-[:IS_PART_OF]->(g:Group {`~id`: 'g1'})
```

 파라미터 포함: 

```
parameters={sections: [{id: '1'}, {id: '2'}]}
```

 위의 쿼리에서 섹션의 `id`가 쿼리 내에서 계산되고 있습니다. 계산은 동적이므로 엔진은 정적 인라인 ID를 사용할 수 없으며 결국 모든 섹션 노드를 스캔합니다. 그런 다음 엔진은 필요한 노드에 대해 후처리 필터링을 수행합니다. 데이터베이스에 섹션 노드가 많은 경우 비용이 많이 들 수 있습니다.

 이를 달성하는 더 나은 방법은 데이터베이스로 전달되는 id 앞에 `Sec-`를 접두사로 추가하는 것입니다.

```
UNWIND $sections as section
MATCH (s:Section {`~id`: section.id})
MERGE (s)-[:IS_PART_OF]->(g:Group {`~id`: 'g1'})
```

 파라미터 포함: 

```
parameters={sections: [{id: 'Sec-1'}, {id: 'Sec-2'}]}
```

# 여러 노드 업데이트/병합
<a name="best-practices-merge-multiple-nodes"></a>

 여러 노드에서 `MERGE` 또는 `CREATE` 쿼리를 실행할 때는 각 노드마다 MERGE/CREATE 절을 사용하는 대신 단일 MERGE/CREATE 절과 함께 `UNWIND`를 사용하는 것이 좋습니다. 노드마다 하나의 절을 사용하는 쿼리는 각 행에 대한 최적화가 필요하기 때문에 비효율적인 실행 계획을 초래합니다. 이로 인해 쿼리 실행 시간의 대부분이 실제 업데이트가 아닌 정적 처리 단계에서 소모됩니다.

 노드당 하나의 절 방식은 노드 수가 증가함에 따라 확장성이 떨어지므로 최적의 방법이 아닙니다.

```
MERGE (p1:Person {name: 'NameA'})
ON CREATE SET p1 += {prop1: 'prop1V1', prop2: 'prop2V1'}
MERGE (p2:Person {name: 'NameB'})
ON CREATE SET p2 += {prop1: 'prop1V2', prop2: 'prop2V2'}
MERGE (p3:Person {name: 'NameC'})
ON CREATE SET p3 += {prop1: 'prop1V3', prop2: 'prop1V3'}
```

 `UNWIND`를 하나의 MERGE/CREATE 절과 함께 사용하면 동일한 동작을 유지하면서도 더 최적화된 실행 계획을 얻을 수 있습니다. 이를 고려하여 변경된 쿼리는 다음과 같습니다.

```
## If not using custom id for nodes/relationship
UNWIND [{name: 'NameA', prop1: 'prop1V1', prop2: 'prop2V1'}, {name: 'NameB', prop1: 'prop1V2', prop2: 'prop2V2'}, {name: 'NameC', prop1: 'prop1V3', prop2: 'prop1V3'}] AS props
MERGE (p:Person {name: props.name})
ON CREATE SET p = props

## If using custom id for nodes/relationship
UNWIND [{`~id`: '1', 'name': 'NameA', 'prop1: 'prop1V1', prop2: 'prop2V1'}, {`~id`: '2', name: 'NameB', prop1: 'prop1V2', prop2: 'prop2V2'}, {`~id`: '3', name: 'NameC', prop1: 'prop1V3', prop2: 'prop1V3'}] AS props
MERGE (p:Person {`~id`: props.id})
ON CREATE SET p = removeKeyFromMap(props, '~id')
```