翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。
OpenCypher と Bolt を使用した Neptune のベストプラクティス
Neptune で openCypher クエリ言語と Bolt プロトコルを使用する場合は、これらのベストプラクティスに従ってください。Neptune で openCypher を使用する方法については、を使用した Neptune グラフへのアクセス openCypher を参照してください。
クエリでは双方向のエッジを優先する
Neptune がクエリの最適化を行う場合、双方向のエッジでは、最適なクエリプランを作成することが難しくなります。最適ではないプランでは、エンジンが不必要な作業を行う必要があり、その結果、パフォーマンスが低下します。
そのため、可能な限り、双方向のエッジではなく有向エッジを使用してください。例えば
MATCH p=(:airport {code: 'ANC'})-[:route]->(d) RETURN p)
ではなく、を使用します。
MATCH p=(:airport {code: 'ANC'})-[:route]-(d) RETURN p)
ほとんどのデータモデルは実際には両方向のエッジをトラバースする必要はないため、有向エッジを使用するように切り替えることでクエリのパフォーマンスを大幅に向上させることができます。
データモデルで双方向のエッジをトラバースする必要がある場合は、MATCH
パターン内の最初のノード (左側) をフィルタリングの制限が最も厳しいノードにします。
「routes
空港と ANC
空港の間のすべてのルートを見つけて」という例を考えてみましょう。ANC
空港から出発する場合、このクエリは次のようになります。
MATCH p=(src:airport {code: 'ANC'})-[:route]-(d) RETURN p
最も制限の厳しいノードがパターン内の最初のノード (左側) に配置されるため、エンジンは最小限の作業でクエリを満たすことができます。その後、エンジンはクエリを最適化できます。
これは、次のように、パターンの最後で ANC
空港をフィルタリングするよりもはるかに望ましい方法です。
MATCH p=(d)-[:route]-(src:airport {code: 'ANC'}) RETURN p
最も制限の厳しいノードがパターン内の最初に配置されない場合、エンジンはクエリを最適化できず、結果を得るために追加の検索を実行する必要があるため、追加の作業を行う必要があります。
Neptune は 1 つのトランザクションでの複数の同時クエリをサポートしていません。
Bolt ドライバー自体ではトランザクション内での同時クエリが可能ですが、Neptune は同時に実行されているトランザクション内の複数のクエリをサポートしていません。その代わり、Neptune では、トランザクション内の複数のクエリを順次実行し、各クエリの結果を次のクエリが開始される前に完全に処理する必要があります。
以下の例は、Bolt を使用して 1 つのトランザクションで複数のクエリを連続して実行する方法を示しています。これにより、次のクエリが始まる前にそれぞれの結果が完全に消費されます。
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()); } } }
フェイルオーバー後の新しい接続の作成
フェイルオーバーが発生した場合、DNS 名が特定の IP アドレスに解決されるため、Bolt ドライバーは新しいアクティブなライターインスタンスではなく古いライターインスタンスに接続し続けることがあります。
これを防ぐには、フェイルオーバー後に Driver
オブジェクトを閉じて、再接続します。
存続期間の長いアプリケーションの接続処理
コンテナ内や Amazon EC2 インスタンスで実行されるアプリケーションなど、長期間有効なアプリケーションを構築する場合は、Driver
オブジェクトを一度インスタンス化し、そのオブジェクトをアプリケーションの存続期間中再利用します。Driver
オブジェクトはスレッドセーフで、初期化にはかなりのオーバーヘッドがかかります。
の接続処理 AWS Lambda
接続オーバーヘッドと管理要件のため、Bolt AWS Lambda ドライバーを機能内で使用することはお勧めしません。代わりに HTTPS エンドポイントを使用してください。
完了したら、ドライバーオブジェクトを閉じます
Bolt 接続がサーバーによって閉じられ、その接続に関連付けられているリソースがすべて解放されるように、終了時にはクライアントを閉じることが重要です。driver.close()
を使用してドライバーを閉じると、この処理が自動的に行われます。
ドライバーが正しく閉じられていない場合、Neptune は 20 分後に、または IAM 認証を使用している場合は 10 日後に、アイドル状態の Bolt 接続をすべて終了します。
Neptune がサポートする Bolt の同時接続数は 1000 個までです。終了後に接続を明示的に閉じず、ライブ接続の数が 1000 という制限数に達すると、新しい接続試行は失敗します。
読み取りと書き込みには明示的なトランザクションモードを使用してください。
Neptune と Bolt ドライバーでトランザクションを使用するときは、読み取りトランザクションと書き込みトランザクションの両方のアクセスモードを適切な設定に明示的に設定するのが最善です。
読み取り専用トランザクション
読み取り専用トランザクションでは、セッションを構築するときに適切なアクセスモード構成を渡さないと、デフォルトの分離レベル、つまりミューテーションクエリ分離が使用されます。そのため、読み取り専用トランザクションでは、アクセスモードを 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)
} } );
いずれの場合も、SNAPSHOT 分離は Neptune の読み取り専用トランザクションセマンティクスを使用して実現されます。
リードレプリカは読み取り専用クエリしか受け付けないため、リードレプリカに送信されるクエリはすべて SNAPSHOT
分離セマンティクスで実行されます。
読み取り専用トランザクションには、ダーティリードや繰り返し不可能なリードはありません。
読み取り専用トランザクション
ミューテーションクエリでは、書き込みトランザクションを作成するための 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
トランザクション分離のもとで実行されます。ミューテーションクエリの一部として行われる読み取りにはダーティリードはありません。
ミューテーションクエリを読み込むと、レコードとレコード範囲はロックされます。
つまり、インデックスの範囲がミューテーショントランザクションによって読み取られた場合、この範囲は読み取りトランザクションが終了するまで同時トランザクションによって変更されないという強力な保証があります。
ミューテーションクエリはスレッドセーフではありません。
コンフリクトについては、ロック待機タイムアウトを使用した競合の解決 を参照してください。
ミューテーションクエリは、失敗しても自動的に再試行されません。
例外の場合の再試行ロジック
再試行を許可するすべての例外については、ConcurrentModificationException
エラーなどの一時的な問題をより適切に処理するために、再試行までの待機時間を徐々に長くするエクスポネンシャルバックオフおよび再試行戦略を使用するのが一般的には最適です。以下は、エクスポネンシャルバックオフおよび再試行のパターンの例を示しています。
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); } }
1 つの SET 句で複数のプロパティを一度に設定できます。
複数の 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 句は 1 つのプロパティまたはマップのいずれかを受け入れます。1 つのエンティティの複数のプロパティを更新する場合、マップに 1 つの SET 句を使用すると、更新を複数の操作ではなく 1 回の操作で実行できるため、より効率的に実行できます。
SET 句を使用すると、複数のプロパティを一度に削除できます。
OpenCypher 言語を使用する場合、REMOVE を使用してエンティティからプロパティを削除します。Neptune では、削除するプロパティごとに個別の操作が必要になり、クエリの待ち時間が長くなります。代わりに SET をマップと一緒に使用してすべてのプロパティ値をに設定できます。Neptune ではnull
、これはプロパティを削除するのと同じです。1 つのエンティティの複数のプロパティを削除する必要がある場合、Neptune のパフォーマンスは向上します。
使用アイテム:
WITH {prop1: null, prop2: null, prop3: null} as propertiesToRemove MATCH (n) SET n += propertiesToRemove
代わりに:
MATCH (n) REMOVE n.prop1, n.prop2, n.prop3
パラメータ化されたクエリを使う
OpenCypher を使用してクエリを実行する場合は、常にパラメータ化されたクエリを使用することをおすすめします。クエリエンジンは、クエリプランキャッシュなどの機能に対して、パラメータ化されたクエリを繰り返し利用できます。同じパラメータ化された構造を異なるパラメータで繰り返し呼び出すと、キャッシュされたプランを利用できます。パラメータ化されたクエリ用に生成されたクエリプランは、100 ミリ秒以内に完了し、パラメータータイプが数値、BOOLEAN、または文字列のいずれかになった場合にのみキャッシュされ、再利用されます。
使用アイテム:
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 句では、ネストされたマップの代わりにフラット化されたマップを使用してください。
ネストされた構造が深いと、クエリエンジンが最適なクエリプランを生成する機能が制限されることがあります。この問題を部分的に軽減するため、以下の定義済みパターンによって以下のシナリオに最適なプランが作成されます。
-
シナリオ 1: 数値、文字列、ブール値を含む暗号リテラルのリストを見て安心してください。
-
シナリオ 2: 暗号リテラル (数値、文字列、BOOLEAN) のみを値として含むフラット化されたマップのリストを見て安心してください。
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) 式では、より制限の厳しいノードを左側に配置します。
可変長パス (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 エクスプレッションの左側に配置します。
詳細なリレーションシップ名を使用することにより、ノードラベルのチェックが重複しないようにします。
パフォーマンスを最適化する場合、ノードパターン専用のリレーションシップラベルを使用すると、ノード上のラベルフィルタリングを解除できます。リレーションシップが 2 likes
person
つのノード間のリレーションシップを定義するためだけに使用されるグラフモデルを考えてみましょう。このパターンを見つけるには、次のクエリを書くことができます。
MATCH (n:person)-[:likes]->(m:person) RETURN n, m
n と m person
のラベルチェックは重複しています。なぜなら、両方が同じタイプの場合にのみ関係が表示されるように定義したからです。person
パフォーマンスを最適化するために、クエリを次のように記述できます。
MATCH (n)-[:likes]->(m) RETURN n, m
このパターンは、プロパティが 1 つのノードラベル専用である場合にも適用できます。person
ノードだけがプロパティを持っていると仮定するとemail
、person
ノードラベルが一致することを確認するのは冗長です。このクエリを次のように記述します。
MATCH (n:person) WHERE n.email = 'xxx@gmail.com' RETURN n
このクエリを次のように記述するよりも効率が悪くなります。
MATCH (n) WHERE n.email = 'xxx@gmail.com' RETURN n
このパターンを採用するのは、パフォーマンスが重要で、モデリングプロセスでエッジラベルが他のノードラベルに関係するパターンに再利用されないようにチェックする必要がある場合だけにしてください。email
後でなどの別のノードラベルにプロパティを導入した場合company
、これら 2 つのクエリのバージョンでは結果が異なることになります。
可能な場合はエッジ・ラベルを指定してください。
パターン内のエッジを指定するときは、可能な限りエッジラベルを付けることを推奨します。次のクエリの例を考えてみましょう。このクエリは、ある都市に住むすべての人々と、その都市を訪れたすべての人々をリンクさせるために使用されます。
MATCH (person)-->(city {country: "US"})-->(anotherPerson) RETURN person, anotherPerson
エンドラベルを指定しないことで、複数のエッジラベルを使用して人々を都市以外のノードにリンクするグラフモデルでは、Neptune eは後で破棄される追加のパスを評価する必要があります。上記のクエリでは、エッジラベルが指定されていないため、エンジンは最初に多くの作業を行い、次に値をフィルタリングして正しい結果を得ます。上記のクエリのより良いバージョンは、以下のようになります。
MATCH (person)-[:livesIn]->(city {country: "US"})-[:visitedBy]->(anotherPerson) RETURN person, anotherPerson
これは評価に役立つだけでなく、クエリプランナーがより良いプランを作成できるようにします。このベストプラクティスを冗長ノードラベルチェックと組み合わせて、都市ラベルチェックを削除し、クエリを次のように記述することもできます。
MATCH (person)-[:livesIn]->({country: "US"})-[:visitedBy]->(anotherPerson) RETURN person, anotherPerson
WITH 句はできるだけ使用しないでください。
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 句は以前のパターンの配置を制限しています (city)-[:part_of]->(country {name: 'India'})
(より制限が厳しい)。(person)-[:lives_in]->(city)
そのため、プランは最適とは言えません。このクエリの最適化は、WITH 句を削除して、プランナーが最適なプランを計算できるようにすることです。
MATCH (person)-[:lives_in]->(city) MATCH (city)-[:part_of]->(country {name: 'India'}) RETURN collect(person) AS result
制限付きフィルターはクエリのできるだけ早い段階で配置してください。
どのシナリオでも、クエリの早い段階でフィルターを配置しておくと、クエリプランで検討しなければならない中間ソリューションを減らすのに役立ちます。つまり、クエリの実行に必要なメモリやコンピューティングリソースも少なくて済みます。
次の例は、これらの影響を理解するのに役立ちます。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
一般的なルールは、変数が導入されたらできるだけ早くフィルターを設定することです。
プロパティが存在するかどうかを明示的にチェックしてください。
OpenCypher のセマンティクスに基づくと、プロパティにアクセスするとオプションの結合と同等になり、プロパティが存在しない場合でもすべての行を保持する必要があります。グラフスキーマに基づいて、そのエンティティには特定のプロパティが常に存在することがわかっている場合、そのプロパティが存在するかどうかを明示的に確認することで、クエリエンジンは最適なプランを作成し、パフォーマンスを向上させることができます。
person
あるタイプのノードには必ずプロパティがあるグラフモデルを考えてみましょうname
。これを行う代わりに:
MATCH (n:person) RETURN n.name
IS NOT NULL チェックを使用して、クエリ内のプロパティの存在を明示的に確認します。
MATCH (n:person) WHERE n.name IS NOT NULL RETURN n.name
名前付きパスは (必要でない限り) 使用しないでください。
クエリ内の名前付きパスには常に追加コストがかかるため、レイテンシーやメモリ使用量の増加という点でペナルティが加わる可能性があります。次のクエリについて考えます。
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
コレクト (DISTINCT ()) は避けてください。
COLLECT (DISTINCT ()) は、異なる値を含むリストを作成する場合には必ず使用されます。COLLECT は集計関数で、同じステートメントに追加されるキーに基づいてグループ化されます。distinct を使用すると、入力は複数のチャンクに分割され、各チャンクは削減対象の 1 つのグループを表します。グループの数が増えると、パフォーマンスに影響が出ます。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
すべてのプロパティ値を取得する場合は、個別のプロパティ検索よりもプロパティ関数を使用する方がよいでしょう。
properties()
この関数はエンティティのすべてのプロパティを含むマップを返すために使用され、プロパティを個別に返すよりもはるかに効率的です。
Person
ノードに 5 つのプロパティ (、、firstName
lastName
age
、dept
) が含まれていると仮定するとcompany
、次のクエリが推奨されます。
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
クエリーの外部で静的計算を実行する。
静的計算 (単純な数学/文字列演算) はクライアント側で解決することが推奨されます。筆者より 1 歳年上かそれ以下の人をすべて検索する例を考えてみましょう。
MATCH (m:Message)-[:HAS_CREATOR]->(p:person) WHERE p.age <= ($age + 1) RETURN m
ここでは$age
、パラメータを使ってクエリに注入し、固定値に加算します。次に、p.age
この値がと比較されます。代わりに、クライアント側で加算を行い、計算された値をパラメーター $ageplusone として渡す方がよいでしょう。これにより、クエリエンジンが最適なプランを作成できるようになり、各入力行の静的な計算を回避できます。これらのガイドラインに従うと、クエリのより効率的なバージョンは次のようになります。
MATCH (m:Message)-[:HAS_CREATOR]->(p:person) WHERE p.age <= $ageplusone RETURN m
個々のステートメントの代わりに UNWIND を使用するBatch 入力
同じクエリを異なる入力に対して実行する必要があるときは、入力ごとに 1 つのクエリを実行するよりも、複数の入力に対してクエリを実行するほうがはるかに効率的です。
複数のノードをマージする場合、1 つの選択肢は、入力ごとにマージクエリを実行することです。
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 を使用することを推奨します。
Neptune では、ユーザーがノードとリレーションシップに ID を明示的に割り当てることができます。ID はデータセット内でグローバルに一意で、かつ確定的でなければ有効ではありません。決定的 ID は、プロパティと同様にルックアップやフィルタリングメカニズムとして使用できますが、ID を使用する方がプロパティを使用するよりもクエリ実行の観点からはるかに最適化されます。カスタム ID を使用する利点はいくつかあります。
-
既存のエンティティのプロパティは NULL でもかまいませんが、ID は存在している必要があります。これにより、クエリエンジンは実行時に最適化された結合を使用できます。
-
ミューテーションクエリを並行実行すると、ID を使用してノードにアクセスするときに ID を使用する場合に同時変更例外 (CME) が発生する可能性が大幅に低下します。これは、ID が適用される一意性により、プロパティよりもロックの数が少なくなるためです。
-
Neptune はプロパティとは異なり ID に一意性を強制するため、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
という名前の付いた 2 つ目のノードが作成されます。john
このシナリオでは、(alice と john) の名前がそれぞれ異なる 2 つのノードができあがります。id=1
~id
クエリでは計算は行わないでください。
クエリでカスタム 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'}]}