DataAIHub
DataAIHubNews · Research · Tools · Learning
Knowledge Graphs

Cypher Guide

Complete guide to Cypher - the declarative query language for property graphs. Pattern matching, traversals, mutations, and Neo4j production patterns.

11 min readIntermediateUpdated Jul 5, 2026
PrerequisitesProperty Graphs

TL;DR

  • Cypher describes graph patterns in ASCII art - (a:Person)-[:KNOWS]->(b:Person) - and the engine finds matching subgraphs.

  • MATCH reads, CREATE/MERGE writes - MERGE provides idempotent upsert semantics critical for streaming ingestion.

  • Variable-length paths - (a)-[:DEPENDS_ON*1..5]->(b) traverse multiple hops in one query.

  • openCypher standardizes Cypher across Neo4j, Neptune, Memgraph with minor dialect differences.

  • Profile every production query - unindexed property lookups and uncapped traversals are the top causes of graph DB outages.

Why This Matters

You deployed Neo4j for a property graph. Applications need to find fraud rings, recommend products, and map service dependencies. Cypher is the language you write daily - in the Neo4j Browser, Spring Data repositories, Python drivers, and GraphQL resolvers.

Cypher's pattern syntax mirrors whiteboard diagrams. Engineers who sketched (User)-[:PURCHASED]->(Product) can translate that directly to executable queries without learning SPARQL algebra or Gremlin traversers.

Cypher powers:

  • Neo4j application backends - 90%+ of Neo4j deployments query via Cypher over Bolt protocol.

  • Amazon Neptune - openCypher support on the property graph engine.

  • Graph analytics pipelines - Export subgraphs, run GDS algorithms, write results back with Cypher.

  • GraphRAG retrieval - k-hop neighborhood expansion around entities extracted from documents.

If your graph is a labeled property graph, Cypher (or its Gremlin alternative) is your query interface.

The Problem Cypher Solves

Imperative traversal code is brittle. Walking a graph in application code requires manual queue management, cycle detection, and depth tracking. Cypher declaratively specifies the pattern; the query planner optimizes index usage and traversal order.

Graph queries in SQL are painful. Recursive CTEs for "friends of friends" work but degrade and are hard to read. Cypher's native path syntax is clearer and faster on graph-native indexes.

Idempotent graph upserts. Streaming events need "create if not exists, update if exists" for nodes and relationships. MERGE handles this atomically - avoiding race conditions from check-then-create application logic.

What Is Cypher?

Cypher is a declarative graph query language created by Neo4j, standardized as openCypher. Queries consist of clauses:

Clause Purpose
MATCH Find patterns in the graph
WHERE Filter matched patterns
RETURN Project results
CREATE Create new nodes/relationships
MERGE Match or create (upsert)
SET Update properties
DELETE Remove nodes/relationships
WITH Pipeline results between subqueries
UNWIND Expand lists into rows
CALL Invoke procedures (APOC, GDS)

Query Languages

Language Model Syntax style
Cypher Property graph ASCII-art patterns
SPARQL RDF triples Triple patterns with ?vars
Gremlin Property graph Step-by-step traversal chain

How Cypher Works

Basic pattern matching

// Find people and their organizations
MATCH (p:Person)-[:WORKS_AT]->(o:Organization)
WHERE p.name STARTS WITH 'A'
RETURN p.name AS person, o.name AS organization
ORDER BY p.name
LIMIT 50

Creating data

CREATE (alice:Person {id: 'alice', name: 'Alice Chen'})
CREATE (acme:Organization {id: 'acme', name: 'Acme Corp'})
CREATE (alice)-[:WORKS_AT {since: 2019, role: 'Engineer'}]->(acme)

MERGE - idempotent upsert

MERGE (p:Person {id: $personId})
ON CREATE SET p.createdAt = datetime(), p.name = $name
ON MATCH SET p.lastSeen = datetime()
MERGE (o:Organization {id: $orgId})
MERGE (p)-[r:WORKS_AT]->(o)
ON CREATE SET r.since = date()

Parameters ($personId) bind from driver - never string-interpolate user input.

Variable-length paths

// Shortest path between two services, max 6 hops
MATCH (start:Service {id: 'auth-api'}),
      (end:Service {id: 'payment-api'}),
      path = shortestPath((start)-[:DEPENDS_ON*..6]-(end))
RETURN [n IN nodes(path) | n.id] AS pathIds,
       length(path) AS hops

