mcpField
N
neo4j-query-tuning-skill

neo4j-query-tuning-skill

github.com/neo4j-contribデータ・データベース118921 回インストール1 週間前に更新
リポジトリ

neo4j-query-tuning-skill について

Neo4jのCypherクエリを診断するためのチューニングガイドです。実行計画、インデックス、ランタイム、監視コマンドを解説し、修正の適用と効果測定の手順も示します。

主な機能

  1. 01EXPLAINとPROFILEの計画を解釈
  2. 02スキャンや直積を特定
  3. 03適切なインデックスを確認・作成
  4. 04Cypherランタイムを比較
  5. 05クエリの監視と終了

活用シーン

  1. 01予想外に遅いCypherクエリの調査
  2. 02計画指標からボトルネックを特定
  3. 03不足インデックスや結合条件を修正
  4. 04長時間実行中のクエリを監視・処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-query-tuning-skill

インストール後、Claude Code で/neo4j-query-tuning-skillを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
references/
plan-operators.md
stats-and-monitoring.md

詳細

使用する場面

  • クエリの実行に予想以上に時間がかかり、根本原因の分析が必要な場合
  • EXPLAIN/PROFILE の出力があり、その解釈が必要な場合
  • 欠落しているインデックスや未使用のインデックスを特定する場合
  • スロット型/パイプライン型/並列ランタイムのどれを使うか判断する場合
  • ライブクエリを監視する場合:SHOW QUERIES、SHOW TRANSACTIONS
  • カーディナリティの推定が誤っている場合(プランの再計画が必要)

使用しない場面

  • Cypher を一から作成する場合 → neo4j-cypher-skill
  • GDS アルゴリズムのパフォーマンス → neo4j-gds-skill
  • スキーマ設計/データモデリング → neo4j-modeling-skill

EXPLAIN と PROFILE の比較

EXPLAINPROFILE
クエリを実行しますか?いいえはい
データを返しますか?いいえはい
rows(実測値)を表示しますか?いいえはい
dbHits(実測値)を表示しますか?いいえはい
estimatedRows を表示しますか?はいはい
コストゼロクエリ全体のコスト

PROFILE は 2 回実行してください。1 回目でページキャッシュがウォームアップされ、2 回目に代表的なメトリクスが得られます。

EXPLAIN MATCH (p:Person {email: $email}) RETURN p.name
PROFILE MATCH (p:Person {email: $email}) RETURN p.name

クエリ API を使う方法(ドライバー不要):

curl -X POST https://<host>/db/<db>/query/v2 \
  -u <user>:<pass> -H "Content-Type: application/json" \
  -d '{"statement": "EXPLAIN MATCH (p:Person {email: $email}) RETURN p.name", "parameters": {"email": "[email protected]"}}'

主要なプランメトリクス

メトリクス良好調査が必要な場合
dbHits低い。インデックス追加後に減少するrows に対して高い
rowsプランの早い段階で減少する最終オペレーターまで大きい
estimatedRowsrows に近い実測値との乖離が 10 倍を超える
pageCacheHitRatio>0.99<0.90(ディスク I/O がボトルネック)
pageCacheHits多い—
pageCacheMissesほぼ 0増加している(ページキャッシュが小さすぎる)

プランは 下から上へ読みます。最下部のリーフオペレーターがデータ取得を開始します。


オペレーターリファレンス

