Performance Best Practices
This guide outlines some key performance best practices for using SurrealDB. While SurrealDB offers powerful and flexible features to help you meet your desired performance standards, your use of those features will ultimately determine whether or not you meet them.
To achieve the best performance from SurrealDB, there are a number of configuration options and runtime design choices to be considered which can improve and affect the performance of the database.
The following is a non-exhaustive list of performance best practices you should consider when building services and applications with SurrealDB to help you address common performance challenges while preventing frequent pitfalls.
SurrealDB architecture
While SurrealDB is a multi-model database, at its core, SurrealDB stores data in documents on transactional key-value stores.
This means that SurrealDB is a general-purpose databases optimised for a combination of various workloads such as operational, AI and real-time workloads.
While SurrealDB can perform well with real-time and advanced analytical workloads, its architecture is not a columnar one. As such, it is not optimised for large ad-hoc analytical queries in the same way as specialised columnar data warehouses.
SurrealDB is built using a layered approach, with compute separated from the storage. This allows you, if necessary, to scale up the compute and storage layers independently from each other.
Read more about the architecture of SurrealDB and the supported storage engines.
Running SurrealDB
Using SurrealDB Cloud
The easiest way to deploy and scale up SurrealDB is by using SurrealDB Cloud, which allows you to focus on building great products while we take care of running and maintaining it in the most performant and scalable way.
Read more about running SurrealDB using SurrealDB Cloud .
Running SurrealDB as a server
When starting the SurrealDB server, it is important to run the server using the correct configuration options and
settings. For production environments or for performance benchmarking, the --log command-line argument or the
SURREAL_LOG environment variable should be set to info (the default option when not specified), warn, or error.
Other log verbosity levels (such as debug, trace, or full) are only recommended for use in debugging, testing, or development scenarios. This is because verbosity of the log level impacts the performance by increasing the amount of information being logged for each single operation.
The same applies for hte SurrealDB Docker container, in which the --log argument should be omitted or specifically set to a log verbosity level that does not impact performance.
Read more about running SurrealDB as a single-node server or multi-node cluster.
TODO: update this later when there is a good page to point to re: benchmarking one vs. the other
Additionally, ensure that the rocksdb storage engine is used to store data.
Running SurrealDB embedded in Rust
It is common knowledge among Rust developers that the --release flag is used to ensure that a build is optimized for performance as opposed to compilation speed. However, the SurrealDB source code also contains a few additional flags when the --release flag is passed in as seen below. As this configuration will not be present by default inside the Cargo.toml for your own project when adding the surrealdb dependency, be sure to add it if performance is crucial.
In addition, using the correct memory allocator can greatly improve the performance of the database core engine when running SurrealDB as an embedded database within Rust. Using an optimised asynchronous runtime configuration can also help speed up concurrent queries and increase database throughput.
In your project's Cargo.toml file, ensure that the allocator feature is enabled on the surrealdb dependency:
When running SurrealDB within your Rust code, ensure that the asynchronous runtime is configured correctly, making use of multiple threads, an increased stack size, and an optimised number of threads:
Read more about running SurrealDB embedded in Rust.
Running SurrealDB embedded in Tauri
When running SurrealDB as an embedded database within Rust, default options of Tauri can make SurrealDB run slower, as it processes and outputs the database information logs. Configuring Tauri correctly, can result in much improved performance with the core database engine and any queries which are run on SurrealDB.
When building a desktop application with Tauri, ensure that the Tauri plugin log is disabled by configuring the
tauri.conf.json file:
Alternatively you can disable logs at compile time when building your Tauri app:
Performing queries
Selecting single records
Certain queries in SurrealDB can be more efficiently written in certain ways which ensure that full table scans or indexes are not necessary when executing the query.
In traditional SQL, the following query can be used to query for a particular row from a table:
In SurrealDB 3.0 and above, the query planner is able to identify this as a record ID scan if the id field after the WHERE clause is a record ID.
However, using a WHERE clause will always perform a table scan in versions of SurrealDB before 3.0. If this is the case, just remove the WHERE clause and select directly from the record ID itself.
Selecting multiple records
In traditional SQL, the following queries can be used to query for getting particular rows from a table:
However, currently in SurrealDB this query will perform a scan to find the record, although this is not necessary and you don't need to index the id field when using SurrealDB. Instead the following query can be used to select the specific record without needing to perform any scan:
Simplifying logic in WHERE clauses
If a WHERE clause cannot be avoided, performance can still be improved by optimizing the portion after the WHERE clause. As a boolean check is the simplest possible operation, having a boolean field that can be used in a WHERE clause can significantly improve performance.
Using indexes
SurrealDB has native built-in support for a number of different index types, without leveraging external libraries or implementations.
With native support for indexes in the core database engine, SurrealDB leverages indexes where possible within the SurrealQL query language, without pushing queries down to a separate indexing engine.
In addition, data is indexed in the same way for embedded systems, single-node database servers, and multi-node highly-available clusters, ensuring that the same indexing functionality is available regardless of the SurrealDB deployment model.
Indexing support in SurrealDB is in active development, with work focusing on increased support for additional operators, compound indexes, additional index types, and overall improved index performance.
Note
In the meantime, you can improve the performance of UPDATE and DELETE statements by combining these with a SELECT statement.
To improve the performance of an UPDATE statement, use a SELECT statement within a subquery, selecting only the id
field. This will use any defined indexes:
To improve the performance of an DELETE statement, use a SELECT statement within a subquery, selecting only the id
field. This will use any defined indexes:
Index strategies explained
When using SELECT, SurrealDB uses a query planner whose role is to identify if it can use the index to speed the
execution of the query.
Without indexes, SurrealDB will operate a SELECT query on a table by using the table iterator. It mainly scans every
record of a given table. If there is a condition (WHERE ...), an ordering (ORDER BY ...), or an aggregation (GROUP
BY ...), it will load the value in memory and execute the operation. This process is commonly called a "table full
scan".
Under certain conditions, if an index exists, and the condition or ordering involves exclusively fields that are indexed, the query planner will suggest an execution plan that involves one or multiple indexes to achieve these potential optimisations:
Only collect records that match the condition(s), as opposed to performing a table full scan.
As the index already stores the records in order, the scanning collects the records pre-ordered, sparing an additional ordering phase.
If there are several clauses separated with OR operators, the query planner may do several index-based iterations:
Use UPSERT to take advantage of unique indexes
UPSERT statements have a unique performance advantage when paired with a unique index.
A unique index on its own is used to prevent more than one record from containing the same data, such as a name or email address.
An UPSERT statement works like a CREATE statement in this case as well, except that if the value for email is already present, it will modify the existing record instead of creating a new one. An UPSERT will only fail in this case if a user attempts to upsert to a certain record ID (like user:bob instead of just the user table) when another record holds this value.
The key point here is that in either case, UPSERT is using the index to find the record instead of a table scan.
As such, when updating a single record on a table that contains a unique index, UPSERT is much more performant than UPDATE.
Index lookup on remote fields
SurrealDB document record IDs store both the table name and the record identifier. This design provides a straightforward and consistent way to reference records across the database. One particularly powerful feature is the ability to filter a table based on conditions that relate to a referenced table.
Here is a concrete example, where the statement SELECT * FROM access WHERE user.role = 'admin' will retrieve records
from the access table for which the referenced record in the user table has the name field set to 'admin'.
Consider the following example:
The query retrieves records from the access table whose associated record in the user table has the role field set
to 'admin'.
To optimize this query, you can create indexes on both the user.role field and the access.user field.
With these indexes, the query planner can leverage an index-based join strategy: