Telerik blogs

Kick the tires on SurrealDB. It’s like SQL, noSQL and a graph database all in one, with a solid cloud offering and a chance to leave joins in the dust.

SurrealDB is impressive and versatile. We should never have to deal with joins, as that is not how we actually read data. Getting rid of a schema makes developing very quick. Surreal Cloud will be an excellent tool for creating your database in 2025 and beyond.

Surreal Cloud Setup

First, log in to Surreal Cloud and create a Database Instance.

Surreal Cloud Database Instance details

Select a region.

Surreal Cloud Database region

And you can have exactly one free instance.

Surreal Cloud Database free instance

Once your database instance is created and provisioned, you need to connect to it.

Connect Surreal Cloud Database

We will connect to the database with Surrealist.

Create new namespace

We need a namespace. Namespaces are beautiful because they allow you to use the same database for multiple apps.

create new database

And we need a database to store our data. Before we can create any queries, we need both the namespace and database configured.

New query

Queries

SurrealDB stores every record as an ID. Every record has an id field, which is how it is referenced. 📝 An ID is created automatically with the table name and the : prefix.

Insert a Record

We can create a new record, or we can insert a record like in traditional SQL.

CREATE profiles SET
    name = 'Jon',
    created_at = time::now();

INSERT INTO profiles {
    name: 'Bill',
    created_at: time::now()
};

// OR

INSERT INTO profiles (name, created_at)
		VALUES ('Tim', time::now());

These statements have the same result.

You could also create a profile with a username for an ID.

create profiles:jon set created_at = time::now();

📝 Like SQL, query commands are not case-sensitive.

Update a Record

UPDATE profiles:6bdt2zwtujsb1csf2i59
		SET name = 'Bill';
    
// OR
UPDATE profiles
		SET name = 'Phil'
		WHERE id = profiles:vp40hcw66sx217qhzc5l;

You can update directly by the ID.

Delete a Record

DELETE profiles:vp40hcw66sx217qhzc5l;

You delete by the ID.

REMOVE TABLE profiles;

You can also remove the entire table.

Select a Record

SELECT * FROM profiles.

The SurrealQL is very similar to SQL, minus joins. Joins are attained through other means.

Our First Note App

Let’s see how to build a note app.

Create a User

CREATE users:jon SET
    name = 'Jon Bon Jovi',
    created_at = time::now();

We will use the username as the ID.

📝 Notice time::now() handles the current date object.

Insert Notes

INSERT INTO notes [
    {
        title: "Grocery List",
        content: "Buy milk, bread, and eggs.",
        created_by: users:jon,
        tags: [
            (CREATE tags SET name = "personal"),
            (CREATE tags SET name = "shopping")
        ]
    },
    {
        title: "Work Meeting",
        content: "Prepare slides for Monday's meeting.",
        created_by: users:jon,
        tags: [
            (CREATE tags SET name = "work"),
            (CREATE tags SET name = "urgent")
        ]
    }
];

📌 Notice how we can use nested statements to create nested tags.

📝 We could save tags in an array, but they would not be accessible on the top level. When we put nested data inside an array directly, the data is stored like a noSQL database. If we set tags as a query, which we can, it will only query the tags at the creation time. We want this dynamic.

SELECT * FROM tags;
SELECT * FROM notes;
SELECT * FROM users;

It is essential to be able to query all tags easily.

Query All User’s Notes

Without joins, this was not as intuitive. There are helper variables like $parent to help.

SELECT *, (SELECT * FROM notes WHERE created_by = $parent.id) AS notes FROM users;

I HATE joins, but nested queries like this can be ugly. Luckily, there is a new feature coming in a future version called References.

  • Results

    -------- Query 1 (330.889µs) --------
    
    [
    	{
    		created_at: d'2025-01-26T01:51:19.546Z',
    		id: users:jon,
    		name: 'Jon Bon Jovi',
    		notes: [
    			{
    				content: "Prepare slides for Monday's meeting.",
    				created_by: users:jon,
    				id: notes:jaljrarnbop94hs6lq3b,
    				tags: [
    					[
    						{
    							id: tags:zwtvzp2absdzfk6px3tr,
    							name: 'work'
    						}
    					],
    					[
    						{
    							id: tags:d47oioglv7s9548l9b7y,
    							name: 'urgent'
    						}
    					]
    				],
    				title: 'Work Meeting'
    			},
    			{
    				content: 'Buy milk, bread, and eggs.',
    				created_by: users:jon,
    				id: notes:wlu8y3kamqyzxnz6ah4r,
    				tags: [
    					[
    						{
    							id: tags:snrpfzvabw131btgay5m,
    							name: 'personal'
    						}
    					],
    					[
    						{
    							id: tags:0tk6mr1eg2v1g17y4nji,
    							name: 'shopping'
    						}
    					]
    				],
    				title: 'Grocery List'
    			}
    		]
    	}
    ]
    

