Skip to main content

Nodes and Labels

A node is a vertex in the graph. Every node has:

  • Zero or more labels — tags that describe what kind of thing it is (Person, Movie).
  • Zero or more properties — typed key/value pairs.
  • A stable internal id — see Graph Model → Identity.

Each node is stored once. Multiple references to the same node via different matches bind to the same identity.

Create

CREATE (:Person {name: 'Ada', born: 1815})     -- one label
CREATE (:Person:Admin {name: 'Bob'}) -- multiple labels
CREATE (:Temp) -- no properties
CREATE () -- no labels, no properties

Even a fully-naked () is a valid node. Most real graphs give every node at least one label — it's the primary way to scope queries.

Bind a variable at creation

CREATE (ada:Person {name: 'Ada'})
CREATE (ada)-[:WROTE]->(n:Note {text: 'Bernoulli numbers'})
RETURN ada, n

Variables (ada, n) stay in scope for the rest of the query.

Match by label

Labels are the primary way to scope a query.

MATCH (p:Person)         RETURN p                -- single label
MATCH (a:Person:Admin) RETURN a -- must have both
MATCH (n) RETURN labels(n) -- any nodeall labels

Match by label + property

MATCH (u:User {email: $email})        RETURN u
MATCH (u:User {email: $email, active: true}) RETURN u

Inline maps are equality-only. For ranges, regex, IN, or null checks, move the predicate into WHERE:

MATCH (u:User)
WHERE u.age BETWEEN 18 AND 65 -- NOT supported
MATCH (u:User)
WHERE u.age >= 18 AND u.age <= 65 -- idiomatic in LoraDB
RETURN u

Labels

MATCH (n:Person {name: 'Ada'}) SET    n:Pioneer   RETURN labels(n)
MATCH (n:Person {name: 'Ada'}) REMOVE n:Pioneer RETURN labels(n)

Multiple labels

A node can have any number of labels, including zero:

MATCH (n:Person {name: 'Ada'}) SET n:Admin, n:Verified

Inspect labels

MATCH (n) RETURN labels(n), count(*)
ORDER BY count(*) DESC

One row per distinct label-set in the graph.

Conventions

  • Case-sensitive strings.
  • Conventionally PascalCase (Person, not person).
  • Use singular nouns (User, not Users).

See Troubleshooting → Queries return empty results for the classic :user vs :User mistake.

Properties on nodes

Any supported data type:

CREATE (c:City {
name: 'Amsterdam',
population: 918000,
founded: date('1275-10-27'),
tags: ['capital', 'port'],
location: point({latitude: 52.37, longitude: 4.89})
})

Read, patch, and remove with SET / REMOVE:

MATCH (c:City {name: 'Amsterdam'}) RETURN c.population, c.tags

MATCH (c:City {name: 'Amsterdam'})
SET c.population = 920000, c.updated = timestamp()
RETURN c

See Properties for the full reference.

Upsert

MERGE finds a node, or creates one. Pair with ON MATCH / ON CREATE to run different side-effects per branch:

MERGE (u:User {email: $email})
ON CREATE SET u.created_at = timestamp()
SET u.last_seen = timestamp()
RETURN u

Delete

-- Standalone node (no edges)
MATCH (n:Temp) DELETE n

-- Node + all edges
MATCH (n:User {id: $id}) DETACH DELETE n

See DETACH DELETE for details.

Common patterns

Count by label

MATCH (n) RETURN labels(n) AS labels, count(*) AS n
ORDER BY n DESC

Ensure uniqueness at write time

LoraDB has no uniqueness constraints — enforce it yourself with MERGE:

MERGE (u:User {email: $email})
ON CREATE SET u.created_at = timestamp()

Pattern-match on one label, filter on another

MATCH (n:Person)
WHERE NOT n:Admin
RETURN n

Sample a few of each

MATCH (n)
WITH labels(n)[0] AS label, n
WITH label, collect(n)[..3] AS sample
RETURN label, sample

Find nodes without a given relationship

Anti-pattern: "who doesn't…" — use NOT EXISTS { … }:

MATCH (u:User)
WHERE NOT EXISTS { (u)-[:WROTE]->(:Post) }
RETURN u.handle

Bulk label migration

MATCH (u:User)
WHERE u.role = 'admin' AND NOT u:Admin
SET u:Admin

Move properties between nodes

MATCH (src:Person {id: $src}), (dst:Person {id: $dst})
SET dst += properties(src)
REMOVE src:Person
SET src:Archived
SET src.archived_at = timestamp()

Split one node into two

A modelling change where a property becomes its own entity — useful when the value starts being reachable from multiple sides:

MATCH (u:User) WHERE u.company IS NOT NULL
WITH u, u.company AS company
MERGE (c:Company {name: company})
CREATE (u)-[:WORKS_AT]->(c)
REMOVE u.company

Edge cases

Labelless nodes

Valid but rare. They don't show up in MATCH (:Any_Label) patterns and are hard to find without the id() function.

Many labels

Matching (n:A:B:C) requires all listed labels. If you want "any of", UNION two matches or use WHERE:

MATCH (n)
WHERE n:Person OR n:Robot
RETURN n

Property-only match

MATCH (n {external_id: $id}) RETURN n

This scans the entire node set — no label scoping. Always add a label when you can.

Identity vs equality

Two nodes with identical labels and properties are still distinct — they have different internal ids. Use id() to compare identity; use property equality for value-based matching.

See also