Knowledge Graphs

Cypher Guide

Production guide to Cypher and openCypher - pattern matching, Neo4j query tuning, SPARQL comparison, and property graph query patterns at scale.

55 min readIntermediateLast reviewed: 20 July 2026
PrerequisitesProperty Graphs

Quick Summary

Cypher describes graph patterns in ASCII art—the primary query language for property graphs, distinct from SPARQL over RDF knowledge graphs.

One Analogy

Cypher is SQL for whiteboard diagrams: you draw (node)-[:REL]->(node) and the engine finds matching subgraphs.

Engineering Rule

Parameterize every query, index lookup properties, cap path depth, and PROFILE before shipping to production.

TL;DR

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

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

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

  • Cypher queries property graphs, not RDF knowledge graphs. SPARQL queries RDF triples. They solve different models—see comparison table below.

  • 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 fraud rings, product recommendations, and service dependency maps. Cypher is the language you write daily—in Neo4j Browser, Spring Data repositories, Python drivers, and GraphQL resolvers.

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

Cypher powers:

  • Neo4j application backends — 90%+ of Neo4j deployments query via Bolt.

  • Amazon Neptune openCypher — property graph engine mode (separate from Neptune RDF/SPARQL).

  • Graph analytics — GDS algorithms with Cypher write-back.

  • GraphRAG retrieval — k-hop neighborhood expansion around entities, often hybrid with vector RAG via LangChain or LlamaIndex.

Important: Cypher is a graph database query language. It does not validate against OWL ontologies or merge datasets by URI—that is the RDF/SPARQL stack. Many enterprise knowledge graphs use both: RDF canonical layer, Cypher on the LPG projection.

The Problem

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.

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." MERGE handles this atomically—avoiding race conditions from check-then-create logic.

Query sprawl without governance. Ad-hoc Cypher embedded in application code becomes unmaintainable. Production teams maintain version-controlled query libraries with documented parameters and SLAs.

How We Got Here

Cypher evolved from Neo4j's need for a developer-friendly graph query language:

Diagram: Cypher and openCypher timeline

flowchart LR
    A[Gremlin TinkerPop 2009] --> B[Neo4j Cypher 2011]
    B --> C[openCypher 2015]
    C --> D[Neptune openCypher 2018]
    D --> E[Neo4j 5 relationship indexes]
    E --> F[Cypher + GDS + GraphRAG 2020s]
Era Development
2011 Neo4j introduces Cypher—ASCII-art patterns
2015 openCypher specification for vendor portability
2018+ Neptune, Memgraph adopt openCypher
2020s CALL subqueries, relationship indexes, GDS integration, AI retrieval patterns

ISO GQL is emerging as a broader standard, but openCypher remains the de facto language for property graph applications today.

Architecture

Cypher execution in Neo4j follows a predictable pipeline:

Stage Function
Parser AST from query string
Planner Logical plan using statistics (label counts, degree distribution)
Runtime Slotted, pipelined, or parallel execution
Operators NodeIndexSeek, Expand(All), Filter, EagerAggregation

Diagram: Cypher query execution

sequenceDiagram
    participant App as Application
    participant Driver as Bolt Driver
    participant Parser as Cypher Parser
    participant Planner as Query Planner
    participant Runtime as Runtime Engine
    participant Store as Graph Store
    App->>Driver: parameterized query
    Driver->>Parser: parse AST
    Parser->>Planner: logical plan
    Planner->>Runtime: physical plan
    Runtime->>Store: index seek / expand
    Store-->>Runtime: rows
    Runtime-->>App: result set

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 Cypher lifecycle:

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

Step 2: Write parameterized Cypher. $userId, never string concatenation.

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

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

Step 5: Document in query library. Parameters, SLA, expected cardinality.

Step 6: Deploy via centralized repository. No ad-hoc queries in hot paths.

Step 7: Monitor in production. Neo4j query log, slow query alerts.

Step 8: Iterate. Rewrite top slow queries each sprint.

Real Production Example

Telecom: microservice dependency blast radius (Neo4j)

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);

Streaming ingestion:

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.ownerTeam AS team,
       min(length(path)) AS minDistance
