DevTools

Cheatsheet Neo4j

Base de dados de grafos com linguagem de consulta Cypher

Back to languages
Neo4j
72 cards found
Categories:
Versions:

Installation and Setup


9 cards
Neo4j Desktop
# Windows/macOS/Linux:
# 1. Download at neo4j.com/download
# 2. Install Neo4j Desktop (free)
# 3. Create a "Project"
# 4. "Add Database" > Create Local
#    Database (set a password)
# 5. "Start" button

# The Desktop includes:
#   - Neo4j Server
#   - Neo4j Browser (interface)
#   - Bloom (visualization)

# Stop: "Stop" button

The Neo4j Desktop is the simplet way (free for local use). Create a Project, add a Local Database with a password and click Home. It includes the Neo4j Browser for visual queries.

First query
// Create a node:
CREATE (p:Person {name: "Anna", age: 30})

// See all nodes:
MATCH (n) RETURN n

// Create a relationship:
CREATE (a:Person {name: "Ray"})
MATCH (x:Person {name: "Anna"}),
      (y:Person {name: "Ray"})
CREATE (x)-[:FRIEND_OF]->(y)

// Query:
MATCH (a:Person)-[:FRIEND_OF]->(b)
RETURN a.name, b.name

Basic flow: CREATE creates nodes, MATCH searches for patterns and RETURN returns results. The syntax (nodes)-[relationship]->(node) is the heart of Cypher — draw the pattern you are looking for.

Neo4j vs SQL
// SQL (tables + JOINs):
// SELECT p.name, a.title
// FROM people p
// JOIN friends f ON f.person_id = p.id
// JOIN people a ON a.id = f.friend_id
// (slow with many JOINs)

// Cypher (native graph):
MATCH (p:Person)-[:FRIEND_OF]->(a)
RETURN p.name, a.name

// Graphs: relationships are a "physical
// pointer" — traversing is O(1)
// per hop, no JOINs or indexes

In SQL, relationships live in JOIN tables (cost grows with depth). In Neo4j, relationships are native links — traversing N hops is fast regardless of total size. Ideal for social networks, recommendations and knowledge graphs.

Neo4j Aura (cloud)
# Managed cloud service:
#   console.neo4j.io

# Aura Free (free forever):
#   200k nodes + 400k relationships
#   1 instance

# Steps:
# 1. Create an account
# 2. "Create Instance"
# 3. Choose region and password
# 4. Save the credentials
#    (shown only once!)

# Connection: neo4j+s://xxxx.databases.neo4j.io

Neo4j Aura is the managed cloud version — the Free plan is good for learning (200k nodes). Create the instance at console.neo4j.io and save the credentials (shown only once). Connect via neo4j+s://.

Sample dataset (movies)
// In the Browser:
:play movies

// Loads a movie graph with:
//   Person, Movie (nodes)
//   ACTED_IN, DIRECTED (relationships)

// Try:
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN p.name, m.title LIMIT 5

// Who directed "The Matrix"?
MATCH (d:Person)-[:DIRECTED]->
      (m:Movie {title: "The Matrix"})
RETURN d.name

:play movies loads a sample graph (actors, movies, directors) perfect for learning. Use MATCH with patterns like (p:Person)-[:ACTED_IN]->(m:Movie) to explore real relationships.

Docker
# Run Neo4j in Docker:
docker run \
  --name neo4j \
  -p 7474:7474 -p 7687:7687 \
  -v $HOME/neo4j/data:/data \
  -e NEO4J_AUTH=neo4j/password123 \
  neo4j:latest

# Ports:
#   7474 → Neo4j Browser (HTTP)
#   7687 → Bolt (drivers/app)

# -v persists the data
# NEO4J_AUTH sets user/password
# (without it, prompts setup in the browser)

In Docker: expose ports 7474 (browser) and 7687 (Bolt for drivers). Set NEO4J_AUTH to user/password and mount a volume at /data to persist. Ideal for development.

Configuration (neo4j.conf)
# File: conf/neo4j.conf
# (in Desktop: Database > "..."
#  > Settings)

# Memory (adjust to your hardware):
server.memory.heap.initial_size=1G
server.memory.heap.max_size=2G
server.memory.pagecache.size=1G

# Allow remote connections:
server.default_listen_address=0.0.0.0

# Restart after changes:
# Database > Stop > Start

Settings in conf/neo4j.conf: heap and pagecache control memory (pagecache ≈ data size). default_listen_address=0.0.0.0 allows remote connections. Restart the service after changes.

Neo4j Browser
# Open: http://localhost:7474
# (or "Open" button in Desktop)

# Login:
#   Connect URL: bolt://localhost:7687
#   Username: neo4j
#   Password: (yours)

# Interface:
#   :help          → help
#   :clear         → clear screen
#   :play movies   → sample dataset
#   Ctrl+Enter     → run query

# Results the graph or table
# (Graph | Table | Text buttons)

The Neo4j Browser (port 7474) is the query interface. Useful commands: :help, :clear, :play movies (sample dataset). Run with Ctrl+Enter and see results the a graph or table.

Users and roles
// Change password:
ALTER CURRENT USER
SET PASSWORD FROM "current" TO "new"

// Create user:
CREATE USER maria
SET PASSWORD "password123"
SET PASSWORD CHANGE NOT REQUIRED

// Grant roles:
GRANT ROLE reader TO maria
GRANT ROLE editor TO maria
GRANT ROLE admin TO maria

