Skip to main content

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 a CREATE is 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

See Paths → variable-length.

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.

RelationshipDirection
FOLLOWS, KNOWS, LIKESfollower -> followee
WROTE, AUTHOREDauthor -> work
CONTAINS, OWNScontainer -> item
IN, PART_OFchild -> 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