ORDER BY minDistance

Outcome: MTTR for impact identification dropped from 15 minutes to under 30 seconds.

Finance: fraud ring detection

MATCH (a:Account)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(b:Account)
WHERE a <> b
MATCH (a)-[t1:TRANSFERRED]->(), (b)-[t2:TRANSFERRED]->()
WHERE t1.amount > 100000 AND t2.amount > 100000
  AND duration.between(t1.timestamp, t2.timestamp).hours < 1
RETURN a.id, b.id, d.fingerprint, count(*) AS linkStrength
ORDER BY linkStrength DESC

Edge property filters on TRANSFERRED are native—no reification required as in RDF.

Manufacturing: supply chain traceability (Memgraph)

Real-time :SUPPLIES traversals with {leadTimeDays, lotNumber} on edges. Memgraph in-memory engine serves <50ms queries on the factory floor.

Life sciences: GraphRAG neighborhood expansion

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

Combine with vector-retrieved document chunks from Best Vector Databases via Neo4j Vector for hybrid GraphRAG + RAG.

GDS PageRank write-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.

Query catalog pattern

Maintain version-controlled .cypher files with header comments documenting params, SLA, and owner. CI runs PROFILE against staging; fails merge if db hits exceed baseline. Separates Browser ad-hoc queries from production-approved templates.

Parameter binding

Never interpolate user input into query strings—use driver parameter maps ($id) to prevent Cypher injection and enable plan cache reuse across requests.

EXPLAIN output reading guide

When reviewing PROFILE output, prioritize these operators:

Operator Good sign Bad sign
NodeIndexSeek Low db hits, uses index
Expand(All) Small label cardinality Large label, high db hits
Filter Early in plan, reduces rows After large expand
EagerAggregation After filters Before filters on huge sets

If Expand(All) dominates on a million-node label, add or fix index on the property in MATCH/WHERE.

Ingestion transaction sizing

Batch MERGE operations in transactions of 10K–50K operations. Too small: commit overhead dominates. Too large: heap pressure and long lock holds. Monitor transaction rollback rate during peak ingestion.

OPTIONAL MATCH patterns for nullable data

Use OPTIONAL MATCH when relationships may not exist—employees without managers, services without owners:

MATCH (e:Employee)
OPTIONAL MATCH (e)-[:REPORTS_TO]->(mgr:Employee)
RETURN e.name AS employee, coalesce(mgr.name, 'No manager') AS manager

Left-outer-join semantics prevent dropping rows when the optional pattern fails—critical for reporting queries over incomplete graphs.

UNWIND for bulk operations

Expand list parameters into rows for batch upserts:

UNWIND $rows AS row
MERGE (p:Product {sku: row.sku})
SET p.name = row.name, p.price = toFloat(row.price), p.updatedAt = datetime()

UNWIND is the standard pattern for Kafka micro-batch ingestion—one Cypher statement processes hundreds of events per transaction.

CALL subquery for correlated counts

Neo4j 4.1+ CALL { } subqueries isolate correlated aggregations:

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

Subqueries prevent intermediate row explosion when counting dependencies per team—each team gets an isolated count without cross-product effects in the outer MATCH.

Relationship index example (Neo4j 5+)

CREATE REL INDEX transfer_amount IF NOT EXISTS
FOR ()-[r:TRANSFERRED]-() ON (r.amount);

Speeds fraud queries filtering on r.amount > 100000 mid-traversal.

Detach delete and graph maintenance

// Remove deactivated user and all relationships
MATCH (u:Person {id: $userId, status: 'deactivated'})
DETACH DELETE u

Schedule periodic orphan detection: MATCH (n) WHERE NOT (n)--() RETURN labels(n), count(n)—orphans indicate ingestion bugs or incomplete MERGE patterns.

Shortest path with constraints

MATCH (start:Service {id: $from}), (end:Service {id: $to}),
      path = shortestPath((start)-[:DEPENDS_ON*..8]-(end))
WHERE all(r IN relationships(path) WHERE r.critical = true)
RETURN [n IN nodes(path) | n.id] AS pathIds

Filter paths by edge properties during shortest-path computation—common in telecom route planning where only active links qualify.

