There are a lot of places to learn how to use SurrealDB, including the documentation, our fundamentals course, the Tour of SurrealDB, and even a full book that teaches SurrealDB through a story set centuries in the future!
But sometimes a quick rundown of a few tips and tricks hits the spot, and that’s what this blogpost is about. In this post, let’s look at some of the ways to put together a schema that works for you and not the other way around.
Did you know that you can…
set
 for an array of distinct itemsThe lowly set
 is a subtype of array
 that doesn’t get a great deal of attention, but has the convenience of holding no duplicate items.
As the query above shows, a set
 is just a subtype and not its own type so you can pass in an array
 into anything expecting a set
. The only difference is that it will never hold a duplicate item.
In addition to a type, both arrays and sets can have a maximum number of items built into the type definition itself. The definition below pairs this with an assertion using the array::all()
 function to also  ensure that every item in the bytes
 field is between 0 and 255.
DEFINE FIELD bytes ON data TYPE array<int, 640> ASSERT $value.all(|$int| $int IN 0..=255);
Even the individual indexes of an array can be defined. This is useful for data types like RGB colours that can must be exactly three items in length. This time the schema uses an ASSERT value.len() = 3
 instead of array<3>
 to ensure that the array is an exact length instead of a maximum length.
FLEXIBLE
 objects and defined fields in SCHEMAFULL
 tablesThe documentation mentions that a SCHEMAFULL
 table requires objects to have the FLEXIBLE
 keyword in order to treat them as free-form objects. Without this keyword, a TYPE object
 only tells the database that an object is to be expected but a SCHEMAFULL
 table disallows any field that hasn’t been defined yet.
With the FLEXIBLE
 keyword the metadata
 field will now work, accepting any and all input.
However, you can also simply define each field of an object in the same way you would with the field of a table. This allows the metadata
 field to hold these fields and ignore all other data used during a CREATE
 or INSERT
 statement.
THROW
 to add more detailed error messages to ASSERT
 clausesA DEFINE FIELD
 statement allows an ASSERT
 clause to be added in order to ensure that the value, which here is represented as the parameter $value
, meets certain expectations. A simple example here makes sure that the name
 field on the person
 table is under 20 characters in length.
In this case, the default error message is pretty good.
"Found 'Mr. Longname who has much too long a name' for field `name`, with record `person:2gpvut914k1qfysqs3lc`, but field must conform to: $value.len() < 20"
However, ASSERT
 only expects a truthy value at the end and otherwise isn’t concerned at all with what happens before. This means that you can outright customize the logic, including a custom error message. Let’s give this a try.
Not bad!
'An error occurred: `Mr. Longname who has much too long a name` too long, must be under 20 characters. Up to `Mr. Longname who ha` is acceptable'
A lot of legacy systems require datetimes to be displayed in a format that doesn’t quite match a datetime
.
That doesn’t mean that you have to give up the precision of a datetime
 though. By using the time::format()
 function, you can keep the actual stored date as a precise SurrealQL datetime
 and then use that to output a string in any format you like.
!!$value
 in DEFINE
 statementsAs the !
 operator reverses the truthiness of a value, using it twice in a row as !!
 returns a value’s truthiness. As empty and default values (such as 0 for numbers) are considered to be non-truthy, this operator is handy if you want to ensure that a value is both present and not empty.
DEFINE PARAM
 for clarityIf you find that parts of your table- or field-specific code are getting a bit long, it might be time to think about moving parts of it to a database-wide parameter.
DEFINE FIELD month_published ON book TYPE string ASSERT $value IN ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
Doing so not only makes the code cleaner, but makes it easy to reuse in other parts of the schema as well.
Error types in programming languages often take the form of a long list of possible things that could go wrong. SurrealQL’s literal type allows you to specify a list of all possible forms it could take, making it the perfect type for error logic.
While graph queries are usually seen in SELECT
 statements in the documentation, they can live inside your database schema just like any other datatype or expression. In the schema below for a family tree, any inserted record must either have a parent (via the <-parent_of<-person
 path) or be first_generation
.
By the way, this pattern is possible because RELATE
 statements can be used before the records to relate exist. To disallow this, you can add the ENFORCED
 clause to a DEFINE TABLE table_name TYPE RECORD
 definition.
Got your own tip or have any other schema tips you’d like to see? Drop by anywhere in the SurrealDB community to share your thoughts!
featured
Jun 13, 2025 6 min read
featured
Jun 17, 2025 3 min read
 
 Explore our releases, news, events, and much more