// See users:
SHOW USERS

// Remove:
DROP USER maria

Manage users with CREATE USER and roles (reader, editor, admin) via GRANT ROLE. SHOW USERS lists them all. Change your password with ALTER CURRENT USER.

Create Nodes


9 cards
CREATE (simple node)
// Node without label or properties:
CREATE ()

// Node with label:
CREATE (:Person)

// Node with properties:
CREATE (:Person {
  name: "Anna",
  age: 30,
  active: true
})

// Parentheses = node
// {} = properties (map)
// :Label = type/label

CREATE () creates a node. :Label gives it a type (e.g., Person) and {} defines properties (key/value map). Labels let you filter in queries — always use them.

Create multiple nodes
// Multiple nodes in one query:
CREATE (:Country {name: "Portugal"}),
       (:Country {name: "Spain"}),
       (:Country {name: "France"})

// Already-linked nodes:
CREATE (a:Person {name: "Anna"})
       -[:FOLLOWS]->
       (b:Person {name: "Ray"})

// Count created nodes:
CREATE (n:Test)
RETURN count(n)

Create several nodes separated by commas in a single CREATE. You can create already-linked nodes with the full pattern syntax — more efficient than creating and linking in separate queries.

UNWIND for bulk create
// Create many nodes from a list:
UNWIND ["Anna", "Ray", "Mary"] AS name
CREATE (:Person {name: name})

// With maps (multiple properties):
UNWIND [
  {name: "Anna", age: 30},
  {name: "Ray", age: 25}
] AS data
CREATE (p:Person)
SET p = data

// SET p = map sets all
// the properties at once

UNWIND turns lists into rows — perfect for creating many nodes in one query. With maps, SET p = data applies all properties at once. Much faster than separate CREATEs.

Labels
// A node can have MULTIPLE labels:
CREATE (n:Employee:Manager
        {name: "Ray"})

// Labels in CamelCase
// (convention: Person, BankAccount)

// See existing labels:
CALL db.labels()

// Remove label:
MATCH (n:Manager {name: "Ray"})
REMOVE n:Manager

// Add label:
MATCH (n {name: "Ray"})
SET n:Admin

Labels are tags — a node can have multiple (e.g., :Employee:Manager). Convention: CamelCase. CALL db.labels() lists them all. Add with SET n:Label, remove with REMOVE n:Label.

MERGE (create or find)
// CREATE duplicates if it already exists:
CREATE (:Person {name: "Anna"})
CREATE (:Person {name: "Anna"})  // 2 Anas!

// MERGE = "ensure it exists":
MERGE (p:Person {name: "Anna"})
ON CREATE SET p.created = timestamp()
ON MATCH SET p.seen = timestamp()
RETURN p

// If it does not exist → create
// If it exists → use the existing one

// MERGE matches the WHOLE pattern
// (label + given properties)

MERGE is the idempotent CREATE: it searches for the pattern and only creates it if it does not exist — avoids duplicates. ON CREATE SET runs on creation, ON MATCH SET when it already exists. Essential for imports.

Properties
// Supported types:
CREATE (:Product {
  name: "Laptop",        // String
  price: 999.99,          // Double
  stock: 5,               // Integer
  available: true,        // Boolean
  tags: ["tech", "new"],  // List
  release: date()         // Temporal
})

// Properties CANNOT be:
//   nested maps, null
//   (null = absent property)

// See a node's properties:
MATCH (p:Product) RETURN properties(p)

Properties accept String, numbers, Boolean, lists and temporal types (date(), datetime()). They do not accept nested maps or null (null = nonexistent property). properties(n) shows them all.

MERGE with relationships
// Ensure person AND relationship:
MERGE (a:Person {name: "Anna"})
MERGE (b:Person {name: "Ray"})
MERGE (a)-[:FRIEND_OF]->(b)

// Common mistake: MERGE on the full
// pattern without the nodes existing
// → creates everything at once (ok),
// but slow on large graphs

// Better: MERGE nodes first,
// then MERGE the relationship

Use a separate MERGE for each node and then for the relationship — ensures nothing is duplicated. Doing MERGE on the full pattern works but is slower and less explicit.

Node variables
// Assign the node to a variable:
CREATE (p:Person {name: "Anna"})
RETURN p

// RETURN returns the created node
// (with internal id and properties)

// Use in multi-statement:
CREATE (a:Person {name: "Ray"})
CREATE (b:City {name: "Lisbon"})
CREATE (a)-[:LIVES_IN]->(b)
RETURN a, b

// Variables only exist
// within the current query

Variables (p, a, b) reference created nodes for use in RETURN or in relationships within the same query. They do not persist between queries — to reuse them, search with MATCH.

Internal ID and elementId
// Each node has an internal id:
MATCH (p:Person)
RETURN id(p), p.name

// Neo4j 5+: elementId (String):
MATCH (p:Person)
RETURN elementId(p), p.name

// WARNING: ids are REUSED
// after DELETE — never use them the
// a permanent external key!

// Stable key: create your own
// property (e.g., uuid, email)

id() returns the internal id (integer) and elementId() (Neo4j 5+) a string. Both are reused after DELETE — they never work the a permanent key. Create your own unique property (uuid, email).

Relations


9 cards
CREATE (relationship)
// Link two existing nodes:
MATCH (a:Person {name: "Anna"}),
      (b:Person {name: "Ray"})
CREATE (a)-[:FRIEND_OF]->(b)