Insert Differently

CREATE users:jon SET
    name = 'Jon Bon Jovi',
    created_at = time::now(),
    notes = [
        (INSERT INTO notes {
            title: "Grocery List",
            content: "Buy milk, bread, and eggs.",
            created_by: users:jon,
            tags: [
                (CREATE tags SET name = "personal"),
                (CREATE tags SET name = "shopping")
            ]
        }),
        (INSERT INTO notes {                    
            title: "Work Meeting",
            content: "Prepare slides for Monday's meeting.",
            created_by: users:jon,
            tags: [
                (CREATE tags SET name = "work"),
                (CREATE tags SET name = "urgent")
            ]
        })
    ];

I could have inserted the data initially like this, but then I would be manually creating the nodes in each direction with created_by and notes[]. I want this to be dynamic.

Our Second Note App

SurrealDB is also a graph database. Let’s consider it graphically and see if dynamic queries are better.

CREATE users:jon SET
    name = 'Jon Bon Jovi',
    created_at = time::now();

LET $notes = INSERT INTO notes [
    {
        title: "Grocery List",
        content: "Buy milk, bread, and eggs."
    },
    {
        title: "Work Meeting",
        content: "Prepare slides for Monday's meeting."
    }
];

FOR $note IN $notes {
    RELATE users:jon->created->$note;
};

Let and Loops

You can loop through an array of items with FOR and IN. In our case, we return the INSERT statement and set it as a variable with LET. The variable now contains the created id for each note.

Relate

RELATE @from_record_id->@table->@to_record_id

We create a relation in which each user created a note.

RELATE users:jon->created->$note;

📝 Standard practice is to use the past tense.

SELECT *, ->created->notes.* AS notes FROM users;
SELECT *, <-created<-users.* AS created_by FROM notes;

Now, we can dynamically get users with notes OR notes by users.

📌 Both directions work by only declaring one direction. We simply change between -> and <-.

Delete a Relationship

If we initially wanted to define from the other direction, we would need to remove the relationships.

// all created relationships
DELETE users:jon->created;

// individually
DELETE users:jon->created->notes:2xeslis2isljelbsiehq

Reverse Relationship

Then, we would redeclare our original insert with the reverse edge.

...
FOR $note IN $notes {
		RELATE $note->created_by->users:jon;
};

And our selects would be reversed as well.

SELECT *, <-created_by<-notes.* AS notes FROM users;
SELECT *, ->created_by->users.* AS created_by FROM notes;

⚠️ Pick one direction or the other when you initially define RELATE. SurrealDB will allow you to traverse in reverse. Creating two RELATE statements for the same relationship would be redundant, and you would be wasting database space.

Array to Single

SELECT *, (->created_by->users)[0].* AS created_by FROM notes;

Sometimes, we may return an array when we only have one record. We can select only the first result and change the shape of our result to be an object instead, just like an array in JS.

  • Results

    -------- Query 1 (385.723µs) --------
    
    [
    	{
    		created_at: d'2025-01-26T15:04:14.661Z',
    		id: users:jon,
    		name: 'Jon Bon Jovi',
    		notes: [
    			{
    				content: 'Buy milk, bread, and eggs.',
    				id: notes:56l2cw48oodjvnmy6b30,
    				title: 'Grocery List'
    			},
    			{
    				content: "Prepare slides for Monday's meeting.",
    				id: notes:ntuhp5danslcihrgotm0,
    				title: 'Work Meeting'
    			}
    		]
    	}
    ]
    
    -------- Query 2 (349.703µs) --------
    
    [
    	{
    		content: 'Buy milk, bread, and eggs.',
    		created_by: {
    			created_at: d'2025-01-26T15:04:14.661Z',
    			id: users:jon,
    			name: 'Jon Bon Jovi'
    		},
    		id: notes:56l2cw48oodjvnmy6b30,
    		title: 'Grocery List'
    	},
    	{
    		content: "Prepare slides for Monday's meeting.",
    		created_by: {
    			created_at: d'2025-01-26T15:04:14.661Z',
    			id: users:jon,
    			name: 'Jon Bon Jovi'
    		},
    		id: notes:ntuhp5danslcihrgotm0,
    		title: 'Work Meeting'
    	}
    ]
    

Tags

// users with notes
SELECT *, (SELECT *, ->has_tag->tags.* AS tags FROM <-created_by<-notes) AS notes FROM users;
// OR
SELECT *, <-created_by<-notes.{title, content, tags: ->has_tag->tags.*} AS notes FROM users;

// notes with users
SELECT *, (->created_by->users)[0].* as created_by, ->has_tag->tags.* as tags from notes;

And we need to get our tags as well from both directions.

  • Results

    -------- Query 1 (680.018µs) --------
    
    [
    	{
    		created_at: d'2025-01-26T16:38:49.420Z',
    		id: users:jon,
    		name: 'Jon Bon Jovi',
    		notes: [
    			{
    				content: 'Buy milk, bread, and eggs.',
    				id: notes:vi8zarmcqvqva2y0vfkv,
    				tags: [
    					{
    						id: tags:personal
    					},
    					{
    						id: tags:shopping
    					}
    				],
    				title: 'Grocery List'
    			},
    			{
    				content: "Prepare slides for Monday's meeting.",
    				id: notes:la6nzl7xc25r8lk8vv8n,
    				tags: [
    					{
    						id: tags:work
    					},
    					{
    						id: tags:urgent
    					}
    				],
    				title: 'Work Meeting'
    			}
    		]
    	}
    ]
    
    -------- Query 2 (505.966µs) --------
    
    [
    	{
    		content: "Prepare slides for Monday's meeting.",
    		created_by: {
    			created_at: d'2025-01-26T16:38:49.420Z',
    			id: users:jon,
    			name: 'Jon Bon Jovi'
    		},
    		id: notes:la6nzl7xc25r8lk8vv8n,
    		tags: [
    			{
    				id: tags:work
    			},
    			{
    				id: tags:urgent
    			}
    		],
    		title: 'Work Meeting'
    	},
    	{
    		content: 'Buy milk, bread, and eggs.',
    		created_by: {
    			created_at: d'2025-01-26T16:38:49.420Z',
    			id: users:jon,
    			name: 'Jon Bon Jovi'
    		},
    		id: notes:vi8zarmcqvqva2y0vfkv,
    		tags: [
    			{
    				id: tags:personal
    			},
    			{
    				id: tags:shopping
    			}
    		],
    		title: 'Grocery List'
    	}
    ]
    

Counting

count(SELECT 1 FROM notes);

// OR WITH GROUP BY
SELECT count() from notes GROUP BY count LIMIT 1;

// RETURNING OBJECT
(SELECT count() from notes GROUP BY count LIMIT 1)[0];

// RETURNS
{
	count: 2
}

Counting is pretty simple. Just wrap your query in a count() function. If we are not actually selecting anything, we want to use 1 instead of the fields we want to return.

Schema

Schema

We can see our schema from the tables we have created. SurrealDB is a multi-type database. It is noSQL, SQL and a graph database all in one. A schema is not necessary but helpful as our applications evolve.

Constraint

A schema is just a constraint on our data. We can easily add this to SurrealDB as well. When we create a table, we can use the SCHEMAFULL keyword and define our fields individually.

DEFINE TABLE profiles SCHEMAFULL;

DEFINE FIELD name ON profiles TYPE string;
DEFINE FIELD created_by ON profiles TYPE string;

TypeScript

// Create a record with a specific ID
const person = await db.create<Person>(new RecordId('person', 'tobie'), {
	name: 'Tobie',
	settings: {
		active: true,
		marketing: true,
	},
});

SurrealDB has a beautiful TypeScript package. While the types are not generated automatically like other ORMs or databases, this is to be expected since we are dealing with multiple database types, schemaless and using a schema.

Final Thoughts

I really enjoyed working with SurrealQL. The database is a teenager, but not a baby anymore. There were a few features I asked for that were recently released in the beta version and not yet available on the cloud. The learning curve was slightly higher than I hoped because the docs lacked enough straightforward and simple examples. However, when I asked questions on Discord, they were usually answered within an hour. The community and staff are very helpful.

SurrealDB is powerful and can handle all your query needs.

I highly recommend it.


About the Author

Jonathan Gamble

Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.

 

 

Related Posts

Comments

Comments are disabled in preview mode.