Python driver production pattern

from neo4j import GraphDatabase

class ServiceGraph:
    def __init__(self, uri, auth):
        self.driver = GraphDatabase.driver(uri, auth=auth, max_connection_pool_size=50)

    def blast_radius(self, service_id: str) -> list[dict]:
        query = """
        MATCH (failed:Service {id: $id})
        MATCH (dep:Service)-[:DEPENDS_ON*1..4]->(failed)
        RETURN DISTINCT dep.id AS id, dep.ownerTeam AS team
        """
        with self.driver.session() as session:
            return [r.data() for r in session.run(query, id=service_id)]

    def close(self):
        self.driver.close()

Use connection pooling, parameterized queries, and explicit session scoping. Never create a new driver per request.

Query timeout and guardrails

Set transaction timeouts in production drivers (transaction_timeout in Neo4j config) to kill runaway traversals. Application layer should enforce max result rows and max path depth before executing user-supplied patterns. For admin tools exposing Cypher, whitelist query templates—never pass raw user text to session.run().

Design Decisions

Decision Option A Option B When to choose
CREATE vs MERGE CREATE always new MERGE upsert MERGE for ingestion; CREATE for seed data
Path direction Directed -[:REL]-> Undirected -[:REL]- Directed when semantics matter
WITH vs subquery WITH pipelines rows CALL {} subquery Subqueries for correlated isolation
APOC vs native APOC procedures Pure Cypher APOC for import/export; native for hot paths
Cypher vs SPARQL Property graph queries RDF triple queries Cypher for LPG apps; SPARQL for RDF KG
GraphQL vs Bolt Neo4j GraphQL Library Direct driver GraphQL for frontends; Bolt for backend control

Comparisons

Cypher vs SPARQL

Capability Cypher (Property Graph) SPARQL (RDF)
Data model Labeled nodes + typed relationships Subject-predicate-object triples
Pattern syntax (a)-[:REL]->(b) ?s ?p ?o triple patterns
Edge properties Native {key: value} on relationships RDF-star, reification, or n-ary nodes
Multi-hop -[:REL*1..4]-> Property paths ex:rel+
Optional match OPTIONAL MATCH OPTIONAL { }
Aggregation RETURN count(*), collect() GROUP BY, COUNT, SUM
Shortest path shortestPath((a)-[:REL*]-(b)) Path queries with property paths
Inference None built-in OWL reasoner + materialized triples
Identity Internal ID + business key Global URI
Standards openCypher (de facto) W3C SPARQL 1.1
Best for App traversals, fraud, recommendations Ontology queries, linked data merge
Typical stores Neo4j, Memgraph, Neptune PG GraphDB, Jena, Oxigraph, Neptune RDF

Same blast-radius query: Cypher uses variable-length -[:DEPENDS_ON*1..4]->; SPARQL uses ex:dependsOn+ property paths over URIs. Cypher is more compact for edge property filters mid-path.

Cypher vs Gremlin

Dimension Cypher Gremlin
Style Declarative patterns Imperative traversal steps
Learning curve Lower for SQL developers Higher; more flexible
Neptune support openCypher mode Native Gremlin mode
Ecosystem Neo4j-centric TinkerPop multi-vendor

openCypher portability

Feature Neo4j Neptune Memgraph
MATCH/CREATE/MERGE
shortestPath
APOC procedures partial
GDS library partial
Relationship indexes varies

Test queries against target engine before migration.

Common Mistakes

  1. String interpolation instead of parameters—Cypher injection risk. 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.

  4. Returning entire nodes—Bloated responses. RETURN specific properties.

  5. Eager aggregation before filter—Use WITH to pipeline: filter before count.

  6. Using Cypher against RDF triples—Wrong tool. Use SPARQL for RDF knowledge graphs.

  7. Skipping query plan review after data growth. Label cardinality doubles after Black Friday; queries that used index seek may fall back to Expand(All). Re-PROFILE on schedule.

Where It Breaks Down

Cypher fails in production when teams treat it like SQL with graph syntax:

Cross-database joins. Cypher queries one graph. Joining Neo4j with PostgreSQL requires application federation—no foreign data wrappers. Pre-join critical dimensions or sync selective properties to the graph.

