Neo4j Cypher Cheatsheet - Neo4j Graph Database Command Reference
Neo4j's advantage over a relational database is not generically that it queries faster, but that relationships themselves are queryable: who is reachable within two hops of a person, what the shortest dependency chain between two nodes is. In a relational model those questions need self-joins across several join tables; in a graph model each is one typed path pattern. This sheet moves from CREATE (a)-[:KNOWS]->(b) through variable-length patterns like [:KNOWS*1..3], shortestPath, UNWIND, subqueries and percentileCont aggregation, grouped into creating nodes and relationships, matching, WITH and subqueries, updates and deletes, aggregation and ordering, and indexes and constraints. Reach for it while writing a query: decide first whether relationships carry types, because typed patterns beat indiscriminate matching, and always cap path length so a variable-length pattern cannot walk the whole graph. One production rule is not negotiable - create uniqueness constraints before ingesting data, because MERGE cannot remove duplicates that already exist in history.
Typical Use Case
For engineers who need traversal of arbitrary depth: friend recommendations, knowledge graphs, permission inheritance, organisation hierarchies and fraud ring detection. Relational databases need recursive CTEs for these, a graph database states in one line. Covers node and relationship creation and the choice between CREATE and MERGE, variable-length patterns and shortestPath, deletion including DETACH DELETE, aggregation, and indexes. Without indexes a lookup scans every label.
Create Nodes & Relationships 5
CREATE (n:Person {name: "Alice"})CREATE (a:Person)-[:KNOWS]->(b:Person)CREATE (a:Person {name: "Alice"})-[:KNOWS {since: 2020}]->(b:Person {name: "Bob"})MERGE (n:Person {name: "Alice"})MERGE (a)-[:KNOWS]->(b) ON CREATE SET a.created = timestamp()Query & Match 9
MATCH (n:Person) RETURN nMATCH (n:Person {name: "Alice"}) RETURN nMATCH (n:Person) WHERE n.age > 18 RETURN nMATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, bMATCH (a:Person)-[:KNOWS*2]->(b:Person) RETURN a, bMATCH (a:Person)-[:KNOWS*1..3]->(b:Person) RETURN a, bMATCH (n) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN n, mMATCH path = (a)-[*]->(b) RETURN pathMATCH path = shortestPath((a)-[:KNOWS*]->(b)) RETURN pathWITH & Subqueries 5
MATCH (n:Person) WITH n WHERE n.age > 18 RETURN n.nameMATCH (n:Person)-[:KNOWS]->(m) WITH n, count(m) AS friends RETURN n.name, friendsUNWIND [1, 2, 3] AS x RETURN xCALL { MATCH (n:Person) RETURN n ORDER BY n.name LIMIT 5 } RETURN count(*)MATCH (n:Person) RETURN n.name AS name UNION MATCH (n:Company) RETURN n.name AS nameUpdate & Delete 6
MATCH (n:Person {name: "Alice"}) SET n.age = 30MATCH (n:Person {name: "Alice"}) SET n += {age: 30, city: "Beijing"}MATCH (n:Person {name: "Alice"}) REMOVE n.ageMATCH (n:Person {name: "Alice"}) DELETE nMATCH (n:Person {name: "Alice"}) DETACH DELETE nMATCH (a)-[r:KNOWS]->(b) DELETE rAggregation & Order 9
MATCH (n:Person) RETURN count(n)MATCH (n:Person) RETURN DISTINCT n.cityMATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESCMATCH (n:Person) RETURN n.age, count(*) ORDER BY n.ageMATCH (n:Person) RETURN collect(n.name)MATCH (n:Person) RETURN avg(n.age), min(n.age), max(n.age)MATCH (n:Person) RETURN percentileCont(n.age, 0.5)MATCH (n:Person) RETURN n LIMIT 10MATCH (n:Person) RETURN n SKIP 5 LIMIT 10Index & Constraint 6
CREATE INDEX ON :Person(name)CREATE CONSTRAINT ON (n:Person) ASSERT n.name IS UNIQUECREATE CONSTRAINT ON (n:Person) ASSERT exists(n.name)DROP INDEX ON :Person(name)DROP CONSTRAINT ON (n:Person) ASSERT n.name IS UNIQUECALL db.indexes()Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
CREATE (n:Person {name: "Alice"}) | 无条件创建节点;重复执行会产生重复节点,是 MERGE 的常见反面教材 | CREATE (n:Person {name: "Alice"}) |
CREATE (a:Person)-[:KNOWS]->(b:Person) | 创建带关系结构的模式;未写属性的关系会以 null 收尾 | CREATE (a:Person)-[:KNOWS]->(b:Person) |
CREATE (a)-[:KNOWS {since: 2020}]->(b) | 在关系上挂属性;关系的属性是给边加权的关键手段 | CREATE (a:Person {name: "Alice"})-[:KNOWS {since: 2020}]->(b:Person {name: "Bob"}) |
MERGE (n:Person {name: "Alice"}) | 存在则匹配、不存在则创建,写入脚本的默认选择,天然幂等 | MERGE (n:Person {name: "Alice"}) |
MERGE (a)-[:KNOWS]->(b) ON CREATE SET a.created = timestamp() | 只在真正创建时写属性,避免每次运行都刷新时间戳 | MERGE (a)-[:KNOWS]->(b) ON CREATE SET a.created = timestamp() |
MATCH (n:Person) RETURN n | 按标签匹配全量节点;没有索引时是全标签扫描 | MATCH (n:Person) RETURN n |
MATCH (n:Person {name: "Alice"}) | 带属性匹配;为该属性建索引后才能真正走索引查找 | MATCH (n:Person {name: "Alice"}) RETURN n |
MATCH (a)-[:KNOWS*2]->(b) | 定长变长路径,恰好两跳;这是最短关系链查询的标准写法 | MATCH (a:Person)-[:KNOWS*2]->(b:Person) RETURN a, b |
MATCH (a)-[:KNOWS*1..3]->(b) | 深度区间遍历;范围越界上限越高代价越大,必须配合上限使用 | MATCH (a:Person)-[:KNOWS*1..3]->(b:Person) RETURN a, b |
MATCH path = (a)-[*]->(b) RETURN path | 任意类型与深度遍历并返回完整路径;不加限制时在稠密图上代价极高 | MATCH path = (a)-[*]->(b) RETURN path |
shortestPath((a)-[:KNOWS*]->(b)) | 最短路径,遍历会在找到第一条最短路径后停止,比取全部路径便宜 | MATCH path = shortestPath((a)-[:KNOWS*]->(b)) RETURN path |
OPTIONAL MATCH | 找不到匹配时返回 null 而非丢弃整行,做「有则展示、无则留空」的关键 | MATCH (n) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN n, m |
WITH n WHERE n.age > 18 RETURN n.name | 在管道中插入过滤与投影,用于先收敛再展开,是控制中间结果量的关键 | MATCH (n:Person) WITH n WHERE n.age > 18 RETURN n.name |
WITH n, count(m) AS friends RETURN n.name, friends | 聚合后按 n 分组统计,是「每个人的好友数」这类查询的写法 | MATCH (n:Person)-[:KNOWS]->(m) WITH n, count(m) AS friends RETURN n.name, friends |
UNWIND [1, 2, 3] AS x RETURN x | 把列表展开成多行,常用于构造测试数据或展开数组属性 | UNWIND [1, 2, 3] AS x RETURN x |
CALL { MATCH ... RETURN ... } RETURN count(*) | 子查询,把一段匹配封装为独立单元后在外层聚合 | CALL { MATCH (n:Person) RETURN n ORDER BY n.name LIMIT 5 } RETURN count(*) |
RETURN ... UNION MATCH ... | 合并同构结果集;两侧列名必须一致,否则查询报错 | MATCH (n:Person) RETURN n.name AS name UNION MATCH (n:Company) RETURN n.name AS name |
SET n += {age: 30, city: "Beijing"} | 合并写入属性,不会清掉未列出的已有属性(对比 SET n = {...}) | MATCH (n:Person {name: "Alice"}) SET n += {age: 30, city: "Beijing"} |
REMOVE n.age | 删除单个属性;删掉的是属性不是节点 | MATCH (n:Person {name: "Alice"}) REMOVE n.age |
DELETE n vs DETACH DELETE n | DELETE 只删节点;节点上还有关系时会报错,需要 DETACH DELETE 连带删除所有关系 | MATCH (n:Person {name: "Alice"}) DETACH DELETE n |
DELETE r | 删除关系本身;只删边时用它比删节点安全 | MATCH (a)-[r:KNOWS]->(b) DELETE r |
RETURN DISTINCT n.city | 去重;图查询里 DISTINCT 会随数据量增长明显变慢,能提前聚合更好 | MATCH (n:Person) RETURN DISTINCT n.city |
RETURN collect(n.name) | 把多行聚成一个列表,是「查某人全部好友名」的标准聚合 | MATCH (n:Person) RETURN collect(n.name) |
percentileCont(n.age, 0.5) | 百分位数(中位数用 0.5);cont 表示连续插值口径,另有离散口径可选 | MATCH (n:Person) RETURN percentileCont(n.age, 0.5) |
RETURN n SKIP 5 LIMIT 10 | 分页;SKIP 在深度分页时依然会线性扫描,生产上应优先用 seek 式分页 | MATCH (n:Person) RETURN n SKIP 5 LIMIT 10 |
CREATE INDEX ON :Person(name) | 建属性索引,让等值匹配走索引而不是全标签扫描 | CREATE INDEX ON :Person(name) |
CREATE CONSTRAINT ... IS UNIQUE | 唯一约束既保证数据正确性也让 MERGE 能走索引;MERGE 高频字段建议加 | CREATE CONSTRAINT ON (n:Person) ASSERT n.name IS UNIQUE |
CREATE CONSTRAINT ... ASSERT exists(n.name) | 存在性约束,保证属性非空,避免下游查询遇到 null | CREATE CONSTRAINT ON (n:Person) ASSERT exists(n.name) |
DROP INDEX / DROP CONSTRAINT | 删除索引或约束;删索引会让查询退化为全扫描,需评估线上影响 | DROP INDEX ON :Person(name) |
CALL db.indexes() | 列出当前所有索引与约束,是确认「索引到底建上没有」的直接手段 | CALL db.indexes() |
Common pitfalls
Symptom脚本重复跑之后出现大量同名重复节点。
Cause用 CREATE 而不是 MERGE;CREATE 无条件插入,每次执行都新增。
Fix改用 MERGE 并为匹配字段建唯一约束;先用 MATCH 查重再决定是否写。
Symptom删除节点时报 "Node cannot be deleted because it still has relationships"。
CauseDELETE 只删节点本身,节点上挂着关系时会被拒绝。
Fix改用 DETACH DELETE;或先单独 DELETE 掉相关关系再删节点(需要保留其它关系时)。
Symptom按 name 查询越来越慢,PROFILE 显示走了全标签扫描。
Cause等值匹配的属性上没有索引,或索引刚建完还没生效。
FixCREATE INDEX ON :Person(name) 并加唯一约束;用 EXPLAIN / PROFILE 确认走的是 NodeIndexSeek。
Symptom变长路径查询把内存打满或直接超时。
Cause深度上限没设或设得过大,稠密图上的路径数量呈指数增长。
Fix给深度设明确上限并用 shortestPath;必要时先按属性或标签剪枝再遍历。
Symptom路径少了一条,OPTIONAL MATCH 返回的 m 是 null。
Cause关系方向写反了:(:A)-[:KNOWS]->(:B) 与 (:A)<-[:KNOWS]-(:B) 语义不同。
Fix先统计单向关系数量确认方向,再统一箭头的写法。
SymptomWITH 之后原本想继续匹配的节点消失了。
CauseWITH 起到过滤作用:不在 WITH 里保留的变量后续不可见;DISTINCT 语义也会改变行数。
Fix在 WITH 中显式写出需要继续传递的变量,并在必要时加 count 做聚合后再展开。
SymptomUNION 报错,提示结果列名或数量不一致。
CauseUNION 要求两侧返回相同数量与类型的列,且按位置对齐而非名字。
Fix两侧用别名统一成同一批列名,并核对类型一致(数字与字符串不能混)。
Symptom数据导入跑几小时后磁盘暴涨,节点数却没明显增加。
CauseMERGE 在无索引字段上执行会退化为全扫描,并留下大量事务日志等待落盘。
Fix导入前先建好索引与约束;大批量导入时用 APOC 的分批能力并控制事务大小。
Troubleshooting
1确认索引与约束是否真的存在
CALL db.indexes()列表里没有目标索引,就是全扫描的直接原因。
2核对标签与关系类型是否拼写一致
grep -rn "CREATE \(:\|MERGE (:" data大小写与下划线不一致是「明明建了却查不到」的常见原因。
3看查询实际走了什么计划
PROFILE MATCH (n:Person {name: "Alice"}) RETURN n关注 operators 里的 NodeIndexSeek 与 TotalDbHits。
4先统计节点规模再决定优化方向
MATCH (n:Person) RETURN count(n)数据量决定是否需要索引、分页与深度限制。
5检查写入脚本是否用了 MERGE
grep -rn "CREATE (\|MERGE (" load重复导入的根因几乎都在这里。
6确认删除是否会牵连关系
grep -rn "DELETE" data需要连带删除关系的场景应显式写成 DETACH DELETE。
Command Examples
MERGE for idempotent writes
MERGE (a:Person {name: "Alice"})
MERGE (a)-[:KNOWS {since: 2020}]->(b:Person {name: "Bob"})CREATE adds another copy every run, so re-running an import script pollutes the graph; MERGE matches the whole pattern first, reuses what it finds, and only creates when there is no match, which makes the script safe to run repeatedly. MERGE matches the entire pattern: with two property conditions both must hold, and omitting one degrades it to a single-property match.
Output
Added 1 node, set 2 properties, created 0 relationships
Narrow with WITH before the second match
MATCH (n:Person) WITH n WHERE n.age > 18 RETURN n.name
MATCH (a:Person)-[:KNOWS]->(b:Person) WITH a, count(b) AS friends RETURN a.name, friendsChaining two MATCH clauses makes the second run once per row produced by the first; an intervening WITH filters and aggregates first, shrinking cardinality before the next match. Traversal cost grows linearly with row count, so on a large graph the missing WITH is the difference between minutes and seconds.
Output
friends 30
Detach relationships before deleting a node
MATCH (n:Person {name: "Alice"}) DELETE n
MATCH (n:Person {name: "Alice"}) DETACH DELETE nA node that still has relationships is rejected by DELETE and the whole transaction rolls back; DETACH DELETE removes the relationships as well. On a hub node referenced by many others, DETACH DELETE severs a large part of the graph, so check the blast radius with MATCH (n)-[r]->(m) RETURN count(*) first.
Output
Cannot delete node<4>, because it still has relationships. (ConstraintViolation)
Count per label instead of scanning the whole database
MATCH (n:Person) RETURN count(n)
MATCH (n:Person) RETURN n.age, count(*) ORDER BY n.age
CREATE INDEX ON :Person(name)
CALL db.indexes()MATCH (n) walks every node in the database just to test labels, which costs seconds on a multi-million-node graph. Naming the label lets Neo4j use a label scan, and an index turns property filtering into an index seek. Verify with EXPLAIN: you want NodeByLabelScan plus NodeIndexSeek, not AllNodesScan.
Bound the hop count instead of an unbounded graph search
MATCH (a:Person)-[:KNOWS*2]->(b:Person) RETURN a, b
MATCH (a:Person)-[:KNOWS*1..3]->(b:Person) RETURN a, b
MATCH path = shortestPath((a)-[:KNOWS*]->(b)) RETURN pathA variable-length -[:KNOWS*] with no upper bound expands in proportion to how connected the graph is and can exhaust memory on a few hundred thousand nodes. Write *2 or *1..3 for routine queries, and reserve shortestPath for real shortest-path work while pinning both ends to specific nodes so the search does not start from the whole graph.
Common Pitfalls
- MATCH (a)-[r]-(b) with no label or property on either side is a full database scan: a few characters of difference turn an index lookup into a whole-graph traversal. Add labels and property predicates, and use PROFILE to confirm an index is actually used.
- A variable-length [:KNOWS*] with no upper bound is the most common way to take down a database: expansion grows explosively with connectivity and aggregations materialize the whole result in memory. Always bound it as *1..3, or make shortestPath the explicit intent.
- MATCH (n) RETURN n ships every node with all its properties to the client, so on millions of nodes the result set itself is the memory problem. An index is not enough — project only the properties you need in RETURN, or add LIMIT / SKIP.
- MATCH (a:Person)-[:KNOWS]->(b:Person) and MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) look almost identical but return very different counts, and an off-by-one hop silently produces wrong answers for friend counts and second-degree reach. Comment the intended hop count next to every variable-length relationship.
- Forgetting CREATE INDEX ON :Person(name) means the WHERE equality check compares against every node. The difference is invisible at small data volumes and becomes minutes instead of milliseconds in production. Index the property your predicates filter on.
- DETACH DELETE quietly severs links other code depends on, while DELETE rejects a node that still has relationships and rolls back the whole transaction. Before bulk cleanup check MATCH (n:Person {name: "Alice"})-[r]-() RETURN count(r); after a DELETE failure, do not retry the same query — first decide whether to detach or to migrate the references.
Tips
- Run EXPLAIN before MATCH to avoid full-graph scans.
- MERGE is safer than CREATE — it avoids duplicating nodes.
- Long path queries (*1..10) can be slow; add LIMIT or use graph algorithms.
FAQ
Should I use CREATE or MERGE, and will re-running an import create duplicates?
It will, which is why MERGE is the default choice. CREATE (n:Person {name: "Alice"}) always inserts, so a second run adds a second node. MERGE (n:Person {name: "Alice"}) matches on label plus properties first and only creates when nothing matches. Back the property with a unique constraint via CREATE CONSTRAINT ON (n:Person) ASSERT n.name IS UNIQUE, adding an exists(n.name) constraint when presence matters. Reserve CREATE for test fixtures you know will not be re-run. MERGE also takes a callback: MERGE (a)-[:KNOWS]->(b) ON CREATE SET a.created = timestamp() writes the property only on actual creation.
Deleting a node errors because relationships are still attached — what now?
Cypher refuses to delete a node that still has relationships. Pick by intent: MATCH (a)-[r:KNOWS]->(b) DELETE r removes relationships on their own; MATCH (n:Person {name: "Alice"}) DETACH DELETE n drops the node together with every attached relationship in one statement, which suits test-data cleanup. Property-level edits are not deletes — use SET and REMOVE instead, such as SET n += {age: 30, city: "Beijing"} for a bulk update or REMOVE n.age to drop a key. Before any large delete, run the MATCH with RETURN first and confirm how many rows you actually matched.
What does WITH actually do in a query, and can it replace subqueries?
WITH passes an intermediate result down the pipeline, allowing both filtering and aggregation, which is how you narrow in stages. MATCH (n:Person) WITH n WHERE n.age > 18 RETURN n.name filters first and only then outputs; MATCH (n:Person)-[:KNOWS]->(m) WITH n, count(m) AS friends RETURN n.name, friends aggregates per person before returning. For a bounded sub-result use CALL: CALL { MATCH (n:Person) RETURN n ORDER BY n.name LIMIT 5 } RETURN count(*). UNWIND [1, 2, 3] AS x RETURN x expands an array into rows, and UNION combines the results of two similarly shaped queries.
How do I write multi-hop queries like friends-of-friends or grandparent chains without killing performance?
Variable-length relationships use an asterisk with a hop count. Exactly two hops: (a:Person)-[:KNOWS*2]->(b:Person); a hop range: (a:Person)-[:KNOWS*1..3]->(b:Person). A fixed-depth grandparent chain is better expressed by self-joining three times, as in the tree t1 / t2 / t3 pattern. To return the path itself use MATCH path = (a)-[*]->(b) RETURN path, and for the shortest route MATCH path = shortestPath((a)-[:KNOWS*]->(b)) RETURN path. Unbounded patterns such as *1..10 can be extremely slow, so EXPLAIN the plan to avoid a full graph scan and add LIMIT to bound the result.
My queries keep getting slower — what should I optimise first?
In order of payoff. First, create indexes: CREATE INDEX ON :Person(name) lets property lookups avoid scanning, confirm them with CALL db.indexes() and clean up with DROP INDEX ON :Person(name) when obsolete. Second, run EXPLAIN before the real query to confirm no full graph scan. Third, constraints help lookup performance as well, e.g. CREATE CONSTRAINT ON (n:Person) ASSERT n.name IS UNIQUE. Fourth, bound the output with RETURN n LIMIT 10, or page with RETURN n SKIP 5 LIMIT 10. Fifth, push filtering into an early WITH instead of materialising the whole graph and filtering afterwards.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us