Record Links or RELATE Edges? Modeling Relationships in SurrealDB
SurrealDB offers record links and RELATE graph edges for modeling relationships. Here's a worked follow-graph example and a practical rule for choosing between them.
24 Jul 2025, 22:39 UTC

You're building a small social app and you need a feed. Users follow users, posts belong to authors, and sooner or later someone asks for "people you might know." SurrealDB gives you two ways to express these relationships — plain record links and first-class graph edges created with RELATE — and picking the wrong one early means a painful remodel later. The short version: use record links when a relationship is owned by one side and carries no data of its own; use RELATE edges when the relationship is many-to-many, has its own attributes, or needs to be walked in both directions.
Everything below assumes SurrealDB 2.x, a single-node server with the default in-memory or file backend, and statements run through surreal sql or the HTTP endpoint as a root or namespace user. Syntax here has been broadly stable across 1.x and 2.x, but pin your version and re-test before copying into production.
Record links: a pointer that lives on one record
A record link is just a field whose value is a record ID, like author:alice. It costs nothing extra to store and needs no join table. For a post that belongs to exactly one author, this is the right tool:
CREATE post:hello SET
title = "Hello graph",
author = user:alice;
-- Pull the linked record into the result
SELECT title, author.name FROM post:hello FETCH author;FETCH author tells SurrealDB to resolve the linked record so author.name returns the actual name instead of a bare ID. This is ideal for one-to-one and one-to-many relationships where the "many" side points at the "one": a post's author, an order's customer, a comment's parent post. The link is owned by the record it sits on, updates are a single write, and the data model stays obvious.
Where links fall apart is the reverse question. "Give me all posts by Alice" works fine with an index on author, but "give me everyone who follows Alice, where each follow has a date and a notification preference" does not — the relationship itself now has data, and a plain ID field can't hold it.
RELATE edges: relationships as first-class records
Graph edges are real records in an edge table, created with RELATE. Because they're records, they can carry their own fields:
CREATE user:alice SET name = "Alice";
CREATE user:bob SET name = "Bob";
CREATE user:carol SET name = "Carol";
RELATE user:alice->follows->user:bob SET since = time::now();
RELATE user:bob->follows->user:carol SET since = time::now();
RELATE user:alice->follows->user:carol SET since = time::now();Traversal uses arrow syntax. The arrow points in the direction of the edge, and you name the edge table and the target table you expect at the other end:
-- Who does Alice follow?
SELECT ->follows->user AS following FROM user:alice;
-- Who follows Alice? (reverse direction)
SELECT <-follows<-user AS followers FROM user:alice;
-- Friends of friends: a cheap recommendation seed
SELECT ->follows->user->follows->user AS suggestions FROM user:alice;That last query is the payoff. Chaining arrows gives you a friend-of-friend recommendation in one statement, with no self-join and no application-side loop. And because the edge is a record, you can query the relationship's own data — for example, SELECT ->follows.since FROM user:alice — or filter on it.
Choosing between them
A workable rule of thumb:
- Record link when the relationship is fixed, owned by one side, and attribute-free: a post's author, a line item's order.
- RELATE edge when it's many-to-many (follows, likes, memberships), when the relationship has fields of its own (
since,role,weight), or when you'll traverse both directions regularly. - Both is fine. A post can link to its author with a record field while likes on that post are edges. Mixing models is the point of a multi-model database.
The trade-off you actually pay
Edges shift cost from write-time modeling to read-time traversal. A record link is resolved with a direct lookup; a two-hop arrow query touches every intermediate edge, so cost grows with fan-out — a user who follows ten thousand people makes that recommendation query genuinely expensive. Real performance depends on your data shape, indexes, and storage backend, not on anything a feature list promises, so measure with your own graph before committing.
Two more sharp edges, so to speak. First, edges are directed: alice->follows->bob does not imply anything about Bob to Alice. Reverse lookups work via the <- syntax, but if you need symmetric relationships (a mutual "friendship"), you either write two edges or accept that semantics get subtle — test both directions before assuming. Second, edges are records, which means they need the same hygiene as any table: think about uniqueness (do you want to allow duplicate follows edges between the same pair?), permissions, and what happens when a user is deleted.
Try it yourself
Spin up a local instance — surreal start memory or the official Docker image — paste the statements above into surreal sql, and check three things: that the forward query returns Bob and Carol, that the reverse query from user:carol returns Alice and Bob, and that the friend-of-friend query returns the shape you expect. Then create a second follows edge between the same pair and see whether your queries double-count; if they do, add a uniqueness constraint or use RELATE ... CONTENT with an explicit edge ID. Fifteen minutes of this tells you more about your model than any amount of schema sketching — and if a relationship in your app is starting to grow its own fields, that's your signal to promote it from a link to an edge.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.