オペレーター良否意味対処法
NodeIndexSeek✓RANGE/LOOKUP インデックスによる完全一致—
NodeUniqueIndexSeek✓一意性制約のインデックスヒット—
NodeIndexContainsScan✓TEXT インデックスの CONTAINS/STARTS WITH—
NodeIndexScan~インデックス全体のスキャン(述語なし)WHERE 述語または複合インデックスを追加する
NodeByLabelScan✗ラベルが付いたすべてのノードをスキャンする検索対象のプロパティに RANGE インデックスを追加する
AllNodesScan✗✗ノードストア全体をスキャンするMATCH にラベルとインデックスを追加する
Expand(All)~ノードからリレーションシップをたどる通常の動作。LIMIT または WHERE で制限する
Expand(Into)~マッチした 2 つのノード間のリレーションシップを検索する接続先が既知のノード間結合では通常の動作
Filter~スキャン後に述語を適用するインデックスを使って述語を WHERE に移動する
CartesianProduct✗2 つの MATCH 間に結合述語がないWHERE に結合条件を追加するか、MATCH の間に WITH を使う
NodeHashJoin~ノード ID によるハッシュ結合通常の動作。プランナーがハッシュ結合を選択している
ValueHashJoin~値によるハッシュ結合通常の動作。入力が大きい場合はメモリに注意する
EagerAggregation~全件集約(ORDER BY、count(*))集約では通常の動作
Aggregation✓ストリーミング集約—
Eager✗読み取り/書き込みの競合があり、すべての行を実体化する以下の Eager 対処方法を参照
Sort~全件ソート — O(n log n)Sort の前に LIMIT を追加する。LIMIT をより早い段階に移動する
Top✓Sort と Limit を結合 — O(n log k)Sort と Limit の組み合わせより優先する
Limit✓行を早い段階で切り詰めるできるだけ早い段階に移動する
Skip~オフセットページネーション大規模グラフではキーセットページネーションを使う
ProduceResults—最終出力オペレーターツリーのルート
UndirectedRelationshipByIdSeekPipe~リレーションシップ ID による検索id(r) を避け、elementId(r) を使う

オペレーターの完全なリファレンス → references/plan-operators.md


診断ワークフロー(エージェント用手順書)

ステップ 1 — ベースラインプラン
EXPLAIN <query>

出力を確認し、AllNodesScan、NodeByLabelScan、CartesianProduct、Eager を探します。

ステップ 2 — インデックスを確認
SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state
WHERE state = 'ONLINE'

問題のオペレーターに関係するラベル/プロパティにインデックスがあるか確認します。

ステップ 3 — 不足しているインデックスを作成
// RANGE index for equality/range predicates:
CREATE INDEX person_email IF NOT EXISTS FOR (n:Person) ON (n.email)
// TEXT index for CONTAINS/ENDS WITH:
CREATE TEXT INDEX person_bio IF NOT EXISTS FOR (n:Person) ON (n.bio)
// Composite for multi-property lookup:
CREATE INDEX order_status_date IF NOT EXISTS FOR (n:Order) ON (n.status, n.createdAt)

計測する前に、state = 'ONLINE' になるまで待ちます。

ステップ 4 — 修正後にプロファイルを取得
PROFILE <query>

修正前後の dbHits と経過時間(ミリ秒)を比較します。目標は、スキャンオペレーターが NodeIndexSeek に置き換わることです。

ステップ 5 — 古い統計情報(estimatedRows が大きく乖離している場合)
CALL db.prepareForReplanning()
// or resample a specific index:
CALL db.resampleIndex("person_email")
// or resample all outdated:
CALL db.resampleOutdatedIndexes()

設定:dbms.cypher.statistics_divergence_threshold(デフォルトは 0.75 — 統計情報が 75% を超えて変化すると、プランの有効期限が切れます)。


よくあるプランの問題を解決する

インデックスがない → NodeByLabelScan / AllNodesScan
// Force index hint when planner ignores it:
MATCH (p:Person {email: $email})
USING INDEX p:Person(email)
RETURN p.name
// Force label scan (sometimes faster for high selectivity):
MATCH (p:Person {email: $email})
USING SCAN p:Person
RETURN p.name
アンカーが不適切 — プランナーが誤った開始ノードを選択する

MATCH の順序を入れ替えるか、ヒントを使用します:

// Force join at specific node:
MATCH (a:Author)-[:WROTE]->(b:Book)-[:IN_CATEGORY]->(c:Category {name: $cat})
USING JOIN ON b
RETURN a.name, b.title
CartesianProduct — 接続されていない 2 つの MATCH
// Bad (Cartesian product):
MATCH (a:Author {id: $aid})
MATCH (b:Book  {id: $bid})
RETURN a.name, b.title

// Good (explicit join or WITH):
MATCH (a:Author {id: $aid})-[:WROTE]->(b:Book {id: $bid})
RETURN a.name, b.title
// Or: WITH between them to reset planning context
Eager — 読み取り/書き込みの競合

