• Start

Graph

Graph traversal

Use SurrealQL arrow syntax, bidirectional edges, traversals from record IDs, automatic flattening, graph paths in schema fields, and Surrealist’s Explorer to debug paths step by step.

Graph queries in SurrealDB use SurrealQL’s -> arrow syntax to walk relationships between records.

SELECT ->wrote->post.* AS userPosts
FROM users:alice;
  • FROM users:alice starts at that node.

  • ->wrote->posts.* follows the wrote edge to posts and returns full post records as userPosts.

Traverse in the reverse direction (for example, from a post to its authors):

SELECT <-wrote<-author AS authors
FROM post:helloworld;

<-wrote<-author means “follow any wrote edge into this post from an author”.

From comments, find authors and then all of their comments:

SELECT <-wrote<-user->wrote->comment FROM comment;

For edges like friends_with, it may be unclear whether a person is on the in or out side. The <-> operator traverses both directions on that edge table:

SELECT *, <->friends_with<->person AS friends FROM person;

Each row lists the other people in the relation, regardless of which side they were on:

[
{
friends: [
person:one,
person:two
],
id: person:one
},
{
friends: [
person:one,
person:two
],
id: person:two
}
]

To drop the current record’s own id from the list, use array::complement():

SELECT 
*,
array::complement(<->friends_with<->person, [id]) AS friends
FROM person;

Output

[
{
friends: [
person:two
],
id: person:one
},
{
friends: [
person:one
],
id: person:two
}
]

For more on this pattern, see the RELATE statement and Chapter 7 of Aeon's Surreal Renaissance.

Traversal can start at one or more record IDs without wrapping everything in SELECT:

CREATE ONLY user:mcuserson SET name = "User McUserson";
CREATE ONLY comment:one SET
text = "I learned something new!",
created_at = time::now();
CREATE ONLY cat:pumpkin SET name = "Pumpkin";

RELATE user:mcuserson->wrote->comment:one SET
location = "Arizona",
os = "Windows 11",
mood = "happy";

RELATE user:mcuserson->likes->cat:pumpkin;
-- Equivalent to:
-- SELECT VALUE <-wrote<-user FROM ONLY comment:one;
comment:one<-wrote<-user;

-- Equivalent to:
-- SELECT VALUE ->likes->cat FROM ONLY user:mcuserson;
user:mcuserson->likes->cat;

Output

-------- Query --------

[user:mcuserson]

-------- Query --------

[cat:pumpkin]

To include fields when starting from an id, use destructuring:

-- Equivalent to:
-- SELECT name, ->likes->cat AS cats FROM ONLY user:mcuserson;
user:mcuserson.{ name, cats: ->likes->cat };

When two lookups follow each other, or a filter follows a lookup, results can flatten in a way that mirrors the graph shape. This example builds a small org chart and walks up and down works_for:

CREATE 
// One president
person:president,
// Two managers
person:manager1, person:manager2,
// Four employees
person:employee1, person:employee2, person:employee3, person:employee4;

// Employees work two to a manager, managers work two to a president
RELATE [person:manager1, person:manager2]->works_for->person:president;
RELATE [person:employee1, person:employee2]->works_for->person:manager1;
RELATE [person:employee3, person:employee4]->works_for->person:manager2;

[person:employee1, person:employee2, person:employee3, person:employee4]
->works_for->person
->works_for->person
<-works_for<-person
<-works_for<-person;

The result is four arrays, each reflecting the up-and-down path through the president.

You can approximate the same with nested SELECT / map, but nesting grows quickly:

[person:employee1, person:employee2, person:employee3, person:employee4]
// Each $p is a single record: an array<record>
.map(|$p| SELECT VALUE out FROM works_for WHERE in = $p.id)
// Each $p is an array, so now you have to map each item inside that
.map(|$p| $p.map(|$p| SELECT VALUE out FROM works_for WHERE in = $p.id))
// Now an array<array<array<record>>>
.map(|$p| $p.map(|$p| $p.map(|$p| SELECT VALUE in FROM works_for WHERE out = $p.id)))
// Now an array<array<array<array<record>>>>
.map(|$p| $p.map(|$p| $p.map(|$p| $p.map(|$p| SELECT VALUE in FROM works_for WHERE out = $p.id))));

Graph syntax keeps structure aligned with the traversal. Calling flatten() at each step produces a flat list of every record seen along the way instead:

[person:employee1, person:employee2, person:employee3, person:employee4]
.map(|$p| SELECT VALUE out FROM works_for WHERE in = $p.id).flatten()
.map(|$p| SELECT VALUE out FROM works_for WHERE in = $p.id).flatten()
.map(|$p| SELECT VALUE in FROM works_for WHERE out = $p.id).flatten()
.map(|$p| SELECT VALUE in FROM works_for WHERE out = $p.id).flatten();

Treat graph flattening as preserving the shape of the walk, not necessarily a single post-hoc array.

Paths are not limited to SELECT; they can appear in DEFINE FIELD:

DEFINE FIELD employers ON TABLE person VALUE SELECT VALUE <-employs<-company FROM ONLY $this;

CREATE person:1, person:2, company:1;
RELATE company:1->employs->person:1;
person:1.*;

A plain VALUE field is computed when the record is created or updated; if the RELATE runs later, the field can be stale until an UPDATE:

UPDATE person:1;

Output

[
{
employers: [
company:1
],
id: person:1
}
]

A computed field is recomputed on read:

DEFINE FIELD employers ON TABLE person COMPUTED <-employs<-company;

CREATE person:1, person:2, company:1;
RELATE company:1->employs->person:1;
person:1.*;

Output

{
employers: [
company:1
],
id: person:1
}

Surrealist’s Explorer view lets you step through records and relations one hop at a time, useful for building intuition for queries like SELECT ->wrote->comment FROM user.

Example setup with two outgoing edges:

CREATE user:mcuserson SET name = "User McUserson";
CREATE comment:one SET
text = "I learned something new!",
created_at = time::now();
CREATE cat:pumpkin SET name = "Pumpkin";

RELATE user:mcuserson->wrote->comment:one SET
location = "Arizona",
os = "Windows 11",
mood = "happy";

RELATE user:mcuserson->likes->cat:pumpkin;

Walkthrough:

  • Open user, then user:mcuserson.

  • Open the Relations tab (outgoing ->).

  • Follow wrote into its edge row, then comment to the comment row, matching ->wrote->comment.

Working backward in the Explorer is a good way to assemble a path while learning the syntax.

Was this page helpful?