关于 neo4j-query-tuning-skill
面向 Neo4j 使用者的查询调优技能指南,用于分析 Cypher 执行计划、定位索引和计划问题,并通过 PROFILE 与监控命令验证调整效果。
主要功能
- 01解读 EXPLAIN 与 PROFILE 计划
- 02识别扫描、笛卡尔积等问题
- 03检查并创建合适索引
- 04比较 Cypher 运行时
- 05监控与终止查询事务
适用场景
- 01调查 Cypher 查询耗时异常
- 02根据计划指标定位性能瓶颈
- 03修复缺失索引或连接条件
- 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.mdreferences/plan-operators.mdstats-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
| EXPLAIN | PROFILE | |
|---|---|---|
| 是否执行查询? | 否 | 是 |
| 是否返回数据? | 否 | 是 |
是否显示 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 — 读写冲突
三种策略(选择最简单的一种):
- 为 MATCH 节点添加特定标签,让规划器区分读写集合
- 先收集再写入:
WITH collect(n) AS nodes UNWIND nodes AS n SET n.x = 1 - 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 运行时选择
| 运行时 | 选择方式 | 最适用于 | 避免用于 |
|---|---|---|---|
pipelined | CYPHER runtime=pipelined | 默认 OLTP;流式处理、低内存占用 | 不支持的运算符会回退到 slotted |
slotted | CYPHER runtime=slotted | 保证行为稳定;调试 | 对性能要求高的 OLTP |
parallel | CYPHER 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