// Syntax:
//   (source)-[:TYPE]->(target)
//   -[:TYPE]->  = direction →
//   <-[:TYPE]-  = direction ←
//   -[:TYPE]-   = no direction

// TYPE in UPPERCASE_WITH_UNDERSCORE
// (convention: FRIEND_OF, WORKS_AT)

Relationships are created with (a)-[:TYPE]->(b) — the arrow defines the direction. Types follow the UPPERCASE_WITH_UNDERSCORE convention. MATCH the nodes first, then CREATE the relationship.

Relationship patterns
// Filter by type:
MATCH (a)-[:FRIEND_OF]->(b)
RETURN a, b

// Multiple types (OR):
MATCH (a)-[:FRIEND_OF|COLLEAGUE]->(b)
RETURN a, b

// With a label on the target:
MATCH (p:Person)-[:BOUGHT]->
      (pr:Product)
RETURN p.name, pr.name

// Relationship without type (any):
MATCH (a)-[r]->(b)
RETURN type(r), count(*)

Flexible patterns: [:TYPE1|TYPE2] matches multiple types (OR), labels on the target filter nodes, and [r] without a type matches any relationship. Combine with count(*) to analyze the type distribution.

Self-loop and multiple relationships
// Self-loop (node linked to itself):
MATCH (p:Person {name: "Anna"})
CREATE (p)-[:MENTOR_OF]->(p)

// Multiple relationships of the same type
// between the same nodes (allowed):
MATCH (a {name: "Anna"}), (b {name: "Ray"})
CREATE (a)-[:PAID {amount: 10}]->(b)
CREATE (a)-[:PAID {amount: 20}]->(b)

// Query both:
MATCH (a {name: "Anna"})-[r:PAID]->(b)
RETURN r.amount   // 2 rows

Neo4j allows self-loops (a node linked to itself) and multiple relationships of the same type between the same pair of nodes (e.g., several payments). Each relationship is an independent entity with its own properties.

Properties on relationships
// Relationships also have properties:
MATCH (a:Person {name: "Anna"}),
      (m:Movie {title: "Matrix"})
CREATE (a)-[:RATED {
  rating: 5,
  date: date(),
  comment: "Excellent!"
}]->(m)

// Read properties:
MATCH (a)-[r:RATED]->(m)
RETURN a.name, r.rating, m.title

// Relationship variable: [r:TYPE]

Relationships accept properties like nodes — ideal for metadata (rating, date). Capture the relationship in a variable with [r:TYPE] to access its properties in RETURN.

Variable-length paths
// Friends of friends (2 hops):
MATCH (a:Person {name: "Anna"})
      -[:FRIEND_OF]->()-[:FRIEND_OF]->(b)
RETURN b.name

// With variable length:
MATCH (a {name: "Anna"})
      -[:FRIEND_OF*1..3]->(b)
RETURN DISTINCT b.name

// *1..3 = 1 to 3 hops
// *2    = exactly 2
// *..5  = up to 5
// *     = any (careful!)

// DISTINCT avoids duplicates

-[:TYPE*1..3]-> traverses variable-length paths (1 to 3 hops) — the great advantage of graphs. Use DISTINCT to avoid duplicates and always limit the maximum (unbounded paths can explode).

Direction
// Create with direction:
CREATE (a)-[:FOLLOWS]->(b)    // a follows b

// Query with direction:
MATCH (a:Person)-[:FOLLOWS]->(b)
RETURN a.name, b.name

// Query WITHOUT direction
// (finds both ways):
MATCH (a:Person)-[:FOLLOWS]-(b)
RETURN a.name, b.name

// The direction is ALWAYS stored
// (there is no "directionless" relationship
//  — you just ignore it in the query)

The direction is always stored-[]-> creates/queries in one way, -[]- ignores direction in the query (finds both). Choose the direction when modeling (e.g., FOLLOWS has a natural direction).

shortestPath
// Shortest path between 2 nodes:
MATCH (a:Person {name: "Anna"}),
      (z:Person {name: "Zé"}),
      path = shortestPath(
        (a)-[:FRIEND_OF*..10]-(z)
      )
RETURN path

// Nodes of the path:
RETURN nodes(path)

// Relationships of the path:
RETURN relationships(path)

// Length:
RETURN length(path)

shortestPath() finds the minimum path between two nodes (native BFS somethingrithm). Extract details with nodes(), relationships() and length(). Limit with *..10 for performance.

Multiple relationship types
// Create different relationships:
MATCH (a:Person {name: "Anna"}),
      (e:Company {name: "TechCorp"})
CREATE (a)-[:WORKS_AT {since: 2020}]->(e)

MATCH (a:Person {name: "Anna"}),
      (c:City {name: "Porto"})
CREATE (a)-[:LIVES_IN]->(c)

// Query any of them:
MATCH (a:Person {name: "Anna"})-[r]->()
RETURN type(r)

// type(r) returns the type name

A node can have relationships of multiple types (WORKS_AT, LIVES_IN...). [r]->() matches any outgoing relationship; type(r) returns the type the a string. () without a label matches any node.

Delete relationships
// Delete a specific relationship:
MATCH (a:Person {name: "Anna"})
      -[r:FRIEND_OF]->(b {name: "Ray"})
DELETE r

// Delete ALL relationships of a node:
MATCH (p:Person {name: "Anna"})-[r]-()
DELETE r

// The node still exists
// (only the relationships are deleted)

// Delete node AND relationships:
MATCH (p:Person {name: "Anna"})
DETACH DELETE p

