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:
-
Parser - AST from query string.
-
Planner - Generates logical plan using statistics (label counts, degree distribution).
-
Runtime - Slotted, pipelined, or parallel execution engines.
-
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:
-
Identify access pattern - Lookup by ID? Multi-hop traversal? Aggregation?
-
Write Cypher with parameters -
$userId, not string concatenation. -
Create supporting indexes -
CREATE INDEX ... FOR (n:Label) ON (n.prop). -
PROFILE in staging - Verify index usage; db hits should be O(log n), not O(n).
-
Deploy via query library - Centralized repository of approved queries.
-
Monitor in production - Neo4j query log,
db.stats.queriesmetrics. -
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
-
String interpolation instead of parameters - Cypher injection allows arbitrary graph reads. Always use
$params. -
Unbounded variable-length paths -
-[*]->explores exponentially. Cap with*1..N. -
Matching without indexes - Full label scans on million-node labels timeout. Index every high-selectivity lookup property.
-
Returning entire nodes/relationships - Bloated responses and serialization cost.
RETURN specific properties.
- 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
PROFILEon 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).
Related Technologies
-
Property Graphs: Data model Cypher queries.
-
Graph Databases: Neo4j and alternatives running Cypher.
-
SPARQL: RDF query language counterpart.
-
Knowledge Graphs: Conceptual foundation.
-
Enterprise Knowledge Graphs: Cypher at scale in organizations.
-
GraphRAG: Cypher for neighborhood retrieval in AI pipelines.
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
- From Local to Global: A Graph RAG Approach (Microsoft, 2024)
- Microsoft GraphRAG Documentation
- Neo4j LLM Knowledge Graph Builder
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.