Full graph scans at scale. MATCH (n) RETURN count(n) on billion-node graphs blocks the store. Use CALL db.stats.retrieve('GRAPH COUNTS') or approximate metrics.

APOC dependency in portable queries. Queries using apoc.* fail on Neptune. Maintain a portable query tier and Neo4j-specific analytics tier separately.

GraphQL layer N+1 patterns. Neo4j GraphQL can generate unbounded traversals if resolvers lack depth limits—mirror Cypher path caps in API schema.

Hybrid RAG without entity resolution. Cypher expands neighborhoods around string-matched entity names that don't align with vector chunk entities—ground extractions to stable business keys first.

Log p95 latency per query template, db hits from PROFILE samples, and transaction rollback rate. When latency spikes, check: missing index, uncapped path, or lock contention on hot MERGE keys.

When NOT

Skip Cypher (use SPARQL or SQL instead) when:

  1. Data is RDF triples with URI identity and OWL inference is required—SPARQL on a triple store.

  2. Cross-org linked data merge is primary—RDF/SPARQL ecosystem.

  3. Queries are primarily aggregations over entire graph—warehouse SQL may be faster.

  4. Ultra-low-latency key-value lookup only—graph DB overhead unnecessary.

  5. Team standardized on Gremlin on Neptune—porting has cost.

Use Cypher when you have a property graph and traversal-heavy, edge-property-rich queries dominate.

Running in Production

Best Practice

Best Practices — Parameterize all queries, PROFILE hot paths, index lookup properties, cap path depth, maintain query library with SLAs.

Dimension Consideration
Scaling Read replicas for query-heavy workloads. Causal cluster with routing driver. Neo4j Fabric for sharded domains.
Latency Target p99 < 50ms indexed lookups; < 500ms 3-hop traversals. PROFILE queries > 200ms.
Cost Bolt connection pooling. Batch MERGE in 10K–50K op transactions for ingestion throughput.
Monitoring Query log, plan cache hit rate, transaction rollback rate, store size growth.
Evaluation Testcontainers Neo4j tests; assert row counts and paths for golden queries.
Security Parameterized queries only; restrict APOC export procedures in production.

Important

Run PROFILE on every query executing >100 times per minute. Index seeks should dominate; Expand(All) on large labels is a red flag.

Diagram: Query language selection

flowchart TD
    A[Graph query needed?] --> B{Data model?}
    B -->|LPG nodes + rels| C[Cypher / Gremlin]
    B -->|RDF triples + URIs| D[SPARQL]
    C --> E{Engine?}
    E -->|Neo4j| F[Cypher + GDS]
    E -->|Neptune PG| G[openCypher]
    D --> H[GraphDB / Jena / Oxigraph]

Prerequisites: Property Graphs · Knowledge Graphs

Next topics: Graph Databases · SPARQL · GraphRAG

Interview Questions

  1. Cypher vs SPARQL—when do you choose each?

    • Expected: Cypher for LPG traversals with edge props; SPARQL for RDF triples, URIs, OWL materialization.
  2. Why MERGE instead of CREATE in ingestion?

    • Expected: idempotent upsert; safe for streaming retries and concurrent writers.
  3. What does PROFILE tell you that EXPLAIN doesn't?

    • Expected: actual row counts and db hits after execution—reveals Expand(All) vs index seek.
  4. How cap variable-length paths and why?

    • Expected: *1..N prevents exponential expansion; unbounded paths cause outages.
  5. How integrate Cypher with RAG?

    • Expected: entity extraction to graph; k-hop Cypher expansion; combine with vector chunks via LangChain/LlamaIndex.
  6. CREATE vs MERGE performance implications?

    • Expected: MERGE requires index lookup first—ensure unique constraints on merge keys.
  7. Can Cypher query RDF in Neo4j natively?

    • Expected: no—neosemantics imports RDF as LPG; not a SPARQL engine.
  8. Neptune openCypher vs Neo4j Cypher differences?

    • Expected: no APOC/GDS; test MERGE, shortestPath, temporal functions for portability.

