Relationships Between Nodes
A relationship is a directed, typed edge between two nodes. Like nodes, it can carry properties. Every relationship has:
- exactly one type (e.g.
KNOWS,ACTED_IN); - exactly one direction — start node
→end node; - any number of properties.
Quick reference
| You want… | Pattern |
|---|---|
| Create with direction | (a)-[:T]->(b) |
| Reversed | (a)<-[:T]-(b) |
| Match either direction | (a)-[:T]-(b) (MATCH only) |
| Anonymous | (a)-[:T]->(b) (no rel variable) |
| Bind the rel | (a)-[r:T]->(b) |
| With properties | (a)-[:T {k: v}]->(b) |
| Any type | (a)-[r]->(b) (MATCH only) |
| Multiple types | `(a)-[:T1 |
Create
Endpoints must exist first — either bound by an earlier MATCH or
created in the same clause.
Match then create
MATCH (a:Person {name: 'Ada'}), (b:Person {name: 'Grace'})
CREATE (a)-[:KNOWS {since: 1843}]->(b)
Inline in one CREATE
CREATE (a:Person {name: 'Ada'})-[:INFLUENCED]->(b:Person {name: 'Grace'})
Chained patterns
A single CREATE can chain several edges through the same variables:
CREATE
(ada:Person {name: 'Ada'}),
(grace:Person {name: 'Grace'}),
(alan:Person {name: 'Alan'}),
(ada)-[:INFLUENCED]->(grace),
(grace)-[:INFLUENCED]->(alan)
Idempotent create — MERGE
CREATE doesn't deduplicate. MERGE
does:
MATCH (a:Person {name: 'Ada'}), (b:Person {name: 'Grace'})
MERGE (a)-[:KNOWS]->(b)
Running this twice yields exactly one KNOWS edge.
Rules on CREATE
- Direction is mandatory —
(a)-[:T]-(b)in aCREATEis an error. - Type is mandatory —
(a)-[]->(b)is an error. - Both endpoints must be in scope.
See Troubleshooting → Parse errors.
Match
Relationships can be matched with or without a type, with or without a direction.
MATCH (a)-[r:KNOWS]->(b) RETURN a, r, b -- outgoing
MATCH (a)<-[r:KNOWS]-(b) RETURN a, r, b -- incoming
MATCH (a)-[r:KNOWS]-(b) RETURN a, r, b -- either direction
MATCH (a)-[r]->(b) RETURN a, r, b -- any type
MATCH (a)-[r:FOLLOWS|KNOWS]->(b) RETURN a, r, b -- multiple types
MATCH (a)-[:FOLLOWS {since: 2020}]->(b) RETURN a, b
Projection
MATCH (a)-[r:FOLLOWS]->(b)
RETURN type(r), r.since, a.name, b.name
type(r) returns the
relationship's type as a string.
Variable-length
MATCH (a)-[:FOLLOWS*1..3]->(b) RETURN b
Mutate or delete
MATCH (a)-[r:KNOWS]->(b) SET r.since = 2025 RETURN r
MATCH (a)-[r:KNOWS]->(b) DELETE r
Deleting a node that has relationships requires
DETACH DELETE:
MATCH (n:User {id: $id}) DETACH DELETE n
Properties on relationships
Exactly the same shape as on nodes:
MATCH (a)-[r:FOLLOWS]->(b)
SET r.since = 2025, r.visibility = 'public'
MATCH (a)-[r:FOLLOWS]->(b)
SET r += {muted: true}
MATCH (a)-[r:FOLLOWS]->(b)
REMOVE r.muted
Access and project them like any other property:
MATCH (a)-[r:FOLLOWS]->(b)
RETURN a.name, b.name, r.since
Direction conventions
LoraDB's direction is semantic — use it to reflect the real-world direction of the relationship.
| Relationship | Direction |
|---|---|
FOLLOWS, KNOWS, LIKES | follower -> followee |
WROTE, AUTHORED | author -> work |
CONTAINS, OWNS | container -> item |
IN, PART_OF | child -> parent |
When in doubt, pick one and document it. Queries can always match
either direction with (a)-[:T]-(b) if you later need symmetry.
Common patterns
"Mutual" follows
MATCH (a)-[:FOLLOWS]->(b)-[:FOLLOWS]->(a)
WHERE id(a) < id(b)
RETURN a.name, b.name
Count by type
MATCH (a)-[r]->(b)
RETURN type(r), count(*) ORDER BY count(*) DESC
Per-node degree
MATCH (n)-[r]->()
RETURN n.name, count(r) AS out_degree
ORDER BY out_degree DESC
MATCH (n)<-[r]-()
RETURN n.name, count(r) AS in_degree
ORDER BY in_degree DESC
Self-loops
Rare but sometimes the right modelling choice:
MATCH (n)
WHERE (n)-[:RECURSES_INTO]->(n)
RETURN n
Ensure this edge (once)
MATCH (a:User {id: $u}), (r:Role {name: $role})
MERGE (a)-[:HAS_ROLE]->(r)
Repeatable without creating duplicates.
Remove every edge of a type
MATCH ()-[r:OBSOLETE_LINK]->()
DELETE r
Oldest / newest edge per pair
Relationships carry their own properties and can be ranked like nodes:
MATCH (a:User)-[r:MESSAGED]->(b:User)
RETURN a.handle, b.handle,
min(r.at) AS first,
max(r.at) AS last,
count(r) AS total
ORDER BY total DESC
LIMIT 20
"Any edge at all" between two nodes
MATCH (a:User {id: $a}), (b:User {id: $b})
RETURN EXISTS { (a)-[]-(b) } AS connected
Count in-degree vs out-degree in one pass
MATCH (n:User)
OPTIONAL MATCH (n)-[out]->()
WITH n, count(out) AS out_deg
OPTIONAL MATCH (n)<-[in]-()
RETURN n.handle, out_deg, count(in) AS in_deg
ORDER BY in_deg + out_deg DESC
LIMIT 20
Note the two OPTIONAL MATCH stages — any node with zero edges in
either direction still appears.
Convert one relationship type to another
Useful during a schema-change migration.
MATCH (a)-[r:OLD_TYPE]->(b)
CREATE (a)-[r2:NEW_TYPE]->(b)
SET r2 = properties(r)
DELETE r
Edge cases
Zero-or-more traversals
[:R*0..] includes the starting node itself as a zero-hop match — see
Paths → zero-hop.
Matching either direction on CREATE
CREATE (a)-[:T]-(b) -- error
Direction is required on writes. Decide which way makes sense.
Deleting a relationship that's been matched twice
A query like MATCH (a)-[r]->(b)-[r]->(c) won't compile — r must be
unique. Use two distinct variables:
MATCH (a)-[r1:T]->(b)-[r2:T]->(c)
Multiple parallel edges
Nothing prevents two KNOWS edges between the same pair:
CREATE (a)-[:KNOWS]->(b)
CREATE (a)-[:KNOWS]->(b)
-- now there are two :KNOWS edges
Use MERGE (a)-[:KNOWS]->(b) for dedup. For modelling-time
uniqueness (one edge only), enforce in application code.
Notes
- Relationship types are case-sensitive, conventionally
UPPER_SNAKE. - A relationship has one type — not a list.
- Relationships cannot be dangling — src and dst must both exist.
- Deleting a relationship does not delete its endpoints.
See also
- Graph Model — where relationships fit.
- Nodes — endpoints.
- Properties — key/value payload.
- CREATE, MATCH, MERGE, SET / DELETE — clause syntax.
- Paths — variable-length and shortest-path traversals.
- Functions → Entity introspection
—
id,type,labels,keys,properties.