The SELECT statement can be used for selecting and querying data in a database. Each SELECT statement supports selecting from multiple targets, which can include tables, records, edges, subqueries, parameters, arrays, objects, and other values.
In the Learn more section, you can find a video that explains how to use the SELECT statement to retrieve and query data from SurrealDB.
-- Select all fields from a table SELECT * FROMperson;
-- Select specific fields from a table SELECTname, address, emailFROMperson;
-- Select all fields from a specific record SELECT * FROMperson:tobie;
-- Select specific fields from a specific record SELECTname, address, emailFROMperson:tobie;
-- Select just a single record -- Using the ONLY keyword, just an object -- for the record in question will be returned. -- This, instead of an array with a single object. SELECT * FROMONLYperson:tobie;
An alias can be used to rename fields or change the structure of an object.
SELECT * FROMperson;
-- Field `address` now shows up as "string::uppercase" -- name.first structure now flattened into a simple field SELECT name.firstASuser_name, string::uppercase(address) FROMperson;
-- "Morgan Hitchcock" added to `name` field structure, -- `angry_address` for field name instead of automatically -- generated "string::uppercase(address) + '!!!'" SELECT name.first, "Morgan Hitchcock"ASname.last, string::uppercase(address) +"!!!"ASangry_address FROMperson;
SurrealDB can also return specific fields as an array of values instead of the default array of objects. This only works if you select a single un-nested field from a table or a record.
-- Select the values of a single field from a table SELECTVALUEnameFROMperson;
-- Select the values of a single field from a specific record SELECTVALUEnameFROMperson:00e1nc508h9f7v63x72O;
Advanced expressions
SELECT queries support advanced expression in the field projections.
-- Select all nested array values -- note the .* syntax works to select everything from an array or object-like values SELECTaddress.*.coordinatesAScoordinatesFROMperson; -- Equivalent to SELECTaddress.coordinatesAScoordinatesFROMperson;
-- Select one item from an array SELECTaddress.coordinates[0]ASlatitudeFROMperson;
-- Select unique values from an array SELECTarray::distinct(tags) FROMarticle;
-- Select unique values from a nested array across an entire table SELECTarray::group(tags) AStagsFROMarticleGROUPALL;
-- Use mathematical calculations in a select expression SELECT (( celsius*1.8 ) +32) ASfahrenheit FROMtemperature;
-- Return boolean expressions with an alias SELECTrating>=4aspositiveFROMreview;
-- Select a person who has reacted to a post using a celebration -- Path can be conceptualized as: -- person->(reacted_to WHERE type='celebrate')->post SELECT * FROMpersonWHERE->(reacted_toWHEREtype='celebrate')->post;
-- Select a remote field from connected out graph edges SELECT->likes->friend.nameASfriendsFROMperson:tobie;
-- Use the result of a subquery as a returned field SELECT *, (SELECT * FROMeventsWHEREtype='activity'LIMIT5) AShistoryFROMuser;
-- Restructure objects in a select expression after `.` operator SELECTaddress.{city, country}FROMperson;
Using parameters
Parameters can be used like variables to store a value which can then be used in a subsequent query.
-- Store the subquery result in a variable and query that result. LET$avg_price = ( SELECTmath::mean(price) ASavg_priceFROMproductGROUPALL ).avg_price;
-- Find the name of the product where the price is higher than the avg price SELECTnameFROMproduct WHERE[price]>$avg_price;
-- Use the parent instance's field in a subquery (predefined variable) SELECT *, (SELECT * FROMeventsWHEREhost==$parent.id) AShosted_eventsFROMuser;
Numeric ranges in a WHERE clause
A numeric range inside a WHERE clause can improve performance if the range is able to replace multiple checks on a certain condition. The following code should show a modest but measurable improvement in performance between the first and second SELECT statement, as only one condition needs to be checked instead of two.
-- Assign output to a parameter so the SELECT output is not displayed LET$_ = SELECT * FROMpersonWHEREage>18ANDage<65; LET$_ = SELECT * FROMpersonWHEREagein18..=65;
A numeric range inside a WHERE also tends to produce shorter code that is easier to read and maintain.
SurrealDB supports the ability to query a range of records, using the record ID. The record ID ranges, retrieve records using the natural sorting order of the record IDs. These range queries can be used to query a range of records in a timeseries context. You can see more here about array-based Record IDs.
-- Select all person records with IDs between the given range SELECT * FROMperson:1..1000; -- Select all records for a particular location, inclusive SELECT * FROMtemperature:['London', NONE]..=['London', time::now()]; -- Select all temperature records with IDs less than a maximum value SELECT * FROMtemperature:..['London', '2022-08-29T08:09:31']; -- Select all temperature records with IDs greater than a minimum value SELECT * FROMtemperature:['London', '2022-08-29T08:03:39']..; -- Select all temperature records with IDs between the specified range SELECT * FROMtemperature:['London', '2022-08-29T08:03:39']..['London', '2022-08-29T08:09:31'];
Using a record range is more performant than the WHERE clause, as it does not require a table scan.
-- Create 5000 `person` records CREATE |person:1..5000| RETURNNONE;
-- Set the starting time LET$now = time::now(); -- Put the output somewhere so it won't clutter the screen LET$_ = SELECT * FROMperson:1..5000; -- Get the elapsed time LET$time1 = time::now() -$now;
Sometimes, especially with tables containing numerous columns, it is desirable to select all columns except a few specific ones. The OMIT clause can be used in this case.
SELECT * FROMperson; -- Omit the password field and security field in the options object SELECT * OMITpassword, opts.securityFROMperson;
-- Using destructuring syntax SELECT * OMITpassword, opts.{security, enabled}FROMperson;
More on using the FROM clause
The FROM clause can be used on targets beyond just a single table or record name.
-- Selects all records from both 'user' and 'admin' tables. SELECT * FROMuser, admin;
-- Selects all records from the table named in the variable '$table', -- but only if the 'admin' field of those records is true. -- Equivalent to 'SELECT * FROM user WHERE admin = true'. LET$table = "user"; SELECT * FROMtype::table($table) WHEREadmin=true;
-- Selects a single record from: -- * the table named in the variable '$table', -- * and the identifier named in the variable '$id'. -- This query is equivalent to 'SELECT * FROM user:admin'. LET$table = "user"; LET$id = "admin"; SELECT * FROMtype::record($table, $id);
-- Selects all records for specific users 'tobie' and 'jaime', -- as well as all records for the company 'surrealdb'. SELECT * FROMuser:tobie, user:jaime, company:surrealdb;
-- Selects records from a list of identifiers. The identifiers can be numerical, -- string, or specific records such as 'person:lrym5gur8hzws72ux5fa'. SELECT * FROM[3648937, "test", person:lrym5gur8hzws72ux5fa, person:4luro9170uwcv1xrfvby];
-- Selects data from an object that includes a 'person' key, -- which is associated with a specific person record, and an 'embedded' key set to true. SELECT * FROM{person: person:lrym5gur8hzws72ux5fa, embedded: true};
-- This command first performs a subquery, which selects all 'user' records and adds a -- computed 'adult' field that is true if the user's 'age' is 18 or older. -- The main query then selects all records from this subquery where 'adult' is true. SELECT * FROM (SELECTage>=18ASadultFROMuser) WHEREadult=true;
Filter queries using the WHERE clause
As with traditional SQL queries, a SurrealDB SELECT query supports conditional filtering using a WHERE clause. If the expression in the WHERE clause is truthy (is present and not an empty value), then the respective record will be returned.
-- Select names for 'person' records as long as 'name' is present -- and not an empty string "" SELECTnameFROMpersonWHEREname;
The SPLIT clause
As SurrealDB supports arrays and nested fields within arrays, it is possible to use the SPLIT clause to split the result on a specific field name, returning each value in an array as a separate value, along with the record content itself. This is useful in data analysis contexts.
-- Split the results by each value in a nested array SELECT * FROMcountrySPLITlocations.cities;
-- Filter the result of a subquery SELECT * FROM (SELECT * FROMpersonSPLITloggedin) WHEREloggedin>'2023-05-01';
The GROUP BY and GROUP ALL clause
SurrealDB supports data aggregation and grouping, with support for multiple fields, nested fields, and aggregate functions. In SurrealDB, every field which appears in the field projections of the select statement (and which is not an aggregate function), must also be present in the GROUP BY clause.
-- Group records by a single field SELECTcountryFROMuserGROUPBYcountry;
-- Group results by a nested field SELECTsettings.publishedFROMarticleGROUPBYsettings.published;
-- Group results by multiple fields SELECTgender, country, cityFROMpersonGROUPBYgender, country, city;
-- Use an aggregate function to select unique values from a nested array across an entire table SELECTarray::group(tags) AStagsFROMarticleGROUPALL;
A longer example of grouping using aggregate functions:
The GROUP and SPLIT clauses are incompatible with each other due to opposing behaviour: while SPLIT is a post-processing clause that multiplies the output of a query, GROUP works in the other way by collapsing the output.
Versions before 3.0.0-beta allowed these two clauses to be used together, after which attempting to do so results in a parsing error.
SELECT * FROMpersonSPLITnameGROUPBYname;
Output
'Parse error: SPLIT and GROUP are mutually exclusive --> [6:22] | 6 | SELECT * FROM person SPLIT name GROUP BY name; | ^^^^^^^^^^ SPLIT cannot be used with GROUP --> [6:33] | 6 | SELECT * FROM person SPLIT name GROUP BY name; | ^^^^^^^^^^^^^ GROUP cannot be used with SPLIT '
Disallowing the two clauses together forces a query that uses both to have one inside a subquery, which makes it clear which operation is to be performed first.
Using a COUNT index to speed up count() in GROUP ALL queries
To speed up the count() function along with GROUP ALL to get the total number of records in a table, a COUNT index can be used. This keeps track of the total number of records as a single value as opposed to a dynamic iteration of the table to get the full count every time a query is run.
To sort records, SurrealDB allows ordering on multiple fields and nested fields. Use the ORDER BY clause to specify a comma-separated list of field names that should be used to order the resulting records. The ASC and DESC keywords can be used to specify whether results should be sorted in an ascending or descending manner. The COLLATE keyword can be used to use Unicode collation when ordering text in string values, ensuring that different cases, and different languages are sorted in a consistent manner. Finally, the NUMERIC can be used to correctly sort text which contains numeric values.
-- Order records randomly SELECT * FROMuserORDERBYrand();
-- Order records descending by a single field SELECT * FROMsongORDERBYratingDESC;
-- Order records by multiple fields independently SELECT * FROMsongORDERBYartistASC, ratingDESC;
-- Order text fields with Unicode collation SELECT * FROMarticleORDERBYtitleCOLLATEASC;
-- Order text fields with which include numeric values SELECT * FROMarticleORDERBYtitleNUMERICASC;
The LIMIT clause
To limit the number of records returned, use the LIMIT clause.
-- Select only the top 50 records from the person table SELECT * FROMpersonLIMIT50;
When using the LIMIT clause, it is possible to paginate results by using the START clause to start from a specific record from the result set. It is important to note that the START count starts from 0.
-- Start at record 50 and select the following 50 records SELECT * FROMuserLIMIT50START50;
The LIMIT clause followed by 1 is often used along with the ONLY clause to satisfy the requirement that only up to a single record can be returned.
-- Record IDs are unique so guaranteed to be no more than 1 SELECT * FROMONLYperson:jamie;
-- Error because no guarantee that this will return a single record SELECT * FROMONLYpersonWHEREname="Jaime";
-- Add `LIMIT 1` to ensure that only up to one record will be returned SELECT * FROMONLYpersonWHEREname="Jaime"LIMIT1;
-- Select the first 5 records from the array SELECT * FROM[1,2,3,4,5,6,7,8,9,10]LIMIT5START4;
Instead of pulling data from multiple tables and merging that data together, SurrealDB allows you to traverse related records efficiently without needing to use JOINs.
To fetch and replace records with the remote record data, use the FETCH clause to specify the fields and nested fields which should be fetched in-place, and returned in the final statement response output.
-- Select all the review information -- and the artist's email from the artist table SELECT *, artist.emailFROMreviewFETCHartist;
-- Select all the article information -- only if the author's age (from the author table) is under 30. SELECT * FROMarticleWHEREauthor.age<30FETCHauthor;
The TIMEOUT clause
When processing a large result set with many interconnected records, it is possible to use the TIMEOUT keyword to specify a timeout duration for the statement. If the statement continues beyond this duration, then the transaction will fail, and the statement will return an error.
-- Cancel this conditional filtering based on graph edge properties -- if it's not finished within 5 seconds SELECT * FROMpersonWHERE->knows->person->(knowsWHEREinfluencer=true) TIMEOUT5s;
The TEMPFILES clause
When processing a large result set with many records, it is possible to use the TEMPFILES clause to specify that the statement should be processed in temporary files rather than memory.
This significantly reduces memory usage in exchange for slower performance.
-- Select every person and order them by name using temporary files rather than memory. SELECT * FROMpersonORDERBYnameTEMPFILES;
This requires the temporary directory to be set in the server configuration or when using the surreal start command.
The EXPLAIN clause
When EXPLAIN is used, the SELECT statement returns an explanation, essentially revealing the execution plan to provide transparency and understanding of the query performance. EXPLAIN can be followed by FULL to see the number of executed rows.
Here is the result when the field 'email' is not indexed. We can see that the execution plan will iterate over the whole table.
The query planner can replace the standard table iterator with one or several index iterators based on the structure and requirements of the query. However, there may be situations where manual control over these potential optimizations is desired or required.
For instance, the cardinality of an index can be high, potentially even equal to the number of records in the table. The sum of the records iterated by several indexes may end up being larger than the number of records obtained by iterating over the table. In such cases, if there are different index possibilities, the most probable optimal choice would be to use the index known with the lowest cardinality.
WITH INDEX @indexes ... restricts the query planner to using only the specified index(es)
WITH NOINDEX forces the query planner to use the table iterator.
-- forces the query planner to use the specified index(es): SELECT * FROMperson WITHINDEXft_email WHERE email='tobie@surrealdb.com'AND company='SurrealDB';
-- forces the usage of the table iterator SELECTnameFROMpersonWITHNOINDEXWHEREjob='engineer'ANDgender='m';
The ONLY clause
If you are selecting just one single resource, it's possible to use the ONLY clause to filter that result from an array.
SELECT * FROMONLYperson:john;
If you are selecting from a resource where it is possible that multiple resources are returned, it is required to LIMIT the result to just one.
This is needed, because the query would otherwise not be deterministic.
When you are starting a new database with memory or SurrealKV as the storage engine with versioning enabled, you can specify a version for each record. This is useful for time-travel queries. You can query a specific version of a record by using the VERSION clause. The VERSION clause is always followed by a datetime and when the specified timestamp does not exist, an empty array is returned.
Note
The VERSION clause is currently in alpha and is subject to change. We do not recommend this for production.
-- Create a new record CREATEuser:johnSETname='John' VERSION d'2025-08-19T08:00:00Z'; [[{id: user:john, name: 'John'}]]
-- Select the record as it is now SELECT * FROMuser:john; [[{id: user:john, name: 'John'}]]
-- Select the record as it was at a specific point in time SELECT * FROMuser:johnVERSIONd'2025-08-19T08:00:00Z'; [[{id: user:john, name: 'John'}]]
-- Select the record as it was at a specific point in time that doesn't exist SELECT * FROMuser:johnVERSIONd'2025-08-19T07:00:00Z'; [[]]
-- Update the record to the user john updateuser:johnSethight="55" [[{hight: '55', id: user:john, name: 'John'}]]
-- Confirm that the record is updated SELECT * FROMuser:john; [[{hight: '55', id: user:john, name: 'John'}]]
-- Select the record for the timestamp before the update SELECT * FROMuser:johnVERSIONd'2025-08-19T08:00:00Z'; [[{id: user:john, name: 'John'}]]
The VERSION clause can also take a dynamic value or parameter that resolves to a datetime.