Articles
Neo4j Cypher Interview Questions: Paths, Cardinality, and Graph Query Plans
Practice Neo4j Cypher path counts, duplicate endpoint rows, bounded traversals, and PROFILE plans with an original graph and precise answer checks.
An effective Neo4j Cypher interview answer does more than produce a query that runs. It says what one result row represents, why a traversal may multiply those rows, and how to prove whether the execution plan does reasonable work. This guide uses five original practice prompts, a small graph you can reason through by hand, and a plan-reading checklist. These are preparation exercises, not reported questions from a particular employer. For a related algorithm drill, try Enumerate All Simple Directed Paths and then explain how its vertex-simple rule differs from Cypher's default relationship-uniqueness rule.
Start with a graph whose answer you can count
Original practice graph: Ada-[:KNOWS]->Bo, Ada-[:KNOWS]->Cy, Bo-[:KNOWS]->Dee, Cy-[:KNOWS]->Dee, and Dee-[:KNOWS]->Ada. Assume each name identifies exactly one Person. There are two two-hop paths from Ada to Dee, but only one distinct destination, Dee. The back edge introduces a cycle; whether it appears depends on direction, length bounds, and the requested path rule. This tiny fixture is a better interview tool than an impressive-looking production schema because every row can be enumerated.
| Question asked | Correct unit to count | Result on this fixture |
|---|---|---|
How many two-hop KNOWS paths from Ada reach Dee? | Matched paths | Two: via Bo and via Cy |
| How many different people are reached at two hops? | Distinct end nodes | One: Dee |
How many KNOWS relationships leave Ada? | Relationships | Two |
If you say simply “two results,” the interviewer should ask what those results are. A node, a relationship, a path, and a grouped row are different objects. The Neo4j path-pattern rules say that matched paths may revisit nodes but ordinarily may not reuse the same relationship within a graph-pattern match. Do not silently substitute the stricter vertex-simple rule from a generic graph coding problem. Version-specific Cypher features can change path-mode behavior; name the version and mode if you rely on them.
Question 1: Why did a two-hop match return duplicate people?
Suppose the interviewer writes MATCH (a:Person {name:'Ada'})-[:KNOWS]->()-[:KNOWS]->(end:Person) RETURN end.name and asks why Dee appears twice. Both traversals bind the same end node, but each complete match contributes a row. Cypher's pattern match has not promised unique destinations. A clear answer traces the two paths, states that DISTINCT end.name would collapse this particular output to one value, and then asks whether uniqueness should be by node identity or by property. Two different people named Dee would make DISTINCT end.name misleading; RETURN DISTINCT end preserves node identity. If the requirement is “how many introductions exist,” deduplication is actually a bug.
Change the prompt to RETURN end, count(*) AS paths and explain the grouping. end is the grouping key, and count(*) counts matching rows within that group. On this fixture Dee gets paths = 2. Ask what changes if an additional relationship Ada-[:KNOWS]->Dee appears: it changes one-hop reachability, not the two-hop count shown here. This forces the candidate to read the pattern literally. Also distinguish count(end), count(*), and count(DISTINCT end) when optional patterns can introduce nulls; count(expression) ignores null values, while count(*) counts rows. State the intended output contract before editing syntax.
A strong follow-up investigates multiplicity upstream. Two relationships of the same type between the same nodes can themselves create multiple matched paths. A later join to another pattern can multiply rows again. In a real system, show a small sample of full paths with nodes(p) and relationships(p) rather than assuming that repeated end names come from a display bug. The query may be correct for the graph and wrong for the product question.
Question 2: How do you bound a variable-length traversal safely?
Consider “find people reachable from Ada in one to three KNOWS steps.” The current variable-length path documentation describes quantified path patterns and relationships as well as the older variable-length relationship notation. Interview answers should identify which syntax and Cypher version they are using. For a familiar legacy-style illustration, MATCH p=(a:Person {name:'Ada'})-[:KNOWS*1..3]->(b:Person) RETURN p requests paths of bounded length. A bare unbounded expansion changes both the answer set and the worst-case work. It is rarely a responsible default on a dense social graph.
The correct answer is not “add LIMIT 20.” A final limit caps returned rows, but the engine may still expand many paths before that operator. First constrain the starting node with a selective property and index where applicable; specify relationship type and direction; put a meaningful upper bound on hops; and add predicates that reflect the domain. If the product wants only endpoints, consider reducing rows before later joins, but do not discard path multiplicity until the requirement allows it. If the product asks for shortest routes, use documented shortest-path semantics rather than expanding everything and hoping an order/limit pair selects the intended path.
Ask what a cycle does in the fixture. Returning to Ada after three hops via Dee is possible without reusing a relationship. That does not mean a path can run forever under a 1..3 bound. An answer that says “Cypher always prevents cycles” is false: the default constraint concerns relationships, and a node can recur. An answer that says “all paths are simple” quietly imports an algorithmic assumption. Define whether repeated nodes are acceptable, and test a cycle explicitly.
Question 3: Where does cardinality explode in a query plan?
The plan is not merely a list of operator names. It is a record of how estimated and actual rows flow through operations. Neo4j's execution-plan guide distinguishes EXPLAIN, which plans without running the query, from PROFILE, which executes it and shows measurements. Never run PROFILE casually on a write statement in production; it can perform the write. For a read query in a safe environment, compare estimated rows with actual rows, DB hits, and the expansion operator around the widening step.
In the fixture, start at one Ada row, expand to two neighbors, then expand to two rows that both bind Dee. A larger fan-out makes the multiplication obvious: 100 starting nodes, 40 first-hop relationships each, and 30 second-hop relationships per intermediate node can imply about 120,000 raw two-hop matches before filtering, subject to graph overlap and predicates. That number is an illustrative upper-order estimate, not a Neo4j guarantee. It explains why the expensive part may be intermediate rows even if the final DISTINCT returns one node. Do not call a query efficient because its result page is small.
An interview-quality diagnosis names the first unexpectedly large operator, checks whether the start-node lookup is selective, and asks whether a predicate can legally move earlier. It does not automatically demand an index for every label. An index can help find Ada; it cannot make all downstream relationship expansions disappear. A planner estimate can be wrong because statistics or correlations do not match reality. Compare with PROFILE measurements before attributing latency to a specific operator. If the interview gives only a screenshot of a plan, say what is observable and what remains unmeasured.
Question 4: What changes when a second MATCH joins the result?
Now add MATCH (end)-[:WORKS_AT]->(company) after the two-hop path. If Dee has two qualifying WORKS_AT relationships, the two path rows can become four combined rows. The query is joining each path binding to each matching company relationship. The interviewer may show four identical-looking person names and ask whether there are four people. The answer is no: there are four match combinations under these assumptions. Return the path, destination, company, and relationship identity while debugging. Then decide whether the desired output is per path, per person-company pair, or per unique company.
A useful rewrite may aggregate first: WITH end, count(*) AS pathCount before the company match, provided the business question means “count routes to each person, then attach employers.” If the question instead means “count route-employer combinations,” early aggregation changes the question. WITH is not a magic speed keyword; it is a boundary that changes scope and, when aggregated, row cardinality. Explain which variables survive it. If a variable is not projected, later clauses cannot use it. A polished answer states the expected row count after each clause and verifies it with a tiny fixture.
Question 5: How would you make the result stable and testable?
First write down the semantic contract: directed or undirected edges, permitted relationship types, maximum hops, whether a person may appear more than once on a path, whether two relationship instances count separately, and whether a result means path or endpoint. Then encode at least three fixtures: a diamond (two paths, one destination), a cycle (repeated node), and parallel relationships (same endpoints, distinct edges). A fourth fixture with two people sharing a display name catches accidental property-based deduplication. Assert full path identities or stable endpoint IDs, not a hand-waved row total.
For performance, keep a representative larger fixture separate from correctness tests. EXPLAIN checks plan shape without executing; PROFILE measures a controlled read query and should be used with awareness of load. Neo4j's query-tuning overview provides the plan vocabulary, but the particular winning rewrite depends on graph distribution, indexes, Cypher version, and the requested result. Report measured row counts, DB hits, and latency rather than a universal “this syntax is faster” claim. If a proposed optimization changes the number of valid paths, reject it even when it benchmarks well.
A compact way to answer in the interview
Take 30 seconds to draw the diamond graph. Say the unit of output aloud. Walk one MATCH at a time and record expected rows after each clause. Only then write DISTINCT, aggregation, WITH, or a path bound. If the interviewer hands you a plan, point to the first row-count jump and compare estimate to actual execution evidence. Close with one correctness fixture and one measured performance check. This sequence demonstrates graph semantics, query reasoning, and operational judgment without memorizing an operator catalogue.
The first two exercise path reasoning; the last three pressure-test traversal, indexing, and aggregation choices.
A final adversarial check
Imagine the interviewer adds a second KNOWS relationship from Ada to Bo and asks whether your answer still holds. There are now more relationship-distinct ways to reach Dee through Bo, even though the visible node sequence looks the same. If your test compares only names, it misses this change. Return relationship identities during diagnosis and state whether the business requirement treats parallel edges as separate introductions. Next, change Cy's name to Bo. DISTINCT end.name might still work for Dee, but grouping by intermediate display name now merges different people. Stable node identity is safer when the request means people.
Finally, make Dee's WORKS_AT edge optional and include a person with no employer. An OPTIONAL MATCH preserves an outer row with null bindings where a mandatory MATCH would remove it. Recalculate count(*) and count(company) rather than assuming they are equal. This small progression reveals whether the candidate understands Cypher row semantics or has memorized one query shape.