AQL Data Queries
With AQL queries, you can read and write data in the form of documents
There are two fundamental types of AQL queries:
- queries which access data (read documents)
- queries which modify data (create, update, replace, delete documents)
Data Access Queries
Retrieving data from the database with AQL does always include a RETURN operation. It can be used to return a static value, such as a string:
RETURN "Hello ArangoDB!"
The query result is always an array of elements, even if a single element was
returned and contains a single element in that case: ["Hello ArangoDB!"]
The function DOCUMENT()
can be called to retrieve a single document via
its document identifier, for instance:
RETURN DOCUMENT("users/phil")
RETURN
is usually accompanied by a FOR loop to iterate over the
documents of a collection. The following query executes the loop body for all
documents of a collection called users
. Each document is returned unchanged
in this example:
FOR doc IN users
RETURN doc
Instead of returning the raw doc
, one can easily create a projection:
FOR doc IN users
RETURN { user: doc, newAttribute: true }
For every user document, an object with two attributes is returned. The value
of the attribute user
is set to the content of the user document, and
newAttribute
is a static attribute with the boolean value true
.
Operations like FILTER, SORT and LIMIT can be added to the loop body
to narrow and order the result. Instead of above shown call to DOCUMENT()
,
one can also retrieve the document that describes user phil
like so:
FOR doc IN users
FILTER doc._key == "phil"
RETURN doc
The document key is used in this example, but any other attribute could equally
be used for filtering. Since the document key is guaranteed to be unique, no
more than a single document can match this filter. For other attributes this
may not be the case. To return a subset of active users (determined by an
attribute called status
), sorted by name in ascending order, you can do:
FOR doc IN users
FILTER doc.status == "active"
SORT doc.name
LIMIT 10
Note that operations do not have to occur in a fixed order and that their order
can influence the result significantly. Limiting the number of documents
before a filter is usually not what you want, because it easily misses a lot
of documents that would fulfill the filter criterion, but are ignored because
of a premature LIMIT
clause. Because of the aforementioned reasons, LIMIT
is usually put at the very end, after FILTER
, SORT
and other operations.
See the High Level Operations chapter for more details.
Data Modification Queries
AQL supports the following data modification operations:
- INSERT: insert new documents into a collection
- UPDATE: partially update existing documents in a collection
- REPLACE: completely replace existing documents in a collection
- REMOVE: remove existing documents from a collection
- UPSERT: conditionally insert or update documents in a collection
You can use them to modify the data of one or multiple documents with a single query. This is superior to fetching and updating the documents individually with multiple queries. However, if only a single document needs to be modified, ArangoDB’s specialized data modification operations for single documents might execute faster.
Below you find some simple example queries that use these operations. The operations are detailed in the chapter High Level Operations.
Modifying a single document
Let’s start with the basics: INSERT
, UPDATE
and REMOVE
operations on single documents.
Here is an example that inserts a document into a collection called users
with
the INSERT
operation:
INSERT {
firstName: "Anna",
name: "Pavlova",
profession: "artist"
} INTO users
The collection needs to exist before executing the query. AQL queries cannot create collections.
If you run the above query, the result is an empty array because we did
not specify what to return using a RETURN
keyword. It is optional in
modification queries, but mandatory in data access queries. Despite the empty
result, the above query still creates a new user document.
You may provide a key for the new document; if not provided, ArangoDB creates one for you.
INSERT {
_key: "GilbertoGil",
firstName: "Gilberto",
name: "Gil",
city: "Fortalezza"
} INTO users
As ArangoDB is schema-free, attributes of the documents may vary:
INSERT {
_key: "PhilCarpenter",
firstName: "Phil",
name: "Carpenter",
middleName: "G.",
status: "inactive"
} INTO users
INSERT {
_key: "NatachaDeclerck",
firstName: "Natacha",
name: "Declerck",
location: "Antwerp"
} INTO users
The UPDATE
operation lets you add or change
attributes of existing documents. The following query modifies a previously
created user, changing the status
attribute and adding a location
attribute:
UPDATE "PhilCarpenter" WITH {
status: "active",
location: "Beijing"
} IN users
The REPLACE
operation is an alternative to the
UPDATE
operation that lets you replace all attributes of a document
(except for attributes that cannot be changed, like _key
):
REPLACE {
_key: "NatachaDeclerck",
firstName: "Natacha",
name: "Leclerc",
status: "active",
level: "premium"
} IN users
You can delete a document with the REMOVE
operation,
only requiring the document key to identify it:
REMOVE "GilbertoGil" IN users
Modifying multiple documents
Data modification operations are normally combined with FOR
loops to
iterate over a given list of documents. They can optionally be combined with
FILTER
statements and the like.
To create multiple new documents, use the INSERT
operation together with FOR
.
You can also use INSERT
to generate copies of existing documents from other
collections, or to create synthetic documents (e.g. for testing purposes).
The following query creates 1000 test users with some attributes and stores
them in the users
collection:
FOR i IN 1..1000
INSERT {
id: 100000 + i,
age: 18 + FLOOR(RAND() * 25),
name: CONCAT('test', TO_STRING(i)),
status: i % 2 == 0 ? "active" : "not active",
active: false,
gender: i % 3 == 0 ? "male" : i % 3 == 1 ? "female" : "diverse"
} IN users
Let’s modify existing documents that match some condition:
FOR u IN users
FILTER u.status == "not active"
UPDATE u WITH { status: "inactive" } IN users
You can also update existing attributes based on their previous value:
FOR u IN users
FILTER u.active == true
UPDATE u WITH { numberOfLogins: u.numberOfLogins + 1 } IN users
The above query only works if there is already a numberOfLogins
attribute
present in the document. If it is unclear whether there is a numberOfLogins
attribute in the document, the increase must be made conditional:
FOR u IN users
FILTER u.active == true
UPDATE u WITH {
numberOfLogins: HAS(u, "numberOfLogins") ? u.numberOfLogins + 1 : 1
} IN users
Updates of multiple attributes can be combined in a single query:
FOR u IN users
FILTER u.active == true
UPDATE u WITH {
lastLogin: DATE_NOW(),
numberOfLogins: HAS(u, "numberOfLogins") ? u.numberOfLogins + 1 : 1
} IN users
Note than an update query might fail during execution, for example, because a document to be updated does not exist. In this case, the query aborts at the first error. In single server mode, all modifications done by the query are rolled back as if they never happened.
You can copy documents from one collection to another by reading from one
collection but write to another.
Let’s copy the contents of the users
collection into the backup
collection:
FOR u IN users
INSERT u IN backup
Note that both collections must already exist when the query is executed.
The query might fail if the backup
collection already contains documents,
as executing the insert might attempt to insert the same document (identified
by the _key
attribute) again. This triggers a unique key constraint violation
and aborts the query. In single server mode, all changes made by the query
are also rolled back.
To make such a copy operation work in all cases, the target collection can
be emptied beforehand, using a REMOVE
query or by truncating it by other means.
To not just partially update, but completely replace existing documents, use
the REPLACE
operation.
The following query replaces all documents in the backup
collection with
the documents found in the users
collection. Documents common to both
collections are replaced. All other documents remain unchanged.
Documents are compared using their _key
attributes:
FOR u IN users
REPLACE u IN backup
The above query fails if there are documents in the users
collection that are
not in the backup
collection yet. In this case, the query would attempt to replace
documents that do not exist. If such case is detected while executing the query,
the query is aborted. In single server mode, all changes made by the query are
rolled back.
To make the query succeed regardless of the errors, use the ignoreErrors
query option:
FOR u IN users
REPLACE u IN backup OPTIONS { ignoreErrors: true }
This continues the query execution if errors occur during a REPLACE
, UPDATE
,
INSERT
, or REMOVE
operation.
Finally, let’s find some documents in collection users
and remove them
from collection backup
. The link between the documents in both collections is
established via the documents’ keys:
FOR u IN users
FILTER u.status == "deleted"
REMOVE u IN backup
The following example removes all documents from both users
and backup
:
LET r1 = (FOR u IN users REMOVE u IN users)
LET r2 = (FOR u IN backup REMOVE u IN backup)
RETURN true
Altering substructures
To modify lists in documents, for example, to update specific attributes of objects in an array, you can compute a new array and then update the document attribute in question. This may involve the use of subqueries and temporary variables.
Create a collection named complexCollection
and run the following query:
FOR doc IN [
{
"topLevelAttribute": "a",
"subList": [
{
"attributeToAlter": "value to change",
"filterByMe": true
},
{
"attributeToAlter": "another value to change",
"filterByMe": true
},
{
"attributeToAlter": "keep this value",
"filterByMe": false
}
]
},
{
"topLevelAttribute": "b",
"subList": [
{
"attributeToAlter": "keep this value",
"filterByMe": false
}
]
}
] INSERT doc INTO complexCollection
The following query updates the subList
top-level attribute of documents.
The attributeToAlter
values in the nested object are changed if the adjacent
filterByMe
attribute is true
:
FOR doc in complexCollection
LET alteredList = (
FOR element IN doc.subList
RETURN element.filterByMe
? MERGE(element, { attributeToAlter: "new value" })
: element
)
UPDATE doc WITH { subList: alteredList } IN complexCollection
RETURN NEW
[
{
"_key": "2607",
"_id": "complexCollection/2607",
"_rev": "_fWb_iOO---",
"topLevelAttribute": "a",
"subList": [
{
"attributeToAlter": "new value",
"filterByMe": true
},
{
"attributeToAlter": "new value",
"filterByMe": true
},
{
"attributeToAlter": "keep this value",
"filterByMe": false
}
]
},
{
"_key": "2608",
"_id": "complexCollection/2608",
"_rev": "_fWb_iOO--_",
"topLevelAttribute": "b",
"subList": [
{
"attributeToAlter": "keep this value",
"filterByMe": false
}
]
}
]
To improve the query’s performance, you can only update documents if there is
a change to the subList
to be saved. Instead of comparing the current and the
altered list directly, you may compare their hash values using the
HASH()
function, which is faster for
larger objects and arrays. You can also replace the subquery with an
inline expression:
FOR doc in complexCollection
LET alteredList = doc.subList[*
RETURN CURRENT.filterByMe
? MERGE(CURRENT, { attributeToAlter: "new value" })
: CURRENT
]
FILTER HASH(doc.subList) != HASH(alteredList)
UPDATE doc WITH { subList: alteredList } IN complexCollection
RETURN NEW
Returning documents
Data modification queries can optionally return documents. In order to reference
the inserted, removed or modified documents in a RETURN
statement, data modification
statements introduce the OLD
and/or NEW
pseudo-values:
FOR i IN 1..100
INSERT { value: i } IN test
RETURN NEW
FOR u IN users
FILTER u.status == "deleted"
REMOVE u IN users
RETURN OLD
FOR u IN users
FILTER u.status == "not active"
UPDATE u WITH { status: "inactive" } IN users
RETURN NEW
NEW
refers to the inserted or modified document revision, and OLD
refers
to the document revision before update or removal. INSERT
statements can
only refer to the NEW
pseudo-value, and REMOVE
operations only to OLD
.
UPDATE
, REPLACE
and UPSERT
can refer to either.
In all cases, the full documents are returned with all their attributes,
including the potentially auto-generated attributes, such as _id
, _key
, and _rev
,
and the attributes not specified in the update expression of a partial update.
Projections of OLD and NEW
It is possible to return a projection of the documents with OLD
or NEW
instead of
returning the entire documents. This can be used to reduce the amount of data returned
by queries.
For example, the following query returns only the keys of the inserted documents:
FOR i IN 1..100
INSERT { value: i } IN test
RETURN NEW._key
Using OLD and NEW in the same query
For UPDATE
, REPLACE
, and UPSERT
operations, both OLD
and NEW
can be used
to return the previous revision of a document together with the updated revision:
FOR u IN users
FILTER u.status == "not active"
UPDATE u WITH { status: "inactive" } IN users
RETURN { old: OLD, new: NEW }
Calculations with OLD or NEW
It is also possible to run additional calculations with LET
statements between the
data modification part and the final RETURN
of an AQL query. For example, the following
query performs an upsert operation and returns whether an existing document was
updated, or a new document was inserted. It does so by checking the OLD
variable
after the UPSERT
and using a LET
statement to store a temporary string for
the operation type:
UPSERT { name: "test" }
INSERT { name: "test" }
UPDATE { } IN users
LET opType = IS_NULL(OLD) ? "insert" : "update"
RETURN { _key: NEW._key, type: opType }
Restrictions
The name of the modified collection (users
and backup
in the above cases)
must be known to the AQL executor at query-compile time and cannot change at
runtime. Using a bind parameter to specify the
collection name is allowed.
It is not possible to use multiple data modification operations for the same collection in the same query, or follow up a data modification operation for a specific collection with a read operation for the same collection. Neither is it possible to follow up any data modification operation with a traversal query (which may read from arbitrary collections not necessarily known at the start of the traversal).
That means you may not place several REMOVE
or UPDATE
statements for the same
collection into the same query. It is however possible to modify different collections
by using multiple data modification operations for different collections in the
same query.
In case you have a query with several places that need to remove documents from the
same collection, it is recommended to collect these documents or their keys in an array
and have the documents from that array removed using a single REMOVE
operation.
Data modification operations can optionally be followed by LET
operations to
perform further calculations and a RETURN
operation to return data.
Transactional Execution
On a single server, data modification operations are executed transactionally. If a data modification operation fails, any changes made by it are rolled back automatically as if they never happened.
A query may execute intermediate transaction commits in case the running transaction (AQL query) hits the specified size thresholds. In this case, the query’s operations carried out so far are committed and not rolled back in case of a later abort/rollback. This behavior can be controlled by adjusting the intermediate commit settings for the RocksDB engine. See Known limitations for AQL queries.
In a cluster, AQL data modification queries are not executed transactionally.
Additionally, AQL queries with UPDATE
, REPLACE
, UPSERT
, or REMOVE
operations require the _key
attribute to be specified for all documents that
should be modified or removed, even if a shard key attribute other than _key
is chosen for the collection.