Key Takeaways

  • Cypher expresses property graph patterns as ASCII art—( )-[ ]->( ) matched by the query planner.
  • MERGE provides idempotent upserts essential for event-driven ingestion.
  • Parameterize queries, index lookup properties, cap variable-length path depth.
  • PROFILE production queries—index seeks should dominate over Expand(All).
  • Cypher is for property graphs; SPARQL is for RDF knowledge graphs—different models, different tools.
  • Pair Cypher traversals with vector RAG for hybrid GraphRAG pipelines.

FAQs

What is Cypher?

A declarative graph query language using ASCII-art patterns. Created by Neo4j, standardized as openCypher.

Is Cypher only for Neo4j?

Neo4j is primary. Neptune and Memgraph support openCypher with dialect differences.

CREATE vs MERGE?

CREATE always creates new. MERGE finds or creates—idempotent upsert for pipelines.

How pass parameters?

Driver maps: session.run(query, {userId: '123'}) with $userId in query.

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

Exactly two KNOWS relationships in sequence—friends of friends.

EXPLAIN vs PROFILE?

EXPLAIN shows plan without execution. PROFILE executes and reports actual db hits.

Can Cypher do shortest path?

Yes: shortestPath((a)-[:REL*]-(b)). Always cap hop count.

Cypher vs Gremlin on Neptune?

Both supported in different engine modes. Cypher for declarative patterns; Gremlin for imperative traversals.

How use Cypher with GraphRAG?

Extract entities to graph; expand neighborhoods at query time; pass to LLM with vector-retrieved chunks.

openCypher portability tips?

Avoid APOC/GDS in portable queries; test MERGE and path functions on target engine.

How tune variable-length path depth?

Start with *1..3 for interactive APIs; increase only after PROFILE shows acceptable db hits. Fraud investigations may use *1..5 offline with query timeouts disabled and read replicas.

When use CALL {} subquery vs WITH?

CALL { ... } isolates correlated subqueries—each team dependency count without polluting outer row cardinality. WITH pipelines simpler filters and limits between MATCH clauses.

How do I debug slow Cypher queries?

Run PROFILE, identify Expand(All) or high db hits, check index existence with SHOW INDEXES, verify selective START node (indexed property in first MATCH), reduce variable-length depth, and confirm WHERE filters apply before expansion not after.

Should I use Neo4j GraphQL or direct Bolt?

GraphQL suits frontend teams needing typed APIs and rapid iteration. Direct Bolt suits backend services requiring query control, custom timeouts, and PROFILE-governed templates. Many production systems use both—GraphQL for CRUD, Bolt for analytics and fraud queries.

How load CSV data at scale?

For initial bulk loads, use neo4j-admin database import full offline—orders of magnitude faster than LOAD CSV for billions of relationships. For incremental updates, use LOAD CSV or streaming MERGE with UNWIND in manageable batch sizes.

How prevent Cypher injection?

Always bind user input via driver parameters ($userId). Never concatenate strings into query text. For admin interfaces, use allowlisted query templates rather than free-text Cypher input. Audit query logs for suspicious patterns containing embedded literals instead of parameters.

What is the collect() function?

collect() aggregates values into a list per group—useful for returning all neighbors of an entity in one row: RETURN e.name, collect(DISTINCT neighbor.name) AS neighbors. Pair with size(collect(...)) for counts without separate aggregation queries.

How return paths as node lists?

Use list comprehension on path variables: RETURN [node IN nodes(path) | node.id] AS pathIds, length(path) AS hops. This serializes traversals cleanly for API responses without returning full node objects or relationship internals.

References

Next Topics

Learning Path

Continue Learning

Related Guides

Related Tools

ToolCategoryPurposeWebsiteBest For
Neo4j Vector Index
CloudSelf-hosted
Vector DBVector search on Neo4j graph database — combine embeddings with knowledge graphs.neo4j.comGraphRAG
LangChain
PopularOpen SourceAPI
frameworksFramework for building LLM-powered applications and workflows.langchain.comRAG systems
LlamaIndex
Open SourceAPI
frameworksData framework for connecting LLMs to private and structured data.llamaindex.aiRAG over documents

Related Rankings