Ditch the Join Table: Modeling Relationships with SurrealDB Record Links and RELATE Edges
SurrealDB's record links and RELATE edges replace join tables with native graph relationships — here's a worked example, plus the referential integrity trade-off you need to plan for.
30 Dec 2025, 00:01 UTC

If you've ever modeled a "users write articles" relationship in a document database, you know the drill: embed an array of IDs, or create a separate membership collection, then stitch everything together in application code. SurrealDB takes a different path — relationships are first-class citizens in the database itself, and that changes both your schema and your query patterns.
The thesis of this post: for document-graph workloads, SurrealDB's record links and RELATE edges let you delete your join tables and your N+1 query loops, at the cost of taking referential integrity into your own hands.
Two kinds of relationships, both native
SurrealDB gives you two tools:
- Record links — a field that points directly at another record, like
author: record:user. Think of it as a typed pointer stored in the document. - Edge tables — created with
RELATE, these are real tables that sit between two records:user:1->wrote->article:1. Because they're tables, edges can carry their own fields.
That second point is the quiet superpower. In a relational schema, a many-to-many join table with extra columns (like since or role) is exactly this — but you had to name it, index it, and join through it. Here, the edge is the relationship.
A worked example: authors, articles, and metadata on the edge
Run these in the SurrealDB sandbox (surreal start --memory locally, or the web sandbox) with any recent 1.x/2.x build — but confirm syntax against your exact version, since SurrealQL has shifted across releases.
-- Create the records
CREATE user:ada SET name = "Ada";
CREATE article:graph101 SET title = "Graphs 101";
-- Create a relationship WITH data on the edge
RELATE user:ada->wrote->article:graph101
SET since = time::now(), role = "primary";
-- Traverse it in one query
SELECT ->wrote->article.title AS articles FROM user:ada;
-- Query the edge itself
SELECT since, role, out.title AS article FROM wrote;No join table, no second round trip. The arrow syntax (->wrote->article) reads like the traversal it performs: from user:ada, follow wrote edges, land on articles. In an ORM-based stack, fetching "all articles by this author with relationship metadata" typically means either a hand-written join or the classic N+1 pattern of one query per related record.
Pagination and traversal discipline
Graph results paginate with the familiar LIMIT and START clauses, but there's a catch: traversal order is not guaranteed unless you add an explicit ORDER BY. Offset pagination over an unordered traversal can skip or repeat rows between pages. For anything user-facing, sort by a stable field (creation time, record ID) before paginating, or treat the traversal as a set and paginate a materialized result.
Also bound your traversals. A query like SELECT ->knows->person->knows->person FROM user:ada fans out multiplicatively. Test with realistic data volumes — a sandbox with ten records will hide a query that melts at ten thousand.
The trade-off: integrity is your job now
Record links are not enforced foreign keys by default. Delete user:ada and a dangling author: user:ada reference can remain, silently. If you assume relational cascade behavior, you will ship orphaned data.
Your options:
DEFINE FIELD ... ASSERTto validate links at write time.DEFINE EVENTto react to deletes and clean up references.- Application-level checks, if your stack already centralizes writes.
There's also a pragmatic migration path: run tables SCHEMALESS while your relationship model is still moving, then layer on DEFINE FIELD and DEFINE INDEX constraints once it stabilizes. Schema strictness is per-table, so you can harden the hot paths first.
Try it this week
Spin up an in-memory instance, create two records, RELATE them, and traverse with arrow syntax. Then do the uncomfortable part: delete one side and query the dangling reference, so you see the actual behavior instead of assuming it. If your workload is document-shaped with dense relationships — social graphs, permissions, catalogs — the join-table-free model is a genuine simplification. Just budget for the integrity work the database deliberately leaves to you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.