SurrealDB FETCH Clause: One-Hop Graph Traversals in SurrealQL
SurrealDB's FETCH clause resolves one-hop record links in a single SELECT, embedding related records directly. This guide shows the schema setup, a worked example, performance limits, and the most common mistakes that cause silent failures.
05 Oct 2025, 18:43 UTC

Why FETCH Matters for Graph-Like Queries
SurrealDB stores relationships as record links—fields that hold references to other records. Without a dedicated traversal operator, you would need multiple round-trips or application-side joins to materialize those links. The FETCH clause in a SELECT statement resolves one level of links in a single query, returning nested objects instead of raw record IDs. This is the primary mechanism for graph-style reads in SurrealQL today.
How FETCH Works
When you declare a field as array<record> (or record for a single link), SurrealDB treats its values as pointers to other records. Adding FETCH field_name to a SELECT instructs the engine to dereference those pointers and embed the target records directly in the result. The operation is limited to one hop: FETCH friend returns each person with their immediate friends, but not friends-of-friends.
Worked Example: People and Friends
The following sequence runs against SurrealDB 1.0 or later. Start an in-memory instance for testing:
surreal start --log trace --user root --pass root memory:
Connect with the CLI:
surreal sql --conn http://localhost:8000 --user root --pass root
Define the schema, insert two records, and link them:
DEFINE TABLE person SCHEMAFULL;
DEFINE FIELD name ON person TYPE string;
DEFINE FIELD friend ON person TYPE array<record>;
CREATE person SET name = 'Alice';
CREATE person SET name = 'Bob';
UPDATE person SET friend = [person:⟨bob_id⟩] WHERE name = 'Alice';
UPDATE person SET friend = [person:⟨alice_id⟩] WHERE name = 'Bob';
SELECT * FROM person FETCH friend;
Replace ⟨bob_id⟩ and ⟨alice_id⟩ with the actual record IDs returned by the CREATE statements. The final SELECT returns each person document with a friend array containing the full friend record objects, not just IDs.
Expected Output Structure
Each row in the result looks like:
{
"id": "person:...",
"name": "Alice",
"friend": [
{ "id": "person:...", "name": "Bob", "friend": [...] }
]
}
Notice the nested friend field inside the embedded record—it still holds the raw link array. FETCH does not recursively expand unless you nest another FETCH (see Limits).
Limits You Need to Know
Single-Hop Only
FETCH friend resolves exactly one level. To go deeper you must either:
- Nest
FETCHin a subquery (SurrealQL does not yet support recursiveFETCHsyntax). - Issue multiple queries from the application layer.
- Use a recursive CTE if your version supports it (experimental as of 1.5).
Payload Growth
Every linked record is embedded in full. If a person has 500 friends and each friend document is 2 KB, the response exceeds 1 MB. Large arrays also increase serialization time and memory pressure on the client. Consider pagination or projection (SELECT name, friend.name FROM person FETCH friend) to limit payload size.
Indexing and Performance
Link resolution uses the primary index of the target table. There is no secondary index on link fields. Traversal cost scales with the number of links fetched, not the total table size. In single-node or embedded mode, concurrent writes that modify the same linked records can contend on the same storage pages, increasing latency.
Version Requirement
FETCH was introduced in the 1.0 release line. Versions prior to 1.0 return a syntax error. Verify your binary with surreal version before relying on this feature in production.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Declaring the link field as string or object instead of array<record> |
FETCH returns the raw string/object; no dereferencing occurs. |
Use DEFINE FIELD friend ON person TYPE array<record>; (or record for a single link). |
Misspelling the field name in FETCH |
Query succeeds but the field is absent or null in results. | Match the exact field name defined in the schema; case-sensitive. |
| Expecting a flat list of all linked records | Application code breaks because results are nested per parent record. | Post-process with FLATTEN or handle the nested structure in the client. |
| Fetching a field that does not exist on the target table | No error; the embedded record simply lacks that field. | Ensure the target table schema includes the fields you need. |
Verification Checklist
- Run the worked example against a temporary in-memory instance.
- Confirm the output shows each person with a
friendarray of full record objects. - Change
FETCH friendtoFETCH frindand verify the field is missing (no error, just absent). - Change the field type to
string, re-insert data, and observe thatFETCHreturns the raw string. - Measure response size with
SELECT * FROM person FETCH friendversus a projectedSELECT name, friend.name FROM person FETCH friendto quantify payload reduction.
Practical Takeaway
Use FETCH when you need immediate neighbors in a single round-trip and the linked set is small enough to embed. For deeper traversals, large fan-out, or write-heavy concurrent workloads, design your access pattern around multiple queries or application-side recursion, and monitor payload size and lock contention in your deployment topology.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.