SurrealDB Docs Logo

Enter a search query

Using SurrealDB as a Graph Database

A graph database is specifically designed to store data as nodes (sometimes called vertices) and edges (relationships between nodes). With this model, connections are front and center, making it easier (and often faster) to query highly connected datasets—like social networks, supply chain relationships, recommendation engines, or fraud detection graphs.

In a graph database, we typically care about both the entities in a system and how they relate to each other. It might be a user that “follows” another user, a product that “belongs” to a category, or a web page that “links to” another page. These relationships are first-class citizens, rather than just foreign keys or nested objects. This can enable powerful, intuitive traversal-based queries that more accurately reflect real-world systems.

But how do you “think” in a graph database? Instead of focusing on how to break data into tables (relational) or embed data in documents (document model), you concentrate on expressing data as nodes and defining the edges that describe relationships. This mindset puts the connections at the core of your design: each data point is a node with properties, and edges hold properties too, representing the context or metadata about those relationships.

Core Concepts of Graph-Oriented Modeling

In any graph database, you deal with three fundamental elements:

  • Nodes (Vertices): Represent main entities or “things.”, In a social network for example, nodes might be people. In a knowledge graph, nodes might be concepts. In a product recommendation engine, nodes might be items or customers.

  • Edges: Represent relationships between nodes. These could be “follows,” “buys,” “likes,” “is a friend of,” “is in category,” etc. They can be directed or undirected, and they often include properties like timestamps or weights.

  • Properties: Both nodes and edges can contain key-value pairs (properties). For a user node, properties might be name or age. For a “likes” edge, a property might be strength to indicate how strong the user’s affinity is.

When you think in a graph, the modeling process shifts toward identifying the main entities in your application (the nodes) and how they relate to one another (the edges). Rather than flattening these relationships into foreign keys or embedding them in nested structures, you give them explicit representation and, often, explicit properties.

Modelling data as a graph

How this works in practice in most graph databases, is through semantic triples, which is a way to describe a graph in a three-part structure:

  • subject → predicate → object

OR

  • node → edge → node

Another way to think about this is in terms of nouns connected by verbs, such that it forms a sentence.

  • noun → verb → noun

OR

  • person → order → product

Data Modeling Approaches

In relational and document databases, you typically define tables or collections and then store records or documents within them. In a graph database, you ask:

When designing a graph database, follow these key steps:

  1. Identify your entities (nodes) - These represent the core objects in your system, such as Users, Posts, Companies, and Departments.

  2. Map relationships (edges) between nodes - Consider how entities connect and interact. For instance, Users may “follow” other Users, “own” Departments, or Posts may “mention” Companies.

  3. Define properties for both nodes and edges - Nodes like User may have properties such as username, email, and createdAt. Similarly, edges like FOLLOWS can store metadata like a since timestamp.

  4. Optimize for critical queries - Analyze your most important traversal patterns and queries. This helps determine which relationships to model explicitly. While similar to indexing in traditional databases, graph optimization often focuses on structuring relationships to enable efficient path traversals.

This systematic approach ensures your graph model effectively captures both the entities and their interconnections while supporting performant queries across your data.

Creating Nodes and Edges in SurrealDB

Creating Nodes

In SurrealDB, nodes are typically just records in a table—like users, posts, companies, etc. SurrealDB introduces a new statement called RELATE using this three-part structure.

Using the RELATE statement, we can create our primary relationships based on the major actions a person using our e-commerce store would take: wish list, cart, order and review. These will serve as our edge tables.

RELATE person:01GT2ZEF2G8AC8D7H7FMZ1ZYZ3 -> wishlist:ulid() -> product:01HGAR7A0R9BETTCMATM6SSXPT; RELATE person:01HBC4FGG0904R927Q82SVZ1JB -> cart:ulid() -> product:01GXRS3FZG8Y8SDBNHMC14N25X; RELATE person:01GCSHZEP89F1B9T33Y4M9VA9J -> order:ulid() -> product:01H35P394G93AVCEF8KX59H5RY; RELATE person:01FSZ7A4W888FAYSSP8T3NV3MX -> review:ulid() -> product:01GBE3CTMG93XBKM07CFH1S9S6;

The RELATE statement works with one record ID at a time for each table.

Here, we are taking an existing record ID from the person and product table. Then for the middle tables which are the edge tables.

Those get created if they don’t already exist. We are also specifying that these new records should use a ULID as an ID.

Once we run the RELATE statement, we’ll see two new fields on the edge tables: in and out.

Now you might be wondering, when were these created since it didn’t seem like we specify them before. We did actually specify them using the RELATE statement, because another way of looking at the semantic triple is in this three part structure

  • in → id → out

Where the first node is called in, the edge is the id, and the second node is the out.

Adding data to Edge Tables using SET and CONTENT

What really sets SurrealDB apart from graph only databases, is that our edges are also real tables, such that you can store information in them, which allows for even more flexible data models.

RELATE person:01GT2ZEF2G8AC8D7H7FMZ1ZYZ3 -> wishlist:ulid() -> product:01HGAR7A0R9BETTCMATM6SSXPT SET time.created_at = time::now(); RELATE person:01GCSHZEP89F1B9T33Y4M9VA9J -> order:ulid() -> product:01H35P394G93AVCEF8KX59H5RY CONTENT { quantity: 2, product_name: ->product.name, price: ->product.price, shipping_address: <-person.address };

We can both create our order relationship and use it at the same time to fetch connected data from both the product and person tables.

