Skip to main content

SET, REMOVE, DELETE — Mutating the Graph

Mutating clauses act on rows produced by a preceding MATCH. They never run without a binding — a SET whose MATCH found nothing is a silent no-op.

Overview​

GoalClause
Change one propertySET n.prop = value
Merge properties into a mapSET n += {k: v}
Replace the whole property mapSET n = {k: v}
Remove a propertyREMOVE n.prop or SET n.prop = null
Add labelsSET n:Label
Remove labelsREMOVE n:Label
Delete a relationshipMATCH ()-[r]->() DELETE r
Delete a standalone nodeMATCH (n) DELETE n
Delete a node and its edgesDETACH DELETE

SET — properties​

Set a single property​

MATCH (n:User {name: 'Alice'})
SET n.age = 33
RETURN n

Set multiple keys in one clause by chaining with commas:

MATCH (n:User {name: 'Alice'})
SET n.age = 33, n.updated = temporal.timestamp()
RETURN n

Replace all properties (=)​

SET n = {...} replaces the full property map. Every key not in the new map is dropped — including keys you never touched.

MATCH (n:User {name: 'Alice'})
SET n = {name: 'Alice', age: 33}
RETURN n

Almost always a mistake unless you really mean it. Use += to merge. See Troubleshooting → SET wiped my properties.

Merge properties (+=)​

SET n += {...} adds / overwrites keys without removing anything else.

MATCH (n:User {name: 'Alice'})
SET n += {age: 33, city: 'Amsterdam'}
RETURN n

Combines naturally with parameter maps for patch-style updates:

MATCH (u:User {id: $id})
SET u += $patch
RETURN u

Where $patch = {name: 'New Name', active: true}.

Computed expressions​

The right-hand side is any expression — reference other properties, call functions, do arithmetic.

MATCH (n:User) SET n.doubled = n.age * 2       RETURN n;
MATCH (n:User) SET n.greeting = 'Hello ' + n.name RETURN n;
MATCH (n:User) SET n.updated = temporal.timestamp()     RETURN n

Clear a property​

Setting a property to null removes it — the property simply ceases to exist on that entity.

MATCH (n:User {name: 'Alice'})
SET n.archived = null
RETURN n

REMOVE n.prop does the same.

Copy properties from another entity​

MATCH (src:Template {id: $src}), (dst:Record {id: $dst})
SET dst += properties(src)
RETURN dst

properties() returns the full map; += folds it in.

SET — labels​

Adding a label on a node already carrying it is a no-op.

MATCH (n:User {name: 'Alice'})
SET n:Admin
RETURN labels(n);

MATCH (n:User {name: 'Alice'})
SET n:Admin:Verified
RETURN labels(n)

REMOVE​

Remove a label​

MATCH (n:User {name: 'Alice'})
REMOVE n:Admin
RETURN labels(n)

Remove a property​

MATCH (n:User {name: 'Alice'})
REMOVE n.age
RETURN n

Equivalent to SET n.age = null. Use whichever reads more clearly at the call site.

Remove multiple labels / properties​

Chain with commas:

MATCH (n:User {name: 'Alice'})
REMOVE n:Admin, n:Verified, n.lastAudit
RETURN n

DELETE​

Delete a node​

MATCH (n:User {name: 'Temp'})
DELETE n

A plain DELETE on a node requires the node to have no relationships. Otherwise the executor returns DeleteNodeWithRelationships — see Troubleshooting → DELETE fails.

DETACH DELETE​

Removes the node and every relationship attached to it in one step.

MATCH (n:User {name: 'Alice'})
DETACH DELETE n

Use this for ordinary "delete the user" semantics. Plain DELETE is rarely the right call on a node.

Delete a relationship​

MATCH (a:User {name: 'Alice'})-[r:FOLLOWS]->(b:User)
DELETE r

Node endpoints survive — the edge alone is removed.

Many targets in one statement​