3 つの方法(最も簡単なものを選択してください):

  1. MATCH ノードに具体的なラベルを追加することで、プランナーが読み取り/書き込みセットを区別できるようにします
  2. 収集してから書き込む:WITH collect(n) AS nodes UNWIND nodes AS n SET n.x = 1
  3. CALL IN TRANSACTIONS:各バッチを独自のトランザクションに分離します
CYPHER 25
MATCH (p:Person) WHERE p.score > 100
CALL (p) { SET p.tier = 'gold' } IN TRANSACTIONS OF 1000 ROWS
コストの高い CONTAINS / ENDS WITH
// Needs TEXT index (RANGE does NOT support these):
CREATE TEXT INDEX person_bio IF NOT EXISTS FOR (n:Person) ON (n.bio)
MATCH (p:Person) WHERE p.bio CONTAINS $keyword RETURN p.name
過剰なトラバーサル — LIMIT を早い段階で適用する
// Bad: LIMIT after expensive join
MATCH (a:Author)-[:WROTE]->(b:Book)-[:REVIEWED_BY]->(r:Review)
RETURN a.name, b.title, r.text LIMIT 10

// Good: anchor limit before fan-out
MATCH (a:Author)-[:WROTE]->(b:Book)
WITH a, b LIMIT 10
MATCH (b)-[:REVIEWED_BY]->(r:Review)
RETURN a.name, b.title, r.text

Cypher ランタイムの選択

ランタイム選択方法最適な用途避けるべき場合
pipelinedCYPHER runtime=pipelinedデフォルトの OLTP。ストリーミング、低メモリ使用量未サポートの演算子は slotted にフォールバックします
slottedCYPHER runtime=slotted動作の安定性が保証される。デバッグパフォーマンスが重要な OLTP
parallelCYPHER 25 runtime=parallel大規模な分析スキャン。集計OLTP、書き込み、短いクエリ、Aura Free

ほとんどのクエリでは Pipelined がデフォルトです。Parallel を使用するには dbms.cypher.parallel.worker_limit の設定が必要です。Enterprise および Aura Pro 2025+ で利用できます。

// Force parallel for large aggregation:
CYPHER 25 runtime=parallel
MATCH (n:Transaction) WHERE n.amount > 1000
RETURN n.currency, count(*), sum(n.amount)

クエリの監視コマンド

// Live queries + resource usage:
SHOW QUERIES YIELD query, queryId, elapsedTimeMillis, allocatedBytes, status, username

// Running transactions:
SHOW TRANSACTIONS YIELD transactionId, currentQuery, currentQueryProgress, elapsedTime, status, username, cpuTime, activeLockCount  // currentQueryProgress added [2026.03]

// Kill a specific transaction:
TERMINATE TRANSACTION $transactionId

// Kill a query:
TERMINATE QUERY $queryId

// Graph count stats (node/rel counts by label/type — feed into planner):
CALL db.stats.retrieve('GRAPH COUNTS') YIELD section, data RETURN section, data

// Token stats (label/property/rel-type IDs):
CALL db.stats.retrieve('TOKENS') YIELD section, data RETURN section, data

監視に関する完全なリファレンス → references/stats-and-monitoring.md


チェックリスト

  • まず EXPLAIN を実行する — 実行コストをかけずにプランの問題を特定できます
  • AllNodesScan / NodeByLabelScan を確認する — インデックスがありません
  • CartesianProduct を確認する — 結合述語がありません
  • Eager を確認する — 読み取り/書き込みの競合があります
  • SHOW INDEXES — 関連するインデックスが存在し、state = 'ONLINE' であることを確認します
  • 不足しているインデックスを作成し、ONLINE になるまで待ちます
  • PROFILE を 2 回実行する — 1 回目でキャッシュをウォームアップし、2 回目で代表的な結果を得ます
  • 修正前後の dbHits を比較します
  • estimatedRows が大きく乖離している場合 → CALL db.prepareForReplanning()
  • ファンアウトの大きい処理の前に LIMIT / WITH n LIMIT k を適用します
  • CONTAINS/ENDS WITH には RANGE ではなく TEXT インデックスを使用します
  • 大規模な分析クエリでは runtime=parallel を検討します
  • 長時間実行中のクエリは TERMINATE TRANSACTION で終了します

コメント