Notice that the direction of the arrow changes based on the table we are fetching from.

Looking at the RELATE statement, we can see that we only specified one direction, going from person to order to product.

However, the RELATE statement creates a bidirectional graph by default, meaning that even if we only specified Person → order → product, it will also do person ← order ← product in reverse.

Querying Graph Data in SurrealDB

Graph queries in SurrealDB use SurrealQL, which supports traversing relationships with special syntax. For example:

SELECT ->wrote->posts.* AS userPosts
FROM users:alice;

In this query:

FROM users:alice starts at the node identified by users:alice. ->wrote->posts.* instructs SurrealDB to traverse the wrote edge from alice to any posts node, returning the post(s) as userPosts. You can also traverse in the reverse direction. If you’re starting from a post, you can see which user wrote it:

SELECT <-wrote-.* AS authors
FROM posts:helloworld;

Here, <-wrote-.* means “traverse any node that has a wrote edge pointing to this post,” effectively giving you the authors.

These are the nodes in your graph. They look like documents, but in SurrealDB you can also connect them via edges.

Creating Edges (Relationships)

SurrealDB provides a special syntax to RELATE nodes:

RELATE users:alice->wrote->posts:helloworld CONTENT { created_at: "2025-01-01" };

Here’s what’s happening:

  • users:alice is the user node you’re referencing (assuming SurrealDB recognized or assigned alice as the record’s ID).
  • ->wrote-> is the name of the relationship (edge) that indicates the direction and type of connection.
  • posts:helloworld is the post node you’re connecting to.
  • CONTENT { ... } defines properties on this edge, such as created_at.

This single statement creates an edge from the alice user node to the helloworld post node, labeling the relationship as wrote. The edge can store its own properties just like a node.

Using Neo4j as a reference

Quickly learn how to map your Neo4j knowledge to corresponding SurrealDB concepts and syntax.

As a multi-model database, SurrealDB offers a lot of flexibility. Our SQL-like query language SurrealQL is a good example of this, where we often have more than one way to achieve the same result, depending on developer preference. In this mapping guide, we will focus on the syntax that most closely resembles the Cypher query language.

Concepts mapping

Neo4jSurrealDB

database

database

node label

table

node

record

node property

field

index

index

id

record id

transactions

transactions

relationships

record links, embedding and graph relations

Syntax mapping

Let’s get you up to speed with SurrealQL syntax with some CRUD examples.

Create

As Neo4j is schemafull, only the SurrealQL schemafull approach is shown below. For a schemafull option see the DEFINE TABLE page.

For more SurrealQL examples, see the CREATE, INSERT and RELATE pages.

CypherSurrealQL

CREATE (John:Person {name:‘John’}), (Jane:Person {name: ‘Jane’})

INSERT INTO person [ {id: “John”, name: “John”}, {id: “Jane”, name: “Jane”} ] Table implicitly created if it doesn’t exist

MATCH (p:Person {name:‘Jane’}), (pr:Product {name:‘iPhone’}) CREATE (p)-[:ORDER]->(pr)

RELATE person:Jane -> order -> product:iPhone There are many differences between how SurrealDB and Neo4j do graph relations. Check out the relate docs for more.

CREATE INDEX personNameIndex FOR (p:Person) ON (p.name)

DEFINE INDEX idx_name ON TABLE person COLUMNS name

Read

For more SurrealQL examples, see the SELECT, LIVE SELECT and RETURN pages.

CypherSurrealQL

MATCH (p:Person) RETURN p

SELECT * FROM person

MATCH (p:Person) RETURN p.name

SELECT name FROM person

MATCH (p:Person) WHERE p.name = “Jane” RETURN p.name

SELECT name FROM person WHERE name = “Jane”

EXPLAIN MATCH (p:Person) WHERE p.name = “Jane” RETURN p.name

SELECT name FROM person WHERE name = “Jane” EXPLAIN

EXPLAIN MATCH (p:Person) WHERE p.name = “Jane” RETURN p.name

SELECT name FROM person WHERE name = “Jane” EXPLAIN

MATCH (p:Person) RETURN count(*) as person_count

SELECT count() AS person_count FROM person GROUP ALL

MATCH (p:Person) RETURN distinct p.name

SELECT array::distinct(name) FROM person GROUP ALL

MATCH (p:Person) RETURN p LIMIT 10

SELECT * FROM person LIMIT 10

MATCH (p:Person)-[:ORDER]->(pr:Product) RETURN p.name, pr.name

SELECT name, ->order->product.name FROM person

Update

For more SurrealQL examples, see the UPDATE page.

CypherSurrealQL

MATCH (p:Person) WHERE p.name = “Jane” SET p.last_name = ‘Doe’ RETURN p

UPDATE person SET last_name = “Doe” WHERE name = “Jane”

MATCH (p:Person) WHERE p.name = “Jane” REMOVE p.last_name RETURN p

UPDATE person UNSET last_name WHERE name = “Jane”

MATCH (p:Person) WHERE p.name = “Jane” RETURN p.name

SELECT name FROM person WHERE name = “Jane”

Delete

For more SurrealQL examples, see the DELETE and REMOVE pages.

CypherSurrealQL

MATCH (p:Person) WHERE p.name = “Jane” DELETE p

DELETE person WHERE name = “Jane”

MATCH (p:Person) DELETE p

DELETE person Node/Table still exists here but is empty

MATCH (p:Person) DELETE p

REMOVE TABLE person Node/Table no longer exists

Resources