EXPLAIN, PROFILE and query tuning

Neo4j · Advanced · 15 min

A query that is fast on test data can be slow in production; plans show exactly why.

Prefix a query with EXPLAIN to see the plan without running it, or PROFILE to run it and see actual rows and db hits (storage accesses) per operator. Read plans bottom-up: the leaves find starting points, the rest expands and filters.

What to look for:

  • Starting point: NodeIndexSeek or NodeUniqueIndexSeek is good; NodeByLabelScan means scanning all nodes of a label; AllNodesScan (no label) is worst. Add the missing index or label.
  • CartesianProduct: two disconnected patterns multiplied together; connect them or split the query.
  • Eager: the planner must materialise all rows before continuing, usually because a query reads and writes the same data. In big imports this blows up memory; split into separate statements or use CALL ... IN TRANSACTIONS.
  • Row counts that explode mid-plan: aggregate or DISTINCT earlier, or filter before expanding.

The planner is cost-based and uses statistics (counts per label and relationship type, index selectivity). Help it by using parameters (plan cache reuse) and by anchoring patterns on the most selective node. Hints like USING INDEX p:Person(name) exist but are a last resort.

Also check summary.notifications in drivers: Neo4j warns about cartesian products, unbounded paths and deprecated syntax.

Example

PROFILE
MATCH (p:Person {name: 'Ann'})-[:FOLLOWS]->(f)-[:WORKS_AT]->(c:Company)
RETURN c.name, count(*) AS n;

// Before an index exists (read bottom-up):
//   NodeByLabelScan  p:Person          rows 1000000   db hits 1000001
//   Filter           p.name = $autostring_0  rows 1
//   Expand(All)      (p)-[:FOLLOWS]->(f)
//   ...
CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name);
// After:
//   NodeIndexSeek    p:Person(name)    rows 1         db hits 2

Remember

  • EXPLAIN shows the plan; PROFILE runs it with rows and db hits
  • Aim for index seeks as starting points, not label or all-node scans
  • CartesianProduct and Eager operators are red flags
  • Filter and aggregate early to keep row counts small

Watch out for

  • Profiling only on a tiny dev dataset where every plan looks fast.

Check yourself

  1. A PROFILE shows NodeByLabelScan on :Person followed by a Filter on p.email. Likely fix?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: neo4j.com/docs/cypher-manual/current/planning-and-tuning/