Delete relationships with DELETE r (the node remains). MATCH (p)-[r]-() + DELETE r removes all of a node's links. To delete a node with relationships use DETACH DELETE (otherwise it errors).

Queries (MATCH)


9 cards
Basic MATCH
// All nodes:
MATCH (n) RETURN n

// All nodes with a label:
MATCH (p:Person) RETURN p

// With properties:
MATCH (p:Person {name: "Anna"})
RETURN p

// Multiple conditions:
MATCH (p:Person {name: "Anna", age: 30})
RETURN p

// MATCH does not create anything —
// it only searches existing patterns

MATCH searches for patterns in the graph (never creates). Filter by label (:Person) and properties ({name: "Anna"}). Without patterns, it returns all nodes — always limit with WHERE/LIMIT.

OPTIONAL MATCH
// Cypher's LEFT JOIN:
MATCH (p:Person)
OPTIONAL MATCH (p)-[:HAS]->(c:Car)
RETURN p.name, c.model

// People WITHOUT a car appear
// with c.model = null

// Compare:
// normal MATCH → only people
//   with a car (inner join)
// OPTIONAL MATCH → all
//   people (left join)

OPTIONAL MATCH is Cypher's LEFT JOIN: it returns results even when the pattern does not match (with null in the unmatched variables). The normal MATCH is equivalent to INNER JOIN.

collect (lists)
// Combine values into a list:
MATCH (p:Person)
RETURN collect(p.name)

// Friends of each person:
MATCH (p:Person)-[:FRIEND_OF]->(a)
RETURN p.name, collect(a.name) AS friends

// List size:
RETURN p.name,
       size(collect(a.name)) AS n_friends

// collect() never returns null
// (empty list if nothing matches)

collect() aggregates values into a list — perfect for "all the friends of each person". size() gives the length. It never returns null (empty list yes), unlike other aggregations.

WHERE
MATCH (p:Person)
WHERE p.age > 25
  AND p.active = true
RETURN p.name

// Operators: =, <>, <, >, <=, >=
// Logical: AND, OR, NOT

// Property existence:
WHERE p.email IS NOT NULL

// IN:
WHERE p.city IN ["Porto", "Lisbon"]

// String:
WHERE p.name STARTS WITH "An"
WHERE p.name CONTAINS "ari"
WHERE p.name ENDS WITH "na"

WHERE filters with operators (>, <>), AND/OR/NOT, IN for lists and IS NOT NULL for existence. Strings: STARTS WITH, CONTAINS, ENDS WITH (and regex with =~).

Complex patterns
// Friendship triangle:
MATCH (a:Person)-[:FRIEND_OF]->(b),
      (b)-[:FRIEND_OF]->(c),
      (c)-[:FRIEND_OF]->(a)
RETURN a.name, b.name, c.name

// Person who bought the same
// product the another:
MATCH (p1)-[:BOUGHT]->(pr)<-[:BOUGHT]-(p2)
WHERE p1 <> p2
RETURN p1.name, p2.name, pr.name

// Reusing variables creates
// the link between patterns

Complex patterns connect by reusing variables (b appears in two relationships; pr is shared). WHERE p1 <> p2 excludes itself. This is how you express triangles, recommendations and paths.

RETURN
// Complete nodes:
MATCH (p:Person) RETURN p

// Specific properties:
RETURN p.name, p.age

// Alias:
RETURN p.name AS name,
       p.age AS "Age in years"

// Expressions:
RETURN p.name, p.age * 2

// Everything:
MATCH (p:Person) RETURN *

// Distinct:
RETURN DISTINCT p.city

RETURN defines the output: complete nodes, properties, aliases (AS) or expressions. RETURN * returns all variables. DISTINCT removes duplicates from the results.

WHERE with relationships
// Filter by pattern EXISTENCE:
MATCH (p:Person)
WHERE EXISTS {
  (p)-[:HAS]->(:Car)
}
RETURN p.name

// Or negation (no car):
MATCH (p:Person)
WHERE NOT EXISTS {
  (p)-[:HAS]->(:Car)
}
RETURN p.name

// EXISTS {} = existence
// subquery (Neo4j 4.4+)

EXISTS { pattern } filters nodes by the existence of a pattern (without returning it). NOT EXISTS does the negation — "people without a car". Replaces SQL existence subqueries.

ORDER BY, SKIP, LIMIT
// Sort:
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age DESC

// Pagination:
MATCH (p:Person)
RETURN p.name
ORDER BY p.name
SKIP 10        // skip 10
LIMIT 5        // fetch 5

// Top 3 oldest:
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age DESC
LIMIT 3

ORDER BY sorts (DESC for descending). SKIP + LIMIT do pagination. Combine all three for top-N: ORDER BY ... DESC LIMIT 3.

Count and aggregate
// Count nodes:
MATCH (p:Person) RETURN count(p)

// Count relationships:
MATCH ()-[r:FRIEND_OF]->()
RETURN count(r)

// Group (implicit GROUP BY):
MATCH (p:Person)
RETURN p.city, count(p) AS total
ORDER BY total DESC

// Average, sum, min, max:
MATCH (p:Person)
RETURN avg(p.age),
       min(p.age),
       max(p.age),
       sum(p.age)

Aggregation is implicit: count(), avg(), sum(), min(), max(). Grouping is done by the non-aggregated columns (like an automatic GROUP BY). count(r) counts relationships.

Update and Delete


