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
| Goal | Clause |
|---|---|
| Change one property | SET n.prop = value |
| Merge properties into a map | SET n += {k: v} |
| Replace the whole property map | SET n = {k: v} |
| Remove a property | REMOVE n.prop or SET n.prop = null |
| Add labels | SET n:Label |
| Remove labels | REMOVE n:Label |
| Delete a relationship | MATCH ()-[r]->() DELETE r |
| Delete a standalone node | MATCH (n) DELETE n |
| Delete a node and its edges | DETACH 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 = 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 = 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.
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 = timestamp()
SET u.name = $name, u.updated = 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 = 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 = timestamp()
SET u.last_seen = 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.
No uniqueness enforcement
SET can set a property to a value that already exists on another node.
LoraDB has no uniqueness constraints — see
Limitations → Storage. Enforce uniqueness in
the host application or by matching before writing.
See also
- MATCH — source of bindings.
- MERGE — create-or-match semantics.
- CREATE — write-only counterpart.
- Properties — data-model background.
- Data Types — what values
SETaccepts. - Troubleshooting — common mutation pitfalls.