Always cap variable-length patterns: *1..5, not unbounded *.

Aggregations

MATCH (u:Person)-[:PURCHASED]->(p:Product)
RETURN p.category AS category,
       count(DISTINCT u) AS uniqueBuyers,
       sum(p.price) AS totalRevenue
ORDER BY uniqueBuyers DESC

OPTIONAL MATCH - nullable relationships

// Employees with optional manager (not everyone has one)
MATCH (e:Employee)
OPTIONAL MATCH (e)-[:REPORTS_TO]->(mgr:Employee)
RETURN e.name AS employee,
       coalesce(mgr.name, 'No manager') AS manager

UNWIND - expand lists into rows

// Bulk create from parameter list
UNWIND $rows AS row
MERGE (p:Product {sku: row.sku})
SET p.name = row.name, p.price = row.price

CALL subqueries (Neo4j 4.1+)

// Count dependencies per team in isolated subquery
MATCH (t:Team)
CALL {
  WITH t
  MATCH (t)<-[:OWNED_BY]-(s:Service)-[:DEPENDS_ON*1..3]->(dep:Service)
  RETURN count(DISTINCT dep) AS depCount
}
RETURN t.name AS team, depCount
ORDER BY depCount DESC

Query Languages in Production

Teams typically maintain a query catalog - version-controlled Cypher files with parameters documented:

// queries/blast-radius.cypher
// Params: $failedServiceId (string)
// SLA: p99 < 200ms at 10M Service nodes
MATCH (failed:Service {id: $failedServiceId})
MATCH (dependent:Service)-[:DEPENDS_ON*1..4]->(failed)
RETURN dependent.id AS id, dependent.ownerTeam AS team

RAG couples a dense vector index of external knowledge with a sequence-to-sequence generator. At query time, the retriever selects relevant passages and the generator conditions its answer on that evidence.

Architecture

Cypher execution in Neo4j:

  1. Parser - AST from query string.

  2. Planner - Generates logical plan using statistics (label counts, degree distribution).

  3. Runtime - Slotted, pipelined, or parallel execution engines.

  4. Operators - NodeIndexSeek, Expand(All), Filter, EagerAggregation.

Optimization levers:

  • Indexes on properties used in MATCH/WHERE.

  • Constraints enable index-backed MERGE.

  • Relationship indexes (Neo4j 5+) for type + property filters.

  • PROFILE to inspect db hits and row counts.

Step-by-Step Flow

Production query lifecycle:

  1. Identify access pattern - Lookup by ID? Multi-hop traversal? Aggregation?

  2. Write Cypher with parameters - $userId, not string concatenation.

  3. Create supporting indexes - CREATE INDEX ... FOR (n:Label) ON (n.prop).

  4. PROFILE in staging - Verify index usage; db hits should be O(log n), not O(n).

  5. Deploy via query library - Centralized repository of approved queries.

  6. Monitor in production - Neo4j query log, db.stats.queries metrics.

  7. Iterate - Rewrite top slow queries each sprint.

Real Production Example

A platform team maintains a microservice dependency graph for incident response.

Schema setup:

CREATE CONSTRAINT service_id IF NOT EXISTS
FOR (s:Service) REQUIRE s.id IS UNIQUE;

CREATE INDEX service_tier IF NOT EXISTS
FOR (s:Service) ON (s.tier);

CREATE INDEX service_team IF NOT EXISTS
FOR (s:Service) ON (s.ownerTeam);

Ingest from service catalog (streaming):

MERGE (s:Service {id: $serviceId})
SET s.name = $name,
    s.tier = $tier,
    s.ownerTeam = $team,
    s.updatedAt = datetime()
WITH s
UNWIND $dependencies AS dep
MERGE (d:Service {id: dep.targetId})
MERGE (s)-[r:DEPENDS_ON]->(d)
SET r.protocol = dep.protocol,
    r.critical = dep.critical

Blast radius query:

MATCH (failed:Service {id: $failedServiceId})
MATCH path = (dependent:Service)-[:DEPENDS_ON*1..4]->(failed)
WHERE dependent.tier IN ['critical', 'customer-facing']
RETURN DISTINCT dependent.id AS serviceId,
       dependent.name AS name,
       dependent.ownerTeam AS team,
       min(length(path)) AS minDistance
ORDER BY minDistance, dependent.id

PagerDuty integration - who to page:

MATCH (s:Service {id: $serviceId})<-[:DEPENDS_ON*0..3]-(impacted:Service)
WHERE impacted.tier = 'critical'
MATCH (impacted)-[:OWNED_BY]->(t:Team)-[:HAS_ONCALL]->(p:Person)
RETURN DISTINCT p.email AS email, t.name AS team, impacted.id AS impactedService

APOC export for postmortem:

CALL apoc.export.json.query(
  "MATCH (s:Service {id: $id})-[r:DEPENDS_ON*0..2]-(n) RETURN s, r, n",
  "incident-subgraph.json",
  {params: {id: $serviceId}}
)

Outcome: Mean time to identify blast radius dropped from 15 minutes (manual wiki lookup) to under 30 seconds.

Neo4j Graph Data Science integration

Analytics queries often combine Cypher with GDS library calls:

// Project service dependency subgraph, run PageRank, write scores back
CALL gds.graph.project(
  'service-deps',
  'Service',
  {DEPENDS_ON: {orientation: 'REVERSE'}}
)

CALL gds.pageRank.stream('service-deps')
YIELD nodeId, score
WITH gds.util.asNode(nodeId) AS service, score
SET service.pagerank = score
RETURN service.id, score
ORDER BY score DESC
LIMIT 20

PageRank on reversed DEPENDS_ON edges surfaces services whose failure impacts the most downstream dependents - prioritization input for reliability engineering.

Comparison with SPARQL for the same pattern

The dependency blast-radius query in SPARQL uses property paths (ex:dependsOn+). Cypher's variable-length syntax is more compact for property graphs but encodes the same traversal semantics:

Concern Cypher SPARQL
Multi-hop -[:REL*1..4]-> ex:rel+ property path
Edge properties mid-path Filter on r in -[r:REL*]-> Filter on reified/stated edges
Shortest path shortestPath() shortestPath() via path queries

Design Decisions

Decision Option A Option B When to choose
CREATE vs MERGE CREATE always new MERGE upsert MERGE for ingestion; CREATE for one-time seed data
Path direction Directed -[:REL]-> Undirected -[:REL]- Directed when semantics matter (DEPENDS_ON); undirected for symmetric (KNOWS)
WITH vs subquery WITH pipelines rows CALL {} subquery Subqueries for correlated isolation; WITH for simple filtering
APOC vs native APOC procedures Pure Cypher APOC for import/export, graph refactors; native for hot-path queries
GraphQL vs direct Cypher Neo4j GraphQL Library Bolt driver GraphQL for frontend teams; direct Cypher for backend control

⚠ Common Mistakes

  1. String interpolation instead of parameters - Cypher injection allows arbitrary graph reads. Always use $params.

  2. Unbounded variable-length paths - -[*]-> explores exponentially. Cap with *1..N.

  3. Matching without indexes - Full label scans on million-node labels timeout. Index every high-selectivity lookup property.

  4. Returning entire nodes/relationships - Bloated responses and serialization cost.

RETURN specific properties.

  1. Eager aggregation before filter - Aggregate after WHERE reduces row counts. Use WITH to pipeline: MATCH ... WITH ... WHERE ... RETURN count(*).

Where It Breaks Down

Cross-database joins. Cypher queries one graph. Joining Neo4j with PostgreSQL requires application-level federation or sync - no SQL-style foreign data wrappers.

Full graph scans. MATCH (n) RETURN count(n) on billion-node graphs is expensive. Use store statistics APIs or approximate counts.

RDF interoperability. Cypher does not query native RDF triples. Import RDF via neosemantics or maintain parallel RDF store for standards exchange.

Ultra-low-latency key-value access. Single-node lookup by indexed property is fast; complex pattern matching at 10K QPS needs caching layers above Neo4j.

Running in Production

Best Practice

Best Practices - Instrument every stage, version embedding models, enforce access control at retrieval time, and evaluate on a fixed golden set before shipping changes.

Dimension Consideration
Scaling Read replicas for query-heavy workloads. Causal cluster with routing driver. Shard domains across databases (Neo4j Fabric) before single-graph limits.
Latency Target p99 < 50ms for indexed lookups; < 500ms for 3-hop traversals. PROFILE queries exceeding 200ms.
Cost Bolt connection pooling reduces overhead. Batch MERGE in transactions of 10K–50K ops for ingestion throughput.
Monitoring Query log (db.query.logging), execution plan cache hit rate, transaction rollback rate, store size growth.
Evaluation Automated tests with Testcontainers Neo4j; assert result row counts and specific paths for golden queries.
Security Parameterized queries only; role-based access; restrict APOC procedures in production (apoc.export.* disabled for app roles).