9 cards
SET (update)
// Update a property:
MATCH (p:Person {name: "Anna"})
SET p.age = 31
RETURN p

// Multiple properties:
MATCH (p:Person {name: "Anna"})
SET p.age = 31,
    p.city = "Porto",
    p.updated = timestamp()

// Add a new property:
MATCH (p:Person {name: "Anna"})
SET p.email = "ana@mail.com"

// SET only updates existing nodes
// (MATCH has to find them)

SET updates properties on nodes found via MATCH. Used to change values, add new properties and multiple fields at once. It creates nothing — the node must exist.

MERGE ON CREATE / ON MATCH
// Cypher's upsert:
MERGE (p:Person {email: "ana@mail.com"})
ON CREATE SET
  p.name = "Anna",
  p.created = datetime(),
  p.new = true
ON MATCH SET
  p.last_login = datetime()
RETURN p

// First time: creates with everything
// Next times: only updates
//   last_login

// Essential pattern for imports
// and synchronizations

Neo4j's upsert: MERGE + ON CREATE SET (only on creation) + ON MATCH SET (only if it already exists). Perfect for idempotent imports — run the same query several times without duplicating.

Explicit transactions
// In the Browser, everything is a transaction.
// In drivers (e.g., Python):

// BEGIN
//   CREATE (:Person {name: "Anna"})
// COMMIT

// Or rollback:
// BEGIN
//   CREATE (:Person {name: "Error"})
// ROLLBACK   // undoes

// In the Browser:
// :begin  → starts a transaction
// :commit → confirms
// :rollback → undoes

Transactions guarantee atomicity: BEGIN + COMMIT confirms, ROLLBACK undoes. In the Browser use :begin/:commit. In drivers, use the transactional APIs (never leave transactions open).

SET with map (+= and =)
// = replaces ALL props:
MATCH (p:Person {name: "Anna"})
SET p = {name: "Anna", age: 31}
// (email, city... deleted!)

// += MERGEs the props:
MATCH (p:Person {name: "Anna"})
SET p += {age: 31, city: "Porto"}
// (other props kept)

// Rule:
//   SET p = map   → replaces everything
//   SET p += map  → updates only
//                    the given ones

Critical difference: SET p = map replaces all properties (omitted ones are deleted); SET p += map only updates the given ones, keeping the rest. Use += for partial updates.

Update relationships
// SET on a relationship:
MATCH (a:Person {name: "Anna"})
      -[r:RATED]->(m:Movie)
SET r.rating = 4,
    r.reviewed = date()
RETURN r

// Add a property:
MATCH ()-[r:FRIEND_OF]->()
SET r.confirmed = true

// Remove a property:
MATCH ()-[r:RATED]->()
REMOVE r.comment

Relationships are updated like nodes: capture with [r:TYPE] and use SET r.prop = value or REMOVE r.prop. The same += and null rules apply.

REMOVE
// Remove a property:
MATCH (p:Person {name: "Anna"})
REMOVE p.email
RETURN p

// Remove a label:
MATCH (p:Manager {name: "Ray"})
REMOVE p:Manager

// Remove several:
MATCH (p:Person {name: "Anna"})
REMOVE p.email, p.phone

// Alternative: SET p.email = null
// (same effect — removes the prop)

REMOVE deletes properties or labels from a node. SET p.email = null has the same effect (null properties do not exist in Neo4j). Useful for cleaning up obsolete data.

FOREACH
// Update several nodes from a list:
MATCH (p:Person)
WHERE p.city = "Lisbon"
FOREACH (x IN collect(p) |
  SET x.region = "Greater Lisbon"
)

// Useful on paths:
MATCH path = (a)-[*1..3]->(b)
FOREACH (n IN nodes(path) |
  SET n.visited = true
)

// FOREACH iterates lists and
// runs SET/CREATE/MERGE/DELETE

FOREACH (x IN list | SET ...) iterates lists to update multiple elements — useful on nodes(path) to mark all nodes of a path. Supports SET, CREATE, MERGE and DELETE.

DELETE and DETACH DELETE
// Delete a node WITHOUT relationships:
MATCH (p:Person {name: "Test"})
DELETE p

// Error if the node has relationships!
// (Neo4j protects integrity)

// Delete a node WITH relationships:
MATCH (p:Person {name: "Test"})
DETACH DELETE p
// (deletes the relationships first)

// Delete EVERYTHING (careful!):
MATCH (n) DETACH DELETE n

DELETE deletes nodes without relationships — if it has any, it errors (integrity protection). DETACH DELETE deletes the node and all its relationships. MATCH (n) DETACH DELETE n wipes the entire DB — use with care.

Batch delete (safe)
// SLOW (giant transaction):
// MATCH (n) DETACH DELETE n

// FAST (batches of 10k):
MATCH (n:Log)
WITH n LIMIT 10000
DETACH DELETE n

// Repeat until it returns 0
// (or in a script: while affected > 0)

// Delete relationships in batches:
MATCH ()-[r:TEMPORARY]->()
WITH r LIMIT 10000
DELETE r

Deleting millions of nodes at once locks the DB. Do it in batches: WITH n LIMIT 10000 DETACH DELETE n and repeat until it affects 0. Each batch is a small transaction — does not exhaust memory.

Indices and Constraints


9 cards
CREATE INDEX
// Index on a property:
CREATE INDEX person_name
FOR (p:Person) ON (p.name)

// Composite (several props):
CREATE INDEX person_name_age
FOR (p:Person) ON (p.name, p.age)

