The RELATE statement can be used to generate graph edges between two records in the database. This allows you to traverse related records efficiently without needing to pull data from multiple tables and merging that data together using SQL JOINs.
Edges created using the RELATE statement are nearly identical to tables created using other statements, and can contain data. The key differences are that:
Edge tables are deleted once there are no existing relationships left.
Edge tables have two required fields in and out, which specify the directions of the relationships. These cannot be modified in schema declarations except to specify that they must be of a certain record type or to add assertions.
Another option for connecting data is using record links. Record links consist of a field with record IDs that serve as unidirectional links by default, or bidirectional links if reference tracking is used. The key differences are that graph relations have the following benefits over record links:
Graph relations are kept in a separate table as opposed to a field inside a record.
Graph relations allow you to store data alongside the relationship.
Graph relations have their own syntax that makes it easy to build and visualize edge queries.
Graph relations offer built-in bidirectional querying and referential integrity. As of SurrealDB 2.2.0, record links also offer these two advantages if they are defined inside a DEFINE FIELD statement using the REFERENCES clause. For more information, see the page on record references.
RELATE will create a relation regardless of whether the records to relate to exist or not. As such, it is advisable to create the records you want to relate to before using RELATE, or to at least ensure that they exist before making a query on the relation. If the records to relate to don't exist, a query on the relation will still work but will return an empty array. To override this behaviour and return an error if no records exist to relate, you can use a DEFINE TABLE statement that includes the ENFORCED keyword.
Example usage
Basic usage
The following query shows the basic structure of the RELATE statement, which creates a relationship between a record in the person table and a record in the article table.
Instead, an edge table (in this case a table called wrote) stores the relationship information.
SELECT * FROMwrote;
The structure in -> id -> out mirrors the record IDs from the RELATE statement, with the addition of the automatically generated ID for the wrote edge table.
The same structure can be used in a SELECT query, as well as directly from a record ID.
-- Aristotle's id and the articles he wrote SELECTid, ->wrote->articleFROMperson:aristotle; -- Every `person`'s id and written articles -- Same output as above as the database has a single `person` record SELECTid, ->wrote->articleFROMperson; -- Directly follow the path from Aristotle to his written articles RETURNperson:aristotle->wrote->article;
By default, the edge table gets created as a schemaless table when you execute the RELATE statement. You can make the table schemafull by defining a schema.
A common use case is to make sure only unique relationships get created. You can do that by defining an index.
As edge tables are bidirectional by default, there is nothing stopping a query like the following in which an article writes a person instead of the other way around.
An edge table will always include exactly one record for the in field and one record for the out field.
Knowing this, one would assume that a RELATE statement like the following would fail as it seems to be attempting to insert two cat records at the in field.
However, the query works just fine. Instead of trying to create a single parent_of graph edge, it will create one for each record in the first array: one between cat:mr_meow and cat:kitten, and another between cat:mrs_meow and cat:kitten.
Graph edges are standalone tables that can hold other fields besides the default in, out, and id. These can be added during a RELATE statement or during an UPDATE in the same manner as any other SurrealDB table.
Let's look at the two ways you can add record data in the RELATE statement. Both of these queries will produce the same result.
RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SET metadata.time_written =time::now(), metadata.location ="Tallinn";
You can also use parameters to specify the record IDs.
-- These two statements store the result of the subquery in a parameter -- The subquery returns an array of IDs LET$person = (SELECTVALUEidFROMperson); LET$article = (SELECTVALUEidFROMarticle);
-- This statement creates a relationship record for every combination of Record IDs -- Such that if we have 10 records each in the person and article table -- We get 100 records in the wrote edge table (10*10 = 100) -- In this case it would mean that each article would have 10 authors RELATE$person->wrote->$articleSETtime.written =time::now();
By default, the relate statement returns the record value once the changes have been made. To change the return value of each record, specify a RETURN clause, specifying either NONE, BEFORE, AFTER, DIFF, or a comma-separated list of specific fields to return.
-- Don't return any result RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SETtime.written =time::now() RETURNNONE;
-- Return the changeset diff RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SETtime.written =time::now() RETURNDIFF;
-- Return the record before changes were applied RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SETtime.written =time::now() RETURNBEFORE;
-- Return the record after changes were applied (the default) RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SETtime.written =time::now() RETURNAFTER;
-- Return a specific field only from the updated records RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SETtime.written =time::now() RETURNtime;
-- Return only the value of a specific field without the field name RELATEperson:l19zjikkw1p1h9o6ixrg->wrote->article:8nkk6uj4yprt49z7y3zm SETtime.written =time::now() RETURNVALUEtime;
Using the TIMEOUT clause
Adding the TIMEOUT keyword to specify a timeout duration for the statement can be useful when processing a large result set with many interconnected records. If the statement continues beyond this duration, then the transaction will fail, and the statement will return an error.
-- Cancel this conditional filtering based on graph edge properties -- if not finished within 5 seconds SELECT * FROMpersonWHERE->knows->person->(knowsWHEREinfluencer=true) TIMEOUT5s;
Using a TIMEOUT is particularly useful when experimenting with complex queries with an extent that is difficult to imagine, especially if the query is recursive.
Deleting graph edges
You can also delete graph edges between two records in the database by using the DELETE statement.
As mentioned above, a graph edge will also automatically be deleted if it is no longer connected to a record at both in and out.
-- Create three people CREATEperson:one, person:two, person:three;
-- And a love triangle involving them all RELATEperson:one->likes->person:two; RELATEperson:two->likes->person:three; RELATEperson:three->likes->person:one;
-- Person two moves to Venus permanently, so delete DELETEperson:two;
-- Only one `likes` relationship is left SELECT * FROMlikes;
As mentioned at the top of the page, RELATE can be used for records that do not yet exist. While this behaviour can be overridden by using the ENFORCED keyword, it can be useful in certain situations.
For example, the VALUE clause inside a DEFINE FIELD statement is calculated every time a record is altered (that is, every time it is created or updated). If this value depends on a graph edge, creating the record first will cause VALUE to calculate it based on a nonexistent path.
In the following example, a house table has a field called has_road_access that depends on whether any ->has_road paths return an output that is not empty. Meanwhile, the city has a new road under construction but no houses are present and their details have not been set yet.
-- Returns true if $this->has_road path is not empty DEFINEFIELDhas_road_accessONTABLEhouseVALUE !!$this->has_road->road; CREATEroadSETname="Dalhurst Way", length=10.5;
As the addresses of the upcoming houses have been decided, the ->has_road path can be set ahead of time by giving the house records an ID based on their exact address.
Later on, two new houses are completed in the city and registered in the database. As the path to house:["Dalhurst Way", 218] has already been set up, the has_road_access field will evaluate to true, while the other house in the middle of nowhere will evaluate to false.
CREATEhouse:["Dalhurst Way", 218]SETfloors=2, bedrooms=5; CREATEhouse:["Middle of nowhere", 0]SETfloors=4, bedrooms=12;
For the questions below, each of the queries will give you largely the same answer. Note that whether -> and <- are parsed as in or out depends on their direction in relation to the graph edge wrote. An arrow pointing towards wrote corresponds to in, and vice versa.
The following examples show how to make similar queries in a number of different ways, in the context of a database with one person who wrote two articles.
CREATE person:aristotle, article:on_sleep_and_sleeplessness, article:on_dreams; RELATEperson:aristotle->wrote->[ article:on_sleep_and_sleeplessness, article:on_dreams ] // Written sometime around the year 330 BC SETtime_written=d"-0330-01-01";
Who wrote the articles?
-- All queries lead to `person:artistotle` twice, -- via different paths and thus different field names -- and/or structure
-- Directly from the `wrote` table SELECTinFROMwrote;
-- From a single `person` record SELECT->wrote.inFROMperson; SELECT->wrote<-personFROMperson;
-- From two `article` records SELECT<-wrote.inFROMarticle; SELECT<-wrote<-personFROMarticle;
For a more complicated query like the one below you can use a simple rule of thumb:
Place the subject in front of the graph selection, then read it backward.
-- This query SELECT->purchased->product<-purchased<-person->purchased->productFROMperson:tobie
-- Then becomes person:tobie->purchased->product<-purchased<-person->purchased->product SELECT
Reading this backwards then makes more sense:
Select every product that was purchased by a person who purchased a product that was also purchased by person Tobie.
Alternatively, you can break it down into steps over multiple lines.
-- Starting with Tobie person:tobie -- move on to his purchased products ->purchased->product -- that were also purchased by persons... <-purchased<-person -- what are all of those persons' purchased products? ->purchased->product
Putting it all together it would be: based on all the products Tobie purchased, which person also purchased those products and what did they purchase? This sort of query could be used on a social network site to recommend to the user person:tobie a list of people that have similar interests.
Using parentheses to refine graph query logic
Parentheses can be added at any step of a graph query to refine the logic, such as filtering relations based on specific conditions using the WHERE clause.
For example, suppose we want to limit the query to only take recent purchases into account. We can filter purchased graph edge to only include purchases made in last 3 weeks:
-- Select products purchased by people in the last 3 weeks who have purchased the same products that tobie purchased SELECT ->purchased->product <-purchased<-person->(purchasedWHEREcreated_at>time::now() -3w) ->purchased->product FROMperson:tobie;
If the purchased graph table can lead to both a product or a subscription, they can both be added to the query.
The ? wildcard operator can also be used to search for any and all linked records. The following query will allow purchased product, subscription, insurance, or any other linked records to show up.
SELECT -- all tables in which the record is at `in` ->(?).* ASwhat_hesse_did, -- all tables in which the record is at `out` <-(?).* ASwhat_others_did_to_hesse FROMperson:hermann_hesse;
The ? operator can also be used to find all the relations between one record and another. To do this, use the <-> operator to see all relations in which the record ID in question is either at the in or the out of the graph edge. Follow this with (?) to avoid filtering by graph table name, then use a WHERE filter on the output (an array of record IDs) to see if the record ID is present in either the in or the out field of the graph edge.
A small example of this using some of the relations between Anakin Skywalker (Darth Vader), Palpatine (the Emperor), and Luke Skywalker:
-- As a SELECT statement SELECTVALUE<->(?)[WHEREperson:the_emperorIN[in, out]]FROMONLYperson:anakin_skywalker; SELECTVALUE<->(?)[WHEREperson:luke_skywalkerIN[in, out]]FROMONLYperson:anakin_skywalker;
-- Or returned directly from the record ID person:anakin_skywalker<->(?)[WHEREperson:the_emperorIN[in, out]]; person:anakin_skywalker<->(?)[WHEREperson:luke_skywalkerIN[in, out]];
Parentheses can be used at each point of a graph query. The example below includes person records (authors) connected to book records by the wrote table. As both the person and book tables have fields that can be useful when filtering, they can be isolated with parentheses at this point of the graph query in order to filter using the WHERE clause.
CREATEperson:j_r_r_tolkienSET name="J.R.R. Tolkien", born=d'1891-01-03'; -- Very approximate date of birth CREATEperson:platoSET name="Plato", born="-0428-06-01";
CREATEbook:fotrSET name="The Fellowship of the Ring"; CREATEbook:republicSET name="The Republic", original_name="Πολιτεία";
[ { books_about_rings: [ { id: book:fotr, name: 'The Fellowship of the Ring' } ], name: 'J.R.R. Tolkien' }, { books_about_rings: [], name: 'Plato' } ]
Destructuring can also be used to pick and choose which fields to access inside a graph query. The following query will return the same output as above, except that original_name: 'Πολιτεία' will no longer show up.
All of the queries up to now have been clear about what sort of record is found at the in and out fields: in is the record that is doing something, while out is the record that has something done to it:
A person who writes an article: the person writes, the article is written.
A person who purchases a product: the person purchases, the product is purchased.
However, sometimes a relation is such that it is impossible to determine which record is located at the in part of a graph table, and which is located at the out part. This is the case when a relationship is truly bidirectional and equal, such as a friendship, marriage, or sister cities:
Adding a unique key is a good practice for this sort of relation, as it will prevent it from being created twice. This can be done by defining a field as a unique key based on the ordered record IDs involved, followed by a DEFINE INDEX statement.
With the index in place, a relation set from one record to the other now cannot be created a second time.
RELATEcity:calgary->sister_of->city:daejeon; -- OK RELATEcity:daejeon->sister_of->city:calgary; -- "Database index `only_one_sister_city` already contains '[city:calgary, city:daejeon]', with record `sister_of:npab0uoxogmrvpwsvfoa`"
Refining the in and out fields of a relation
As mentioned above, the in and out fields of a graph table are mandatory but can be modified to specify their record type or make assertions.
CREATEbook:demianSETtitle="Demian. Die Geschichte von Emil Sinclairs Jugend", language="German"; CREATEauthor:hesseSETname="Hermann Hesse";
RELATEauthor:hesse->wrote->book:demian;
Output
"Found book:demian for field `out`, with record `wrote:l4xjcgqkgm7vmqqt4iah`, but field must conform to: $value.language = 'English'"
Structure of queries on relations
Using an alias is a common practice in both regular and relation queries in SurrealDB to make output more readable and collapse nested structures. You can create an alias using the AS clause.
SELECT->friends_with->cat->friends_with->catFROMcat:one; -- create an alias for the result using the `AS` clause. SELECT->friends_with->cat->friends_with->catASfriends_of_friendsFROMcat:one;
// Output without alias { "->friends_with": { "->cat": { "->friends_with": { "->cat": [ cat:three ] } } } }
// Output with alias { friends_of_friends: [ cat:three ] }
However, an alias might not be preferred in a case where you have multiple graph queries that resolve to the fields of a large nested structure. Take the following data for example:
However, opting to not use an alias will return the original graph structure which makes the levels of depth of the query clearer. In addition, the population field is clearly the population for the states.
The destructuring syntax can be used to reduce some typing. Here is the same query as the last using destructuring syntax instead of one line for each field.
SELECT id, -- access id and population on a single line ->contains->state.{id, population}, ->contains->state->contains->city.id FROMcountry:usa;
As the query that uses aliases does not maintain the original graph structure, adding population would require clever renaming such as ->contains->state.population AS state_populations to make it clear that the numbers represent state and not city populations.
Multiple graph tables vs. fields
Being able to set fields on graph tables opens up a large variety of custom query methods, one of which is explored here.
Imagine a database that holds detailed information on the relations between NPCs in a game that are made to be as realistic as possible. Two of the characters have a rocky past but finally end up married. During this period, we might have tracked their relationship by adding and removing graph edges between the two of them as they move from a stage of being friends, to dating, to hating each other, to finally ending up married.
CREATEperson:one, person:two; -- These three relations would end up deleted RELATEperson:one->friends_with->person:two; RELATEperson:one->dating->person:two; RELATEperson:one->hates->person:two; -- Finally this would be the graph edge connecting the two RELATEperson:one->married->person:two;
This works well to track the current state of the relationship, but creating a more general table such as knows along with a number of fields can be a better method to track the changing relationship over time. The following shows the relationship between the two person records, along with a third record called person:three who went to the same school and once dated person:one.
Because the WHERE clause simply checks for truthiness (whether a value is present and not empty), these fields do not necessarily need to be booleans and can even be complex objects.
RELATEperson:one->knows->person:twoSET same_high_school=false, has_been_friends=true, has_dated={ from: d'2020-12-25', to: d'2023-12-25' }, has_hated={ from: d'2023-12-25', to: d'2024-03-01' }, married_to={ since: d'2024-03-01' };
RELATEperson:one->knows->person:threeSET same_high_school=true, has_dated={ from: d'2019-09-10', to: d'2020-12-31' };
With these objects, a jealous person:two could do a check on person:one to see how many relationships with has_dated have an end time that overlaps with the has_dated period of person:one and person:two.
Graph edges can also be queried recursively. For a full explanation of this syntax, see the page on recursive paths.
Take the following example which creates five cities, each of which is connected to the next by some type of road of random length.
-- Note: 1..6 used to be inclusive until SurrealDB 3.0.0 -- Now creates 1 up to but not including 6 CREATE |city:1..=6| SETname=<string>id.id() +'ville'; FOR$pairIN (<array>(1..=5)).windows(2) { LET$city1 = type::record("city", $pair[0]); LET$city2 = type::record("city", $pair[1]); RELATE$city1->to->$city2SET type=rand::enum(["train", "road", "bike path"]), distance=<int>(rand::float() *100).ceil() };
While it is possible to manually move three levels down this road network, it involves a good deal of manual typing.
This can be replaced by a @ to refer to the current record, followed by .{3} to represent three levels down the to graph edge. A level between 1 and 256 can be specified here.
SELECT @.{3}->to->cityASfourth_cityFROMcity:1;
A traditional query to show the final road info from city:1 to the city three stops away would look like this.
To use the same query recursively, wrap the part that must be repeated (->to->city) inside parentheses. This will ensure that the .{2} part of the query only repeats ->to->city twice, and not the final ->to.* portion.
A range can be added inside the {} braces. The following query that uses a range of 1 to 20 will follow the ->to->city path up to 20 times, but will stop at the 5th and final depth because the next level returns an empty array.
city:1.{1..20}->to->city;
Response
[ city:5 ]
Ranges can be followed with the destructuring operator to collect fields on each depth, returning them in a single response. The following query goes five depths down the to graph table, returning each city and road along the way.
As noted above, a TIMEOUT can be set for queries that may be computationally expensive. This is particularly useful when experimenting with recursive queries, which, if care is not taken, can run all the way to the maximum possible depth of 256.
Take the following example with two person records that like each other. Following the likes edge will run until the query recurses 256 times and gives up.
CREATEperson:one, person:two; RELATEperson:one->likes->person:two; RELATEperson:two->likes->person:one; -- Open-ended range person:one.{..}->likes->person;
Response
'Exceeded the idiom recursion limit of 256.'
Take the following example in which three person records of created, each of which likes the other two person records. A query on the ->likes->person path shows that the number of records doubles each time.
Since an open-ended range can be specified in a recursive query, this would result in a full 256 attempts to recurse, multiplying the number of results by two each time for a total of 115792089237316195423570985008687907853269984665640564039457584007913129639936 records by the end.
When experimenting with recursive queries, especially open-ended ranges, it is thus recommended to use a timeout.
At the knows path, parentheses can be used to insert clauses or an entirely new SELECT statement based on the records turned up at this point. In the following example, the FROM knows portion applies to all the records that a person knows, not the knows table as a whole.
However, clauses available in SELECT statements such as WHERE, LIMIT, GROUP BY, aliases and so on can be used, making a graph clause a most flexible option.
Multiple graph tables can be selected by separating each table with a comma, in the same way as in any other SELECT statement. In addition, all tables can be selected by using ? as a wildcard operator.
-- Select from both 'feeds' and 'plays_with' SELECT<-(SELECT * FROMfeeds, plays_withORDERBYat) FROMcat:one; -- Or any graph table SELECT<-(SELECT * FROM ? ORDERBYat) FROMcat:one;
Ranges inside graph queries
Range syntax can also be used on the edges of a graph query.
A common usage of range syntax on edges is when their ID has been defined as a ULID, making the id field random yet sortable and significant in terms of time.
RELATEcharacter:one->speaks_to:ulid()->character:twoSETcontent="Greetings, adventurer!"; RELATEcharacter:one->speaks_to:ulid()->character:twoSETcontent="Can you please help me? My sheep have run amok.";
SELECT // Grab the latter part of the record ID, turn it into a datetime time::from_ulid(id.id()) ASat, content FROM // ULID from 2025-04-25, well before today's date character:one->speaks_to:01JSNG0KZSY3HJ5QSZ7JSMQMGR..;
Output
[ { at: d'2025-04-25T03:37:53.246Z', content: 'Greetings, adventurer!' }, { at: d'2025-04-25T03:37:53.248Z', content: 'Can you please help me? My sheep have run amok.' } ]
Array-based record IDs also work well inside range queries on edges.