Skip to content
NEW

Introducing SurrealDB Studio, the new official app of SurrealDB

Learn more

1/5

Course content preview

12: People and indexes

The project planning sample assigns work to employees, and it leans on indexes to keep lookups fast and data honest. This chapter adds people to the schema, then looks at REFERENCE and DEFINE INDEX, including UNIQUE constraints.

Note

SurrealKit-wise, nothing new lands here. You already know sync for a disposable local database and rollout plan → start → complete for a shared one (parts 1011). Put the definitions below into database/schema/, then apply them the same way you applied milestone. The new material is the SurrealQL: employees, REFERENCE, and indexes.

Add a table for the people doing the work. Create database/schema/employee.surql:

DEFINE TABLE employee SCHEMAFULL;
DEFINE FIELD name ON employee TYPE string;
DEFINE FIELD email ON employee TYPE option<string> ASSERT $value IS NONE OR string::is_email($value);


An activity will not always have someone assigned yet, so the field on activity needs to allow for that. Add this to database/schema/activity.surql:

DEFINE FIELD assigned_to ON activity TYPE option<record<employee>>;


option means that "nobody assigned yet" is a valid state, not a missing field. If a later requirement says every activity must have an owner, you would tighten this the same way you tightened fields earlier in the course: backfill the data, then ALTER (part 3).

Create an employee and assign them to an existing activity so you have something to query:

CREATE employee:ada SET name = "Ada", email = "ada@example.com";
UPDATE activity:kickoff SET assigned_to = employee:ada;


As things stand, nothing stops you deleting employee:ada even though an activity points at her. If you would rather SurrealDB refused that delete, add a REFERENCE clause. Replace the assigned_to line you just wrote rather than adding a second one, for the reason given in part 8: one DEFINE FIELD per table and field name, or the metadata key collides.

DEFINE FIELD assigned_to ON activity TYPE option<record<employee>>
    REFERENCE ON DELETE REJECT;


With employee:ada still assigned to activity:kickoff, the delete is now refused:

DELETE employee:ada;


Output
'Cannot delete `employee:ada` as it is referenced by `activity:kickoff` with an ON DELETE REJECT clause'


ClauseEffect
REFERENCE ON DELETE REJECTCannot delete employee while an activity points at them
ON DELETE CASCADEDeleting the parent removes or clears children (use carefully)
ON DELETE IGNOREParent delete allowed; links may dangle unless you clean up


REFERENCE is catalog metadata that SurrealDB enforces on delete. That is different from a bare record<> field (part 4), which just stores a pointer and does nothing special when the record it points to disappears.

DEFINE INDEX project_name ON project FIELDS name;


This supports WHERE name = … on a long list of projects. It does not need to be UNIQUE unless project names must be distinct.

DEFINE INDEX milestone_name_per_project ON milestone FIELDS project, name UNIQUE;


UNIQUE indexes stop duplicates at write time, which is exactly what you want for (project, name). Before adding one on a table that already has data, check for duplicates first (part 3).

We can do this by filtering the grouped result with an outer SELECT:

SELECT * FROM (
    SELECT project, name, count() AS total FROM milestone GROUP BY project, name
) WHERE total > 1;


On a small table you can also just read the counts and skip the outer filter:

SELECT project, name, count() AS total FROM milestone GROUP BY project, name;


If either returns records, backfill or rename the duplicates in a data script before you add the index, whether that is a local sync or a rollout start on a shared database.

Scheduling queries usually filter on start or end:

DEFINE INDEX activity_start ON activity FIELDS start;


Composite indexes are worth adding once your actual query patterns justify them. See DEFINE INDEX for the full syntax.

MechanismEnforcesAlso helps
ASSERT on a fieldRules for a single recordn/a
UNIQUE indexNo duplicate combinations across recordsFast lookups


Uniqueness of (project, name) is a cross-row rule, so it belongs on an index, not a handwritten ASSERT over a subquery.

Adding an index is non-destructive, so it is safe on rollout start (or a local sync). Dropping one is destructive, so save that for rollout complete, once nothing depends on it any more.

surrealkit sync --user root --pass secret --ns main --db main   # local
# or
surrealkit rollout plan --name add_planning_indexes


  • REFERENCE ON DELETE REJECT (or whichever delete policy fits)

  • DEFINE INDEX for lookups, UNIQUE where duplicates would be a bug

Previous

11: Milestones

Next lesson

13: Events and CI

SurrealDB

The context layer for AI agents.

Documents, graphs, vectors, time-series, and memory.
One transaction, one query, one deployment.

Explore with AI

Stay in the loop

Tutorials, AI agent recipes, and product updates, every two weeks.

Independently verified

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

Trust Centre

Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201

Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom

Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom