Stop Joining Tables: Using Record Links and Graph Relations in SurrealDB
Stop fighting complex SQL JOINs. Learn how SurrealDB uses Record Links and Graph Relations to traverse data paths efficiently with a single query.
01 Aug 2026, 22:05 UTC

The Join Tax in Modern Applications
In traditional relational databases, connecting data requires a JOIN operation. As your schema grows and your data becomes more deeply nested, these JOINs become a performance tax. You either write complex SQL that is hard to maintain or execute multiple round-trips from your application code to fetch related records, increasing latency and network overhead.
SurrealDB eliminates this pattern by treating relationships as first-class citizens. Instead of matching IDs across tables at query time, SurrealDB uses Record Links and Graph Relations to store direct pointers to data. The takeaway is simple: you can traverse complex data paths in a single query without the computational overhead of a relational join.
Direct Pointers with Record Links
A Record Link is a field that stores a direct reference to another record rather than a simple string or integer ID. In SurrealQL, these are represented as record:id. Because the database knows exactly where the linked record lives, it doesn't need to scan an index to find the matching row in another table.
To retrieve the actual data behind a link, you use the FETCH clause. This tells the database to replace the record ID with the full content of the linked document before returning the result to the client.
Modeling Complex Paths with Graph Relations
While Record Links are great for simple one-to-one or many-to-one relationships, complex networks (like social graphs or permission hierarchies) require Edges. An edge is a record that connects two other records (nodes), and it can carry its own properties.
Using the RELATE statement, you create a directed connection. You can then traverse these connections using the -> (outgoing) and <- (incoming) operators. This allows you to query paths—such as "find all friends of friends who like a specific product"—without writing a single JOIN statement.
Worked Example: User Permissions Graph
Consider a scenario where users belong to groups, and groups have specific permissions. In a relational DB, this is a three-table join. In SurrealDB, it is a graph traversal.
1. Setup Data
Run these commands in your SurrealDB environment (via Surrealist or the CLI) with administrative permissions:
-- Create the nodes
CREATE user:jane SET name = 'Jane Doe';
CREATE group:admins SET name = 'Administrators';
CREATE permission:root SET level = 'Superuser';
-- Create the edges (The Relations)
-- Jane is a member of the Admins group
RELATE user:jane -> member_of -> group:admins SET joined_at = time::now();
-- The Admins group has root permissions
RELATE group:admins -> has_permission -> permission:root SET granted_by = 'system';
2. The Traversal Query
To find all permissions Jane has, you traverse the graph. You do not need to specify the join keys; you simply follow the edge labels:
SELECT ->member_of->has_permission->level AS permissions
FROM user:jane;
3. Expanding Data with FETCH
If you have a direct Record Link and want the full object, use FETCH:
-- Assuming a user record has a 'profile' field linking to a profile record
SELECT * FROM user:jane FETCH profile;
Trade-offs and Performance Limits
While this approach removes the JOIN overhead, it introduces new considerations:
- Payload Bloat: Using
FETCHon deeply nested structures (e.g.,FETCH user, profile, posts, comments) can result in massive JSON responses that consume significant memory on both the server and the client. Always select only the fields you need if the linked records are large. - Edge Indexing: Graph traversals are fast, but if you filter edges based on their own properties (like the
joined_atfield in the example), you must ensure those properties are indexed. Without indexes, SurrealDB must scan all edges between two nodes.
Verifying Your Implementation
To verify your relations are working correctly, check the response format. A standard query without FETCH will return a record ID (e.g., group:admins). A query with FETCH or a graph traversal will return the expanded object or the value at the end of the path. If you receive a null value during traversal, verify that the RELATE statement was executed successfully and that the edge labels match exactly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.