// On relationships (Neo4j 5+):
CREATE INDEX rated_rating
FOR ()-[r:RATED]-() ON (r.rating)

// Indexes speed up MATCH/WHERE
// but cost on writes

CREATE INDEX ... FOR (n:Label) ON (n.prop) speeds up searches by that property. Supports composite indexes (several props) and on relationships. Trade-off: writes get slower — index only what you need.

SHOW and DROP
// List indexes:
SHOW INDEXES

// List constraints:
SHOW CONSTRAINTS

// Details of an index:
SHOW INDEXES WHERE name = "person_name"

// Drop an index:
DROP INDEX person_name

// Drop a constraint:
DROP CONSTRAINT person_email_unique

// Names appear in the
// "name" column of the results

SHOW INDEXES and SHOW CONSTRAINTS list everything (with names and state). Remove with DROP INDEX name / DROP CONSTRAINT name. Always check before creating duplicates.

Performance rules
// 1. Index props from MATCH/WHERE
// 2. Use labels (never MATCH (n)
//    without filter on large DBs)
// 3. LIMIT early:
MATCH (p:Person)
WHERE p.city = "Porto"
RETURN p LIMIT 100

// 4. Selective patterns first:
//    (start with the rarest node)
// 5. Avoid * without a max limit
// 6. PROFILE on slow queries

Golden rules: index filtered properties, start patterns with the most selective nodes, use LIMIT early, limit variable-length paths and validate with PROFILE. Fast graphs = well-designed patterns.

Uniqueness constraint
// Ensure unique email:
CREATE CONSTRAINT person_email_unique
FOR (p:Person)
REQUIRE p.email IS UNIQUE

// Try to duplicate → ERROR:
CREATE (:Person {email: "a@b.com"})
CREATE (:Person {email: "a@b.com"})
// ConstraintValidationFailed

// Constraints create an index
// automatically (do not duplicate!)

REQUIRE p.email IS UNIQUE prevents duplicates — the second insert fails with an error. The constraint creates an automatic index (do not create another one like it). Fundamental for business keys (email, uuid).

Fulltext indexes
// Full-text index:
CREATE FULLTEXT INDEX search_name
FOR (p:Person|Company)
ON EACH [p.name, p.description]

// Search (with score):
CALL db.index.fulltext.queryNodes(
  "search_name", "ana silva"
)
YIELD node, score
RETURN node.name, score

// Supports: AND, OR, NOT,
// "phrases", wildcards*

fulltext indexes allow text search across several properties/labels with a relevance score. Query via db.index.fulltext.queryNodes(). Supports boolean operators and wildcards.

Existence constraint
// Mandatory property:
CREATE CONSTRAINT person_name
FOR (p:Person)
REQUIRE p.name IS NOT NULL

// Create without name → ERROR:
CREATE (:Person {age: 30})
// fails (name missing)

// Mandatory label (Neo4j 5+):
CREATE CONSTRAINT has_label
FOR (p:Person)
REQUIRE p:Active IS NOT NULL

REQUIRE p.name IS NOT NULL makes the property mandatory — inserts without it fail. Ensures data quality at the source (Enterprise for some; existence constraints vary by version).

point and range indexes
// Spatial index (geo):
CREATE INDEX point_location
FOR (l:Location) ON (l.location)

// Create a point:
CREATE (l:Location {
  name: "HQ",
  location: point({
    latitude: 41.15, longitude: -8.61
  })
})

// Query by distance:
MATCH (l:Location)
WHERE distance(l.location,
  point({latitude: 41.16,
         longitude: -8.60})) < 5000
RETURN l.name   // < 5 km

point indexes speed up geospatial queries. Create coordinates with point({latitude, longitude}) and measure distances with distance() (meters). Useful for "near me".

Node Key
// Composite primary key:
CREATE CONSTRAINT person_key
FOR (p:Person)
REQUIRE (p.name, p.birth_date)
IS NODE KEY

// NODE KEY = UNIQUE + NOT NULL
// on all the properties

// Error if one is missing:
CREATE (:Person {name: "Anna"})
// fails (birth_date missing)

// Error if duplicated:
// (same name + birth_date)

NODE KEY is Neo4j's primary key: it combines uniqueness + NOT NULL across several properties. Ideal for composite business keys (e.g., name + birth date).

EXPLAIN and PROFILE
// See the plan WITHOUT running:
EXPLAIN MATCH (p:Person)
WHERE p.name = "Anna" RETURN p

// See the plan WITH real metrics:
PROFILE MATCH (p:Person)
WHERE p.name = "Anna" RETURN p

// What to look for:
//   NodeByLabelScan → slow!
//     (missing index)
//   NodeIndexSeek → fast ✅
//   db hits → lower is better

EXPLAIN shows the execution plan without running; PROFILE runs it and gives real metrics (db hits, rows). NodeIndexSeek = uses an index (good); NodeByLabelScan = scans everything (create an index!).

Functions and APOC


9 cards
String functions
RETURN upper("ana")        // "ANA"
RETURN lower("ANA")        // "ana"
RETURN trim("  hi  ")      // "hi"
RETURN size("hello")         // 4
RETURN substring("Neo4j", 0, 3)  // "Neo"
RETURN replace("a-b", "-", "+")  // "a+b"
RETURN split("a,b,c", ",") // ["a","b","c"]
RETURN left("Neo4j", 3)    // "Neo"
RETURN right("Neo4j", 2)   // "4j"