A DELETE first reads every row it matches, then deletes all of its targets together: relationships first, then nodes. That has three consequences:

  • A relationship matched more than once, as an undirected pattern matches it from both ends, is deleted once.
  • A plain DELETE of a node succeeds when the same statement deletes all of its relationships, even if they arrive on different rows: MATCH (n:Tag {name: 'old'})-[r]-() DELETE r, n.
  • Deleting many relationships of one busy node (a hub) takes time in proportion to its degree, not its degree squared: each node's adjacency list is edited once per statement.

This holds for execute and for streamed queries alike. A CALL { … } body does it once per incoming row; a DELETE inside FOREACH deletes item by item.

Delete everything​

MATCH (n) DETACH DELETE n

Empties the graph. Note: there is no TRUNCATE clause. All bindings expose a clear() helper that is faster and clearer — see Node → other methods / Python → other methods.

Common patterns​

Upsert (create-or-update)​

MERGE (u:User {id: $id})
ON CREATE SET u.created = temporal.timestamp()
SET u.name = $name, u.updated = temporal.timestamp()
RETURN u

ON CREATE only runs on insert; the trailing SET runs in both branches. See MERGE.

Patch with merge-semantics​

MATCH (u:User {id: $id})
SET u += $patch
RETURN u

Safe pattern for partial updates from a client payload.

Touching a timestamp​

MATCH (n:User {id: $id})
SET n.last_seen = temporal.timestamp()

Convert / migrate a property​

MATCH (u:User)
WHERE u.full_name IS NOT NULL AND u.name IS NULL
SET u.name = u.full_name
REMOVE u.full_name

Moves the value from full_name to name, then drops the old key — two mutations per matched row, in order. The WHERE is important: without the u.name IS NULL guard, users who already have both keys would lose their existing name.

Conditional label​

MATCH (u:User)
WHERE u.score >= 100
SET u:Pro

Adds the :Pro label to every qualifying user. The node keeps its existing labels — SET n:Label never replaces, only adds. Running it again is a no-op for users already labelled :Pro.

Conditional value via CASE​

CASE is an expression, so it composes into SET. Single-row-aware derivation:

MATCH (u:User)
SET u.tier = CASE
             WHEN u.score >= 1000 THEN 'platinum'
             WHEN u.score >=  100 THEN 'gold'
             WHEN u.score >=   10 THEN 'silver'
             ELSE                       'bronze'
           END

Pairs cleanly with MERGE:

MERGE (u:User {id: $id})
ON CREATE SET u.tier = 'bronze', u.created = temporal.timestamp()
SET u.last_seen = temporal.timestamp(),
    u.tier      = CASE
                    WHEN u.score >= 100 THEN 'gold'
                    ELSE                       coalesce(u.tier, 'bronze')
                  END

Bulk patch via UNWIND​

UNWIND $patches AS patch
MATCH (u:User {id: patch.id})
SET u += patch.fields

Where $patches = [{id: 1, fields: {name: '…'}}, …]. One parse, one plan, N updates — per-row MATCH resolves the target, += merges the patch. Rows whose id doesn't match an existing :User produce no update; the query doesn't error.

Edge cases​

Mutate affects every matched row​

A broad MATCH with a SET runs once per matched row. This is intentional and powerful, but easy to misuse:

// Sets archived=true on EVERY user
MATCH (u:User) SET u.archived = true

Narrow the MATCH with WHERE or inline properties to scope the change.

Mutate on OPTIONAL MATCH​

SET n.x = 1 after an OPTIONAL MATCH that missed will run on null — a runtime error. Guard with WHERE n IS NOT NULL.

DETACH DELETE during a pattern with aggregation​

Aggregations, RETURN, and mutations interact in complex ways. Prefer a simple MATCH … DETACH DELETE n step over combining with aggregations in one query.

Atomicity​

Each query is one atomic step. There is no explicit transaction boundary. If a query fails partway through execution, partial writes are possible — keep mutations scoped and small where possible. See Queries → Execution model.

Constraint enforcement​

SET, REMOVE, and property replacement are checked against matching constraints. A write that duplicates a unique value, removes a required property, or changes a property to the wrong declared type fails.

See also​