mcpField
N
neo4j-query-tuning-skill

neo4j-query-tuning-skill

github.com/neo4j-contrib数据与数据库118921 次安装1 周前更新
代码仓库

关于 neo4j-query-tuning-skill

面向 Neo4j 使用者的查询调优技能指南,用于分析 Cypher 执行计划、定位索引和计划问题,并通过 PROFILE 与监控命令验证调整效果。

主要功能

  1. 01解读 EXPLAIN 与 PROFILE 计划
  2. 02识别扫描、笛卡尔积等问题
  3. 03检查并创建合适索引
  4. 04比较 Cypher 运行时
  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 输出,需要解读
  • 找出缺失或未使用的索引
  • 决定使用 slotted / pipelined / parallel 运行时
  • 监控正在运行的查询:SHOW QUERIES、SHOW TRANSACTIONS
  • 基数估算错误(需要重新规划查询计划)

不适用场景

  • 从头编写 Cypher → neo4j-cypher-skill
  • GDS 算法性能 → neo4j-gds-skill
  • 架构设计 / 数据建模 → neo4j-modeling-skill

EXPLAIN 与 PROFILE

EXPLAINPROFILE
是否执行查询?否是
是否返回数据?否是
是否显示 rows(实际值)否是
是否显示 dbHits(实际值)否是
是否显示 estimatedRows是是
开销零完整查询开销

运行两次 PROFILE——首次运行会预热页面缓存;第二次运行会提供具有代表性的指标。

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在计划早期减少直到最后一个算子之前仍很大
estimatedRows接近 rows与实际值相差 >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)~查找两个已匹配节点之间的关系已知端点的连接场景下属于正常情况
Filter~扫描后应用谓词将谓词移入 WHERE,并使用索引
CartesianProduct✗两个 MATCH 之间没有连接谓词添加 WHERE 连接条件,或在两个 MATCH 之间使用 WITH
NodeHashJoin~基于节点 ID 的哈希连接正常;规划器选择了哈希连接
ValueHashJoin~基于值的哈希连接正常;大型输入时注意内存占用
EagerAggregation~完整聚合(ORDER BY、count(*))聚合操作中属于正常情况
Aggregation✓流式聚合—
Eager✗读写冲突;将所有行物化请参阅下文的 Eager 修复策略
Sort~完整排序——O(n log n)在 Sort 前添加 LIMIT;尽早应用 LIMIT
Top✓合并排序与限制——O(n log k)优于 Sort+Limit
Limit✓尽早截断行数尽可能提前应用
Skip~偏移量分页对大型图使用键集分页
ProduceResults—最终输出算子树的根节点
UndirectedRelationshipByIdSeekPipe~按关系 ID 查找避免使用 id(r)——改用 elementId(r)

完整算子参考 → references/plan-operators.md


诊断工作流(Agent 操作手册)

步骤 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
笛卡尔积 — 两个未连接的 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 — 读写冲突

三种策略(选择最简单的一种):

  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 — 第一次预热缓存,第二次结果具有代表性
  • 比较修复前后的 dbHits
  • 如果 estimatedRows 偏差很大 → 运行 CALL db.prepareForReplanning()
  • 在高扇出操作前添加 LIMIT / WITH n LIMIT k
  • 对于 CONTAINS/ENDS WITH — 使用 TEXT 索引,而非 RANGE 索引
  • 对于大型分析查询 — 考虑使用 runtime=parallel
  • 使用 TERMINATE TRANSACTION 终止长时间运行的查询

评论

支持 Markdown