// Concatenate:
RETURN "Hello " + "World"

String functions: upper/lower/trim, substring, replace, split (returns a list), left/right. Concatenation with +. size() gives the length.

List comprehension
// Transform lists:
RETURN [x IN range(1, 5) | x * 2]
// [2, 4, 6, 8, 10]

// Filter:
RETURN [x IN range(1, 10)
        WHERE x % 2 = 0]
// [2, 4, 6, 8, 10]

// Filter + transform:
RETURN [x IN range(1, 10)
        WHERE x > 5 | x * x]
// [36, 49, 64, 81, 100]

// With nodes:
MATCH (p:Person)-[:FRIEND_OF]->(a)
RETURN p.name,
       [x IN collect(a) | x.age]

List comprehension: [x IN list WHERE filter | expression] filters and transforms lists in a single expression — just like in Python. Ideal for extracting values from collections of nodes.

CALL and YIELD
// System procedures:
CALL db.labels()          // labels
CALL db.relationshipTypes() // types
CALL dbms.components()    // version

// Syntax:
CALL procedure(args)
YIELD column1, column2
RETURN column1

// YIELD extracts the columns
// from the procedure result

// Filter results:
CALL db.labels()
YIELD label
WHERE label STARTS WITH "P"
RETURN label

CALL procedure() YIELD columns invokes system or APOC procedures. YIELD extracts columns from the result to use in the rest of the query. db.labels() and db.relationshipTypes() are essential for exploring the schema.

Date functions
// Current date/time:
RETURN date()           // 2026-07-24
RETURN datetime()       // with time
RETURN time()           // time only
RETURN timestamp()      // epoch ms

// Components:
WITH date("2026-07-24") AS d
RETURN d.year, d.month, d.day

// Date arithmetic:
RETURN date() + duration("P30D")
// (30 days from now)

RETURN duration({days: 7, hours: 3})

// Difference:
RETURN date() - date("2026-01-01")

Temporal types: date(), datetime(), time(), timestamp() (epoch). Access components (.year, .month) and add/subtract with duration() (e.g., P30D = 30 days).

CASE
// Simple CASE:
MATCH (p:Person)
RETURN p.name,
  CASE p.city
    WHEN "Porto" THEN "North"
    WHEN "Lisbon" THEN "South"
    ELSE "Other"
  END AS region

// CASE with conditions:
RETURN p.name,
  CASE
    WHEN p.age < 18 THEN "Minor"
    WHEN p.age < 65 THEN "Adult"
    ELSE "Senior"
  END AS bracket

CASE does conditional logic in the RETURN: simple form (CASE prop WHEN value) or with conditions (CASE WHEN cond). Always ends with END. Equivalent to SQL's CASE.

Math functions
RETURN abs(-5)        // 5
RETURN round(3.7)     // 4.0
RETURN floor(3.7)     // 3.0
RETURN ceil(3.2)      // 4.0
RETURN sqrt(16)       // 4.0
RETURN rand()         // 0..1

// Round with precision:
RETURN round(3.14159, 2)  // 3.14

// Conversions:
RETURN toInteger("42")     // 42
RETURN toFloat("3.14")     // 3.14
RETURN toString(42)        // "42"

Math: abs, round (with optional precision), floor/ceil, sqrt, rand(). Conversions: toInteger(), toFloat(), toString() — essential when importing data.

APOC (installation)
// APOC = library of
// essential procedures

// Desktop: Database > Plugins
//   > APOC > Install

// Docker:
// -e NEO4J_PLUGINS='["apoc"]'

// Verify:
CALL apoc.help("")

// Examples of what it enables:
//   apoc.create.node()
//   apoc.export.json.all()
//   apoc.date.format()
//   apoc.coll.* (lists)

APOC (Awesome Procedures on Cypher) is the most used procedure library — hundreds of extra functions. Install via Plugins (Desktop) or an environment variable in Docker. Verify with CALL apoc.help("").

List functions
// Size and access:
WITH [1, 2, 3] AS l
RETURN size(l), l[0], l[-1]

// Slice:
RETURN l[1..3]       // [2, 3]

// Operations:
RETURN [1,2] + [3]   // [1, 2, 3]
RETURN head([1,2,3]) // 1
RETURN tail([1,2,3]) // [2, 3]
RETURN last([1,2,3]) // 3

// Check:
RETURN 2 IN [1, 2, 3]  // true

// Range:
RETURN range(1, 5)     // [1,2,3,4,5]

Lists: size(), indexes (l[0], l[-1]), slices (l[1..3]), head/tail/last, IN for membership and range() to generate sequences. Concatenation with +.

APOC (useful procedures)
// Create node with dynamic label:
CALL apoc.create.node(
  ["Person"], {name: "Anna"}
) YIELD node

// Dynamic relationship:
CALL apoc.create.relationship(
  a, "FRIEND_OF", {}, b
) YIELD rel

// Export DB to JSON:
CALL apoc.export.json.all(
  "backup.json", {}
)

// Convert epoch:
RETURN apoc.date.format(
  1753372800, "s", "yyyy-MM-dd"
)

APOC shines at dynamic operations: labels/relationship types in variables (apoc.create.node), export to JSON/CSV and date conversions. Procedures use CALL ... YIELD.

Advanced Cypher


9 cards
WITH (pipelining)
// WITH passes results to
// the next clause:
MATCH (p:Person)-[:BOUGHT]->(pr)
WITH p, count(pr) AS total
WHERE total > 5
RETURN p.name, total
ORDER BY total DESC

