• Start

Tables and fields

Record IDs and addressing

How record IDs work, ranges, and practical choices for addressing data.

SurrealDB record IDs are composed of a table name and a record identifier separated by a : in between, allowing for a simple and consistent way to reference records across the database. Record IDs are used to uniquely identify records within a table, to query, update, and delete records, and serve as links from one record to another.

Record IDs can be constructed from a number of ways, including alphanumeric text, complex Unicode text and symbols, numbers, arrays, objects, built-in ID generation functions, and a function to generate an ID from values.

All of the following are examples of valid record IDs in SurrealQL.

company:surrealdb
company:w6xb3izpgvz4n0gow6q7
reaction:`🤪`
weather:['London', d'2025-02-14T01:52:50.375Z']

As all record IDs are unique, trying to create a new record with an existing record ID will return an error. To create a record or modify it if the ID already exists, use an UPSERT statement or an INSERT statement with an ON DUPLICATE KEY UPDATE clause.

When you create a record without specifying the full ID, a random identifier is assigned after the table name.

CREATE company;

Output

[
{
id: company:ezs644u19mae2p68404j
}
]

Record IDs can be generated with a number of built-in ID generation functions, which are cryptographically secure and suitable for dispersion across a distributed datastore. These include a 20 digit alphanumeric ID (the default), sequentially incrementing and temporally sortable ULID Record identifiers, and UUID version 7 Record identifiers.

// Generate a random record ID 20 characters in length
// Charset: `abcdefghijklmnopqrstuvwxyz0123456789`
CREATE temperature:rand() SET time = time::now(), celsius = 37.5;
// Identical to the above CREATE statement, because
// :rand() is the default random ID format
CREATE temperature SET time = time::now(), celsius = 37.5;

// Generate a ULID-based record ID
CREATE temperature:ulid() SET time = time::now(), celsius = 37.5;
// Generate a UUIDv7-based record ID
CREATE temperature:uuid() SET time = time::now(), celsius = 37.5;

Text record IDs can contain letters, numbers and _ characters.

CREATE company:surrealdb SET name = 'SurrealDB';
CREATE user_version_2025 SET name = 'Alucard';

To create a record ID with complex characters, use ` (backticks) around the table name and/or record identifier.

CREATE article:`8424486b-85b3-4448-ac8d-5d51083391c7` SET
time = time::now(),
author = person:tobie;

CREATE `Artykuł`:100 SET
author = person:`Lech_Wałęsa`;

If you create a record ID with a number as a string, it will be stored with ` backticks to differentiate it from a number.

CREATE article SET id = 10;
CREATE article SET id = "10";
CREATE article SET id = "article10";
SELECT VALUE id FROM article;

As the record ID article:10 is different from article:10 , no errors are returned when creating and both records turn up in the output of the SELECT statement. Meanwhile, the article with the identifier article10 does not use backticks as there is no article10 number to differentiate it from.

Output

[
article:10,
article:`10`,
article:article10
]

If a numeric value is specified without any decimal point suffix and is within the range -9223372036854775808 to 9223372036854775807 then the value will be parsed, stored, and treated as a 64-bit signed integer.

Any numeric numbers outside of the range of a signed 64-bit integer will be stored as a string.

CREATE temperature:17493 SET time = time::now(), celsius = 37.5;
CREATE year:29878977097987987979232 SET
events = [
"Galactic senate convenes",
"Mr. Bean still waits in a field"
];

Output

-------- Query --------

[
{
celsius: 37.5f,
id: temperature:17493,
time: d'2025-02-17T06:21:08.911Z'
}
]

-------- Query --------

[
{
events: [
'Galactic senate convenes',
'Mr. Bean still waits in a field'
],
id: year:`29878977097987987979232`
}
]

Record IDs can be constructed out of arrays and even objects. This sort of record ID is most used when you have a field or two that will be used to look up records inside a record range, which is extremely performant. This is in contrast to using a WHERE clause to filter, which involves a table scan.

Records in SurrealDB can store arrays of values, including other nested arrays or objects within them. Different types of values can be stored within the same array, unless defined otherwise.

CREATE weather:['London', d'2025-02-13T05:00:00Z'] SET
temperature = 5.7,
conditions = "cloudy";

The main reason why record ranges are so performant is simply because the database knows ahead of time in which area to look for records in a query, and therefore has a smaller "surface area" to work in.

This can be demonstrated by seeing what happens when a single record range query encompasses all of the records in a database. The example below creates 10,000 player records that have an array-based record ID that begins with 'mage', allowing them to be used in a record range query, as well as a field called class that is also 'mage', which will be used in a WHERE clause to compare performance.

Interestingly, in this case a record range query is only somewhat more performant. This is because both queries end up iterating over 10,000 records, with the only difference being that the query with a WHERE clause also checks to see if the value of the class field is equal to 'mage'.

FOR $_ IN 0..10000 {
CREATE player:['mage', rand::id()] SET class = 'mage';
};

LET $_ = SELECT * FROM player:['mage', NONE]..['mage', ..];
LET $_ = SELECT * FROM player WHERE class = 'mage';

If the number of player records is extended to a larger number of classes, however, the difference in performance will be much larger. In this case the record range query is still only iterating a relatively small surface area of 10,000 records, while the second one has ten times this number to go through in addition to the WHERE clause on top.

FOR $_ IN 0..10000 {
CREATE player:['mage', rand::id()] SET class = 'mage';
CREATE player:['barbarian', rand::id()] SET class = 'barbarian';
CREATE player:['rogue', rand::id()] SET class = 'rogue';
CREATE player:['bard', rand::id()] SET class = 'bard';
CREATE player:['sage', rand::id()] SET class = 'sage';
CREATE player:['psionic', rand::id()] SET class = 'psionic';
CREATE player:['thief', rand::id()] SET class = 'thief';
CREATE player:['paladin', rand::id()] SET class = 'paladin';
CREATE player:['ranger', rand::id()] SET class = 'ranger';
CREATE player:['cleric', rand::id()] SET class = 'cleric';
};

LET $_ = SELECT * FROM player:['mage', NONE]..['mage', ..];
LET $_ = SELECT * FROM player WHERE class = 'mage';

The type name of a record ID is record, which by default allows any sort of record. This type can be set inside a DEFINE FIELD statement.

DEFINE FIELD possessions ON TABLE person TYPE option<array<record>>;
DEFINE FIELD friends ON TABLE person TYPE option<array<record<person>>>;

Was this page helpful?