The UPSERT statement can be used to insert records into the database, or to update them if they exist.
Note
In versions of SurrealDB between 2.0.0 and 2.0.4, an UPSERT statement was treated as an "UPDATE, otherwise INSERT" statement. It has since been changed to a statement which defaults to insertion, and updates otherwise. Please see the examples below for details.
When using the WHERE clause and no specified ID, SurrealDB will check to see if any records match the clause. If nothing matches, a new record will be created.
As such, the following UPSERT statement will return a new record:
Now that no records have the name 'Jaime', the same query as above will now create a new record because no records match the WHERE clause. The database will now have two person records.
UPSERT behaviour with a specific ID and a WHERE clause differs slightly from the examples above. In this case, there is the possibility that a record ID already exists but the WHERE clause does not match. As such, there is no way to create a new record as the statement only pertains to an ID for an already existing record.
The following query will return a record, because the person:test record does not yet exist. The WHERE clause makes no difference as there is no record to apply it to.
The following query will update the person:test record, because the record exists and the WHERE clause matches. The person:test record will now have the name 'Tobie'.
However, this third query will return nothing. The WHERE clause does not match and thus person:test cannot be updated, and the statement itself only pertains to the person:test record, so a new record using a random ID will not be returned.
Improved performance via UPSERT and a unique index
Unique indexes can be used to ensure that no field or combination of fields is ever present more than once. For example, a game might have a rule that duplicate names can exist, but not within the same class.
-------- Query -------- "Database index `unique_key` already contains ['Billy', 'wizard'], with record `user:billy`"
To change an existing record's metadata field to the value { likes: ["strawberries", "fields"] }, an UPDATE with a WHERE can be used. This performs a scan on the user table to check for all records that match the WHERE clause.
UPDATEuserSET metadata={likes: ["strawberries", "fields"]} WHERE name="Billy"AND class="wizard";
However, a much more efficient method is available if you only need to update one record and have a unique index that can be used instead. This optimization is available when using UPSERT and a unique index, because the statement will always access the index in any case to first see if the record is a duplicate.
-- Checks the index for ["Mandy", "wizard"], no existing -- record found so no problem UPSERTuserSETname="Mandy", class="wizard";
-- Fails because statement tries to upsert a new user:mandy -- on top of the previous randomly generated one UPSERTuser:mandySET name="Mandy", class="wizard", metadata={likes: ["strawberries"]};
"Database index `unique_key` already contains ['Mandy', 'wizard'], with record `user:kdvh401gofckvvy6nbiw`"
Since an UPSERT statement already checks unique indexes by default, it uses this to recognize that the user with name = "Billy" and the = "Wizard" corresponds to the record user:j2ecdb2tf4ou29mr0yp5 and update it without needing to scan the entire user table.
To compare the performance difference between using a WHERE clause and a unique index yourself, here is an example that creates a crowded field of 50000 user records, followed by one more user named "Billy". An UPDATE using WHERE name = "Billy" AND class = "wizard" requires a full table scan, while an UPSERT using the two fields used to build the index is much faster.
DEFINEINDEXunique_keyONTABLEuserFIELDSname, classUNIQUE; DEFINEFIELDofficial_nameONTABLEuserVALUEname+" the "+class;
-- Add 50000 users to fill up the database FOR$iIN<array>0..50000{CREATEuserSETname=<string>$i, class=<string>$i; };
-- Create Billy, one of 50,001 records CREATEuserSETname="Billy", class="wizard";
-- Updating Billy requires a table scan UPDATEuserSET interests+="music" WHERE name="Billy"AND class="wizard";
-- But UPSERT uses 'name' and 'class' to check the index anyway, -- and thus can use it to access the record without a scan UPSERTuserSET name="Billy", class="wizard", interests+="travel";
Using the ONLY clause
The ONLY clause can be used to return a single record instead of an array of records.
-- UPSERT 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. UPSERTONLYperson:tobieSET name='Tobie', company='SurrealDB', skills=['Rust', 'Go', 'JavaScript'];
Type inference when using UPSERT
The += operator in the following query is enough for SurrealDB to infer that the interests field must be an array<string>.
-- UPSERT a document and remove a tag from an array UPSERTperson:tobieSETinterests+='Java';
Type inference will also work with a numeric value such as the click_count field below, in which case it will infer the field to be of type int with a default value of 0.
-- UPSERT a document and increment a numeric value UPSERTwebpage:homeSETclick_count+=1;
Creating a record by default makes the UPSERT statement an ideal way to manage an incrementing field.
Doing the same with an UPDATE statement would require much more manual work.
IF (SELECT * FROMevent_for:[time::now().format("%Y-%m-%d")]).is_empty() { CREATEevent_for:[time::now().format("%Y-%m-%d")]SETnumber=1; }ELSE{ UPDATEevent_for:[time::now().format("%Y-%m-%d")]SETnumber+=1; };
CONTENT clause
Instead of specifying record data using the SET clause, it is also possible to use the CONTENT keyword to specify the record data using a SurrealQL object.
-- UPSERT all records with the same content UPSERTpersonCONTENT{ name: 'Tobie', company: 'SurrealDB', skills: ['Rust', 'Go', 'JavaScript'], };
-- UPSERT a specific record with some content UPSERTperson:tobieCONTENT{ name: 'Tobie', company: 'SurrealDB', skills: ['Rust', 'Go', 'JavaScript'], };
Since version 2.1.0, a statement with a CONTENT clause will bypass READONLY fields instead of generating an error.
DEFINEFIELDcreatedONpersonTYPEdatetimeDEFAULTd'2024-01-01T00:00:00Z'READONLY; UPSERTperson:gladysSETage=90; -- Does not try to modify `created` field, no error UPSERTperson:gladysCONTENT{age: 70};
Originally an alias for CONTENT, the REPLACE clause maintains the previous behaviour regarding READONLY fields. If the content following REPLACE does not match a record's READONLY fields, an error will be generated.
DEFINEFIELDcreatedONpersonTYPEdatetimeDEFAULTd'2024-01-01T00:00:00Z'READONLY; UPSERTperson:gladysSETage=90; -- Attempts to change `created` field, error UPSERTperson:gladysREPLACE{age: 70}; -- `created` equals current value, query works UPSERTperson:gladysREPLACE{age: 70, created: d'2024-01-01T00:00:00Z'};
Instead of specifying the full record data using the SET clause or the CONTENT keyword, it is also possible to merge-UPSERT only specific fields by using the MERGE keyword and specifying only the fields which are to be upserted.
-- Inserts a new record with a single field and random ID UPSERTpersonMERGE{ settings: { marketing: true, }, };
-- Updates certain fields on a specific record UPSERTperson:tobieMERGE{ settings: { marketing: true, }, };
PATCH clause
You can also specify changes to be applied to your query response, using the PATCH clause which works similar to the JSON Patch specification
By default, the UPSERT statement returns the record value once the changes have been made. To change the return value of each record, specify a RETURN clause, specifying either NONE, BEFORE, AFTER, DIFF, or a comma-separated list of specific fields to return.
-- Don't return any result UPSERTperson:tobieSETinterests+='reading'RETURNNONE;
-- Return the changeset diff UPSERTperson:tobieSETinterests+='reading'RETURNDIFF;
-- Return the record before changes were applied UPSERTperson:tobieSETinterests+='reading'RETURNBEFORE;
-- Return the record after changes were applied (the default) UPSERTperson:tobieSETinterests+='reading'RETURNAFTER;
-- Return a specific field only from the upserted records UPSERTperson:tobieSETinterests=['skiing', 'music']RETURNname, interests;
When processing a large result set with many interconnected records, it is possible to use the TIMEOUT keywords to specify a timeout duration for the statement. If the statement continues beyond this duration, then the transaction will fail, no records will be upserted in the database, and the statement will return an error.
The UPSERT statement returns an explanation, essentially revealing the execution plan to provide transparency and understanding of the query performance.
The records are not updated.
EXPLAIN can be followed by FULL to see the number of executed rows.