// Without WITH you cannot filter
// aggregations with WHERE!

// WITH also limits the scope:
// only the passed variables
// remain available

WITH is Cypher's pipe: it passes results (and aggregations) to the next clause. It allows filtering aggregations (WHERE after count) and controlling the scope — only the variables in the WITH remain available.

UNION
// Combine results:
MATCH (p:Person) RETURN p.name AS name
UNION
MATCH (c:Company) RETURN c.name AS name

// UNION removes duplicates
// UNION ALL keeps them all:
MATCH (p:Person) RETURN p.name AS name
UNION ALL
MATCH (c:Company) RETURN c.name AS name

// The columns must have
// the SAME names in both

UNION combines results from queries with the same columns (removes duplicates); UNION ALL keeps them all. Useful for querying several labels with the same output structure.

Modeling (best practices)
// 1. Nodes = entities (Person, Movie)
// 2. Relationships = verbs (ACTED_IN)
// 3. Props on nodes AND relationships
// 4. Labels in CamelCase
// 5. Types in UPPERCASE
// 6. Direction = natural sense
//    (Person)-[:BOUGHT]->(Product)

// Anti-pattern: huge lists
// the a property
// WRONG:  p.friends = [1,2,...1M]
// RIGHT:  (p)-[:FRIEND]->(a)

// Relationships > arrays of ids!

Modeling best practices: nodes = entities, relationships = verbs with a natural direction, labels CamelCase, types UPPERCASE. Never use huge arrays as properties — relationships are the graph's native structure.

UNWIND
// List → rows:
UNWIND [1, 2, 3] AS x
RETURN x   // 3 rows

// With MATCH (join):
UNWIND ["Anna", "Ray"] AS name
MATCH (p:Person {name: name})
RETURN p

// List of maps → nodes:
UNWIND [{n: "Anna"}, {n: "Ray"}] AS d
MERGE (:Person {name: d.n})

// Reverse collect:
MATCH (p:Person)
WITH collect(p) AS all
UNWIND all AS p
RETURN p.name

UNWIND expands lists into rows (the inverse of collect()). Combine with MATCH to look up several values and with MERGE to create nodes from lists of maps. The basis of imports.

LOAD CSV (import)
// Import CSV (local file
// in import/ or a URL):
LOAD CSV WITH HEADERS
FROM "file:///people.csv" AS row
MERGE (p:Person {email: row.email})
SET p.name = row.name,
    p.age = toInteger(row.age)

// With a custom delimiter:
LOAD CSV WITH HEADERS
FROM "file:///data.csv" AS l
FIELDTERMINATOR ";"

// Remote CSV:
FROM "https://example.com/data.csv"

LOAD CSV WITH HEADERS imports CSVs row by row (each row = a map). Combine with MERGE for idempotent imports and toInteger() to convert types. Supports URLs and a custom delimiter with FIELDTERMINATOR.

Parameters ($param)
// Instead of hardcoding:
// MATCH (p {name: "Anna"})

// Use a parameter:
MATCH (p:Person {name: $name})
RETURN p

// In the Browser, define first:
:param name => "Anna"

// Several:
:param params => {
  name: "Anna", age: 30
}

// In drivers: pass in execute()
// Prevents Cypher injection!

Parameters ($name) separate data from code — they prevent Cypher injection and let you reuse queries. In as Browser define with :param name => "Anna"; in drivers, pass them in the execution method.

Complete import (example)
// 1. Nodes (with constraint first):
CREATE CONSTRAINT FOR (p:Person)
REQUIRE p.email IS UNIQUE

LOAD CSV WITH HEADERS
FROM "file:///people.csv" AS l
MERGE (p:Person {email: l.email})
SET p.name = l.name

// 2. Relationships:
LOAD CSV WITH HEADERS
FROM "file:///friendships.csv" AS l
MATCH (a:Person {email: l.from})
MATCH (b:Person {email: l.to})
MERGE (a)-[:FRIEND_OF]->(b)

// Order: constraints → nodes
// → relationships

Real import in 3 steps: create constraints first (uniqueness), then nodes with MERGE, and finally relationships matching the nodes by key. This order ensures integrity and performance.

Subqueries (CALL {})
// Correlated subquery:
MATCH (p:Person)
CALL {
  WITH p
  MATCH (p)-[:FRIEND_OF]->(a)
  RETURN count(a) AS n_friends
}
WHERE n_friends > 3
RETURN p.name, n_friends

// EXISTS subquery:
MATCH (p:Person)
WHERE EXISTS {
  MATCH (p)-[:HAS]->(:Car)
}
RETURN p.name

CALL { ... } (Neo4j 4.1+) runs correlated subqueries — use WITH p to import variables from the outside. It enables per-row aggregations and complex filters impossible with a plain MATCH.

Export and backup
// Full dump (CLI):
neo4j-admin database dump neo4j
  --to-path=/backups

// Restore:
neo4j-admin database load neo4j
  --from-path=/backups

// Export via APOC:
CALL apoc.export.cypher.all(
  "backup.cypher", {}
)
// generates a re-runnable Cypher script

// Export only results:
CALL apoc.export.csv.query(
  "MATCH (p:Person) RETURN p",
  "people.csv", {}
)

Full backup with neo4j-admin database dump/load. APOC export generates re-runnable Cypher scripts or CSV from specific queries. Take regular dumps before large changes.