Important

Run PROFILE on every query that will execute more than 100 times per minute. Index seeks should dominate; Expand(All) on large labels is a red flag.

Ecosystem

  • Implementations: Neo4j (reference), Amazon Neptune openCypher, Memgraph, SAP HANA Graph.

  • Drivers: Official drivers for Java, Python, JavaScript, Go, .NET - Bolt protocol.

  • Tooling: Neo4j Browser, Bloom (visual exploration), neo4j-migrations, Cypher Shell.

  • Extensions: APOC (utilities), Graph Data Science (algorithms), neosemantics (RDF import).

  • Frameworks: Spring Data Neo4j, Neo4j GraphQL Library, Quine (streaming graph).

Learning Path

Prerequisites: Property Graphs · Knowledge Graphs

Next topics: Graph Databases · Enterprise Knowledge Graphs · GraphRAG

Estimated time: 50 min · Difficulty: Intermediate

FAQs

What is Cypher?

A declarative graph query language using ASCII-art patterns to match, create, and update labeled property graphs. Created by Neo4j, standardized as openCypher.

Is Cypher only for Neo4j?

Neo4j is the primary implementation. Amazon Neptune, Memgraph, and others support openCypher with dialect differences. Check compatibility for MERGE, APOC, and GDS features.

What is the difference between CREATE and MERGE?

CREATE always creates new elements. MERGE finds existing pattern or creates if absent - idempotent upsert for ingestion pipelines.

How do I pass parameters to Cypher?

Via driver maps: session.run(query, {userId: '123'}) with $userId in the query. Never embed values in query strings.

What does -[:KNOWS*2]-> mean?

Exactly two KNOWS relationships in sequence - friends of friends, not including the start node as an intermediate.

How do I delete a node and its relationships?

MATCH (n:Person {id: $id})
DETACH DELETE n

DETACH DELETE removes all connected relationships first.

What is WITH used for?

Pipelines query results between clauses - filter, aggregate, or limit before the next MATCH. Like SQL subqueries or intermediate result sets.

How do I return a path as a list of node names?

RETURN [node IN nodes(path) | node.name] AS pathNames

Can Cypher do shortest path?

Yes: shortestPath((a)-[:REL*]-(b)) or allShortestPaths for all equally short paths. Always cap hop count.

What is EXPLAIN vs PROFILE?

EXPLAIN shows planned operators without execution. PROFILE executes and reports actual row counts and db hits - use PROFILE for optimization.

How do I import CSV data?

LOAD CSV WITH HEADERS FROM 'file:///products.csv' AS row
MERGE (p:Product {sku: row.sku})
SET p.name = row.name, p.price = toFloat(row.price)

For large imports, use neo4j-admin database import full.

How does Cypher compare to Gremlin?

Cypher is declarative pattern matching. Gremlin is imperative traversal steps. Neptune supports both; choose based on team preference and library ecosystem.

What is openCypher?

An open specification of Cypher led by Neo4j, implemented by multiple vendors. Core MATCH/CREATE/MERGE syntax is portable; procedures (APOC, GDS) and some functions are Neo4j-specific.

How do I handle temporal validity in Cypher?

Store validFrom/validTo on relationships. Query active edges only:

MATCH (a)-[r:WORKS_AT]->(b)
WHERE r.validFrom <= date() AND (r.validTo IS NULL OR r.validTo >= date())
RETURN a, b

Can I use Cypher with GraphRAG?

Yes. Extract entities to Neo4j during indexing; at query time expand neighborhoods:

MATCH (e:Entity)
WHERE e.name IN $extractedEntityNames
MATCH (e)-[r*1..2]-(neighbor)
RETURN e, collect(DISTINCT neighbor) AS context

Pass serialized neighborhood to the LLM alongside vector-retrieved chunks.

References

Further Reading

Summary

  • Cypher expresses graph patterns as ASCII art - ( )-[ ]->( ) - matched by the query planner. - MERGE provides idempotent upserts essential for event-driven graph ingestion. - Always parameterize queries, index lookup properties, and cap variable-length path depth. - PROFILE production queries to verify index usage and avoid Expand(All) on large labels. - Cypher is the primary interface for Neo4j and openCypher-compatible graph databases.

  • Pair Cypher traversals with application caching for high-QPS read patterns.

Next Topics

Learning Path

Continue Learning