arangodb基础语法及使用教程

总有一些人,原本只是生命的过客,后来却成了记忆的常客

Posted by yishuifengxiao on 2021-12-17

ArangoDB 查询语言 (AQL) 可用于检索和修改存储在 ArangoDB 中的数据。

执行查询时的常规工作流如下:

  • 客户端应用程序将 AQL 查询发送到 ArangoDB 服务器。查询文本包含 ArangoDB 编译结果集所需的一切
  • ArangoDB将解析查询,执行它并编译结果。如果查询无效或无法执行,服务器将返回客户端可以处理和响应的错误。如果查询可以成功执行,服务器会将查询结果(如果有)返回给客户端

AQL 主要是一种声明性语言,这意味着查询表示应该实现什么结果,而不是应该如何实现。AQL旨在使人类可读,因此使用英语中的关键字。AQL 的另一个设计目标是客户端独立性,这意味着无论客户端使用哪种编程语言,所有客户端的语言和语法都是相同的。AQL 的进一步设计目标是支持复杂的查询模式和 ArangoDB 提供的不同数据模型。

就其目的而言,AQL 类似于结构化查询语言 (SQL)。AQL 支持读取和修改集合数据,但不支持数据定义操作,例如创建和删除数据库、集合和索引。它是一种纯数据操作语言 (DML),而不是数据定义语言 (DDL) 或数据控制语言 (DCL)。

AQL 查询的语法与 SQL 不同,即使某些关键字重叠也是如此。尽管如此,对于任何具有SQL背景的人来说,AQL都应该很容易理解。

本文章基于arangodb官方文档 3.10翻译引用而来

一 AQL 教程

这是对ArangoDB查询语言AQL的介绍,AQL是围绕小说和奇幻电视剧《权力的游戏》(截至第1季)中的一个小角色数据集构建的。它包括两种语言的性格特征,一些家庭关系,最后但并非最不重要的是一小部分拍摄地点,这使得数据组合变得有趣。

在开始之前,无需导入数据。它是本教程中 AQL 查询的一部分提供的。您可以使用其Web 界面与 ArangoDB 交互,以管理集合和执行查询。

1.1 数据

1.1.1 人物

该数据集包含 43 个字符,包括姓名、姓氏、年龄、存活状态和特征参考。 姓氏和年龄属性并不总是存在。 列特征(已解析)不是本教程中使用的实际数据的一部分,但为了您的方便而包含在内。

Characters table

1.1.2 特征

有18个独特的特征。每个特征都有一个随机字母作为文档键。特征标签有英语和德语版本。

Traits table

1.1.3 地点

这个由8个拍摄地点组成的小型集合具有两个属性,一个名称和一个坐标。坐标建模为数字数组,每个数组由纬度和经度值组成。

Locations table

1.2 基本CURD

1.2.1 创建文档

在我们可以使用 AQL 插入文档之前,我们需要一个地方来放置它们—一个集合。 可以通过 Web 界面、arangosh 或驱动程序管理集合,但是使用 AQL 无法做到这一点。

Add Collection

Create Characters collection

单击 Web 界面中的 COLLECTIONS,然后单击 Add Collection 并键入 Characters 作为名称。 用保存确认。 新集合应出现在列表中。

接下来,单击查询。 要使用 AQL 创建第一个用于收集的文档,请使用以下 AQL 查询,您可以将其粘贴到查询文本框中并通过单击执行来运行:

Insert query in query editor

1
2
3
4
5
6
7
INSERT {
"name": "Ned",
"surname": "Stark",
"alive": true,
"age": 41,
"traits": ["A","H","C","N","P"]
} INTO Characters

语法形式如下:

1
INSERT document INTO collectionName

文档是一个对象,就像您可能从 JavaScript 或 JSON 中知道的那样,它由属性键和值对组成。 属性键周围的引号在 AQL 中是可选的。 键总是字符序列(字符串),而属性值可以有不同的类型:

  • null
  • boolean (true, false)
  • number (integer and floating point)
  • string
  • array
  • object

我们插入的字符文档的名字和姓氏都是字符串值。 活动状态使用布尔值。 年龄是一个数值。 特征是一个字符串数组。 整个文档是一个对象。

让我们在单个查询中添加一堆其他字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
LET data = [
{ "name": "Robert", "surname": "Baratheon", "alive": false, "traits": ["A","H","C"] },
{ "name": "Jaime", "surname": "Lannister", "alive": true, "age": 36, "traits": ["A","F","B"] },
{ "name": "Catelyn", "surname": "Stark", "alive": false, "age": 40, "traits": ["D","H","C"] },
{ "name": "Cersei", "surname": "Lannister", "alive": true, "age": 36, "traits": ["H","E","F"] },
{ "name": "Daenerys", "surname": "Targaryen", "alive": true, "age": 16, "traits": ["D","H","C"] },
{ "name": "Jorah", "surname": "Mormont", "alive": false, "traits": ["A","B","C","F"] },
{ "name": "Petyr", "surname": "Baelish", "alive": false, "traits": ["E","G","F"] },
{ "name": "Viserys", "surname": "Targaryen", "alive": false, "traits": ["O","L","N"] },
{ "name": "Jon", "surname": "Snow", "alive": true, "age": 16, "traits": ["A","B","C","F"] },
{ "name": "Sansa", "surname": "Stark", "alive": true, "age": 13, "traits": ["D","I","J"] },
{ "name": "Arya", "surname": "Stark", "alive": true, "age": 11, "traits": ["C","K","L"] },
{ "name": "Robb", "surname": "Stark", "alive": false, "traits": ["A","B","C","K"] },
{ "name": "Theon", "surname": "Greyjoy", "alive": true, "age": 16, "traits": ["E","R","K"] },
{ "name": "Bran", "surname": "Stark", "alive": true, "age": 10, "traits": ["L","J"] },
{ "name": "Joffrey", "surname": "Baratheon", "alive": false, "age": 19, "traits": ["I","L","O"] },
{ "name": "Sandor", "surname": "Clegane", "alive": true, "traits": ["A","P","K","F"] },
{ "name": "Tyrion", "surname": "Lannister", "alive": true, "age": 32, "traits": ["F","K","M","N"] },
{ "name": "Khal", "surname": "Drogo", "alive": false, "traits": ["A","C","O","P"] },
{ "name": "Tywin", "surname": "Lannister", "alive": false, "traits": ["O","M","H","F"] },
{ "name": "Davos", "surname": "Seaworth", "alive": true, "age": 49, "traits": ["C","K","P","F"] },
{ "name": "Samwell", "surname": "Tarly", "alive": true, "age": 17, "traits": ["C","L","I"] },
{ "name": "Stannis", "surname": "Baratheon", "alive": false, "traits": ["H","O","P","M"] },
{ "name": "Melisandre", "alive": true, "traits": ["G","E","H"] },
{ "name": "Margaery", "surname": "Tyrell", "alive": false, "traits": ["M","D","B"] },
{ "name": "Jeor", "surname": "Mormont", "alive": false, "traits": ["C","H","M","P"] },
{ "name": "Bronn", "alive": true, "traits": ["K","E","C"] },
{ "name": "Varys", "alive": true, "traits": ["M","F","N","E"] },
{ "name": "Shae", "alive": false, "traits": ["M","D","G"] },
{ "name": "Talisa", "surname": "Maegyr", "alive": false, "traits": ["D","C","B"] },
{ "name": "Gendry", "alive": false, "traits": ["K","C","A"] },
{ "name": "Ygritte", "alive": false, "traits": ["A","P","K"] },
{ "name": "Tormund", "surname": "Giantsbane", "alive": true, "traits": ["C","P","A","I"] },
{ "name": "Gilly", "alive": true, "traits": ["L","J"] },
{ "name": "Brienne", "surname": "Tarth", "alive": true, "age": 32, "traits": ["P","C","A","K"] },
{ "name": "Ramsay", "surname": "Bolton", "alive": true, "traits": ["E","O","G","A"] },
{ "name": "Ellaria", "surname": "Sand", "alive": true, "traits": ["P","O","A","E"] },
{ "name": "Daario", "surname": "Naharis", "alive": true, "traits": ["K","P","A"] },
{ "name": "Missandei", "alive": true, "traits": ["D","L","C","M"] },
{ "name": "Tommen", "surname": "Baratheon", "alive": true, "traits": ["I","L","B"] },
{ "name": "Jaqen", "surname": "H'ghar", "alive": true, "traits": ["H","F","K"] },
{ "name": "Roose", "surname": "Bolton", "alive": true, "traits": ["H","E","F","A"] },
{ "name": "The High Sparrow", "alive": true, "traits": ["H","M","F","O"] }
]

FOR d IN data
INSERT d INTO Characters

LET 关键字定义了一个带有名称数据的变量和一个对象数组作为值,所以LET variableName = valueExpression并且表达式是一个文字数组定义,如 [ {…}, {…}, … ]。

FOR variableName IN表达式用于迭代数据数组的每个元素。 在每个循环中,一个元素被分配给变量 d。 然后在 INSERT 语句中使用此变量而不是文字对象定义。 它的作用基本上是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INSERT {
"name": "Robert",
"surname": "Baratheon",
"alive": false,
"traits": ["A","H","C"]
} INTO Characters

INSERT {
"name": "Jaime",
"surname": "Lannister",
"alive": true,
"age": 36,
"traits": ["A","F","B"]
} INTO Characters

...

AQL 不允许在单个查询中针对同一集合的多个 INSERT 操作。 然而,它被允许作为 FOR 循环的主体,插入多个文档,就像我们在上面的查询中所做的那样。

1.2.2 查询文档

现在 Characters 集合中有几个文档。 我们可以再次使用 FOR 循环检索它们。 然而,这一次,我们使用它来遍历集合中的所有文档而不是数组:

1
2
FOR c IN Characters
RETURN c

循环的语法是 FOR variableName IN collectionName。 对于集合中的每个文档,c 被分配一个文档,然后根据循环体返回该文档。 查询返回我们之前存储的所有字符。

其中应该是Ned Stark,类似这个例子:

1
2
3
4
5
6
7
8
9
10
{
"_key": "2861650",
"_id": "Characters/2861650",
"_rev": "_V1bzsXa---",
"name": "Ned",
"surname": "Stark",
"alive": true,
"age": 41,
"traits": ["A","H","C","N","P"]
},

该文档具有我们存储的四个属性,以及数据库系统添加的三个属性。 每个文档都需要一个唯一的_key,用于在集合中标识它。_id 是一个计算属性,是集合名称、正斜杠 / 和文档键的串联。 它唯一标识数据库中的文档。_rev是系统管理的修订 ID。

文档密钥可以由用户在创建文档时提供,或者自动分配唯一值。 以后无法更改。 以下划线 _ 开头的所有三个系统属性都是只读的。

在 AQL 函数 DOCUMENT() 的帮助下,我们可以使用文档键或文档 ID 来检索特定文档

语法形式如下:

1
2
3
RETURN DOCUMENT("Characters", "2861650")
// --- or ---
RETURN DOCUMENT("Characters/2861650")
1
2
3
4
5
6
7
8
9
10
11
12
[
{
"_key": "2861650",
"_id": "Characters/2861650",
"_rev": "_V1bzsXa---",
"name": "Ned",
"surname": "Stark",
"alive": true,
"age": 41,
"traits": ["A","H","C","N","P"]
}
]

文档密钥对您来说会有所不同。 相应地更改查询。 此处,“2861650”是 Ned Stark 文档的密钥,“2861653”是 Catelyn Stark 的密钥。

DOCUMENT() 函数还允许一次获取多个文档:

1
2
3
RETURN DOCUMENT("Characters", ["2861650", "2861653"])
// --- or ---
RETURN DOCUMENT(["Characters/2861650", "Characters/2861653"])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[
[
{
"_key": "2861650",
"_id": "Characters/2861650",
"_rev": "_V1bzsXa---",
"name": "Ned",
"surname": "Stark",
"alive": true,
"age": 41,
"traits": ["A","H","C","N","P"]
},
{
"_key": "2861653",
"_id": "Characters/2861653",
"_rev": "_V1bzsXa--B",
"name": "Catelyn",
"surname": "Stark",
"alive": false,
"age": 40,
"traits": ["D","H","C"]
}
]
]

有关更多详细信息,请参阅 DOCUMENT() 函数文档。

1.2.3 更新文档

根据我们的 Ned Stark 文件,他还活着。 当我们知道他死了,我们需要改变alive 属性。 让我们修改现有文件:

1
UPDATE "2861650" WITH { alive: false } IN Characters

语法形式如下:

1
UPDATE documentKey WITH object IN collectionName

它使用列出的属性更新指定的文档(如果它们不存在,则添加它们),但保持其余部分不变。 要替换整个文档内容,您可以使用 REPLACE 而不是 UPDATE:

1
2
3
4
5
6
7
REPLACE "2861650" WITH {
name: "Ned",
surname: "Stark",
alive: false,
age: 41,
traits: ["A","H","C","N","P"]
} IN Characters

这也适用于循环,为所有文档添加一个新属性,例如:

1
2
FOR c IN Characters
UPDATE c WITH { season: 1 } IN Characters

使用变量而不是文字文档键来更新每个文档。 该查询将属性季节添加到文档的顶级。 您可以通过重新运行返回集合中所有文档的查询来检查结果:

1
2
FOR c IN Characters
RETURN c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[
[
{
"_key": "2861650",
"_id": "Characters/2861650",
"_rev": "_V1bzsXa---",
"name": "Ned",
"surname": "Stark",
"alive": false,
"age": 41,
"traits": ["A","H","C","N","P"],
"season": 1
},
{
"_key": "2861653",
"_id": "Characters/2861653",
"_rev": "_V1bzsXa--B",
"name": "Catelyn",
"surname": "Stark",
"alive": false,
"age": 40,
"traits": ["D","H","C"],
"season": 1
},
{
...
}
]
]

1.2.4 删除文档

要从集合中完全删除文档,有 REMOVE 操作。 它的工作原理类似于其他修改操作,但没有 WITH 子句:

1
REMOVE "2861650" IN Characters

它也可以用在循环体中以有效地截断集合:

1
2
FOR c IN Characters
REMOVE c IN Characters

在继续下一章之前,对所有字符文档重新运行顶部的插入查询,以便再次使用数据。

1.2 匹配文档

到目前为止,我们要么查找单个文档,要么返回整个字符集合。 对于查找,我们使用了 DOCUMENT() 函数,这意味着我们只能通过键或 ID 查找文档。

为了找到满足某些比键相等更复杂的标准的文档,AQL 中有 FILTER 操作,它使我们能够为文档制定任意条件来匹配。

1.2.1 Equality condition

1
2
3
FOR c IN Characters
FILTER c.name == "Ned"
RETURN c

过滤条件读作:“字符文档的属性名称必须等于字符串 Ned”。 如果条件适用,则返回字符文档。 这同样适用于任何属性:

1
2
3
FOR c IN Characters
FILTER c.surname == "Stark"
RETURN c

1.2.2 Range conditions

严格相等是我们可以陈述的一种可能条件。 但是,我们可以制定许多其他条件。 例如,我们可以要求所有成人角色:

1
2
3
FOR c IN Characters
FILTER c.age >= 13
RETURN c.name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
"Joffrey",
"Tyrion",
"Samwell",
"Ned",
"Catelyn",
"Cersei",
"Jon",
"Sansa",
"Brienne",
"Theon",
"Davos",
"Jaime",
"Daenerys"
]

运算符>=代表大于或等于,因此返回 13 岁或以上的每个字符(在示例中仅返回他们的名字)。 我们可以通过将运算符更改为小于并使用对象语法定义要返回的属性子集来返回所有小于 13 的字符的名称和年龄:

1
2
3
FOR c IN Characters
FILTER c.age < 13
RETURN { name: c.name, age: c.age }
1
2
3
4
5
6
[
{ "name": "Tommen", "age": null },
{ "name": "Arya", "age": 11 },
{ "name": "Roose", "age": null },
...
]

您可能会注意到它返回 30 个字符的名称和年龄,大多数年龄为空。 这样做的原因是,如果查询请求属性,则 null 是回退值,但文档中不存在此类属性,并且将 null 与较低的数字进行比较(请参阅类型和值顺序)。 因此,它意外地满足了年龄标准 c.age < 13 (null < 13)。

1.2.3 Multiple conditions

为了不让没有年龄属性的文档通过过滤器,我们可以添加第二个条件:

1
2
3
4
FOR c IN Characters
FILTER c.age < 13
FILTER c.age != null
RETURN { name: c.name, age: c.age }
1
2
3
4
[
{ "name": "Arya", "age": 11 },
{ "name": "Bran", "age": 10 }
]

这同样可以用布尔 AND 运算符编写为:

1
2
3
FOR c IN Characters
FILTER c.age < 13 AND c.age != null
RETURN { name: c.name, age: c.age }

第二个条件也可以是 c.age > null。

1.2.4 Alternative conditions

如果您希望文档满足一个或另一个条件,也可能满足不同的属性,请使用 OR:

1
2
3
FOR c IN Characters
FILTER c.name == "Jon" OR c.name == "Joffrey"
RETURN { name: c.name, surname: c.surname }
1
2
3
4
[
{ "name": "Joffrey", "surname": "Baratheon" },
{ "name": "Jon", "surname": "Snow" }
]

查看有关过滤器操作的更多详细信息。

1.3 排序和限制

1.3.1 限制结果计数

可能并不总是需要返回所有文档,FOR 循环通常会返回这些文档。 在这些情况下,我们可以使用 LIMIT() 操作限制文档数量:

1
2
3
FOR c IN Characters
LIMIT 5
RETURN c.name
1
2
3
4
5
6
7
[
"Joffrey",
"Tommen",
"Tyrion",
"Roose",
"Tywin"
]

LIMIT 后跟一个表示最大文档数的数字。 然而,还有第二种语法,它允许您跳过一定数量的记录并返回接下来的 n 个文档:

1
2
3
FOR c IN Characters
LIMIT 2, 5
RETURN c.name
1
2
3
4
5
6
7
[
"Tyrion",
"Roose",
"Tywin",
"Samwell",
"Melisandre"
]

看看第二个查询如何跳过前两个名字并返回接下来的五个(两个结果都包含 Tyrion、Roose 和 Tywin)。

1.3.2 按照名字排序

直到这里显示的查询返回匹配记录的顺序基本上是随机的。 要以定义的顺序返回它们,我们可以添加 SORT() 操作。 如果与 LIMIT() 结合使用,它会对结果产生很大影响,因为如果您先排序,结果将变得可预测。

1
2
3
4
FOR c IN Characters
SORT c.name
LIMIT 10
RETURN c.name
1
2
3
4
5
6
7
8
9
10
11
12
[
"Arya",
"Bran",
"Brienne",
"Bronn",
"Catelyn",
"Cersei",
"Daario",
"Daenerys",
"Davos",
"Ellaria"
]

看看它如何按名称排序,然后返回按字母顺序排列的前十个名称。 我们可以像降序一样使用 DESC 反转排序顺序:

1
2
3
4
FOR c IN Characters
SORT c.name DESC
LIMIT 10
RETURN c.name
1
2
3
4
5
6
7
8
9
10
11
12
[
"Ygritte",
"Viserys",
"Varys",
"Tywin",
"Tyrion",
"Tormund",
"Tommen",
"Theon",
"The High Sparrow",
"Talisa"
]

第一种排序是升序,这是默认顺序。 因为它是默认的,所以不需要明确要求 ASC 顺序。

1.3.3 按多个属性排序

假设我们要按姓氏排序。 许多角色都有一个姓氏。 同姓字符之间的结果顺序未定义。 我们可以先按姓氏排序,再按名字排序:

1
2
3
4
5
6
7
8
FOR c IN Characters
FILTER c.surname
SORT c.surname, c.name
LIMIT 10
RETURN {
surname: c.surname,
name: c.name
}
1
2
3
4
5
6
7
8
9
10
11
12
[
{ "surname": "Baelish", "name": "Petyr" },
{ "surname": "Baratheon", "name": "Joffrey" },
{ "surname": "Baratheon", "name": "Robert" },
{ "surname": "Baratheon", "name": "Stannis" },
{ "surname": "Baratheon", "name": "Tommen" },
{ "surname": "Bolton", "name": "Ramsay" },
{ "surname": "Bolton", "name": "Roose" },
{ "surname": "Clegane", "name": "Sandor" },
{ "surname": "Drogo", "name": "Khal" },
{ "surname": "Giantsbane", "name": "Tormund" }
]

总体而言,文档按姓氏排序。 如果两个字符的姓氏相同,则比较姓名值并对结果进行排序。

请注意,在排序之前应用过滤器,只让文档通过,实际上具有姓氏值(许多没有它并且会导致结果中的空值)。

1.3.4 按照年纪排序

顺序也可以由数值确定,例如年龄:

1
2
3
4
5
6
7
8
FOR c IN Characters
FILTER c.age
SORT c.age
LIMIT 10
RETURN {
name: c.name,
age: c.age
}
1
2
3
4
5
6
7
8
9
10
11
12
[
{ "name": "Bran", "age": 10 },
{ "name": "Arya", "age": 11 },
{ "name": "Sansa", "age": 13 },
{ "name": "Jon", "age": 16 },
{ "name": "Theon", "age": 16 },
{ "name": "Daenerys", "age": 16 },
{ "name": "Samwell", "age": 17 },
{ "name": "Joffrey", "age": 19 },
{ "name": "Tyrion", "age": 32 },
{ "name": "Brienne", "age": 32 }
]

应用过滤器来避免没有年龄属性的文档。 其余文档按年龄升序排列,返回最年轻的十个字符的姓名和年龄。

有关更多详细信息,请参阅 SORT 操作LIMIT 操作文档。

1.3 Joining together

1.3.1 关联其他文档

我们导入的字符数据,每个字符都有一个属性traits,是一个字符串数组。 但是它不直接存储字符特征:

1
2
3
4
5
6
7
{
"name": "Ned",
"surname": "Stark",
"alive": false,
"age": 41,
"traits": ["A","H","C","N","P"]
}

它更像是一个没有明显含义的字母列表。 这里的想法是traits 应该存储另一个集合的文档键,我们可以使用它来将字母解析为诸如“strong”之类的标签。 为实际特征使用另一个集合的好处是,我们可以稍后轻松查询所有现有特征,并以多种语言存储标签,例如在一个中心位置。 如果我们直接嵌入特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"name": "Ned",
"surname": "Stark",
"alive": false,
"age": 41,
"traits": [
{
"de": "stark",
"en": "strong"
},
{
"de": "einflussreich",
"en": "powerful"
},
{
"de": "loyal",
"en": "loyal"
},
{
"de": "rational",
"en": "rational"
},
{
"de": "mutig",
"en": "brave"
}
]
}

保持特征变得非常困难。 如果您要重命名或翻译其中之一,则需要找到所有其他具有相同特征的字符文档并在那里执行更改。 如果我们只引用另一个集合中的特征,就像更新单个文档一样简单

Data model comparison

1.3.2 导入特征

您可以在下方找到特征数据。 按照创建文档中显示的模式导入它:

  • 创建文档集合 Traits
  • 将数据分配给 AQL 中的变量,LET data = [ … ]
  • 使用 FOR 循环遍历数据的每个数组元素
  • INSERT 元素 INTO Traits

Create Traits collection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{ "_key": "A", "en": "strong", "de": "stark" },
{ "_key": "B", "en": "polite", "de": "freundlich" },
{ "_key": "C", "en": "loyal", "de": "loyal" },
{ "_key": "D", "en": "beautiful", "de": "schön" },
{ "_key": "E", "en": "sneaky", "de": "hinterlistig" },
{ "_key": "F", "en": "experienced", "de": "erfahren" },
{ "_key": "G", "en": "corrupt", "de": "korrupt" },
{ "_key": "H", "en": "powerful", "de": "einflussreich" },
{ "_key": "I", "en": "naive", "de": "naiv" },
{ "_key": "J", "en": "unmarried", "de": "unverheiratet" },
{ "_key": "K", "en": "skillful", "de": "geschickt" },
{ "_key": "L", "en": "young", "de": "jung" },
{ "_key": "M", "en": "smart", "de": "klug" },
{ "_key": "N", "en": "rational", "de": "rational" },
{ "_key": "O", "en": "ruthless", "de": "skrupellos" },
{ "_key": "P", "en": "brave", "de": "mutig" },
{ "_key": "Q", "en": "mighty", "de": "mächtig" },
{ "_key": "R", "en": "weak", "de": "schwach" }
]

完整的语句如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LET data =[
{ "_key": "A", "en": "strong", "de": "stark" },
{ "_key": "B", "en": "polite", "de": "freundlich" },
{ "_key": "C", "en": "loyal", "de": "loyal" },
{ "_key": "D", "en": "beautiful", "de": "schön" },
{ "_key": "E", "en": "sneaky", "de": "hinterlistig" },
{ "_key": "F", "en": "experienced", "de": "erfahren" },
{ "_key": "G", "en": "corrupt", "de": "korrupt" },
{ "_key": "H", "en": "powerful", "de": "einflussreich" },
{ "_key": "I", "en": "naive", "de": "naiv" },
{ "_key": "J", "en": "unmarried", "de": "unverheiratet" },
{ "_key": "K", "en": "skillful", "de": "geschickt" },
{ "_key": "L", "en": "young", "de": "jung" },
{ "_key": "M", "en": "smart", "de": "klug" },
{ "_key": "N", "en": "rational", "de": "rational" },
{ "_key": "O", "en": "ruthless", "de": "skrupellos" },
{ "_key": "P", "en": "brave", "de": "mutig" },
{ "_key": "Q", "en": "mighty", "de": "mächtig" },
{ "_key": "R", "en": "weak", "de": "schwach" }
]

for d in data
INSERT d INTO Traits

1.3.3 解决特征

让我们从简单的开始,只返回每个字符的 traits 属性:

1
2
FOR c IN Characters
RETURN c.traits
1
2
3
4
5
[
{ "traits": ["A","H","C","N","P"] },
{ "traits": ["D","H","C"] },
...
]

另请参阅有关属性访问的对象/文档基础知识。

我们可以将 traits 数组与 DOCUMENT() 函数一起使用,以将元素用作文档键并在 Traits 集合中查找它们:

1
2
FOR c IN Characters
RETURN DOCUMENT("Traits", c.traits)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
[
[
{
"_key": "A",
"_id": "Traits/A",
"_rev": "_V5oRUS2---",
"en": "strong",
"de": "stark"
},
{
"_key": "H",
"_id": "Traits/H",
"_rev": "_V5oRUS6--E",
"en": "powerful",
"de": "einflussreich"
},
{
"_key": "C",
"_id": "Traits/C",
"_rev": "_V5oRUS6--_",
"en": "loyal",
"de": "loyal"
},
{
"_key": "N",
"_id": "Traits/N",
"_rev": "_V5oRUT---D",
"en": "rational",
"de": "rational"
},
{
"_key": "P",
"_id": "Traits/P",
"_rev": "_V5oRUTC---",
"en": "brave",
"de": "mutig"
}
],
[
{
"_key": "D",
"_id": "Traits/D",
"_rev": "_V5oRUS6--A",
"en": "beautiful",
"de": "schön"
},
{
"_key": "H",
"_id": "Traits/H",
"_rev": "_V5oRUS6--E",
"en": "powerful",
"de": "einflussreich"
},
{
"_key": "C",
"_id": "Traits/C",
"_rev": "_V5oRUS6--_",
"en": "loyal",
"de": "loyal"
}
],
...
]

DOCUMENT() 函数可用于通过文档标识符查找单个或多个文档。 在我们的示例中,我们将要从中获取文档的集合名称作为第一个参数(“Traits”)传递,并将文档键数组(_key 属性)作为第二个参数传递。 作为回报,我们获得了每个字符的完整特征文档的数组。

这有点太多信息,所以我们只使用数组扩展符号返回英文标签:

1
2
FOR c IN Characters
RETURN DOCUMENT("Traits", c.traits)[*].en
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
[
"strong",
"powerful",
"loyal",
"rational",
"brave"
],
[
"beautiful",
"powerful",
"loyal"
],
...
]

1.3.3 合并字符和特征

太好了,我们将字母解析为有意义的特征! 但是我们还需要知道它们属于哪个角色。 因此,我们需要合并字符文档和特征文档中的数据:

1
2
FOR c IN Characters
RETURN MERGE(c, { traits: DOCUMENT("Traits", c.traits)[*].en } )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[
{
"_id": "Characters/2861650",
"_key": "2861650",
"_rev": "_V1bzsXa---",
"age": 41,
"alive": false,
"name": "Ned",
"surname": "Stark",
"traits": [
"strong",
"powerful",
"loyal",
"rational",
"brave"
]
},
{
"_id": "Characters/2861653",
"_key": "2861653",
"_rev": "_V1bzsXa--B",
"age": 40,
"alive": false,
"name": "Catelyn",
"surname": "Stark",
"traits": [
"beautiful",
"powerful",
"loyal"
]
},
...
]

MERGE()函数将对象合并在一起。 因为我们使用了一个对象 { traits: … } ,它与原始字符属性具有相同的属性名称 traits,后者被合并操作覆盖。

1.3.4 加入另一种方式

DOCUMENT()函数利用主要索引快速查找文档。 但是,它仅限于通过标识符查找文档。 对于我们示例中的用例,完成一个简单的连接就足够了。

还有另一种更灵活的连接语法:在多个集合上嵌套 FOR 循环,使用 FILTER 条件来匹配属性。 对于traits 键数组,需要第三个循环来迭代键:

1
2
3
4
5
6
7
8
9
FOR c IN Characters
RETURN MERGE(c, {
traits: (
FOR key IN c.traits
FOR t IN Traits
FILTER t._key == key
RETURN t.en
)
})

对于每个字符,它循环遍历其特征属性(例如 [“D”,”H”,”C”]),并且对于该数组中的每个文档引用,循环遍历特征集合。 存在将文档键与键引用匹配的条件。 在这种情况下,内部 FOR 循环和 FILTER 被转换为主索引查找,而不是构建笛卡尔积,仅过滤掉除单个匹配项之外的所有内容:集合中的文档键是唯一的,因此只能有一个匹配项。

返回每个写出的英文特征,然后将所有特征与字符文档合并。 结果与使用 DOCUMENT() 的查询相同。 但是,这种带有嵌套 FOR 循环和 FILTER 的方法不限于主键。 您也可以使用任何其他属性执行此操作。 为了高效查找,请确保为此属性添加哈希索引。 如果其值是唯一的,则还将索引选项设置为唯一。

1.4 图的遍历

诸如父母和孩子之间的关系可以建模为图。 在 ArangoDB 中,可以通过边缘文档链接两个文档(父字符文档和子字符文档)。 边缘文档存储在边缘集合中,并具有两个附加属性:_from_to。 他们通过文档 ID (_id) 引用任意两个文档。

1.4.1ChildOf relations

我们的角色在父母和孩子之间有以下关系(名字只是为了更好地概述):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Robb -> Ned
Sansa -> Ned
Arya -> Ned
Bran -> Ned
Jon -> Ned
Robb -> Catelyn
Sansa -> Catelyn
Arya -> Catelyn
Bran -> Catelyn
Jaime -> Tywin
Cersei -> Tywin
Tyrion -> Tywin
Joffrey -> Jaime
Joffrey -> Cersei

可视化为图表:

ChildOf graph visualization

1.4.2 创建edges

要创建所需的边缘文档以将这些关系存储在数据库中,我们可以运行一个结合连接和过滤的查询来匹配正确的字符文档,然后使用它们的 _id 属性将边缘插入到边缘集合 ChildOf 中。

首先,创建一个名为 ChildOf 的新集合,并确保将集合类型更改为 Edge。

Create ChildOf edge collection

然后运行以下查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
LET data = [
{
"parent": { "name": "Ned", "surname": "Stark" },
"child": { "name": "Robb", "surname": "Stark" }
}, {
"parent": { "name": "Ned", "surname": "Stark" },
"child": { "name": "Sansa", "surname": "Stark" }
}, {
"parent": { "name": "Ned", "surname": "Stark" },
"child": { "name": "Arya", "surname": "Stark" }
}, {
"parent": { "name": "Ned", "surname": "Stark" },
"child": { "name": "Bran", "surname": "Stark" }
}, {
"parent": { "name": "Catelyn", "surname": "Stark" },
"child": { "name": "Robb", "surname": "Stark" }
}, {
"parent": { "name": "Catelyn", "surname": "Stark" },
"child": { "name": "Sansa", "surname": "Stark" }
}, {
"parent": { "name": "Catelyn", "surname": "Stark" },
"child": { "name": "Arya", "surname": "Stark" }
}, {
"parent": { "name": "Catelyn", "surname": "Stark" },
"child": { "name": "Bran", "surname": "Stark" }
}, {
"parent": { "name": "Ned", "surname": "Stark" },
"child": { "name": "Jon", "surname": "Snow" }
}, {
"parent": { "name": "Tywin", "surname": "Lannister" },
"child": { "name": "Jaime", "surname": "Lannister" }
}, {
"parent": { "name": "Tywin", "surname": "Lannister" },
"child": { "name": "Cersei", "surname": "Lannister" }
}, {
"parent": { "name": "Tywin", "surname": "Lannister" },
"child": { "name": "Tyrion", "surname": "Lannister" }
}, {
"parent": { "name": "Cersei", "surname": "Lannister" },
"child": { "name": "Joffrey", "surname": "Baratheon" }
}, {
"parent": { "name": "Jaime", "surname": "Lannister" },
"child": { "name": "Joffrey", "surname": "Baratheon" }
}
]

FOR rel in data
LET parentId = FIRST(
FOR c IN Characters
FILTER c.name == rel.parent.name
FILTER c.surname == rel.parent.surname
LIMIT 1
RETURN c._id
)
LET childId = FIRST(
FOR c IN Characters
FILTER c.name == rel.child.name
FILTER c.surname == rel.child.surname
LIMIT 1
RETURN c._id
)
FILTER parentId != null AND childId != null
INSERT { _from: childId, _to: parentId } INTO ChildOf
RETURN NEW

执行完上述命令后,得到如下所示图形关系结果:

image-20211208133610694

字符文档没有用户定义的键。 如果他们有,它将使我们能够更轻松地创建边缘,例如:

1
INSERT { _from: "Characters/robb", _to: "Characters/ned" } INTO ChildOf

但是,基于字符名称以编程方式创建边是一个很好的练习。查询细分:

  • 将具有父属性和子属性的对象数组形式的关系分配给变量data
  • 对这个数组中的每个元素,给一个变量rel赋值一个关系,然后执行后面的指令
  • 将表达式的结果分配给变量 parentId
    • 取一个子查询结果的第一个元素(子查询用括号括起来,但这里也是函数调用)
      • 对于 Characters 集合中的每个文档,将文档分配给变量 c
      • 应用两个过滤条件:字符文档中的名字必须等于 rel 中的父名,并且姓氏也必须等于关系 data 中给出的姓氏
      • 在第一场匹配后停止以提高效率
      • 返回字符文档的ID(子查询的结果是一个只有一个元素的数组,FIRST()取这个元素赋值给parentId变量)
  • 将表达式的结果分配给变量 childId
    • 子查询用于查找子字符文档并返回ID,与父文档ID相同(见上文)
  • 如果两个子查询中的一个或两个都无法找到匹配项,则跳过当前关系,因为创建一个边需要两个 ID(这只是预防措施)
  • 在 ChildOf 集合中插入一个新的边缘文档,边缘从 childId 到 parentId,没有其他属性
  • 返回新的边缘文档(可选)

1.4.3 穿越到父母身边

现在边链接字符文档(顶点),我们有一个图,我们可以查询以找出谁是另一个字符的父母——或者用图的术语来说,我们想从一个顶点开始,沿着边到 AQL 中的其他顶点 图遍历:

1
2
FOR v IN 1..1 OUTBOUND "Characters/2901776" ChildOf
RETURN v.name

这个 FOR 循环不迭代集合或数组,它遍历图形并迭代它找到的连接顶点,顶点文档分配给一个变量(这里是:v)。 它还可以发出它走过的边缘以及从开始到结束到另外两个变量的完整路径。

在上面的查询中,遍历被限制为最小和最大遍历深度为 1(从起始顶点走多少步),并且只沿着 OUTBOUND 方向的边。 我们的边从孩子指向父母,父母离孩子一步之遥,因此它给了我们开始的孩子的父母。 “Characters/2901776”是起始顶点。 请注意,您的文档 ID 会有所不同,因此请将其调整为您的文档 ID,例如 布兰史塔克文件:

1
2
3
FOR c IN Characters
FILTER c.name == "Bran"
RETURN c._id
1
[ "Characters/<YourDocumentkey>" ]

您也可以直接将此查询与遍历结合起来,通过调整过滤条件轻松更改起始顶点:

1
2
3
4
FOR c IN Characters
FILTER c.name == "Bran"
FOR v IN 1..1 OUTBOUND c ChildOf
RETURN v.name

起始顶点之后是 ChildOf,它是我们的边集合。 示例查询仅返回每个父级的名称以保持结果简短:

1
2
3
4
[
"Ned",
"Catelyn"
]

Robb、Arya 和 Sansa 作为起点将返回相同的结果。 对于琼恩·雪诺来说,只会是奈德。

1.4.4 穿越到孩子们身边

我们也可以从父节点沿着反向边方向(即INBOUND)走到子节点:

1
2
3
4
FOR c IN Characters
FILTER c.name == "Ned"
FOR v IN 1..1 INBOUND c ChildOf
RETURN v.name
1
2
3
4
5
6
7
[
"Robb",
"Sansa",
"Jon",
"Arya",
"Bran"
]

1.4.5 穿越到孙子

对于兰尼斯特家族,我们有着从父母到孙子的关系。 让我们改变遍历深度来返回孙子,这意味着正好走两步:

1
2
3
4
FOR c IN Characters
FILTER c.name == "Tywin"
FOR v IN 2..2 INBOUND c ChildOf
RETURN v.name
1
2
3
4
[
"Joffrey",
"Joffrey"
]

乔佛里被返回两次可能有点出乎意料。 但是,如果您查看图形可视化,您可以看到从 Joffrey(右下角)到 Tywin 的多条路径:

ChildOf graph visualization

1
2
Tywin <- Jaime <- Joffrey
Tywin <- Cersei <- Joffrey

作为快速修复,将查询的最后一行更改为RETURN DISTINCT v.name 以仅返回每个值一次。 但请记住,有遍历选项可以在早期抑制重复顶点。

还可以查看 ArangoDB 图课程,它涵盖了基础知识,但也解释了不同的遍历选项和高级图查询。

1.4.6 可变深度遍历

为了返回 Joffrey 的父母和祖父母,我们可以在 OUTBOUND 方向上走边并调整遍历深度至少走 1 步,最多 2 步:

1
2
3
4
FOR c IN Characters
FILTER c.name == "Joffrey"
FOR v IN 1..2 OUTBOUND c ChildOf
RETURN DISTINCT v.name
1
2
3
4
5
[
"Cersei",
"Tywin",
"Jaime"
]

如果我们有更深的家谱,那么只需要改变深度值来查询曾孙和类似的关系。

1.5 地理空间查询

由纬度和经度值组成的地理空间坐标可以存储为两个单独的属性,也可以存储为具有两个数值的数组形式的单个属性。 ArangoDB 可以索引此类坐标以进行快速地理空间查询。

1.5.1 位置数据

让我们将一些拍摄位置插入到一个新的集合 Locations 中,您需要先创建它,然后在 AQL 查询下运行:

Create Locations collection

1
2
3
4
5
6
7
8
9
10
11
12
13
LET places = [
{ "name": "Dragonstone", "coordinate": [ 55.167801, -6.815096 ] },
{ "name": "King's Landing", "coordinate": [ 42.639752, 18.110189 ] },
{ "name": "The Red Keep", "coordinate": [ 35.896447, 14.446442 ] },
{ "name": "Yunkai", "coordinate": [ 31.046642, -7.129532 ] },
{ "name": "Astapor", "coordinate": [ 31.50974, -9.774249 ] },
{ "name": "Winterfell", "coordinate": [ 54.368321, -5.581312 ] },
{ "name": "Vaes Dothrak", "coordinate": [ 54.16776, -6.096125 ] },
{ "name": "Beyond the wall", "coordinate": [ 64.265473, -21.094093 ] }
]

FOR place IN places
INSERT place INTO Locations

地图上的坐标及其标签的可视化:

Locations on map

1.5.2 地理空间索引

要基于坐标查询,需要一个地理索引。 它确定哪些字段包含纬度和经度值。

  • 转到收藏
  • 单击位置集合
  • 切换到顶部的索引选项卡
  • 单击右侧带有加号的绿色按钮
  • 将类型更改为地理索引
  • 在字段字段中输入坐标
  • 点击创建确认

Create geospatial index on coordinate attribute

Indexes of Locations collection

1.5.3 查找附近的位置

再次使用 FOR 循环,随后的 SORT 操作基于存储的坐标和查询中给定的坐标之间的 DISTANCE()。 这种模式被查询优化器识别。 如果可用,地理索引将用于加速此类查询。

默认排序方向是升序,因此查询首先找到最接近参考点的坐标(距离最近)。 LIMIT 可用于将结果数量限制为最多 n 个匹配项。

在下面的示例中,限制设置为 3。原点(参考点)是爱尔兰都柏林市中心某处的坐标:

1
2
3
4
5
6
7
8
9
10
FOR loc IN Locations
LET distance = DISTANCE(loc.coordinate[0], loc.coordinate[1], 53.35, -6.25)
SORT distance
LIMIT 3
RETURN {
name: loc.name,
latitude: loc.coordinate[0],
longitude: loc.coordinate[1],
distance
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
"name": "Vaes Dothrak",
"latitude": 54.16776,
"longitude": -6.096125,
"distance": 91491.58596795711
},
{
"name": "Winterfell",
"latitude": 54.368321,
"longitude": -5.581312,
"distance": 121425.66829502625
},
{
"name": "Dragonstone",
"latitude": 55.167801,
"longitude": -6.815096,
"distance": 205433.7784182078
}
]

查询返回位置名称,以及以米为单位的坐标和计算距离。 坐标作为两个单独的属性返回。 如果需要,您可以只返回带有简单 RETURN loc 的文档。 或者使用 RETURN MERGE(loc, { distance }) 返回带有添加的距离属性的整个文档。

1.5.4 查找半径内的位置

LIMIT 可以用检查距离的 FILTER 替换,以查找距参考点给定半径内的位置。 请记住,单位是米。 该示例使用半径为 200,000 米(200 公里):

1
2
3
4
5
6
7
8
9
10
FOR loc IN Locations
LET distance = DISTANCE(loc.coordinate[0], loc.coordinate[1], 53.35, -6.25)
SORT distance
FILTER distance < 200 * 1000
RETURN {
name: loc.name,
latitude: loc.coordinate[0],
longitude: loc.coordinate[1],
distance: ROUND(distance / 1000)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"name": "Vaes Dothrak",
"latitude": 54.16776,
"longitude": -6.096125,
"distance": 91
},
{
"name": "Winterfell",
"latitude": 54.368321,
"longitude": -5.581312,
"distance": 121
}
]

距离转换为公里并四舍五入以提高可读性。

二 如何调用 AQL

AQL 查询可以使用:

  • 网页界面,
  • db 对象(在 arangosh 或 Foxx 服务中)
  • 原始 HTTP API。

在后台总是有对服务器 API 的调用,但 Web 界面和 db 对象抽象了低级通信细节,因此更易于使用。

ArangoDB Web 界面具有用于 AQL 查询执行的特定选项卡

您可以使用 db 对象的 _query 和 _createStatement 方法从 ArangoDB Shell 运行 AQL 查询。 本章还介绍了如何在 arangosh 中使用绑定参数、统计、计数和游标。

如果您使用 Foxx,请参阅如何编写数据库查询以获取包括标记模板字符串在内的示例。

如果您想通过 HTTP REST API 从您的应用程序运行 AQL 查询,请参阅 AQL 查询游标的 HTTP 接口中的完整 API 说明。

具体使用方法参见https://www.arangodb.com/docs/3.10/aql/invocation.html

三 AQL基础

  • AQL 语法解释了 AQL 语言的结构。
  • 数据类型描述了 AQL 支持的原始和复合数据类型。
  • 绑定参数:AQL 支持绑定参数的使用。 这允许将查询文本与查询中使用的文字值分开。
  • 类型和值顺序:AQL 使用一组规则(使用值和类型)进行相等检查和比较。
  • 从集合访问数据:描述不存在或空属性对选择查询的影响。
  • 查询结果:AQL 查询的结果是一个值数组。
  • 查询错误:错误可能来自 AQL 解析或执行。
  • 限制:AQL 查询的已知限制。

3.1 AQL 语法

3.1.1 查询类型

AQL 查询必须返回结果(通过使用 RETURN 关键字表示)或执行数据修改操作(通过使用关键字 INSERTUPDATEREPLACEREMOVEUPSERT 之一表示)。 如果 AQL 解析器在同一查询中检测到多个数据修改操作,或者无法确定查询是数据检索还是修改操作,则 AQL 解析器将返回错误。

AQL 只允许一个查询字符串中的一个查询; 因此,不允许使用分号来表示一个查询的结束和分隔多个查询(如 SQL 中所示)。

3.1.2 空白

可以在查询文本中使用空格(空格、回车、换行和制表位)以提高其可读性。 令牌必须由任意数量的空格分隔。 字符串或名称中的空格必须用引号括起来才能保留。

3.1.3 注释

注释可以嵌入在查询中的任何位置。注释中包含的文本将被 AQL 解析器忽略。

多行注释不能嵌套,这意味着注释中的后续注释开始将被忽略,注释结束将结束注释。

AQL 支持两种类型的注释:

  • 单行注释:这些注释以双正斜杠开头,以行尾或查询字符串的末尾结尾(以第一个开头为准)。
  • 多行注释:这些注释以正斜杠和星号开头,以星号和后正斜杠结尾。它们可以根据需要跨越任意数量的行。
1
2
3
4
5
6
/* this is a comment */ RETURN 1
/* these */ RETURN /* are */ 1 /* multiple */ + /* comments */ 1
/* this is
a multi line
comment */
// a single line comment

3.1.4 关键字

在顶层,AQL 提供以下高级操作

操作 描述
FOR 数组迭代
RETURN 结果预测
FILTER 非视图结果筛选
SEARCH 查看结果筛选
SORT 结果排序
LIMIT 结果切片
LET 变量赋值
COLLECT 结果分组
WINDOW 相关行上的聚合
INSERT 插入新文件
UPDATE (部分)现有文件的更新
REPLACE 现有文件的替换
REMOVE 删除现有文档
UPSERT 插入新的或更新的现有文件
WITH 集合声明

上述每个操作都可以使用同名的关键字在查询中启动。AQL 查询可以(并且通常确实)包含上述多个操作。

AQL 查询示例 如下所示:

1
2
3
FOR u IN users
FILTER u.type == "newbie" && u.active == true
RETURN u.name

在此示例查询中,术语 FORFILTERRETURN 根据其名称启动更高级别的操作。 这些术语也是关键字,这意味着它们在语言中具有特殊含义。

例如,查询解析器将使用关键字找出要执行的高级操作。 这也意味着关键字只能在查询中的特定位置使用。 这也使所有关键字都成为保留字,不得用于其预期用途之外的其他目的。

例如,不能将关键字用作不带引号的字面字符串(标识符)用于集合或属性名称。 如果集合或属性需要与关键字具有相同的名称,则需要在查询中引用/转义集合或属性名称(另请参阅名称)。

关键字不区分大小写,这意味着它们可以在查询中指定为小写、大写或混合大小写。 在本文档中,所有关键字都以大写形式编写,以便与其他查询部分区分开来。

除了更高级别的操作关键字之外,还有一些关键字。 在 ArangoDB 的未来版本中可能会添加其他关键字。 目前完整的关键字列表是:

AGGREGATE AND ANY ASC COLLECT
DESC FALSE FILTER FOR DISTINCT
GRAPH IN INBOUND INSERT INTO
K_PATHS K_SHORTEST_PATHS LET LIKE LIMIT
NONE NOT NULL OR OUTBOUND
REMOVE REPLACE RETURN SHORTEST_PATH SORT
TRUE UPDATE UPSERT WITH WINDOW

最重要的是,在语言结构中使用了一些不是保留关键字的词。 因此,它们可以用作集合或属性名称而无需引用或转义。 查询解析器可以根据上下文将它们识别为类似关键字:

最后但并非最不重要的一点是,在某些上下文中可以使用特殊变量。与关键字不同,它们区分大小写

如果在同一作用域中定义具有相同名称的变量,则其值将保持为您设置的值。因此,如果要访问特殊变量值,则需要避免使用自己的变量的这些名称。

3.1.5 命名

通常,名称用于标识 AQL 查询中的以下内容:

  • 收集
  • 属性
  • 变量
  • 功能

AQL 中的名称始终区分大小写。 集合/视图名称支持的最大长度为 256 字节。 变量名称可以更长,但不鼓励使用。

关键字不得用作名称。 如果保留关键字应用作名称,则名称必须包含在反引号或正引号中。

1
2
FOR doc IN `filter`
RETURN doc.`sort`

由于反引号,filtersort在这里被解释为名称而不是关键字。

该示例可以写为:

1
2
FOR f IN ´filter´
RETURN f.´sort´

您可以使用括号表示法来代替刻度线来访问属性:

1
2
FOR f IN `filter`
RETURN f["sort"]

sort在这个替代方案中是一个带引号的字符串文字,因此不与保留字冲突。

如果名称中包含连字符减号 (-) 等特殊字符,也需要转义:

1
2
FOR doc IN `my-coll`
RETURN doc

集合 my-coll 的名称中有一个破折号,但是 - 是 AQL 中用于减法的算术运算符。 反引号将集合名称转义以正确引用集合。 请注意,集合不能用 “ 或 ‘ 引用名称。

3.1.6 集合名称

集合名称可以按原样用于查询。 如果集合碰巧与关键字同名,则名称必须用反引号括起来。

请参阅ArangoDB 中有关集合命名约定的命名约定。

AQL 当前在一个 AQL 查询中最多只能使用 256 个集合。此限制适用于所有涉及的文档和边缘集合的总和。

3.1.7 属性名称

当从集合中引用文档的属性时,必须使用完全限定的属性名称。 这是因为在查询中可能会使用多个属性名称不明确的集合。 为避免歧义,不允许引用不合格的属性名称。

有关属性命名约定的更多信息,请参阅ArangoDB 中的命名约定。

1
2
3
4
FOR u IN users
FOR f IN friends
FILTER u.active == true && f.active == true && u.id == f.userId
RETURN u.name

在上面的示例中,属性名称 active、name、id 和 userId 使用它们所属的集合名称(分别为 u 和 f)进行限定。

3.1.8 变量名称

AQL 允许用户为查询中的其他变量赋值。 分配了值的所有变量都必须具有在查询上下文中唯一的名称。 变量名称必须与同一查询中使用的任何集合名称的名称不同。

1
2
3
FOR u IN users
LET friends = u.friends
RETURN { "name" : u.name, "friends" : friends }

在上面的查询中,users是一个集合名,u和friend都是变量名。 这是因为 FOR 和 LET 操作需要目标变量来存储它们的中间结果。

变量名称中允许的字符是字母 a 到 z(大小写)、数字 0 到 9、下划线 (_) 符号和美元 ($) 符号。 变量名不能以数字开头。 如果变量名称以一个或多个下划线字符开头,则下划线后面必须至少跟一个字母(a-z 或 A-Z)。 美元符号只能用作变量名的第一个字符,并且后面必须跟一个字母。

3.2 数据类型

AQL 支持由一个值组成的基元数据类型和由多个值组成的复合数据类型。以下类型可用:

数据类型 描述
null An empty value, also: the absence of a value
boolean Boolean truth value with possible values false and true
number Signed (real) number
string UTF-8 encoded text value
array / list Sequence of values, referred to by their positions
object / document Sequence of values, referred to by their names

3.2.1 基元类型

3.1.1.1 空值

值可用于表示空值或不存在的值。 它不同于数字值零 () 和其他假值(或零长度字符串)。 它在其他语言中也称为 nil 或 None。null null != 0 false “”

系统可能会在没有值的情况下返回,例如,如果您使用不受支持的值作为参数调用函数,或者您尝试访问不存在的属性。null

3.2.1.2 布尔数据类型

Boolean 数据类型有两个可能的值 。 它们代表逻辑和数学中的两个真值。true false

3.2.1.3 数字文本

数字文字可以是整数或实数值(浮点数)。 它们可以选择使用 或 符号进行签名。 小数点用作可选小数部分的分隔符。 还支持科学记数法(E-notation)+-.

1
2
3
4
5
6
7
8
9
10
11
  1
+1
42
-1
-42
1.23
-99.99
0.5
.5
-4.87e103
-4.87E103

以下符号无效,将引发语法错误:

1
2
3
4
 1.
01.23
00.23
00

所有数值在内部都被视为 64 位有符号整数或 64 位双精度浮点值。 使用的内部浮点格式是 IEEE 754。

当通过用户定义的 AQL 函数向 JavaScript 公开任何数字整数值时,超过 32 位精度的数字将转换为浮点值,因此大整数可能会丢失一些精度位。 将 AQL 数值结果转换为 JavaScript(例如将它们返回给 Foxx)时也是如此。

从 ArangoDB v3.7.7 开始,数字整数文字也可以表示为二进制(基数 2)或十六进制(基数 16)数字文字。

  • 二进制整数文字的前缀是,例如 .0b 0b10101110
  • 十六进制整数文字的前缀是,例如 .0x 0xabcdef02

二进制和十六进制整数文字只能用于无符号整数。 二进制和十六进制数字文字的最大支持值为 232 - 1,即(二进制)或(十六进制)。0b11111111111111111111111111111111 0xffffffff

3.2.1.4 字符串

字符串文字必须用单引号或双引号括起来。 如果要在字符串文字中使用所使用的引号字符本身,则必须使用反斜杠符号对其进行转义。 文字反斜杠也需要用反斜杠转义。

1
2
3
4
5
6
7
8
9
10
11
"yikes!"
"don't know"
"this is a \"quoted\" word"
"this is a longer string."
"the path separator on Windows is \\"

'yikes!'
'don\'t know'
'this is a "quoted" word'
'this is a longer string.'
'the path separator on Windows is \\'

所有字符串文字都必须是 UTF-8 编码的。 如果不是 UTF-8 编码,目前无法使用任意二进制数据。 使用二进制数据的一种解决方法是在存储之前在应用程序端使用 Base64 或其他算法对数据进行编码,并在检索后在应用程序端对其进行解码。

3.2.1.5 复合类型

AQL 支持两种复合类型:

  • array:未命名值的组合,每个值都可以通过它们的位置访问。 有时称为列表。
  • 对象:命名值的组合,每个值都可以通过它们的名称访问。 文档是顶层的对象。

3.2.1.6 数组/列表

第一个支持的复合类型是数组类型。 数组实际上是(未命名/匿名)值的序列。 可以通过它们的位置访问单个数组元素。 数组中元素的顺序很重要。

数组声明以左方括号开始,以右方括号结束。 声明包含零个、一个或多个表达式,用逗号分隔。 在声明中忽略元素周围的空格,因此可以使用换行符、制表位和空格进行格式化[].

在最简单的情况下,数组为空,因此如下所示:

1
[ ]

数组元素可以是任何合法的表达式值。支持数组嵌套。

1
2
3
4
[ true ]
[ 1, 2, 3 ]
[ -99, "yikes!", [ false, ["no"], [] ], 1 ]
[ [ "fox", "marshal" ] ]

允许最后一个元素后的尾随逗号(在 v3.7.0 中引入):

1
2
3
4
5
[
1,
2,
3, // trailing comma
]

稍后可以使用访问器通过它们的位置访问单个数组值。 被访问元素的位置必须是一个数值。 位置从 0 开始。也可以使用负索引值来访问从数组末尾开始的数组值。 如果数组的长度未知并且需要访问数组末尾的元素,这会很方便。[]

1
2
3
4
5
6
7
8
9
10
11
// access 1st array element (elements start at index 0)
u.friends[0]

// access 3rd array element
u.friends[2]

// access last array element
u.friends[-1]

// access second to last array element
u.friends[-2]

3.2.1.7 对象/文档

另一个受支持的复合类型是对象(或文档)类型。 对象是零到多个属性的组合。 每个属性都是一个名称/值对。 可以通过名称单独访问对象属性。 这种数据类型也称为字典、映射、关联数组和其他名称。

对象声明以左大括号开始,以右大括号结束。 一个对象包含零到多个属性声明,用符号相互分隔。 声明中会忽略元素周围的空格,因此可以使用换行符、制表位和空格进行格式化 {}、

在最简单的情况下,对象是空的。 它的声明将是:

1
{ }

对象中的每个属性都是一个名称/值对。 属性的名称和值使用冒号分隔。 名称始终是字符串,而值可以是任何类型,包括子对象。:

属性名称是强制性的 - 对象中不能有匿名值。 它可以指定为带引号或不带引号的字符串:

1
2
3
{ name: … }    // unquoted
{ 'name': … } // quoted (apostrophe / "single quote mark")
{ "name": … } // quoted (quotation mark / "double quote mark")

如果它包含空格、转义序列或除 ASCII 字母 (-、-)、数字 (-)、下划线 () 和美元符号 () 以外的字符,则必须用引号引起来。 第一个字符必须是字母、下划线或美元符号。a z A Z 0 9 _ $

如果关键字用作属性名称,则属性名称必须用引号或反引号引用或转义:

1
2
3
4
5
{ return: … }    // error, return is a keyword!
{ 'return': … } // quoted
{ "return": … } // quoted
{ `return`: … } // escaped (backticks)
{ ´return´: … } // escaped (ticks)

允许在最后一个元素后使用尾随逗号(在 v3.7.0 中引入):

1
2
3
4
5
{
"a": 1,
"b": 2,
"c": 3, // trailing comma
}

也可以使用动态表达式计算属性名称。 要从属性名称表达式中消除常规属性名称的歧义,计算出的属性名称必须括在方括号中:[ … ]

1
{ [ CONCAT("test/", "bar") ] : "someValue" }

属性还有速记符号,这对于轻松返回现有变量非常方便:

1
2
3
LET name = "Peter"
LET age = 42
RETURN { name, age }

以上是通用形式的速记等价物:

1
2
3
LET name = "Peter"
LET age = 42
RETURN { name: name, age: age }

任何有效的表达式都可以用作属性值。 这也意味着嵌套对象可以用作属性值:

1
2
3
{ name : "Peter" }
{ "name" : "Vanessa", "age" : 15 }
{ "name" : "John", likes : [ "Swimming", "Skiing" ], "address" : { "street" : "Cucumber lane", "zip" : "94242" } }

以后可以使用点访问器通过名称访问各个对象属性:

1
2
u.address.city.name
u.friends[0].name.first

也可以使用方括号访问器访问属性:[]

1
2
u["address"]["city"]["name"]
u["friends"][0]["name"]["first"]

与点访问器相比,方括号允许表达式:

1
2
3
LET attr1 = "friends"
LET attr2 = "name"
u[attr1][0][attr2][ CONCAT("fir", "st") ]

如果以一种或另一种方式访问不存在的属性,结果将为 , 没有错误或警告.null

3.3 绑定参数

AQL 支持使用绑定参数,从而允许将查询文本与查询中使用的文字值分开。 将查询文本与文字值分开是一种很好的做法,因为这将防止(恶意)将关键字和其他集合名称注入现有查询中。 这种注入很危险,因为它可能会改变现有查询的含义。

使用绑定参数,无法更改现有查询的含义。 绑定参数可以在查询中可以使用文字的任何地方使用。

3.3.1 语法

绑定参数的一般语法是@name,其中@ 表示这是一个值绑定参数,而name 是实际参数名称。 它可用于替换查询中的值。

1
RETURN @value

对于集合,@@coll 的语法略有不同,其中@@ 表示它是一个集合绑定参数,coll 是参数名称。

1
2
FOR doc IN @@coll
RETURN doc

关键字和其他语言结构不能被绑定值替换,例如 FOR、FILTER、IN、INBOUND 或函数调用。

绑定参数名称必须以字母 a 到 z(大写或小写)或数字(0 到 9)中的任何一个开头,并且后面可以跟任何字母、数字或下划线符号。

不得在查询代码中引用它们:

1
2
3
4
FILTER u.name == "@name" // wrong
FILTER u.name == @name // correct
FOR doc IN "@@collection" // wrong
FOR doc IN @@collection // correct

如果需要在查询中做字符串处理(连接等),则需要使用字符串函数来做:

1
2
3
FOR u IN users
FILTER u.id == CONCAT('prefix', @id, 'suffix') && u.name == @name
RETURN u

3.3.2 用法

3.3.2.1 常规

绑定参数值需要在执行时与查询一起传递,但不能作为查询文本本身的一部分。 在 Web 界面中,查询编辑器旁边有一个窗格,可以在其中输入绑定参数。 对于以下查询,将显示两个输入字段以输入参数 id 和 name 的值。

1
2
3
FOR u IN users
FILTER u.id == @id && u.name == @name
RETURN u

当使用 db._query() 时(例如在 arangosh 中),则可以为参数传递键值对的对象。 这样的对象也可以传递给 HTTP API 端点 _api/cursor,作为键 bindVars 的属性值:

1
2
3
4
5
6
7
{
"query": "FOR u IN users FILTER u.id == @id && u.name == @name RETURN u",
"bindVars": {
"id": 123,
"name": "John Smith"
}
}

查询中声明的绑定参数也必须传递参数值,否则查询将失败。 指定未在查询中声明的参数也会导致错误。

还可以在以下位置找到有关参数绑定的具体信息:

3.3.2.2 嵌套属性

绑定参数可用于子属性访问的点表示法和方括号表示法。 它们也可以链接:

1
2
3
4
5
6
7
8
9
LET doc = { foo: { bar: "baz" } }

RETURN doc.@attr.@subattr
// or
RETURN doc[@attr][@subattr]
{
"attr": "foo",
"subattr": "bar"
}

上面示例中的两个变体都返回 [ “baz” ] 作为查询结果。

整个属性路径,特别是对于高度嵌套的数据,也可以使用点表示法和单个绑定参数指定,通过将字符串数组作为参数值传递。 数组的元素代表路径的属性键:

1
2
3
LET doc = { a: { b: { c: 1 } } }
RETURN doc.@attr
{ "attr": [ "a", "b", "c" ] }

示例查询返回 [ 1 ] 作为结果。 请注意, { “attr”: “a.b.c” } 将返回名为 a.b.c 的属性的值,而不是具有父级 a 和 b 的属性 c 的值,如 [ “a”, “b”, “c” ] 会。

3.3.3 集合绑定参数

存在一种特殊类型的绑定参数用于注入集合名称。 这种类型的绑定参数有一个以附加@ 符号为前缀的名称,因此在查询中使用@@name。

1
2
3
FOR u IN @@collection
FILTER u.active == true
RETURN u

第二个@ 将是绑定参数名称的一部分,在指定 bindVars 时要记住这一点很重要(注意前导@):

1
2
3
4
5
6
{
"query": "FOR u IN @@collection FILTER u.active == true RETURN u",
"bindVars": {
"@collection": "users"
}
}

3.4 类型和值顺序

在检查相等或不相等或确定值的排序顺序时,AQL 使用确定性算法,该算法将数据类型和实际值都考虑在内。

比较的操作数首先按其数据类型进行比较,如果操作数具有相同的数据类型,则仅按其数据值进行比较。

比较数据类型时使用以下类型顺序:

1
null  <  bool  <  number  <  string  <  array/list  <  object/document

这意味着 null 是 AQL 中最小的类型,而文档是最高阶的类型。 如果比较的操作数具有不同的类型,则确定比较结果并完成比较。

例如,布尔真值将始终小于任何数字或字符串值、任何数组(甚至是空数组)或任何对象/文档。 此外,任何字符串值(即使是空字符串)将始终大于任何数字值、布尔值、真或假。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
null  <  false
null < true
null < 0
null < ''
null < ' '
null < '0'
null < 'abc'
null < [ ]
null < { }

false < true
false < 0
false < ''
false < ' '
false < '0'
false < 'abc'
false < [ ]
false < { }

true < 0
true < ''
true < ' '
true < '0'
true < 'abc'
true < [ ]
true < { }

0 < ''
0 < ' '
0 < '0'
0 < 'abc'
0 < [ ]
0 < { }

'' < ' '
'' < '0'
'' < 'abc'
'' < [ ]
'' < { }

[ ] < { }

如果两个比较的操作数具有相同的数据类型,则比较操作数的值。 对于原始类型(null、boolean、number 和 string),结果定义如下:

  • 空值:null 等于 null
  • 布尔值:false小于true
  • 数字:数值按其值排序
  • string:字符串值使用本地化比较进行排序,使用配置的服务器语言根据该语言的字母顺序规则进行排序

注意:与 SQL 不同,null 可以与任何值进行比较,包括 null 本身,而不会将结果自动转换为 null。

对于复合类型,应用以下特殊规则:

从第一个元素开始,通过逐个位置比较它们的各个元素来比较两个数组值。 对于每个位置,首先比较元素类型。 如果类型不相等,则确定比较结果,比较结束。 如果类型相等,则比较两个元素的值。 如果其中一个数组已完成,而另一个数组在比较位置仍有元素,则 null 将用作完全遍历数组的元素值。

如果数组元素本身是复合值(数组或对象/文档),则比较算法将递归检查元素的子值。 递归地比较元素的子元素。

1
2
3
4
5
6
[ ]  <  [ 0 ]
[ 1 ] < [ 2 ]
[ 1, 2 ] < [ 2 ]
[ 99, 99 ] < [ 100 ]
[ false ] < [ true ]
[ false, 1 ] < [ false, '' ]

通过检查属性名称和值来比较两个对象/文档操作数。首先比较属性名称。在比较属性名称之前,会创建一个包含两个操作数的所有属性名称的组合数组,并按字典顺序排序。这意味着在比较两个对象/文档时,在对象/文档中声明属性的顺序无关紧要。

然后遍历组合和排序的属性名称数组,然后查找来自两个比较操作数的相应属性。如果对象/文档之一不具有具有所寻求名称的属性,则其属性值被认为是空的。最后,使用前面提到的数据类型和值比较来比较两个对象/文档的属性值。对所有对象/文档属性执行比较,直到有明确的比较结果。如果发现比较结果明确,则比较结束。如果没有明确的比较结果,则认为两个比较的对象/文档相等。

1
2
3
4
5
6
7
8
9
{ }  ==  { "a" : null }

{ } < { "a" : 1 }
{ "a" : 1 } < { "a" : 2 }
{ "b" : 1 } < { "a" : 0 }
{ "a" : { "c" : true } } < { "a" : { "c" : 0 } }
{ "a" : { "c" : true, "a" : 0 } } < { "a" : { "c" : false, "a" : 1 } }

{ "a" : 1, "b" : 2 } == { "b" : 2, "a" : 1 }

3.5 访问集合中的数据

可以通过在查询中指定集合名称来访问集合数据。集合可以理解为文档数组,这就是它们在 AQL 中的处理方式。集合中的文档通常使用 FOR 关键字访问。请注意,在迭代集合中的文档时,文档的顺序是未定义的。要以明确且确定的顺序遍历文档,还应使用 SORT 关键字。

集合中的数据存储在文档中,每个文档可能具有与其他文档不同的属性。即使对于同一集合的文档也是如此。

因此,遇到不具有在 AQL 查询中查询的部分或全部属性的文档是很正常的。在这种情况下,文档中不存在的属性将被视为它们以 null 值存在。这意味着如果文档具有特定属性并且该属性具有 null 值,或者文档根本没有特定属性,则将文档属性与 null 进行比较将返回 true。

例如,以下查询将返回来自集合 users 中属性 name 值为 null 的所有文档,以及来自根本没有 name 属性的用户的所有文档:

1
2
3
FOR u IN users
FILTER u.name == null
RETURN u

此外,null 小于任何其他值(不包括 null 本身)。 这意味着在将属性值与小于或小于等于运算符进行比较时,结果中可能包含具有不存在属性的文档。

例如,以下查询将返回来自集合 users 中具有值小于 39 的属性 age 的所有文档,以及来自集合中根本没有属性 age 的所有文档。

1
2
3
FOR u IN users
FILTER u.age < 39
RETURN u

编写查询时应始终考虑此行为。

3.5 查询结果

3.5.1 结果集

AQL 查询的结果是一个值数组。 结果数组中的各个值可能具有也可能不具有同构结构,具体取决于实际查询的内容。

例如,当从具有非同构文档(集合中的各个文档具有不同的属性名称)的集合中返回数据时未经修改,结果值也将具有非同构的结构。 每个结果值本身就是一个文档:

1
2
3
4
5
6
7
FOR u IN users
RETURN u
[
{ "id": 1, "name": "John", "active": false },
{ "age": 32, "id": 2, "name": "Vanessa" },
{ "friends": [ "John", "Vanessa" ], "id": 3, "name": "Amy" }
]

但是,如果查询集合中的一组固定属性,则查询结果值将具有同构结构。 每个结果值仍然是一个文档:

1
2
3
4
5
6
7
FOR u IN users
RETURN { "id": u.id, "name": u.name }
[
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Vanessa" },
{ "id": 3, "name": "Amy" }
]

也可以只查询标量值。 在这种情况下,结果集是一个标量数组,每个结果值都是一个标量值:

1
2
3
FOR u IN users
RETURN u.id
[ 1, 2, 3 ]

如果查询由于找不到匹配的数据而未生成任何结果,它将生成一个空的结果数组:

1
[ ]

3.6 错误

如果查询在语法上无效,则向服务器发出无效查询将导致解析错误。 ArangoDB 将在查询检查期间检测此类错误并中止进一步处理。 相反,会返回错误编号和错误消息,以便修复错误。

如果查询通过解析阶段,则将打开查询中引用的所有集合。 如果任何引用的集合不存在,查询执行将再次中止并返回适当的错误消息。

在某些情况下,执行查询还可能产生运行时错误或警告,这些错误或警告无法通过单独检查查询文本来预测。 这是因为查询可能使用集合中的数据,这些数据也可能是非同质的。 会导致运行时错误或警告的一些示例是:

  • 被零除:当在算术除法或模运算中尝试使用值 0 作为除数时将被触发
  • 算术运算的操作数无效:当尝试在算术运算中使用任何非数字值作为操作数时将被触发。 这包括一元(一元减、一元加)和二元运算(加、减、乘、除和模)
  • 逻辑运算的操作数无效:当尝试在逻辑运算中使用任何非布尔值作为操作数时将被触发。 这包括一元(逻辑非/否定)、二元(逻辑与、逻辑或)和三元运算符

请参阅Arango错误页面,了解错误代码和含义的列表。

3.7 AQL 查询的已知限制

AQL 查询存在以下硬编码限制:

  • AQL 查询不能使用超过 1000 个结果寄存器。 每个命名查询变量和内部/匿名查询变量都需要一个结果寄存器,例如 对于中间结果。 子查询也需要结果寄存器。
  • AQL 查询在其初始查询执行计划中不能有超过 4000 个执行节点。
  • AQL 查询不能使用超过 2048 个集合/分片。
  • AQL 查询中的表达式不能嵌套超过 500 级。 例如,表达式 1 + 2 + 3 + 4 有 3 层深(因为它被解释和执行为 1 + (2 + (3 + 4)))

请注意,即使查询仍然低于这些限制,也可能不会产生良好的性能,尤其是当它们必须将来自许多不同集合的数据放在一起时。 还请考虑大型查询(就中间结果大小或最终结果大小而言)可能会使用大量内存,并且可能会达到 AQL 查询的可配置内存限制。

对于 AQL 查询,已知以下其他限制:

  • 表达式中使用的子查询从这些表达式中取出并预先执行。 这意味着子查询不参与操作数的惰性求值,例如在三元运算符中。 另见评估子查询。
  • 在同一 AQL 查询中将集合用于写入操作后,无法在读取操作中使用该集合。
  • 在集群中,所有通过使用集合集(而不是命名图)的遍历动态访问的集合必须在查询的初始 WITH 语句中声明。 要在单个服务器中也需要 WITH 语句(例如,用于测试迁移到集群),请使用选项 —query.require-with 启动服务器

四 运算符

AQL 支持许多可在表达式中使用的运算符。有比较,逻辑,算术和三元运算符。

4.1 比较运算符

比较(或关系)运算符比较两个操作数。它们可以与任何输入数据类型一起使用,并将返回布尔结果值。

支持以下比较运算符:

算子 描述
== 平等
!= 不等式
< 小于
<= 小于或等于
> 大于
>= 大于或等于
IN 测试数组中是否包含值
NOT IN 测试数组中是否不包含值
LIKE 测试字符串值是否与模式匹配
NOT LIKE 测试字符串值是否与模式不匹配
=~ 测试字符串值是否与正则表达式匹配
!~ 测试字符串值是否与正则表达式不匹配

如果比较可以被评估,则每个比较运算符都返回一个布尔值,如果比较评估为真则返回真,否则返回假。

比较运算符接受第一个和第二个操作数的任何数据类型。 但是,如果 IN 和 NOT IN 的右手操作数是数组,则它们只会返回有意义的结果。 LIKE 和 NOT LIKE 只有在两个操作数都是字符串值时才会执行。 如果比较的操作数具有不同的类型,则所有四个运算符都不会执行隐式类型转换,即它们测试严格相等或不相等(例如,0 与“0”、[0]、false 和 null 不同)。

AQL 中比较操作的一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
     0  ==  null            // false
1 > 0 // true
true != null // true
45 <= "yikes!" // true
65 != "65" // true
65 == 65 // true
1.23 > 1.32 // false
1.5 IN [ 2, 3, 1.5 ] // true
"foo" IN null // false
42 NOT IN [ 17, 40, 50 ] // true
"abc" == "abc" // true
"abc" == "ABC" // false
"foo" LIKE "f%" // true
"foo" NOT LIKE "f%" // false
"foo" =~ "^f[o].$" // true
"foo" !~ "[a-z]+bar$" // true

LIKE 运算符检查其左操作数是否与其右操作数中指定的模式匹配。 模式可以由常规字符和通配符组成。 支持的通配符是 匹配单个任意字符,% 匹配任意数量的任意字符。 文字 % 和 需要用反斜杠转义。 反斜杠需要自己转义,这实际上意味着两个反斜杠字符需要位于文字百分号或下划线之前。 在 arangosh 中,需要额外的转义,使其在要转义的字符之前总共有四个反斜杠。

1
2
3
    "abc" LIKE "a%"          // true
"abc" LIKE "_bc" // true
"a_b_foo" LIKE "a\\_b\\_foo" // true

LIKE 运算符执行的模式匹配区分大小写。

NOT LIKE 运算符与 LIKE 运算符具有相同的特征,但结果为否定。 因此它与 NOT (… LIKE …) 相同。 请注意括号,这是某些表达式所必需的:

1
2
FOR doc IN coll
RETURN NOT doc.attr LIKE "…"

返回表达式将被转换为 LIKE(!doc.attr, “…”),给出意想不到的结果。 NOT(doc.attr LIKE “…”) 变成了更合理的 ! 喜欢(doc.attr,“…”)。

正则表达式运算符 =~ 和 !~ 期望它们的左侧操作数是字符串,而它们的右侧操作数是包含 AQL 函数 REGEX_TEST() 文档中指定的有效正则表达式的字符串。

4.2 数组比较运算符

比较运算符也作为数组变体存在。 在数组变体中,运算符的前缀是关键字 ALL、ANY 或 NONE 之一。 使用这些关键字之一会更改运算符行为,以便对其左侧参数值的所有、任意或不执行比较操作。 因此,预计数组运算符的左侧参数是一个数组。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[ 1, 2, 3 ]  ALL IN  [ 2, 3, 4 ]  // false
[ 1, 2, 3 ] ALL IN [ 1, 2, 3 ] // true
[ 1, 2, 3 ] NONE IN [ 3 ] // false
[ 1, 2, 3 ] NONE IN [ 23, 42 ] // true
[ 1, 2, 3 ] ANY IN [ 4, 5, 6 ] // false
[ 1, 2, 3 ] ANY IN [ 1, 42 ] // true
[ 1, 2, 3 ] ANY == 2 // true
[ 1, 2, 3 ] ANY == 4 // false
[ 1, 2, 3 ] ANY > 0 // true
[ 1, 2, 3 ] ANY <= 1 // true
[ 1, 2, 3 ] NONE < 99 // false
[ 1, 2, 3 ] NONE > 10 // true
[ 1, 2, 3 ] ALL > 2 // false
[ 1, 2, 3 ] ALL > 0 // true
[ 1, 2, 3 ] ALL >= 3 // false
["foo", "bar"] ALL != "moo" // true
["foo", "bar"] NONE == "bar" // false
["foo", "bar"] ANY == "foo" // true

请注意,这些运算符不会在常规查询中使用索引。 SEARCH 表达式中也支持运算符,其中可以使用 ArangoSearch 的索引。 但是语义有所不同,请参阅 AQL SEARCH 操作。

4.3 逻辑运算符

AQL 中支持以下逻辑运算符:

  • &&逻辑和运算符
  • ||逻辑或运算符
  • !逻辑非/否定运算符

AQL 还支持逻辑运算符的以下替代形式:

  • AND逻辑和运算符
  • OR逻辑或运算符
  • NOT逻辑非/否定运算符

替代形式是别名并且在功能上等同于常规运算符。

AQL 中的两个操作数逻辑运算符将通过短路求值执行(除非操作数之一是或包含子查询。在这种情况下,子查询将在逻辑运算符之前被拉出求值)。

AQL 中逻辑运算符的结果定义如下:

  • 如果 lhs && rhs 为 false 或在转换为布尔值时为 false,则将返回 lhs。 如果 lhs 为真或在转换为布尔值时为真,则将返回 rhs
  • lh|| rhs 将返回 lhs 如果它为真或在转换为布尔值时为真。 如果 lhs 为 false 或在转换为布尔值时为 false,则将返回 rhs
  • ! value 将返回转换为布尔值的 value 的否定值

AQL 中逻辑运算的一些示例:

1
2
3
4
u.age > 15 && u.address.city != ""
true || false
NOT u.isInvalid
1 || ! 0

允许将非布尔值传递给逻辑运算符。 任何非布尔操作数都将被运算符隐式转换为布尔值,而不会中止查询。

转换为布尔值的工作原理如下:

  • null 将被转换为 false
  • 布尔值保持不变
  • 所有不等于零的数字都为真,零为假
  • 空字符串为假,所有其他字符串为真
  • 数组 ([ ]) 和对象/文档 ({ }) 为真,无论它们的内容如何

逻辑和和逻辑或操作的结果现在可以具有任何数据类型,并且不一定是布尔值。

例如,以下逻辑运算将返回布尔值:

1
2
3
25 > 1  &&  42 != 7                        // true
22 IN [ 23, 42 ] || 23 NOT IN [ 22, 7 ] // true
25 != 25 // false

…而以下逻辑运算不会返回布尔值:

1
2
3
4
   1 || 7                                  // 1
null || "foo" // "foo"
null && true // null
true && 23 // 23

4.4 算术运算符

算术运算符对两个数字操作数执行算术运算。算术运算的结果再次是数值。

AQL 支持以下算术运算符:

  • +加法
  • -减法
  • *乘法
  • /划分
  • %

还支持一元加号和一元减号:

1
2
3
4
LET x = -5
LET y = 1
RETURN [-x, +y]
// [5, 1]

对于求幂,有一个数字函数 POW()。 不支持语法 base ** exp

对于字符串连接,您必须使用字符串函数 CONCAT()。 将两个字符串与加号运算符(“foo”+“bar”)组合将不起作用! 另请参阅常见错误。

一些算术运算示例:

1
2
3
4
5
6
7
1 + 1
33 - 99
12.4 * 4.5
13.0 / 0.1
23 % 7
-15
+9.99

算术运算符接受任何类型的操作数。将非数字值传递给算术运算符将使用 TO_NUMBER() 函数应用的类型转换规则将操作数转换为数字:

  • null 将被转换为 0

  • false 将转换为 0,true 将转换为 1

  • 有效数值保持不变,但 NaN 和 Infinity 将转换为 0

  • 如果字符串值包含数字的有效字符串表示形式,则字符串值将转换为数字。字符串开头或结尾的任何空格都将被忽略。具有任何其他内容的字符串将转换为数字 0

  • 一个空数组被转换为 0,一个具有一个成员的数组被转换为其唯一成员的数字表示。具有更多成员的数组将转换为数字 0。

  • 对象/文档被转换为数字 0。

产生无效值(例如 1 / 0(被零除))的算术运算将产生 null 结果值。查询不会中止,但您可能会看到警告。

这里有一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
   1 + "a"       // 1
1 + "99" // 100
1 + null // 1
null + 1 // 1
3 + [ ] // 3
24 + [ 2 ] // 26
24 + [ 2, 4 ] // 24
25 - null // 25
17 - true // 16
23 * { } // 0
5 * [ 7 ] // 35
24 / "12" // 2
1 / 0 // null (with a 'division by zero' warning)

4.5 三元运算符

AQL 还支持可用于条件评估的三元运算符。 三元运算符期望布尔条件作为它的第一个操作数,如果条件评估为真,则返回第二个操作数的结果,否则返回第三个操作数。

例子

如果 u.age 大于 15 或 u.active 为真,则表达式返回 u.userId。 否则返回空值:

1
u.age > 15 || u.active == true ? u.userId : null

还有一个只有两个操作数的三元运算符的快捷变体。 当布尔条件的表达式和返回值应该相同时,可以使用此变体。

例子

如果 u.value 为真,则表达式计算为 u.value,否则返回固定字符串:

1
u.value ? : 'value is null, 0 or not present'

如果第二个操作数介于 ? 和 : 被省略,而在 u.value 的情况下它将被评估两次? u.value : ‘值为空’。

表达式中使用的子查询从这些表达式中取出并预先执行。 这意味着子查询不参与操作数的惰性求值,例如在三元运算符中。另请参阅子查询的计算

4.6 范围运算符

AQL 支持使用 .. 运算符表示简单的数字范围。 此运算符可用于轻松迭代一系列数值。

.. 运算符将生成定义范围内的整数值数组,包括两个边界值。

例子

1
2010..2013

将产生以下结果:

1
[ 2010, 2011, 2012, 2013 ]

使用范围运算符等效于使用范围边界指定的范围内的整数值编写数组。 如果范围运算符的边界是非整数,它们将首先转换为整数值。

还有一个RANGE() 函数

4.7 数组运算符

AQL 为数组变量扩展提供了数组运算符 [],为数组收缩提供了 [*]。

4.8 运算符优先级

AQL 中的运算符优先级与其他熟悉的语言类似(优先级最低优先):

操作员 描述
, 逗号分隔符
DISTINCT 不同的修饰符(返回操作)
? : 三元运算符
= 变量赋值(LET 操作)
WITH with 运算符(WITH / UPDATE / REPLACE / COLLECT 操作)
INTO into 操作符(INSERT / UPDATE / REPLACE / REMOVE / COLLECT 操作)
` ` 逻辑或
&& 逻辑和
OUTBOUND, , , ,INBOUND``ANY``ALL``NONE 图遍历方向,数组比较运算符
==, , , , ,!=``LIKE``NOT LIKE``=~``!~ (中)相等,通配符(非)匹配,正则表达式(非)匹配
IN,NOT IN (非)在运算符中
<, , ,<=``>=``> 小于、小于等于、大于等于、大于
.. 范围运算符
+,- 加法、减法
*, ,/``% 乘法、除法、模量
!, ,+``- 逻辑否定、一元加号、一元减号
() 函数调用
. 成员访问
[] 索引值访问
[*] 扩张
:: 范围

括号 ( 和 ) 可用于强制执行不同的运算符评估顺序。

五 数据查询

有两种基本类型的 AQL 查询:

  • 访问数据的查询(读取文档)
  • 修改数据的查询(创建、更新、替换、删除文档)

5.1 数据访问查询

使用 AQL 从数据库中检索数据始终包括 RETURN 操作。 它可用于返回静态值,例如字符串:

1
RETURN "Hello ArangoDB!"

查询结果始终是一个元素数组,即使在这种情况下返回单个元素并包含单个元素:["Hello ArangoDB!"]

可以调用函数 DOCUMENT()以通过其文档句柄检索单个文档,例如:

1
RETURN DOCUMENT("users/phil")

RETURN 通常伴随一个 FOR 循环来迭代集合的文档。 以下查询为名为 users 的集合的所有文档执行循环体。 在此示例中,每个文档都保持不变:

1
2
FOR doc IN users
RETURN doc

无需返回原始文档,您可以轻松创建一个投影:

1
2
FOR doc IN users
RETURN { user: doc, newAttribute: true }

对于每个用户文档,返回一个具有两个属性的对象。 属性 user 的值设置为用户文档的内容,newAttribute 是一个布尔值 true 的静态属性。

可以将 FILTERSORTLIMIT 等操作添加到循环体中以缩小和排序结果。 除了上面显示的对DOCUMENT() 的调用之外,还可以像这样检索描述用户 phil 的文档:

1
2
3
FOR doc IN users
FILTER doc._key == "phil"
RETURN doc

本示例中使用了文档键,但任何其他属性同样可以用于过滤。 由于保证文档键是唯一的,因此不会有多个文档与此过滤器匹配。 对于其他属性,情况可能并非如此。 要返回按名称升序排序的活动用户子集(由名为 status 的属性确定),您可以执行以下操作:

1
2
3
4
FOR doc IN users
FILTER doc.status == "active"
SORT doc.name
LIMIT 10

请注意,操作不必以固定顺序发生,它们的顺序会显着影响结果。 限制过滤器之前的文档数量通常不是您想要的,因为它很容易错过很多满足过滤条件的文档,但由于过早的 LIMIT 子句而被忽略。 由于上述原因,LIMIT通常放在最后,在 FILTERSORT 等操作之后。

有关更多详细信息,请参阅高级操作一章。

5.2 数据修改查询

AQL 支持以下数据修改操作:

  • INSERT:将新文档插入集合
  • UPDATE:部分更新集合中的现有文档
  • REPLACE:完全替换集合中的现有文档
  • REMOVE:从集合中删除现有文档
  • UPSERT:有条件地插入或更新集合中的文档

下面是一些使用这些操作的简单示例查询。高级操作 一章 中详细介绍了这些操作

5.2.1 修改单个文档

让我们从基础开始:对单个文档的INSERTUPDATEREMOVE 操作。 以下是在现有用户集合中插入文档的示例:

1
2
3
4
5
INSERT {
firstName: "Anna",
name: "Pavlova",
profession: "artist"
} IN users

您可以提供新文件的密钥; 如果未提供,ArangoDB 将为您创建一个。

1
2
3
4
5
6
INSERT {
_key: "GilbertoGil",
firstName: "Gilberto",
name: "Gil",
city: "Fortalezza"
} IN users

由于 ArangoDB 是无模式的,文档的属性可能会有所不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
INSERT {
_key: "PhilCarpenter",
firstName: "Phil",
name: "Carpenter",
middleName: "G.",
status: "inactive"
} IN users
INSERT {
_key: "NatachaDeclerck",
firstName: "Natacha",
name: "Declerck",
location: "Antwerp"
} IN users

更新很简单。 以下 AQL 语句将添加或更改属性状态和位置

1
2
3
4
UPDATE "PhilCarpenter" WITH {
status: "active",
location: "Beijing"
} IN users

替换是更新文档的所有属性被替换的替代方法。

1
2
3
4
5
6
7
REPLACE {
_key: "NatachaDeclerck",
firstName: "Natacha",
name: "Leclerc",
status: "active",
level: "premium"
} IN users

如果您知道其密钥,则删除文档也很简单:

1
REMOVE "GilbertoGil" IN users

1
REMOVE { _key: "GilbertoGil" } IN users

5.2.2 修改多个文档

数据修改操作通常与 FOR 循环结合使用,以迭代给定的文档列表。 它们可以选择性地与 FILTER 语句等结合使用。

让我们从一个示例开始,该示例修改符合某些条件的用户集合中的现有文档:

1
2
3
FOR u IN users
FILTER u.status == "not active"
UPDATE u WITH { status: "inactive" } IN users

现在,让我们将收藏用户的内容复制到收藏备份中:

1
2
FOR u IN users
INSERT u IN backup

随后,让我们在集合用户中找到一些文档,并将它们从集合备份中删除。 两个集合中的文档之间的链接是通过文档的键建立的:

1
2
3
FOR u IN users
FILTER u.status == "deleted"
REMOVE u IN backup

以下示例将从用户和备份中删除所有文档:

1
2
3
LET r1 = (FOR u IN users  REMOVE u IN users)
LET r2 = (FOR u IN backup REMOVE u IN backup)
RETURN true

5.2.3 返回文档

数据修改查询可以选择返回文档。 为了在 RETURN 语句中引用插入、删除或修改的文档,数据修改语句引入了OLD和(或) NEW伪值:

1
2
3
4
5
6
7
8
9
10
11
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 指插入或修改的文档修订版,OLD 指更新或删除前的文档修订版。 INSERT 语句只能引用 NEW 伪值,而 REMOVE 操作只能引用 OLDUPDATEREPLACEUPSERT 都可以参考。

在所有情况下,将返回完整文档及其所有属性,包括可能自动生成的属性,例如_id_key_rev,以及部分更新的更新表达式中未指定的属性。

5.2.4 预测

可以返回 OLDNEW 文档的投影,而不是返回整个文档。 这可用于减少查询返回的数据量。

例如,以下查询将仅返回插入文档的键:

1
2
3
FOR i IN 1..100
INSERT { value: i } IN test
RETURN NEW._key

5.2.5 在同一查询中使用旧查询和新

对于 UPDATEREPLACEUPSERT 语句,OLDNEW均可用于返回文档的先前修订版以及更新的修订版:

1
2
3
4
FOR u IN users
FILTER u.status == "not active"
UPDATE u WITH { status: "inactive" } IN users
RETURN { old: OLD, new: NEW }

5.2.6 使用 OLD 或 NEW 进行计算

还可以在数据修改部分和 AQL 查询的最终 RETURN之间使用 LET语句运行附加计算。 例如,以下查询执行upsert 操作并返回是否更新了现有文档,或插入了新文档。 它通过检查 UPSERT之后的 OLD 变量并使用 LET语句来存储操作类型的临时字符串来实现:

1
2
3
4
5
UPSERT { name: "test" }
INSERT { name: "test" }
UPDATE { } IN users
LET opType = IS_NULL(OLD) ? "insert" : "update"
RETURN { _key: NEW._key, type: opType }

5.2.7 限制

AQL 执行程序在查询编译时必须知道修改后的集合的名称(上述情况下的用户和备份),并且不能在运行时更改。允许使用绑定参数指定集合名称。

不可能在同一个查询中对同一个集合使用多个数据修改操作,或者用对同一个集合的读取操作来跟进特定集合的数据修改操作。也不可能用遍历查询(可以从遍历开始时不一定知道的任意集合中读取)来跟踪任何数据修改操作。

这意味着您不能将同一集合的多个 REMOVE 或 UPDATE 语句放入同一查询中。但是,可以通过对同一查询中的不同集合使用多个数据修改操作来修改不同的集合。如果您有一个查询需要从同一个集合中删除文档的多个位置,建议将这些文档或它们的键收集在一个数组中,并使用单个 REMOVE 操作从该数组中删除文档。

数据修改操作之后可以选择跟有 LET 操作以执行进一步的计算和 RETURN 操作以返回数据。

5.2.8 事务执行

在单个服务器上,数据修改操作以事务方式执行。如果数据修改操作失败,它所做的任何更改都将自动回滚,就好像它们从未发生过一样。

如果使用 RocksDB 引擎并启用中间提交,则查询可能会执行中间事务提交,以防正在运行的事务(AQL 查询)达到指定的大小阈值。在这种情况下,到目前为止执行的查询操作将被提交,并且不会在以后中止/回滚的情况下回滚。该行为可以通过调整 RocksDB 引擎的中间提交设置来控制。

在集群中,AQL 数据修改查询当前不是以事务方式执行的。此外,更新、替换、更新插入和删除 AQL 查询当前需要为所有应该修改或删除的文档指定 _key 属性,即使为集合选择了 _key 以外的共享密钥属性。这个限制可能会在 ArangoDB 的未来版本中被克服。

六 高级操作

下面将在下面介绍以下高级操作:

  • FOR:迭代集合或视图,数组的所有元素或遍历图形

  • RETURN:产生查询的结果。

  • FILTER:将结果限制为匹配任意逻辑条件的元素。

  • SEARCH:查询 ArangoSearch 视图的(全文)索引

  • SORT:强制对已经产生的中间结果数组进行排序。

  • LIMIT:将结果中的元素数量减少到最多指定的数量,可选地跳过元素(分页)。

  • LET:为变量分配任意值。

  • COLLECT:按一个或多个组标准对数组进行分组。也可以计数和聚合。

  • WINDOW:对相关行执行聚合。

  • REMOVE:从集合中删除文档。

  • UPDATE:部分更新集合中的文档。

  • REPLACE:完全替换集合中的文档。

  • INSERT:将新文档插入到集合中。

  • UPSERT:更新/替换现有文档,或者在它不存在的情况下创建它。

  • WITH:指定查询中使用的集合(仅在查询开始时)。

6.1 FOR

通用 FOR 关键字可用于迭代集合或视图、数组的所有元素或遍历图形。

6.1.1 语法

迭代集合和数组的一般语法为:

1
FOR variableName IN expression

图遍历还有一个特殊的变体:

1
FOR vertexVariableName [, edgeVariableName [, pathVariableName ] ] IN traversalExpression

对于视图,有一个特殊的(可选的)SEARCH关键字

1
FOR variableName IN viewName SEARCH searchExpression

视图不能用作遍历中的边集合:

1
FOR v IN 1..3 ANY startVertex viewName /* invalid! */

所有变体都可以选择以 OPTIONS { ... }子句结尾。

6.1.2 用法

表达式返回的每个数组元素只被访问一次。 要求表达式在所有情况下都返回一个数组。 空数组也是允许的。 当前数组元素可用于在 variableName 指定的变量中进行进一步处理。

1
2
FOR u IN users
RETURN u

这将迭代来自数组 users 的所有元素(注意:在这种情况下,该数组由名为“users”的集合中的所有文档组成)并使当前数组元素在变量 u 中可用。 u 在这个例子中没有被修改,而是使用RETURN关键字简单地推送到结果中。

注意:当迭代基于集合的数组时,除非使用 SORT语句定义了显式排序顺序,否则文档的顺序是未定义的。

FOR引入的变量在 FOR所在的作用域关闭之前一直可用。

另一个使用静态声明的值数组进行迭代的示例:

1
2
FOR year IN [ 2011, 2012, 2013 ]
RETURN { "year" : year, "isLeapYear" : year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) }

也允许嵌套多个FOR语句。 当 FOR 语句嵌套时,将创建单个FOR 语句返回的数组元素的叉积。

1
2
3
FOR u IN users
FOR l IN locations
RETURN { "user" : u, "location" : l }

在此示例中,有两次数组迭代:对数组 users 的外部迭代加上对数组位置的内部迭代。 内部数组的遍历次数与外部数组中元素的次数相同。 对于每次迭代,用户和位置的当前值可用于变量 u 和 l 中的进一步处理。

6.1.3 选项

对于集合和视图,FOR构造支持可选的OPTIONS子句来修改行为。 一般语法是:

1
FOR variableName IN expression OPTIONS { option: value, ... }

6.1.4 indexHint

对于集合,可以使用 indexHint 选项向优化器提供索引提示。 该值可以是单个索引名称或按优先顺序排列的索引名称列表:

1
2
FOR … IN … OPTIONS { indexHint: "byName" }
FOR … IN … OPTIONS { indexHint: ["byName", "byColor"] }

每当有机会为此 FOR 循环使用索引时,优化器将首先检查是否可以使用指定的索引。 在索引数组的情况下,优化器将按照指定的顺序检查每个索引的可行性。 它将使用第一个合适的索引,而不管它通常是否会使用不同的索引。

如果指定的索引都不合适,则它会回退到其正常逻辑以选择另一个索引,或者如果启用 forceForceHint 则失败。

6.1.5 forceIndexHint

默认情况下不强制执行索引提示。 如果 forceIndexHint 设置为 true,那么如果 indexHint 不包含可用索引,而不是使用回退索引或根本不使用索引,则会生成错误。

1
FOR … IN … OPTIONS { indexHint: … , forceIndexHint: true }

6.2 RETURN

RETURN 语句可用于生成查询结果。 在数据选择查询中,必须在每个块的末尾指定一个 RETURN 语句,否则查询结果将是未定义的。 在数据修改查询的主要级别上使用 RETURN 是可选的。

6.2.1 语法

一般语法为:RETURN

1
RETURN expression

还有一个变体 RETURN DISTINCT

RETURN返回的表达式是为放置 RETURN语句的块中的每次迭代生成的。这意味着 RETURN语句的结果始终是一个数组。 如果没有文档与查询匹配,则这包括一个空数组,以及一个作为具有一个元素的数组返回的单个返回值。

要不加修改地返回当前迭代数组中的所有元素,可以使用以下简单形式:

1
2
FOR variableName IN expression
RETURN variableName

由于RETURN允许指定表达式,因此可以执行任意计算来计算结果元素。 在 RETURN 所在的范围内有效的任何变量都可以用于计算。

6.2.2 用法

要遍历名为 users 的集合的所有文档并返回完整文档,您可以编写:

1
2
FOR u IN users
RETURN u

在 for 循环的每次迭代中,users 集合的一个文档被分配给变量 u 并在此示例中未修改地返回。 要仅返回每个文档的一个属性,您可以使用不同的返回表达式:

1
2
FOR u IN users
RETURN u.name

或者要返回多个属性,可以像这样构造一个对象:

1
2
FOR u IN users
RETURN { name: u.name, age: u.age }

注意:RETURN 将关闭当前作用域并消除其中的所有局部变量。 在处理子查询时要记住这一点很重要。

也支持动态属性名称:

1
2
FOR u IN users
RETURN { [ u._id ]: u.age }

在这个例子中,每个用户的文档 _id 被用作计算属性键的表达式:

1
2
3
4
5
6
7
8
9
10
11
[
{
"users/9883": 32
},
{
"users/9915": 27
},
{
"users/10074": 69
}
]

结果包含每个用户一个对象,每个对象都有一个键/值对。 这通常是不希望的。 对于将用户 ID 映射到年龄的单个对象,需要合并各个结果并使用另一个 RETURN 返回:

1
2
3
4
5
6
7
8
9
10
11
RETURN MERGE(
FOR u IN users
RETURN { [ u._id ]: u.age }
)
[
{
"users/10074": 69,
"users/9883": 32,
"users/9915": 27
}
]

请记住,如果键表达式多次计算为相同的值,则只有具有重复名称的键/值对之一会在 MERGE()中幸存下来。 为避免这种情况,您可以不使用动态属性名称,而是使用静态名称并将所有文档属性作为属性值返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FOR u IN users
RETURN { name: u.name, age: u.age }
[
{
"name": "John Smith",
"age": 32
},
{
"name": "James Hendrix",
"age": 69
},
{
"name": "Katie Foster",
"age": 27
}
]

6.2.3 RETURN DISTINCT

RETURN 可以选择后跟DISTINCT关键字。DISTINCT 关键字将确保 RETURN语句返回的值的唯一性:

1
2
FOR variableName IN expression
RETURN DISTINCT expression

如果前面没有FOR循环,则在查询的顶层不允许 RETURN DISTINCT

以下示例返回:["foo", "bar", "baz"]

1
2
FOR value IN ["foo", "bar", "bar", "baz", "foo"]
RETURN DISTINCT value

与 COLLECT 不同,RETURN DISTINCT 不会改变它所应用的结果的顺序

如果 DISTINCT 应用于本身是数组或子查询的表达式,则 DISTINCT不会使每个数组或子查询结果中的值唯一,而是确保结果仅包含不同的数组或子查询结果。 要使数组或子查询的结果唯一,只需为数组或子查询应用 DISTINCT

例如,以下查询将在其子查询结果上应用DISTINCT,但不在子查询内:

1
2
3
4
5
FOR what IN 1..2
RETURN DISTINCT (
FOR i IN [ 1, 2, 3, 4, 1, 3 ]
RETURN i
)

在这里,我们将有一个包含两次迭代的 FOR 循环,每个迭代执行一个子查询。 此处的 DISTINCT 应用于两个子查询结果。 两个子查询都返回相同的结果值(即 [ 1, 2, 3, 4, 1, 3 ] ),因此在 DISTINCT 之后将只会出现一次值 [ 1, 2, 3, 4, 1, 3 ] 剩下:

1
2
3
[
[ 1, 2, 3, 4, 1, 3 ]
]

如果目标是在子查询中应用DISTINCT,则需要将其移动到那里:

1
2
3
4
5
6
FOR what IN 1..2
LET sub = (
FOR i IN [ 1, 2, 3, 4, 1, 3 ]
RETURN DISTINCT i
)
RETURN sub

在上述情况下,DISTINCT 将使子查询结果唯一,因此每个子查询将返回一个唯一的值数组 ([ 1, 2, 3, 4 ])。 由于子查询被执行两次并且顶层没有 DISTINCT,该数组将被返回两次:

1
2
3
4
[
[ 1, 2, 3, 4 ],
[ 1, 2, 3, 4 ]
]

6.3 FILTER

FILTER 语句可用于将结果限制为匹配任意逻辑条件的元素。

6.3.1 语法

1
FILTER expression

表达式必须是计算结果为 false 或 true 的条件。

6.3.2 用法

如果条件结果为假,则跳过当前元素,因此不会对其进行进一步处理,也不会成为结果的一部分。 如果条件为真,则不会跳过当前元素,可以进一步处理。

有关可以在条件中使用的比较运算符、逻辑运算符等的列表,请参阅运算符。

1
2
3
FOR u IN users
FILTER u.active == true && u.age < 39
RETURN u

允许在查询中指定多个FILTER 语句,即使在同一个块中。 如果使用多个FILTER 语句,它们的结果将与逻辑 AND 组合,这意味着所有过滤条件必须为真才能包含一个元素。

1
2
3
4
FOR u IN users
FILTER u.active == true
FILTER u.age < 39
RETURN u

在上面的例子中,所有属性 active 值为 true 且属性 age 小于 39(包括空值)的用户数组元素都将包含在结果中。 用户的所有其他元素将被跳过,不会包含在 RETURN产生的结果中。

有关不存在或空属性的影响的描述,请参阅从集合访问数据。

虽然 FILTER通常与FOR组合出现,但它也可以用于顶层或没有周围FOR循环的子查询。

1
2
FILTER false
RETURN ASSERT(false, "never reached")

6.3.3 操作顺序

请注意,FILTER语句的位置会影响查询的结果。 例如,测试数据中有 16 个活跃用户:

1
2
3
FOR u IN users
FILTER u.active == true
RETURN u

我们可以将结果集限制为最多 5 个用户:

1
2
3
4
FOR u IN users
FILTER u.active == true
LIMIT 5
RETURN u

例如,这可能会返回 Jim、Diego、Anthony、Michael 和 Chloe 的用户文档。 哪些返回是未定义的,因为没有 SORT语句来确保特定的顺序。 如果我们添加第二个 FILTER 语句以仅返回女性……

1
2
3
4
5
FOR u IN users
FILTER u.active == true
LIMIT 5
FILTER u.gender == "f"
RETURN u

它可能只返回 Chloe 文档,因为 LIMIT在第二个FILTER之前应用。 到达第二个FILTER 块的文档不超过 5 个,并且并非所有文档都满足性别标准,即使集合中有超过 5 个活跃的女性用户。 通过添加 SORT块可以实现更具确定性的结果:

1
2
3
4
5
6
FOR u IN users
FILTER u.active == true
SORT u.age ASC
LIMIT 5
FILTER u.gender == "f"
RETURN u

这将返回用户 Mariah 和 Mary。 如果按 DESC 顺序按年龄排序,则返回 Sophia、Emma 和 Madison 文档。 然而,LIMIT之后的FILTER并不常见,您可能需要这样的查询:

1
2
3
4
5
FOR u IN users
FILTER u.active == true AND u.gender == "f"
SORT u.age ASC
LIMIT 5
RETURN u

FILTER 块放置位置的重要性允许这个单个关键字可以承担两个 SQL 关键字的角色,WHERE 和 HAVING。 因此,AQL 的FILTERCOLLECT 聚合一起工作,就像处理任何其他中间结果、文档属性等一样。

SEARCH 关键字启动语言结构以过滤 ArangoSearch 类型的视图。 从概念上讲,视图只是另一个文档数据源,类似于数组或文档/边缘集合,您可以在 AQL 中使用 FOR 操作对其进行迭代:

1
2
FOR doc IN viewName
RETURN doc

可选的 SEARCH 操作提供以下功能:

  • 基于 AQL 布尔表达式和函数过滤文档
  • 匹配位于由快速索引支持的不同集合中的文档
  • 根据每个文档与搜索条件的匹配程度对结果集进行排序

请参阅ArangoSearch 视图,了解如何设置视图。

6.4.1 语法

SEARCH 关键字后面是 ArangoSearch 过滤器表达式,它主要由对 ArangoSearch AQL 函数的调用组成。

1
2
3
4
FOR doc IN viewName
SEARCH expression
OPTIONS { … }
...

6.4.2 用法

FILTER相比,SEARCH语句被视为FOR 操作的一部分,而不是单独的语句。它不能随意放置在查询中,也不能多次放置在 FOR 循环体中。FOR ... IN后面必须跟一个视图的名称,而不是一个集合。SEARCH操作必须紧随其后,该位置不允许在SEARCH 之前进行其他操作,如FILTERCOLLECT 等。然而,在SEARCH 和表达式之后可以进行后续操作,包括 SORT以根据 ArangoSearch 视图计算的排名值对搜索结果进行排序。

表达式必须是 ArangoSearch 表达式。在搜索和排序阶段,ArangoSearch 的全部功能都通过特殊的 ArangoSearch 功能来利用和公开。最重要的是,支持常见的 AQL 运算符。

请注意,SEARCH 不支持内联表达式和其他一些内容。如果表达式无效,服务器将引发查询错误。

OPTIONS 关键字和对象可以选择跟在搜索表达式之后以设置搜索选项。

6.4.2.1 逻辑运算符

逻辑或布尔运算符允许您组合多个搜索条件。

  • AND、(连词)&&
  • OR、(析取)||
  • NOT、(否定/反转)!

需要考虑运算符的优先级,可以用括号控制。

考虑以下人为的表达:

1
doc.value < 0 OR doc.value > 5 AND doc.value IN [-10, 10]

AND 的优先级高于 OR。 该表达式等效于:

1
doc.value < 0 OR (doc.value > 5 AND doc.value IN [-10, 10])

因此,条件是:

  • 小于 0 的值
  • 值大于 5,但仅当它为 10(或 -10,但这永远无法实现)

可以如下使用括号将 AND 条件应用于两个 OR 条件:

1
(doc.value < 0 OR doc.value > 5) AND doc.value IN [-10, 10]

现在的条件是:

  • 值小于 0,但仅当为 -10 时
  • 值大于 5,但仅当值为 10 时

6.4.2.2 比较运算符

  • ==(等于)
  • <=(小于或等于)
  • >=(大于或等于)
  • <(小于)
  • >(大于)
  • !=(不等)
  • IN(包含在数组或范围中),也NOT IN
  • LIKE(与 v3.7.0 中引入的通配符相同),还NOT LIKE
1
2
3
4
5
FOR doc IN viewName
SEARCH ANALYZER(doc.text == "quick" OR doc.text == "brown", "text_en")
// -- or --
SEARCH ANALYZER(doc.text IN ["quick", "brown"], "text_en")
RETURN doc

ArangoSearch 不考虑字符的字母顺序,即针对视图的 SEARCH 操作中的范围查询将不遵循定义的分析器区域设置或服务器语言(启动选项 —default-language)的语言规则!另请参阅已知问题--default-language

6.4.2.3 数组比较运算符

支持数组比较运算符(在 v3.6.0 中引入):

1
2
3
4
5
6
7
LET tokens = TOKENS("some input", "text_en")                 // ["some", "input"]
FOR doc IN myView SEARCH tokens ALL IN doc.text RETURN doc // dynamic conjunction
FOR doc IN myView SEARCH tokens ANY IN doc.text RETURN doc // dynamic disjunction
FOR doc IN myView SEARCH tokens NONE IN doc.text RETURN doc // dynamic negation
FOR doc IN myView SEARCH tokens ALL > doc.text RETURN doc // dynamic conjunction with comparison
FOR doc IN myView SEARCH tokens ANY <= doc.text RETURN doc // dynamic disjunction with comparison
FOR doc IN myView SEARCH tokens NONE < doc.text RETURN doc // dynamic negation with comparison

以下运算符在表达式中是等效的:SEARCH

  • ALL IN, , ,ALL ==``NONE !=``NONE NOT IN
  • ANY IN,ANY ==
  • NONE IN, , ,NONE ==``ALL !=``ALL NOT IN
  • ALL >,NONE <=
  • ALL >=,NONE <
  • ALL <,NONE >=
  • ALL <=,NONE >

运算符右侧引用的存储属性就像一个原始值。 在多个标记的情况下,即使实际的文档属性是一个数组,它就像有多个这样的值而不是一个值数组。 为便于使用,作为数组比较运算符的一部分的 IN 和 == 在 SEARCH 表达式中被视为相同。 在 SEARCH 之外的行为是不同的,其中 IN 需要跟随一个数组。

6.4.3 处理非索引字段

未配置为由视图索引的文档属性被 SEARCH 视为不存在。 这仅影响针对从视图发出的文档的测试。

例如,给定一个包含以下文档的集合 myCol:

1
2
{ "someAttr": "One", "anotherAttr": "One" }
{ "someAttr": "Two", "anotherAttr": "Two" }

带有一个视图,其中 someAttr 由以下视图 myView 索引

1
2
3
4
5
6
7
8
9
10
{
"type": "arangosearch",
"links": {
"myCol": {
"fields": {
"someAttr": {}
}
}
}
}

搜索 someAttr 会产生以下结果:

1
2
3
4
FOR doc IN myView
SEARCH doc.someAttr == "One"
RETURN doc
[ { "someAttr": "One", "anotherAttr": "One" } ]

对 anotherAttr 的搜索会产生一个空结果,因为只有 someAttr 被视图索引:

1
2
3
4
FOR doc IN myView
SEARCH doc.anotherAttr == "One"
RETURN doc
[]

如果需要,您可以使用特殊的 includeAllFields 视图属性来索引源文档的所有(子)字段。

6.4.4 使用排序进行搜索

从视图发出的文档可以使用标准 SORT()操作按属性值排序,使用一个或多个属性,按升序或降序(或两者混合)。

1
2
3
FOR doc IN viewName
SORT doc.text, doc.value DESC
RETURN doc

如果(最左边的)字段及其排序方向与视图的主要排序顺序定义匹配,则 SORT 操作将被优化掉。

除了简单的排序之外,还可以通过相关性分数(或者如果需要,可以结合分数和属性值)对匹配的 View 文档进行排序。 通过 SEARCH 关键字进行的文档搜索和通过 ArangoSearch 评分函数(即 BM25() 和 TFIDF())进行的排序密切相关。 SEARCH 表达式中给出的查询不仅用于过滤文档,还与评分函数一起使用来决定哪个文档与查询最匹配。 视图中的其他文档也会影响此决定。

因此,ArangoSearch 评分函数只能对从视图发出的文档起作用,因为会参考相应的 SEARCH 表达式和视图本身以对结果进行排序。

1
2
3
4
FOR doc IN viewName
SEARCH ...
SORT BM25(doc) DESC
RETURN doc

BOOST() 函数可用于通过对 SEARCH 中的子表达式进行不同加权来微调结果排名。

如果在调用评分函数之前没有 SEARCH 操作,或者如果搜索表达式没有过滤掉文档(例如 SEARCH true),那么将为所有文档返回 0 分。

6.4.5 搜索选项

该操作接受具有以下属性的选项对象:SEARCH

  • collections(数组,可选):具有集合名称的字符串数组,用于将搜索限制为某些源集合

  • conditionOptimization(字符串,可选):控制如何优化搜索条件(在 v3.6.2 中引入)。可能的值:

    • "auto"(默认值):将条件转换为析取法式 (DNF) 并应用优化。删除冗余或重叠条件,但即使对于少量嵌套条件,也可能需要相当长的时间。
    • "none":在不优化条件的情况下搜索索引。
  • countApproximate(字符串,可选):控制在为查询启用选项或执行子句时如何计算总行数(在 v3.7.6 中引入)fullCount COLLECT WITH COUNT

    • "exact"(默认值):实际上枚举行以进行精确计数。
    • "cost":使用基于成本的近似值。不枚举行并返回具有 O(1) 复杂度的近似结果。如果条件为空或仅包含单个术语查询(例如),则给出精确的结果,将视图的通常最终一致性放在一边。SEARCH``SEARCH doc.field == "value"

6.4.6 例子

给定一个包含三个链接集合 col1、col2 和 col3 的视图,可以仅从前两个集合中返回文档,而使用集合选项忽略第三个集合:

1
2
3
FOR doc IN viewName
SEARCH true OPTIONS { collections: ["coll1", "coll2"] }
RETURN doc

搜索表达式 true 匹配所有 View 文档。 您可以在此处使用任何有效表达式,同时将范围限制为所选的源集合。

6.5 SORT

SORT 语句将强制对当前块中已经产生的中间结果数组进行排序。SORT允许指定一个或多个排序标准和方向。

6.5.1 语法

一般语法为:

1
SORT expression direction

6.5.2 用法

按 lastName(升序)、firstName(升序)和 id(降序)排序的示例查询:

1
2
3
FOR u IN users
SORT u.lastName, u.firstName, u.id DESC
RETURN u

指定方向是可选的。 排序表达式的默认(隐式)方向是升序。 要明确指定排序方向,可以使用关键字 ASC(升序)和 DESC。 可以使用逗号分隔多个排序条件。 在这种情况下,方向是为每个表达式单独指定的。 例如

1
SORT doc.lastName, doc.firstName

将首先按姓氏升序排序文档,然后按名字升序排序。

1
SORT doc.lastName DESC, doc.firstName

将首先按姓氏降序排序文档,然后按名字升序排序。

1
SORT doc.lastName, doc.firstName DESC

将首先按姓氏升序排序文档,然后按名字降序排序。

迭代基于集合的数组时,文档的顺序始终未定义,除非使用 SORT 定义了显式排序顺序

常量SORT表达式可用于指示不需要特定的排序顺序

1
SORT null

常量 SORT 表达式将在优化期间被 AQL 优化器优化掉,但如果优化器不需要考虑任何特定的排序顺序,则明确指定它们可能会启用进一步的优化。 在COLLECT语句之后尤其如此,该语句应该产生排序结果。 在 COLLECT语句之后指定一个额外的 SORT null允许 AQL 优化器完全删除收集结果的后排序。 另请参阅 COLLECT选项方法。

6.6 LIMIT

LIMIT语句允许使用偏移量和计数对结果数组进行切片。 它将结果中的元素数量减少到最多指定的数量。

6.6.1 语法

两种一般形式是:LIMIT

1
2
LIMIT count
LIMIT offset, count

第一种形式只允许指定计数值,而第二种形式允许同时指定偏移量和计数。 第一种形式与使用偏移值为 0 的第二种形式相同。

6.6.2 用法

1
2
3
FOR u IN users
LIMIT 5
RETURN u

上面的查询返回 users 集合的前五个文档。 对于相同的结果,它也可以写为 LIMIT 0, 5。 它实际返回的文档是相当随意的,因为没有指定明确的排序顺序。 因此,一个限制通常应该伴随着一个 SORT操作。

偏移值指定应跳过结果中的多少个元素。 它必须为 0 或更大。 计数值指定结果中最多应包含多少个元素。

1
2
3
4
FOR u IN users
SORT u.firstName, u.lastName, u.id DESC
LIMIT 2, 5
RETURN u

在上面的例子中,用户的文档被排序,前两个结果被跳过并返回接下来的五个用户文档。

变量、表达式和子查询不能用于偏移和计数。 offset 和 count 的值必须在查询编译时已知,这意味着您只能使用可以在查询编译时解析的数字文字、绑定参数或表达式

LIMIT与查询中的其他操作相关的使用是有意义的。 特别是FILTER 之前的LIMIT 操作可以显着改变结果,因为这些操作是按照它们在查询中写入的顺序执行的。 有关详细示例,请参阅过滤器。

6.7 LET

LET语句可用于为变量分配任意值。 然后在放置 LET 语句的作用域中引入该变量。

6.7.1 语法

1
LET variableName = expression

表达式可以是简单的表达式或子查询。

对于允许的变量名称 AQL 语法。

6.7.2 用法

变量在 AQL 中是不可变的,这意味着它们不能被重新分配:

1
2
3
4
5
LET a = [1, 2, 3]  // initial assignment

a = PUSH(a, 4) // syntax error, unexpected identifier
LET a = PUSH(a, 4) // parsing error, variable 'a' is assigned multiple times
LET b = PUSH(a, 4) // allowed, result: [1, 2, 3, 4]

LET语句主要用于声明复杂的计算并避免在查询的多个部分重复计算相同的值。

1
2
3
4
5
6
7
FOR u IN users
LET numRecommendations = LENGTH(u.recommendations)
RETURN {
"user" : u,
"numRecommendations" : numRecommendations,
"isPowerUser" : numRecommendations >= 10
}

在上面的例子中,推荐数量的计算是使用 LET 语句计算出来的,从而避免在 RETURN语句中计算两次值。

LET的另一个用例是在子查询中声明复杂的计算,使整个查询更具可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FOR u IN users
LET friends = (
FOR f IN friends
FILTER u.id == f.userId
RETURN f
)
LET memberships = (
FOR m IN memberships
FILTER u.id == m.userId
RETURN m
)
RETURN {
"user" : u,
"friends" : friends,
"numFriends" : LENGTH(friends),
"memberShips" : memberships
}

6.8 COLLECT

COLLECT 操作可用于按一个或多个组标准对数据进行分组。 它还可以用于检索所有不同的值、计算值出现的频率以及有效地计算统计属性。

COLLECT 语句将消除当前作用域中的所有局部变量。 在COLLECT 之后,只有COLLECT本身引入的变量可用。

6.8.1 语法

COLLECT 操作有多种语法变体:

1
2
3
4
5
6
7
8
9
10
COLLECT variableName = expression
COLLECT variableName = expression INTO groupsVariable
COLLECT variableName = expression INTO groupsVariable = projectionExpression
COLLECT variableName = expression INTO groupsVariable KEEP keepVariable
COLLECT variableName = expression WITH COUNT INTO countVariable
COLLECT variableName = expression AGGREGATE variableName = aggregateExpression
COLLECT variableName = expression AGGREGATE variableName = aggregateExpression INTO groupsVariable
COLLECT AGGREGATE variableName = aggregateExpression
COLLECT AGGREGATE variableName = aggregateExpression INTO groupsVariable
COLLECT WITH COUNT INTO countVariable

所有变体都可以选择以OPTIONS { ... }子句结尾。

6.8.2 分组语法

COLLECT 的第一种语法形式仅按表达式中指定的已定义组标准对结果进行分组。 为了进一步处理 COLLECT 产生的结果,引入了一个新变量(由 variableName 指定)。 此变量包含组值。

这是一个示例查询,它在 u.city 中查找不同的值并使它们在变量 city 中可用:

1
2
3
4
5
FOR u IN users
COLLECT city = u.city
RETURN {
"city" : city
}

第二种形式与第一种形式相同,但另外引入了一个变量(由 groupVariable 指定),其中包含落入该组的所有元素。 其工作原理如下:groupsVariable 变量是一个数组,其中包含与组中的元素一样多的元素。 该数组的每个成员都是一个 JSON 对象,其中 AQL 查询中定义的每个变量的值都绑定到相应的属性。 请注意,这会考虑在 COLLECT 语句之前定义的所有变量,但不会考虑顶层(在任何 FOR 之外)的变量,除非 COLLECT 语句本身位于顶层,在这种情况下,所有变量都会被采用。 此外请注意,优化器可能会将 LET 语句移出 FOR 语句以提高性能。

1
2
3
4
5
6
FOR u IN users
COLLECT city = u.city INTO groups
RETURN {
"city" : city,
"usersInCity" : groups
}

在上面的例子中,数组 users 将按属性 city 分组。 结果是一个新的文档数组,每个不同的 u.city 值有一个元素。 每个城市的原始数组(此处:用户)中的元素在变量组中可用。 这是由于 INTO 子句。

COLLECT 还允许指定多个组标准。 单个组标准可以用逗号分隔:

1
2
3
4
5
6
7
FOR u IN users
COLLECT country = u.country, city = u.city INTO groups
RETURN {
"country" : country,
"city" : city,
"usersInCity" : groups
}

在上面的例子中,数组 users 先按国家分组,然后按城市分组,对于国家和城市的每个不同组合,将返回用户。

6.8.3 丢弃过时的变量

第三种形式的 COLLECT 允许使用任意的投影表达式重写 groupVariable 的内容:

1
2
3
4
5
6
7
FOR u IN users
COLLECT country = u.country, city = u.city INTO groups = u.name
RETURN {
"country" : country,
"city" : city,
"userNames" : groups
}

在上面的例子中,只有projectionExpression 是u.name。 因此,对于每个文档,仅将此属性复制到 groupsVariable 中。 这可能比将作用域中的所有变量复制到 groupVariable 中要高效得多,因为它会在没有投影表达式的情况下发生。

INTO 后面的表达式也可用于任意计算:

1
2
3
4
5
6
7
8
9
10
FOR u IN users
COLLECT country = u.country, city = u.city INTO groups = {
"name" : u.name,
"isActive" : u.status == "active"
}
RETURN {
"country" : country,
"city" : city,
"usersInCity" : groups
}

COLLECT 还提供了一个可选的 KEEP 子句,可用于控制将哪些变量复制到由 INTO 创建的变量中。 如果未指定 KEEP 子句,则范围内的所有变量都将作为子属性复制到 groupVariable 中。 这是安全的,但如果作用域中有许多变量或变量包含大量数据,则会对性能产生负面影响。

以下示例将复制到 groupVariable 的变量限制为仅名称。 作用域中也存在的变量 u 和 someCalculation 不会被复制到 groupsVariable 中,因为它们没有列在 KEEP 子句中:

1
2
3
4
5
6
7
8
FOR u IN users
LET name = u.name
LET someCalculation = u.value1 + u.value2
COLLECT city = u.city INTO groups KEEP name
RETURN {
"city" : city,
"userNames" : groups[*].name
}

KEEP 仅与 INTO 结合使用才有效。 在 KEEP 子句中只能使用有效的变量名。 KEEP 支持指定多个变量名。

6.8.4 组长度计算

COLLECT 还提供了一个特殊的WITH COUNT子句,可用于有效地确定组成员的数量。

最简单的形式只返回进入COLLECT的项目数:

1
2
3
FOR u IN users
COLLECT WITH COUNT INTO length
RETURN length

以上等效于,但效率低于:

1
RETURN LENGTH(users)

WITH COUNT子句还可用于有效计算每个组中的项目数:

1
2
3
4
5
6
FOR u IN users
COLLECT age = u.age WITH COUNT INTO length
RETURN {
"age" : age,
"count" : length
}

WITH COUNT子句只能与 INTO 子句一起使用。

6.8.5 集合体

COLLECT 语句可用于按组执行数据聚合。 为了仅确定组长度,可以使用 COLLECTWITH COUNT INTO变体,如前所述。

对于其他聚合,可以在 COLLECT 结果上运行聚合函数:

1
2
3
4
5
6
7
FOR u IN users
COLLECT ageGroup = FLOOR(u.age / 5) * 5 INTO g
RETURN {
"ageGroup" : ageGroup,
"minAge" : MIN(g[*].u.age),
"maxAge" : MAX(g[*].u.age)
}

然而,上述要求在所有组的收集操作期间存储所有组值,这可能是低效的。

COLLECT 的特殊 AGGREGATE 变体允许在收集操作期间以增量方式构建聚合值,因此通常更有效。

使用 AGGREGATE变体,上述查询变为:

1
2
3
4
5
6
7
8
FOR u IN users
COLLECT ageGroup = FLOOR(u.age / 5) * 5
AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
RETURN {
ageGroup,
minAge,
maxAge
}

AGGREGATE关键字只能在 COLLECT关键字之后使用。 如果使用,它必须直接跟在分组键的声明之后。 如果没有使用分组键,则必须直接跟在COLLECT 关键字之后:

1
2
3
4
5
6
FOR u IN users
COLLECT AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
RETURN {
minAge,
maxAge
}

每个 AGGREGATE 赋值的右侧只允许特定的表达式:

  • 在顶层,聚合表达式必须是对支持的聚合函数之一的调用:
    • LENGTH() / COUNT()
    • MIN()
    • MAX()
    • SUM()
    • AVERAGE() / AVG()
    • STDDEV_POPULATION() / STDDEV()
    • STDDEV_SAMPLE()
    • VARIANCE_POPULATION() / VARIANCE()
    • VARIANCE_SAMPLE()
    • UNIQUE()
    • SORTED_UNIQUE()
    • COUNT_DISTINCT() / COUNT_UNIQUE()
    • BIT_AND()
    • BIT_OR()
    • BIT_XOR()
  • 聚合表达式不得引用 COLLECT 本身引入的变量

6.8.6 COLLECTRETURN DISTINCT

为了使结果集唯一,可以使用COLLECTRETURN DISTINCT

1
2
3
4
5
FOR u IN users
RETURN DISTINCT u.age
FOR u IN users
COLLECT age = u.age
RETURN age

在幕后,这两种变体都创建了一个 CollectNode。 但是,它们使用具有不同属性的 COLLECT 的不同实现:

  • RETURN DISTINCT 维护结果的顺序,但仅限于单个值。

  • COLLECT 更改结果的顺序(已排序或未定义),但它支持多个值并且比 RETURN DISTINCT 更灵活。

除了 COLLECT 复杂的分组和聚合功能之外,它还允许您在 RETURN 之前放置一个 LIMIT 操作以可能提前停止 COLLECT 操作。

method

优化器可以选择 COLLECT 的两种变体:排序变体和散列变体。 可以在 COLLECT 语句中使用 method 选项来通知优化器关于首选方法,“排序”或“散列”。

1
COLLECT ... OPTIONS { method: "sorted" }

如果用户未指定任何方法,则优化器将创建一个使用排序方法的计划,如果 COLLECT 语句符合条件,则优化器将创建一个使用散列方法的附加计划。

如果该方法显式设置为 sorted,那么优化器将始终使用 COLLECT 的 sorted 变体,甚至不使用 hash 变体创建计划。如果它显式设置为散列,则优化器将仅在 COLLECT 语句符合条件时使用散列方法创建计划。并非所有 COLLECT 语句都可以使用散列方法,尤其是带有 INTO 子句的语句不符合条件。如果 COLLECT 语句符合条件,则将只有一个使用散列方法的计划。否则,优化器将默认使用 sorted 方法。

sorted 方法要求其输入按 COLLECT 子句中指定的组标准进行排序。为了保证结果的正确性,优化器会自动在查询语句前插入一个 SORT 操作。如果在组标准上存在排序索引,优化器可能能够稍后优化掉该 SORT 操作。

如果 COLLECT 语句有资格使用散列变量,优化器将在规划阶段开始时为其创建一个额外的计划。在这个计划中,不会在 COLLECT 前面添加额外的 SORT 语句。这是因为 COLLECT 的散列变体不需要排序输入。相反,将在 COLLECT 之后添加 SORT 语句以对其输出进行排序。这个 SORT 语句可能会在后面的阶段再次优化掉。

如果 COLLECT 的排序顺序与用户无关,则在 COLLECT 之后添加额外的 SORT null 指令将允许优化器完全删除排序:

1
2
3
4
FOR u IN users
COLLECT age = u.age
SORT null /* note: will be optimized away */
RETURN age

如果没有明确设置首选方法,优化器使用哪个 COLLECT 变体取决于优化器的成本估算。具有不同 COLLECT 变体的创建计划将通过常规优化管道传送。最后,优化器会像往常一样选择估计总成本最低的计划。

通常,在组标准上存在排序索引的情况下,应首选 COLLECT 的排序变体。在这种情况下,优化器可以消除 COLLECT 之前的 SORT 操作,这样就不会留下 SORT。

如果在组标准上没有可用的排序索引,排序变体所需的预先排序可能会很昂贵。在这种情况下,优化器可能更喜欢 COLLECT 的散列变体,它不需要对其输入进行排序。

通过查看查询的执行计划,特别是 CollectNode 的注释,可以确定实际使用 COLLECT 的哪个变体:

1
2
3
4
5
6
7
8
9

Execution plan:
Id NodeType Est. Comment
1 SingletonNode 1 * ROOT
2 EnumerateCollectionNode 5 - FOR doc IN coll /* full collection scan, projections: `name` */
3 CalculationNode 5 - LET #2 = doc.`name` /* attribute expression */ /* collections used: doc : coll */
4 CollectNode 5 - COLLECT name = #2 /* hash */
6 SortNode 5 - SORT name ASC /* sorting strategy: standard */
5 ReturnNode 5 - RETURN name

6.9 WINDOW

使用滑动窗口聚合相邻文档或值范围以计算运行总计、滚动平均值和其他统计属性

WINDOW 操作可用于聚合相邻文档,或者换言之,前行和/或后行。它还可以基于相对于文档属性的值或持续时间范围进行聚合。

该操作对一组查询行执行类似 COLLECT AGGREGATE 的操作。但是,COLLECT 操作将多个查询行分组到一个结果组中,而 WINDOW 操作为每个查询行生成一个结果:

  • 发生函数求值的行称为当前行。
  • 与发生函数求值的当前行相关的查询行构成当前行的窗口框架。

窗口框架是根据当前行确定的:

  • 通过将窗口框架定义为从查询开始到当前行的所有行,您可以计算每行的运行总计。
  • 通过将框架定义为在当前行的任一侧扩展 N 行,您可以计算滚动平均值。

6.9.1 语法

WINDOW 操作有两种语法变体。

基于行(相邻文档):

1
WINDOW { preceding: numPrecedingRows, following: numFollowingRows } AGGREGATE variableName = aggregateExpression

基于范围(值或持续时间范围):

1
2

WINDOW rangeValue WITH { preceding: offsetPreceding, following: offsetFollowing } AGGREGATE variableName = aggregateExpression

聚合表达式支持对以下函数的调用:

  • LENGTH() / COUNT()
  • MIN()
  • MAX()
  • SUM()
  • AVERAGE() / AVG()
  • STDDEV_POPULATION() / STDDEV()
  • STDDEV_SAMPLE()
  • VARIANCE_POPULATION() / VARIANCE()
  • VARIANCE_SAMPLE()
  • UNIQUE()
  • SORTED_UNIQUE()
  • COUNT_DISTINCT() / COUNT_UNIQUE()
  • BIT_AND()
  • BIT_OR()
  • BIT_XOR()

6.9.2 基于行的聚合

WINDOW 的第一种语法形式允许聚合固定数量的行,在当前行之后或之前。 还可以定义所有前面或后面的行都应该聚合(“无界”)。 必须在查询编译时确定行数。

下面的查询演示了使用窗口框架来计算运行总计以及从当前行和紧接其前后的行计算的滚动平均值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FOR t IN observations
SORT t.time
WINDOW { preceding: 1, following: 1 }
AGGREGATE rollingAverage = AVG(t.val), rollingSum = SUM(t.val)
WINDOW { preceding: "unbounded", following: 0}
AGGREGATE cumulativeSum = SUM(t.val)
RETURN {
time: t.time,
subject: t.subject,
val: t.val,
rollingAverage, // average of the window's values
rollingSum, // sum of the window's values
cumulativeSum // running total
}

行顺序由时间属性上的 SORT 操作控制。

第一个 WINDOW 操作聚合前一行、当前行和下一行(前一行和后一行设置为 1)并计算这三个值的平均值和总和。 在第一行的情况下,没有前一行而是后一行,因此将值 10 和 0 相加计算总和,再除以 2 计算平均值。 对于第二行,将值 10、0 和 9 相加并除以 3,依此类推。

第二个 WINDOW 操作聚合所有先前的值(无界)以计算运行总和。 第一行就是 10,第二行是 10 + 0,第三行是 10 + 0 + 9,以此类推。

时间 主题 val rollingAverage rollingSum cumulativeSum
2021-05-25 07:00:00 st113 10 5 10 10
2021-05-25 07:00:00 xh458 0 6.333… 19 10
2021-05-25 07:15:00 st113 9 6.333… 19 19
2021-05-25 07:15:00 xh458 10 14.666… 44 29
2021-05-25 07:30:00 st113 25 13.333… 40 54
2021-05-25 07:30:00 xh458 5 16.666… 50 59
2021-05-25 07:45:00 st113 20 18.333… 55 79
2021-05-25 07:45:00 xh458 30 25 75 109
2021-05-25 08:00:00 xh458 25 27.5 55 134

下面的查询演示了使用窗口框架来计算按时间排序的查询行的每个主题组内的运行总计,以及从当前行和紧接其前后的行计算的滚动总和和平均值,也是每个主题组 并按时间排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

FOR t IN observations
COLLECT subject = t.subject INTO group = t
LET subquery = (FOR t2 IN group
SORT t2.time
WINDOW { preceding: 1, following: 1 }
AGGREGATE rollingAverage = AVG(t2.val), rollingSum = SUM(t2.val)
WINDOW { preceding: "unbounded", following: 0 }
AGGREGATE cumulativeSum = SUM(t2.val)
RETURN {
time: t2.time,
subject: t2.subject,
val: t2.val,
rollingAverage,
rollingSum,
cumulativeSum
}
)
// flatten subquery result
FOR t2 IN subquery
RETURN t2

如果您查看主题为 xh458 的第一行,则可以看到累积总和已重置,并且滚动平均值和总和未考虑属于主题 st113 的前一行。

时间 主题 val rollingAverage rollingSum cumulativeSum
2021-05-25 07:00:00 st113 10 9.5 19 10
2021-05-25 07:15:00 st113 9 14.666… 44 19
2021-05-25 07:30:00 st113 25 18 54 44
2021-05-25 07:45:00 st113 20 22.5 45 64
2021-05-25 07:00:00 xh458 0 5 10 0
2021-05-25 07:15:00 xh458 10 5 15 10
2021-05-25 07:30:00 xh458 5 15 45 15
2021-05-25 07:45:00 xh458 30 20 60 45
2021-05-25 08:00:00 xh458 25 27.5 55 70

6.9.3 基于范围的聚合

WINDOW 的第二种语法形式允许聚合一个值范围内的所有文档。偏移量是与当前文档的属性值的差异。

属性值必须是数字。偏移量计算是通过添加或减去在以下和前面的属性中指定的数字偏移量来执行的。偏移量必须为正数,并且必须在查询编译时确定。默认偏移量为 0。

基于范围的窗口语法要求输入行按行值排序。为了保证结果的正确性,AQL 优化器会自动在 WINDOW 语句前面的查询中插入 SORT 语句。如果在组标准上存在排序索引,优化器可以稍后优化掉该 SORT 语句。

以下查询演示了如何使用窗口框架来计算从当前文档和 t.val 中属性值在 [-10, +5](含)范围内的文档中计算出的总数和平均值,前面和后面:

1
2
3
4
5
6
7
8
9
10
11

FOR t IN observations
WINDOW t.val WITH { preceding: 10, following: 5 }
AGGREGATE rollingAverage = AVG(t.val), rollingSum = SUM(t.val)
RETURN {
time: t.time,
subject: t.subject,
val: t.val,
rollingAverage,
rollingSum
}

第一行的取值范围是[-10, 5],因为val为0,因此第一行和第二行的值相加为5,平均值为2.5。 最后一行的取值范围是 [20, 35],因为 val 是 30,这意味着最后四行的总和为 100,平均值为 25(范围包括在内,即 val 落在具有 值为 20)。

时间 主题 val rollingAverage rollingSum
2021-05-25 07:00:00 xh458 0 2.5 5
2021-05-25 07:30:00 xh458 5 6.8 34
2021-05-25 07:15:00 st113 9 6.8 34
2021-05-25 07:00:00 st113 10 6.8 34
2021-05-25 07:15:00 xh458 10 6.8 34
2021-05-25 07:45:00 st113 20 18 90
2021-05-25 07:30:00 st113 25 25 100
2021-05-25 08:00:00 xh458 25 25 100
2021-05-25 07:45:00 xh458 30 25 100

6.9.4 基于持续时间的聚合

按时间间隔聚合是基于范围的聚合的一个子类型,它使用 WINDOW 的第二种语法形式,但具有 ISO 持续时间。

为了支持时间序列数据上的 WINDOW 帧,WINDOW 操作可以使用正的 ISO 8601 持续时间字符串计算时间戳偏移,如 P1Y6M(1 年零 6 个月)或 PT12H30M(12 小时 30 分钟)。另请参阅日期函数。与 ISO 8601 标准相反,周组件可以与其他组件自由组合。例如,P1WT1H 和 P1M1W 都是有效的。小数值仅支持秒,并且最多只能在分隔符后保留三位小数,即毫秒精度。例如,PT0.123S 是有效时长,而 PT0.5H 和 PT0.1234S 不是。

持续时间可以在后面和前面单独指定。如果使用这样的持续时间,则当前文档的属性值必须是一个数字,并被视为以毫秒为单位的数字时间戳。范围包括在内。如果未指定任一界限,则将其视为空持续时间(即 P0D)。

以下查询演示了如何使用窗口框架根据从日期时间字符串转换为数字时间戳的文档属性时间计算过去 30 分钟(含)内观察的滚动总和和平均值:

1
2
3
4
5
6
7
8
9
10
11

FOR t IN observations
WINDOW DATE_TIMESTAMP(t.time) WITH { preceding: "PT30M" }
AGGREGATE rollingAverage = AVG(t.val), rollingSum = SUM(t.val)
RETURN {
time: t.time,
subject: t.subject,
val: t.val,
rollingAverage,
rollingSum
}

时间为 07:30:00,同一天从 07:00:00 到 07:30:00 的所有内容都在前面的持续时间范围内:“PT30M”,因此将前六行聚合为 59 平均为 9.8333 ……

时间 主题 val rollingAverage rollingSum
2021-05-25 07:00:00 st113 10 5 10
2021-05-25 07:00:00 xh458 0 5 10
2021-05-25 07:15:00 st113 9 7.25 29
2021-05-25 07:15:00 xh458 10 7.25 29
2021-05-25 07:30:00 st113 25 9.8333… 59
2021-05-25 07:30:00 xh458 5 9.8333… 59
2021-05-25 07:45:00 st113 20 16.5 99
2021-05-25 07:45:00 xh458 30 16.5 99
2021-05-25 08:00:00 xh458 25 21 105

6.10 REMOVE

REMOVE关键字可用于从集合中删除文档。

每个REMOVE操作仅限于单个集合,并且集合名称不能是动态的。 每个 AQL 查询只允许每个集合有一个 REMOVE语句,并且后面不能跟访问同一集合的读或写操作、遍历操作或可以读取文档的 AQL 函数。

6.10.1 语法

删除操作的语法为:

1
REMOVE keyExpression IN collection

它可以选择以 OPTIONS { … } 子句结尾。

集合必须包含要从中删除文档的集合的名称。 keyExpression 必须是包含文档标识的表达式。 这可以是一个字符串(它必须包含文档键)或一个文档,它必须包含一个 _key 属性。

因此,以下查询是等效的:

1
2
3
4
5
6
FOR u IN users
REMOVE { _key: u._key } IN users
FOR u IN users
REMOVE u._key IN users
FOR u IN users
REMOVE u IN users

删除操作可以删除任意文档,并且文档不需要与前面 FOR语句生成的文档相同:

1
2
3
4
5
FOR i IN 1..1000
REMOVE { _key: CONCAT('test', i) } IN users
FOR u IN users
FILTER u.active == false
REMOVE { _key: u._key } IN backup

也可以使用文档键字符串或具有 _key 属性的文档删除单个文档:

1
2
3
REMOVE 'john' IN users
LET doc = DOCUMENT('users/john')
REMOVE doc IN users

每个查询和集合的单个删除操作的限制适用。 由于第三次删除操作,以下查询导致数据修改后访问错误:

1
2
3
REMOVE 'john' IN users
REMOVE 'john' IN backups // OK, different collection
REMOVE 'mary' IN users // Error, users collection again

6.10.2 查询选项

6.10.2.1 ignoreErrors

ignoreErrors 可用于抑制在尝试删除不存在的文档时可能发生的查询错误。 例如,如果要删除的文档之一不存在,则以下查询将失败:

1
2
FOR i IN 1..1000
REMOVE { _key: CONCAT('test', i) } IN users

通过指定 ignoreErrors 查询选项,可以抑制这些错误以便查询完成:

1
2
FOR i IN 1..1000
REMOVE { _key: CONCAT('test', i) } IN users OPTIONS { ignoreErrors: true }

6.10.2.2 waitForSync

为了确保在查询返回时数据已写入磁盘,有 waitForSync 查询选项

1
2
FOR i IN 1..1000
REMOVE { _key: CONCAT('test', i) } IN users OPTIONS { waitForSync: true }

6.10.2.3 ignoreRevs

为了不意外删除自上次获取以来已更新的文档,您可以使用选项 ignoreRevs 来让 ArangoDB 比较 _rev 值并且仅在它们仍然匹配时才成功,或者让 ArangoDB 忽略它们(默认):

1
2
FOR i IN 1..1000
REMOVE { _key: CONCAT('test', i), _rev: "1287623" } IN users OPTIONS { ignoreRevs: false }

6.10.2.4 exclusive

RocksDB 引擎不需要集合级锁。 同一个集合上的不同写操作不会相互阻塞,只要同一个文档上不存在写-写冲突。 从应用程序开发的角度来看,可能需要对集合具有独占写入权限,以简化开发。 请注意,写入不会阻止 RocksDB 中的读取。 独占访问还可以加速修改查询,因为我们避免了冲突检查。

使用exclusive选项在每个查询的基础上实现这种效果:

1
2
3
4
FOR doc IN collection
REPLACE doc._key
WITH { replaced: true }
OPTIONS { exclusive: true }

6.10.3 返回已删除的文档

删除的文档也可以由查询返回。 在这种情况下,REMOVE 语句后面必须跟一个 RETURN 语句(也允许中间的 LET 语句)。REMOVE引入伪值 OLD来引用已删除的文档:

1
REMOVE keyExpression IN collection options RETURN OLD

以下是使用名为removed的变量来捕获已移除文档的示例。 对于每个删除的文档,将返回文档密钥。

1
2
3
4
FOR u IN users
REMOVE u IN users
LET removed = OLD
RETURN removed._key

6.10.4 事务性

在单个服务器上,文档删除以全有或全无的方式以事务方式执行。

如果使用 RocksDB 引擎并启用中间提交,则查询可能会执行中间事务提交,以防正在运行的事务(AQL 查询)达到指定的大小阈值。 在这种情况下,到目前为止执行的查询操作将被提交,并且不会在以后中止/回滚的情况下回滚。 该行为可以通过调整 RocksDB 引擎的中间提交设置来控制。

对于分片集合,整个查询和/或删除操作可能不是事务性的,尤其是当它涉及不同的分片和/或数据库服务器时。

6.11 UPDATE

UPDATE关键字可用于部分更新集合中的文档。

每个UPDATE 操作仅限于单个集合,并且集合名称不能是动态的。 每个 AQL 查询只允许每个集合有一个UPDATE 语句,并且后面不能跟访问同一集合的读或写操作、遍历操作或可以读取文档的 AQL 函数。 系统属性_id_key_rev不能更新,_from_to可以

6.11.1 语法

更新操作的两种语法是:

1
2
UPDATE document IN collection
UPDATE keyExpression WITH document IN collection

两种变体都可以选择以 OPTIONS { … } 子句结尾。

集合必须包含应更新文档的集合的名称。 文档必须是包含要更新的属性和值的文档。 使用第一种语法时,文档还必须包含_key 属性以标识要更新的文档。

1
2
FOR u IN users
UPDATE { _key: u._key, name: CONCAT(u.firstName, " ", u.lastName) } IN users

以下查询无效,因为它不包含_key属性,因此无法确定要更新的文档:

1
2
FOR u IN users
UPDATE { name: CONCAT(u.firstName, " ", u.lastName) } IN users

使用第二种语法时,keyExpression 提供文档标识。 这可以是一个字符串(它必须包含文档键)或一个文档,它必须包含一个 _key属性。

具有 _id属性但没有 _key属性的对象以及文档 ID 作为字符串(如“users/john”)不起作用。 但是,您可以使用 DOCUMENT(id)通过其ID获取文档,并使用PARSE_IDENTIFIER(id).key以字符串形式获取文档密钥。

以下查询是等效的:

1
2
3
4
5
6
FOR u IN users
UPDATE u._key WITH { name: CONCAT(u.firstName, " ", u.lastName) } IN users
FOR u IN users
UPDATE { _key: u._key } WITH { name: CONCAT(u.firstName, " ", u.lastName) } IN users
FOR u IN users
UPDATE u WITH { name: CONCAT(u.firstName, " ", u.lastName) } IN users

更新操作可以更新任意文档,这些文档不需要与前面的 FOR 语句生成的文档相同:

1
2
3
4
5
FOR i IN 1..1000
UPDATE CONCAT('test', i) WITH { foobar: true } IN users
FOR u IN users
FILTER u.active == false
UPDATE u WITH { status: 'inactive' } IN backup

6.11.2 使用文档属性的当前值

WITH子句中不支持伪变量OLD(它在UPDATE之后可用)。 要访问当前属性值,通常可以通过 FOR循环的变量来引用文档,该变量用于迭代集合:

1
2
3
4
FOR doc IN users
UPDATE doc WITH {
fullName: CONCAT(doc.firstName, " ", doc.lastName)
} IN users

如果没有循环,因为只更新单个文档,那么可能没有像上面(doc)这样的变量,它可以让您引用正在更新的文档:

1
2
3
UPDATE "john" WITH { ... } IN users
LET key = PARSE_IDENTIFIER("users/john").key
UPDATE key WITH { ... } IN users

要在这种情况下访问当前值,必须首先检索文档并将其存储在变量中:

1
2
3
4
LET doc = DOCUMENT("users/john")
UPDATE doc WITH {
fullName: CONCAT(doc.firstName, " ", doc.lastName)
} IN users

可以通过这种方式根据其当前值修改现有属性,以增加计数器,例如:

1
2
3
UPDATE doc WITH {
karma: doc.karma + 1
} IN users

如果属性 karma 还不存在,则 doc.karma 被评估为 null。 表达式 null + 1 导致将新属性 karma 设置为 1。如果该属性确实存在,则将其增加 1。

数组当然也可以改变:

1
2
3
UPDATE doc WITH {
hobbies: PUSH(doc.hobbies, "swimming")
} IN users

如果属性 hobbies 尚不存在,则方便地将其初始化为 [ “swimming” ] ,否则扩展为

6.11.3 查询选项

您可以选择为 UPDATE 操作设置查询选项:

1
UPDATE ... IN users OPTIONS { ... }

6.11.3.1 ignoreErrors

ignoreErrors 可用于抑制在尝试更新不存在的文档或违反唯一键约束时可能发生的查询错误:

1
2
3
4
5
6
FOR i IN 1..1000
UPDATE {
_key: CONCAT('test', i)
} WITH {
foobar: true
} IN users OPTIONS { ignoreErrors: true }

更新操作只会更新文档中指定的属性,其他属性保持不变。 内部属性(例如 _id_key_rev_from_to)无法更新,并且在文档中指定时会被忽略。 更新文档将使用服务器生成的值修改文档的修订号。

6.11.3.2 keepNull

更新具有空值的属性时,ArangoDB 不会从文档中删除该属性,而是为其存储空值。 要在更新操作中删除属性,请将它们设置为 null 并提供 keepNull 选项:

1
2
3
4
5
FOR u IN users
UPDATE u WITH {
foobar: true,
notNeeded: null
} IN users OPTIONS { keepNull: false }

上述查询将从文档中删除 notNeeded 属性并正常更新 foobar 属性。

6.11.3.3 mergeObjects

如果 UPDATE 查询和要更新的文档中都存在对象属性,则选项 mergeObjects 控制是否合并对象内容。

以下查询将更新文档的 name 属性设置为与查询中指定的完全相同的值。 这是因为 mergeObjects 选项被设置为 false:

1
2
3
4
FOR u IN users
UPDATE u WITH {
name: { first: "foo", middle: "b.", last: "baz" }
} IN users OPTIONS { mergeObjects: false }

相反,以下查询会将原始文档中 name 属性的内容与查询中指定的值合并:

1
2
3
4
FOR u IN users
UPDATE u WITH {
name: { first: "foo", middle: "b.", last: "baz" }
} IN users OPTIONS { mergeObjects: true }

现在将保留在要更新的文档中但不在查询中的 name 属性。 两者中都存在的属性将被查询中指定的值覆盖。

注意:mergeObjects 的默认值为 true,因此无需明确指定。

6.11.3.4 waitForSync

为了确保更新查询返回时数据是持久的,有 waitForSync 查询选项:

1
2
3
4
FOR u IN users
UPDATE u WITH {
foobar: true
} IN users OPTIONS { waitForSync: true }

6.11.3.5 ignoreRevs

为了不意外覆盖自上次获取它们以来已更新的文档,您可以使用选项 ignoreRevs 来让 ArangoDB 比较 _rev 值并且仅在它们仍然匹配时才成功,或者让 ArangoDB 忽略它们(默认):

1
2
3
4
FOR i IN 1..1000
UPDATE { _key: CONCAT('test', i), _rev: "1287623" }
WITH { foobar: true } IN users
OPTIONS { ignoreRevs: false }

6.11.3.6 exclusive

RocksDB 引擎不需要集合级锁。 同一个集合上的不同写操作不会相互阻塞,只要同一个文档上不存在写-写冲突。 从应用程序开发的角度来看,可能需要对集合具有独占写入权限,以简化开发。 请注意,写入不会阻止 RocksDB 中的读取。 独占访问还可以加速修改查询,因为我们避免了冲突检查。

使用exclusive选项在每个查询的基础上实现这种效果:

1
2
3
4
FOR doc IN collection
UPDATE doc
WITH { updated: true } IN collection
OPTIONS { exclusive: true }

6.11.4 返回已修改的文档

修改后的文档也可以由查询返回。 在这种情况下,UPDATE 语句需要跟在 RETURN 语句之后(也允许中间的 LET 语句)。 这些语句可以引用伪值 OLD 和 NEW。 OLD 伪值是指更新前的文档修订,NEW 是指更新后的文档修订。

OLD 和 NEW 都将包含所有文档属性,即使是那些未在更新表达式中指定的属性。

1
2
3
4
UPDATE document IN collection options RETURN OLD
UPDATE document IN collection options RETURN NEW
UPDATE keyExpression WITH document IN collection options RETURN OLD
UPDATE keyExpression WITH document IN collection options RETURN NEW

以下是使用名为 previous 的变量在修改前捕获原始文档的示例。 对于每个修改过的文档,返回文档密钥。

1
2
3
4
5
FOR u IN users
UPDATE u WITH { value: "test" }
IN users
LET previous = OLD
RETURN previous._key

以下查询使用 NEW 伪值返回更新的文档,不包含某些系统属性:

1
2
3
4
5
FOR u IN users
UPDATE u WITH { value: "test" }
IN users
LET updated = NEW
RETURN UNSET(updated, "_key", "_id", "_rev")

也可以同时返回 OLD 和 NEW:

1
2
3
4
FOR u IN users
UPDATE u WITH { value: "test" }
IN users
RETURN { before: OLD, after: NEW }

6.11.5 事务性

在单个服务器上,更新以全有或全无的方式以事务方式执行。

如果使用 RocksDB 引擎并启用中间提交,则查询可能会执行中间事务提交,以防正在运行的事务(AQL 查询)达到指定的大小阈值。 在这种情况下,到目前为止执行的查询操作将被提交,并且不会在以后中止/回滚的情况下回滚。 该行为可以通过调整 RocksDB 引擎的中间提交设置来控制。

对于分片集合,整个查询和/或更新操作可能不是事务性的,尤其是当它涉及不同的分片和/或 DB-Server 时。

6.12 REPLACE

REPLACE关键字可用于完全替换集合中的文档。

每个 REPLACE 操作仅限于单个集合,并且集合名称不能是动态的。 每个 AQL 查询只允许每个集合有一个REPLACE语句,并且后面不能跟访问同一集合的读或写操作、遍历操作或可以读取文档的 AQL 函数。 系统属性_id_key_rev 不能替换,_from_to 可以。

6.12.1 语法

替换操作的两种语法是:

1
2
REPLACE document IN collection
REPLACE keyExpression WITH document IN collection

两种变体都可以选择以 OPTIONS { … } 子句结尾。

集合必须包含应替换文档的集合的名称。 文档是替换文档。 使用第一种语法时,文档还必须包含_key 属性以标识要替换的文档。

1
2
FOR u IN users
REPLACE { _key: u._key, name: CONCAT(u.firstName, u.lastName), status: u.status } IN users

以下查询无效,因为它不包含_key属性,因此无法确定要替换的文档:

1
2
FOR u IN users
REPLACE { name: CONCAT(u.firstName, u.lastName, status: u.status) } IN users

使用第二种语法时,keyExpression 提供文档标识。 这可以是一个字符串(它必须包含文档键)或一个文档,它必须包含一个_key属性。

以下查询是等效的:

1
2
3
4
5
6
7
8
FOR u IN users
REPLACE { _key: u._key, name: CONCAT(u.firstName, u.lastName) } IN users
FOR u IN users
REPLACE u._key WITH { name: CONCAT(u.firstName, u.lastName) } IN users
FOR u IN users
REPLACE { _key: u._key } WITH { name: CONCAT(u.firstName, u.lastName) } IN users
FOR u IN users
REPLACE u WITH { name: CONCAT(u.firstName, u.lastName) } IN users

替换将完全替换现有文档,但不会修改内部属性(例如_id_key_from_to)的值。 替换文档将使用服务器生成的值修改文档的修订号。

替换操作可以更新任意文档,这些文档不需要与前面的 FOR 语句生成的文档相同:

1
2
3
4
5
FOR i IN 1..1000
REPLACE CONCAT('test', i) WITH { foobar: true } IN users
FOR u IN users
FILTER u.active == false
REPLACE u WITH { status: 'inactive', name: u.name } IN backup

6.12.2 查询选项

6.12.2.1 ignoreErrors

ignoreErrors 可用于抑制在尝试替换不存在的文档或违反唯一键约束时可能发生的查询错误:

1
2
FOR i IN 1..1000
REPLACE { _key: CONCAT('test', i) } WITH { foobar: true } IN users OPTIONS { ignoreErrors: true }

6.12.2.2 waitForSync

为了确保替换查询返回时数据是持久的,有 waitForSync 查询选项:

1
2
FOR i IN 1..1000
REPLACE { _key: CONCAT('test', i) } WITH { foobar: true } IN users OPTIONS { waitForSync: true }

6.12.2.3 ignoreRevs

为了不意外覆盖自上次获取它们以来已更新的文档,您可以使用选项 ignoreRevs 来让 ArangoDB 比较 _rev 值并且仅在它们仍然匹配时才成功,或者让 ArangoDB 忽略它们(默认):

1
2
FOR i IN 1..1000
REPLACE { _key: CONCAT('test', i), _rev: "1287623" } WITH { foobar: true } IN users OPTIONS { ignoreRevs: false }

6.12.3.4 exclusive

RocksDB 引擎不需要集合级锁。 同一个集合上的不同写操作不会相互阻塞,只要同一个文档上不存在写-写冲突。 从应用程序开发的角度来看,可能需要对集合具有独占写入权限,以简化开发。 请注意,写入不会阻止 RocksDB 中的读取。 独占访问还可以加速修改查询,因为我们避免了冲突检查。

使用exclusive选项在每个查询的基础上实现这种效果:

1
2
3
4
FOR doc IN collection
REPLACE doc._key
WITH { replaced: true } IN collection
OPTIONS { exclusive: true }

6.12.4 返回已修改的文档

修改后的文档也可以由查询返回。 在这种情况下,REPLACE 语句后面必须跟一个 RETURN 语句(也允许中间的 LET 语句)。 OLD 伪值可用于指代替换前的文档修订,而 NEW 指代替换后的文档修订。

OLD 和 NEW 都将包含所有文档属性,即使是那些没有在替换表达式中指定的属性。

1
2
3
4
REPLACE document IN collection options RETURN OLD
REPLACE document IN collection options RETURN NEW
REPLACE keyExpression WITH document IN collection options RETURN OLD
REPLACE keyExpression WITH document IN collection options RETURN NEW

以下是使用名为 previous 的变量返回修改前的原始文档的示例。 对于每个替换的文档,将返回文档密钥:

1
2
3
4
5
FOR u IN users
REPLACE u WITH { value: "test" }
IN users
LET previous = OLD
RETURN previous._key

以下查询使用 NEW 伪值返回被替换的文档(没有它们的一些系统属性):

1
2
3
4
FOR u IN users
REPLACE u WITH { value: "test" } IN users
LET replaced = NEW
RETURN UNSET(replaced, '_key', '_id', '_rev')

6.12.5 事务性

在单个服务器上,替换操作以全有或全无的方式在事务中执行。

如果使用 RocksDB 引擎并启用中间提交,则查询可能会执行中间事务提交,以防正在运行的事务(AQL 查询)达到指定的大小阈值。 在这种情况下,到目前为止执行的查询操作将被提交,并且不会在以后中止/回滚的情况下回滚。 该行为可以通过调整 RocksDB 引擎的中间提交设置来控制。

对于分片集合,整个查询和/或替换操作可能不是事务性的,尤其是当它涉及不同的分片和/或数据库服务器时。

6.13 INSERT

INSERT关键字可用于将新文档插入到集合中。

每个INSERT 操作仅限于单个集合,并且集合名称不能是动态的。 每个 AQL 查询只允许每个集合有一个 INSERT 语句,并且后面不能跟访问同一集合的读或写操作、遍历操作或可以读取文档的 AQL 函数。

6.13.1 语法

插入操作的语法为:

1
INSERT document INTO collection

它可以选择以 OPTIONS { … } 子句结尾。

IN 关键字可以代替 INTO 并且具有相同的含义。

集合必须包含应插入文档的集合的名称。 document 是要插入的文档,它可能包含也可能不包含 _key 属性。 如果未提供 _key 属性,ArangoDB 将自动为 _key 值生成一个值。 插入文档还将自动生成文档的文档修订号。

1
2
FOR i IN 1..100
INSERT { value: i } INTO numbers

也可以在没有 FOR 循环的情况下执行插入操作以插入单个文档:

1
INSERT { value: 1 } INTO numbers

插入边缘集合时,必须在文档中指定属性 _from_to

1
2
3
4
FOR u IN users
FOR p IN products
FILTER u._key == p.recommendedBy
INSERT { _from: u._id, _to: p._id } INTO recommendations

6.13.2 查询选项

可以选择在 INSERT 操作中提供 OPTIONS 关键字后跟带有查询选项的对象。

6.13.2.1 ignoreErrors

ignoreErrors 可用于抑制违反唯一键约束时可能发生的查询错误:

1
2
3
4
5
6
FOR i IN 1..1000
INSERT {
_key: CONCAT('test', i),
name: "test",
foobar: true
} INTO users OPTIONS { ignoreErrors: true }

6.13.2.2 waitForSync

为了确保在插入查询返回时数据是持久的,有 waitForSync 查询选项:

1
2
3
4
5
6
FOR i IN 1..1000
INSERT {
_key: CONCAT('test', i),
name: "test",
foobar: true
} INTO users OPTIONS { waitForSync: true }

6.13.2.3 overwrite

overwrite 选项已弃用并由 overwriteMode 取代。

如果您想用具有相同键的文档替换现有文档,则可以使用覆盖查询选项。 这将使您安全地替换文档而不是引发“违反唯一约束的错误”:

1
2
3
4
5
6
FOR i IN 1..1000
INSERT {
_key: CONCAT('test', i),
name: "test",
foobar: true
} INTO users OPTIONS { overwrite: true }

6.12.2.4 overwriteMode

为了进一步控制 INSERT 在主索引唯一约束违规时的行为,有 overwriteMode 选项。 它提供以下模式:

  • "ignore":如果指定_key 值的文档已经存在,则什么都不做,也不进行写操作。 在这种情况下,插入操作将返回成功。 此模式不支持返回旧文档版本。 使用 RETURN OLD 将触发解析错误,因为不会返回旧版本。 RETURN NEW 只会在文档被插入的情况下返回文档。 如果文档已经存在, RETURN NEW 将返回 null
  • "replace":如果具有指定 _key 值的文档已经存在,它将被指定的文档值覆盖。 当未指定覆盖模式但覆盖标志设置为 true 时,也将使用此模式
  • "update":如果具有指定 _key 值的文档已经存在,则将使用指定的文档值对其进行修补(部分更新)。
  • "conflict":如果已存在具有指定 _key 值的文档,则返回唯一约束冲突错误,以便插入操作失败。 如果未设置覆盖模式,并且覆盖标志为 false 或未设置,这也是默认行为。

使用覆盖模式忽略插入文档的主要用例是确保某些文档以尽可能便宜的方式存在。 如果目标文档已经存在,忽略模式是最有效的,因为它不会从存储中检索现有文档,也不会向其写入任何更新。

使用更新覆盖模式时,keepNull 和 mergeObjects 选项控制更新的完成方式。 请参阅更新操作。

1
2
3
4
5
6
FOR i IN 1..1000
INSERT {
_key: CONCAT('test', i),
name: "test",
foobar: true
} INTO users OPTIONS { overwriteMode: "update", keepNull: true, mergeObjects: false }

6.12.2.5 exclusive

RocksDB 引擎不需要集合级锁。 同一个集合上的不同写操作不会相互阻塞,只要同一个文档上不存在写-写冲突。 从应用程序开发的角度来看,可能需要对集合具有独占写入权限,以简化开发。 请注意,写入不会阻止 RocksDB 中的读取。 独占访问还可以加速修改查询,因为我们避免了冲突检查。

使用exclusive选项在每个查询的基础上实现这种效果:

1
2
3
FOR doc IN collection
INSERT { myval: doc.val + 1 } INTO users
OPTIONS { exclusive: true }

6.13.3 返回插入的文档

插入的文档也可以由查询返回。 在这种情况下,INSERT 语句可以是 RETURN 语句(也允许中间的 LET 语句)。 为了引用插入的文档,INSERT 语句引入了一个名为 NEW 的伪值。

NEW 中包含的文档将包含所有属性,甚至是数据库自动生成的属性(例如 _id_key_rev)。

1
INSERT document INTO collection RETURN NEW

以下是使用名为 insert 的变量返回插入文档的示例。 对于每个插入的文档,返回文档键:

1
2
3
4
5
FOR i IN 1..100
INSERT { value: i }
INTO users
LET inserted = NEW
RETURN inserted._key

6.13.4 事务性

在单个服务器上,插入操作以全有或全无的方式以事务方式执行。

如果使用 RocksDB 引擎并启用中间提交,则查询可能会执行中间事务提交,以防正在运行的事务(AQL 查询)达到指定的大小阈值。 在这种情况下,到目前为止执行的查询操作将被提交,并且不会在以后中止/回滚的情况下回滚。 该行为可以通过调整 RocksDB 引擎的中间提交设置来控制。

对于分片集合,整个查询和/或插入操作可能不是事务性的,尤其是当它涉及不同的分片和/或 DB-Server 时。

6.14 UPSERT

UPSERT 关键字可用于检查某些文档是否存在,如果存在则更新/替换它们,或者在它们不存在的情况下创建它们。

每个 UPSERT 操作仅限于单个集合,并且集合名称不能是动态的。 每个 AQL 查询只允许每个集合有一个 UPSERT 语句,并且后面不能跟访问同一集合的读或写操作、遍历操作或可以读取文档的 AQL 函数。

6.14.1 语法

upsert 和 repsert 操作的语法是:

1
2
UPSERT searchExpression INSERT insertExpression UPDATE updateExpression IN collection
UPSERT searchExpression INSERT insertExpression REPLACE updateExpression IN collection

两种变体都可以选择以 OPTIONS { … } 子句结尾。

当使用 upsert 操作的 UPDATE 变体时,找到的文档将被部分更新,这意味着只有在 updateExpression 中指定的属性将被更新或添加。使用 upsert (repsert) 的 REPLACE 变体时,现有文档将替换为 updateExpression 的上下文。

更新文档将使用服务器生成的值修改文档的修订号。系统属性_id_key_rev 不能更新,_from_to 可以。

searchExpression 包含要查找的文档。它必须是没有动态属性名称的对象字面量。如果在集合中找不到这样的文档,则将按照 insertExpression 中的指定将新文档插入到集合中。

如果集合中至少有一个文档与 searchExpression 匹配,它将使用 updateExpression 进行更新。当集合中的多个文档与 searchExpression 匹配时,未定义将更新哪个匹配的文档。因此,通过其他方式(例如唯一索引、应用程序逻辑等)确保至多一个文档与 searchExpression 匹配通常是明智的。

以下查询将在用户集合中查找具有特定名称属性值的文档。如果文档存在,它的 logins 属性将增加 1。如果它不存在,将插入一个新文档,由属性 name、logins 和 dateCreated 组成:

1
2
3
UPSERT { name: 'superuser' } 
INSERT { name: 'superuser', logins: 1, dateCreated: DATE_NOW() }
UPDATE { logins: OLD.logins + 1 } IN users

请注意,在 UPDATE 情况下,可以使用 OLD 伪值引用文档的先前版本。

6.14.2 查询选项

6.14.2.1 ignoreErrors

ignoreErrors 选项可用于抑制在尝试违反唯一键约束时可能发生的查询错误。

6.14.2.2 keepNull

使用空值更新或替换属性时,ArangoDB 不会从文档中删除该属性,而是为其存储空值。 要删除 upsert 操作中的属性,请将它们设置为 null 并提供 keepNull 选项。

6.14.2.3 mergeObjects

如果 UPDATE 查询和要更新的文档中都存在对象属性,则选项 mergeObjects 控制是否合并对象内容。

mergeObjects 的默认值为 true,因此无需明确指定。

6.14.2.4 waitForSync

为了确保更新查询返回时数据是持久的,有 waitForSync 查询选项。

6.14.2.5 ignoreRevs

为了不意外更新自上次获取以来已写入和更新的文档,您可以使用选项 ignoreRevs 来让 ArangoDB 比较_rev值并且仅在它们仍然匹配时才成功,或者让 ArangoDB 忽略它们(默认):

1
2
3
4
5
FOR i IN 1..1000
UPSERT { _key: CONCAT('test', i)}
INSERT {foobar: false}
UPDATE {_rev: "1287623", foobar: true }
IN users OPTIONS { ignoreRevs: false }

您需要在 updateExpression 中添加_rev 值。 它不会在 searchExpression 中使用。 更糟糕的是,如果在searchExpression 中使用过时的_rev,UPSERT 将触发INSERT 路径而不是UPDATE 路径,因为它没有找到与searchExpression 完全匹配的文档。

6.14.2.6 exclusive

RocksDB 引擎不需要集合级锁。 同一个集合上的不同写操作不会相互阻塞,只要同一个文档上不存在写-写冲突。 从应用程序开发的角度来看,可能需要对集合具有独占写入权限,以简化开发。 请注意,写入不会阻止 RocksDB 中的读取。 独占访问还可以加速修改查询,因为我们避免了冲突检查。

使用exclusive选项在每个查询的基础上实现这种效果:

1
2
3
4
5
FOR i IN 1..1000
UPSERT { _key: CONCAT('test', i) }
INSERT { foobar: false }
UPDATE { foobar: true }
IN users OPTIONS { exclusive: true }

6.14.2.7 indexHint

indexHint 选项将用作作为 UPSERT 操作的一部分执行的文档查找的提示,并且可以在 UPSERT 未自动选择最佳索引等情况下提供帮助。

1
2
3
4
UPSERT { a: 1234 }
INSERT { a: 1234, name: "AB" }
UPDATE { name: "ABC" } IN myCollection
OPTIONS { indexHint: "index_name" }

索引提示被传递到用于查找的内部 FOR 循环。 另请参阅 FOR 操作的 indexHint 选项。

6.14.2.8 forceIndexHint

如果启用,则使 indexHint 中指定的索引或索引成为必需的。 默认值为假。 另请参阅 FOR 操作的 forceIndexHint 选项。

1
2
3
4
UPSERT { a: 1234 }
INSERT { a: 1234, name: "AB" }
UPDATE { name: "ABC" } IN myCollection
OPTIONS { indexHint: … , forceIndexHint: true }

6.14.3 返回文档

UPSERT 语句可以选择返回数据。 为此,它们需要后跟 RETURN 语句(也允许中间的 LET 语句)。 这些语句可以选择执行计算并引用伪值 OLD 和 NEW。 如果 upsert 执行了插入操作,则 OLD 的值为 null。 如果 upsert 执行了更新或替换操作,则 OLD 将包含更新/替换之前的文档的先前版本。

NEW 将始终被填充。 如果 upsert 执行插入,它将包含插入的文档,或者在执行更新/替换的情况下包含更新/替换的文档。

这也可用于检查 upsert 是否在内部执行了插入或更新:

1
2
3
4
UPSERT { name: 'superuser' } 
INSERT { name: 'superuser', logins: 1, dateCreated: DATE_NOW() }
UPDATE { logins: OLD.logins + 1 } IN users
RETURN { doc: NEW, type: OLD ? 'update' : 'insert' }

6.14.4 事务性

在单个服务器上,更新插入以全有或全无的方式以事务方式执行。

如果使用 RocksDB 引擎并启用中间提交,则查询可能会执行中间事务提交,以防正在运行的事务(AQL 查询)达到指定的大小阈值。 在这种情况下,到目前为止执行的查询操作将被提交,并且不会在以后中止/回滚的情况下回滚。 该行为可以通过调整 RocksDB 引擎的中间提交设置来控制。

对于分片集合,整个查询和/或 upsert 操作可能不是事务性的,尤其是当它涉及不同的分片和/或 DB-Server 时。

6.14.5 局限性

  • 查找和插入/更新/替换部分是非原子地执行的。这意味着如果多个 UPSERT 查询同时运行,它们可能都确定目标文档不存在,然后多次创建它!
  • 请注意,由于查找和插入/更新/替换的非原子性,即使使用唯一索引,也可能存在重复键错误或冲突。但如果它们发生,应用程序/客户端代码可以再次执行相同的查询。
  • 为了防止这种情况发生,应该为查找属性添加一个唯一索引。请注意,在集群中,只有在等于集合的分片键属性或至少包含它作为一部分时,才能创建唯一索引。
  • 使 UPSERT 语句以原子方式工作的另一种方法是使用独占选项将此集合的写入并发性限制为 1,这有助于避免冲突,但对吞吐量不利!
  • 在 UPSERT 中使用非常大的事务(例如,对集合中的所有文档使用 UPSERT)可以触发中间提交。此中间提交将写入到目前为止已修改的数据。然而,这会产生副作用,即
  • 无法再保证此操作的原子性,并且 ArangoDB 无法保证读取您自己在 upsert 中的写入会起作用。
  • 只有当您编写一个查询时,您的搜索条件会多次命中同一个文档,并且只有当您有大笔交易时,这才会成为问题。为了避免此问题,您可以增加数据和操作计数的中间提交阈值。
  • 应该对来自搜索表达式的查找属性进行索引以提高 UPSERT 性能。理想情况下,搜索表达式包含分片键,因为这允许将查找限制为单个分片。

6.15 WITH

AQL 查询可以以 WITH 关键字开头,后跟查询隐式读取的集合列表。

隐式意味着集合没有在语言结构中明确指定,例如

  • FOR ... IN collection
  • INSERT ... INTO collection
  • UPDATE ... IN collection
  • GRAPH "graph-name"(通过图形定义)

等,但仅在查询运行时才知道。 这种动态集合访问在查询编译时对 AQL 查询解析器是不可见的。 可以通过 DOCUMENT() 函数以及图遍历(特别是使用集合集的变体)进行动态访问,因为边可能指向任意顶点集合。

AQL 查询解析器会自动检测在查询中显式使用的集合。 可以使用 WITH 语句手动指定查询中将涉及但查询解析器无法自动检测到的任何其他集合。

6.15.1 语法

1
WITH collection1 [, collection2 [, ... collectionN ] ]

WITH 也是在其他上下文中使用的关键字,例如在 UPDATE 语句中。 它必须放在查询的最开始以声明额外的集合。

6.15.2 用法

使用 RocksDB 作为存储引擎,WITH 操作仅在您使用集群部署时才需要,并且仅适用于从顶点集合动态读取作为图遍历的一部分的 AQL 查询。

您可以启用 —query.require-with 启动选项以使单个服务器实例需要 WITH 声明(如集群部署)以简化开发,请参阅需要 WITH 语句。

通过 DOCUMENT() 函数的动态访问不需要您列出所涉及的集合。在遍历中使用命名图(GRAPH“graph-name”)也不需要它,假设所有顶点都在作为图一部分的集合中,由图 API 强制执行。这意味着,只需要使用匿名图/集合集进行遍历。

以下示例查询指定了一个边集合 usersHaveManagers 来执行图遍历。它是查询中唯一明确指定的集合。它不需要使用 WITH 操作声明。

但是,需要声明涉及的顶点集合。在此示例中,边集合的边引用称为管理器的集合的顶点。这个集合是在查询开始时使用 WITH 操作声明的:

1
2
3
WITH managers
FOR v, e, p IN 1..2 OUTBOUND 'users/1' usersHaveManagers
RETURN { v, e, p }

七 函数

AQL 支持允许更复杂计算的函数。 可以在任何允许使用表达式的查询位置调用函数。 一般的函数调用语法是:

1
FUNCTIONNAME(arguments)

其中 FUNCTIONNAME 是要调用的函数的名称,arguments 是逗号分隔的函数参数列表。 如果函数不需要任何参数,则参数列表可以留空。 然而,即使参数列表为空,它周围的括号仍然是强制性的,以使函数调用与变量名称区分开来。

一些示例函数调用:

1
2
3
HAS(user, "name")
LENGTH(friends)
COLLECTIONS()

与集合名和变量名相反,函数名不区分大小写,即 LENGTH(foo) 和 length(foo) 是等价的。

扩展 AQL

可以使用用户定义的函数来扩展 AQL。 这些函数需要用 JavaScript 编写,并且必须先注册,然后才能在查询中使用。 有关更多详细信息,请参阅扩展 AQL。

7.1 ArangoSearch

ArangoSearch 为搜索查询提供各种 AQL 函数,以控制搜索上下文、过滤和评分

您可以通过组合 ArangoSearch 函数调用、逻辑运算符和比较运算符来形成搜索表达式来过滤视图。

AQL SEARCH 操作接受诸如 ANALYZER(PHRASE(doc.text, "foo bar"), "text_en")之类的搜索表达式。 您可以组合过滤器和上下文函数以及 AND 和 OR 等运算符来形成复杂的搜索条件。

评分功能允许您对匹配项进行排名并按相关性对结果进行排序。

大多数函数也可以在没有 View 和 SEARCH 关键字的情况下使用,但不会被 View 索引加速。

有关介绍,请参阅使用 ArangoSearch 进行信息检索。

7.1.1 上下文函数

ANALYZER()

1
ANALYZER(expr, analyzer) → retVal

为给定的搜索表达式设置分析器。 默认分析器是任何 ArangoSearch 表达式的标识。 此实用程序函数可用于包装复杂的表达式以设置特定的分析器。 它还为所有需要此类参数的嵌套函数设置它以避免重复 Analyzer 参数。 如果无论如何将分析器参数传递给嵌套函数,则它优先于通过 ANALYZER() 设置的分析器。

TOKENS() 函数是一个例外。 它要求在所有情况下都传递分析器名称,即使包装在 ANALYZER() 调用中,因为它不是 ArangoSearch 函数,而是可以在 SEARCH 操作之外使用的常规字符串函数。

  • expr (expression):任何有效的搜索表达式
  • analyzer (string):分析器的名称。
  • returns retVal (any):它包装的表达式结果
示例:使用自定义分析器

假设视图定义带有名称和类型为delimiter的分析器:

1
2
3
4
5
6
7
8
9
{
"links": {
"coll": {
"analyzers": [ "delimiter" ],
"includeAllFields": true,
}
},
...
}

使用分析器属性 { “delimiter”: “|” } 和一个示例文档 { “text”: “foo|bar|baz” } 在集合 coll 中,以下查询将返回文档:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(doc.text == "bar", "delimiter")
RETURN doc

表达式 doc.text == “bar” 必须由 ANALYZER() 包装,以便将分析器设置为分隔符。 否则,表达式将使用默认身份分析器进行评估。 “foo|bar|baz” == “bar” 将不匹配,但视图甚至不使用标识分析器处理索引字段。 由于 Analyzer 不匹配,以下查询也将返回空结果:

1
2
3
4
FOR doc IN viewName
SEARCH doc.text == "foo|bar|baz"
//SEARCH ANALYZER(doc.text == "foo|bar|baz", "identity")
RETURN doc
示例:使用和不使用 ANALYZER() 设置分析器上下文

在下面的查询中,搜索表达式由 ANALYZER() 交换以设置两个 PHRASE() 函数的 text_en Analyzer:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(PHRASE(doc.text, "foo") OR PHRASE(doc.text, "bar"), "text_en")
RETURN doc

不使用 ANALYZER():

1
2
3
FOR doc IN viewName
SEARCH PHRASE(doc.text, "foo", "text_en") OR PHRASE(doc.text, "bar", "text_en")
RETURN doc
示例:TOKENS() 函数的分析器优先级和细节

在以下示例中,ANALYZER() 用于设置分析器 text_en,但在第二次调用 PHRASE() 时设置了一个不同的分析器(身份),它否决了 ANALYZER()。 因此,使用 text_en Analyzer 查找短语 foo 和使用 identity Analyzer 查找 bar:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(PHRASE(doc.text, "foo") OR PHRASE(doc.text, "bar", "identity"), "text_en")
RETURN doc

尽管包装了 ANALYZER() 函数,但在调用 TOKENS() 函数时不能省略分析器名称。 text_en 的两次出现都是必需的,以便为表达式 doc.text IN … 和 TOKENS() 函数本身设置分析器。 这是因为 TOKENS() 函数是一个不考虑 Analyzer 上下文的常规字符串函数:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(doc.text IN TOKENS("foo", "text_en"), "text_en")
RETURN doc

BOOST()

1
BOOST(expr, boost) → retVal

在具有指定值的搜索表达式上下文中覆盖 boost,使其可用于评分器函数。 默认情况下,上下文的提升值等于 1.0。

  • expr(表达式):任何有效的搜索表达式
  • 提升(数字):数值提升值
  • 返回retVal (any):它包装的表达式结果
示例:增强搜索子表达式
1
2
3
4
5
FOR doc IN viewName
SEARCH ANALYZER(BOOST(doc.text == "foo", 2.5) OR doc.text == "bar", "text_en")
LET score = BM25(doc)
SORT score DESC
RETURN { text: doc.text, score }

假设视图包含由分析器编制索引和处理的以下文档:text_en

1
2
3
4
5
{ "text": "foo bar" }
{ "text": "foo" }
{ "text": "bar" }
{ "text": "foo baz" }
{ "text": "baz" }

…以上查询的结果将是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"text": "foo bar",
"score": 2.787301540374756
},
{
"text": "foo baz",
"score": 1.6895781755447388
},
{
"text": "foo",
"score": 1.525835633277893
},
{
"text": "bar",
"score": 0.9913395643234253
}
]

7.1.2 过滤器功能

EXISTS()

EXISTS() 仅在已处理指定属性且在视图定义中将链接属性 storeValues 设置为“id”(默认为“none”)时才匹配值。

测试属性存在
1
EXISTS(path)

匹配存在路径处属性的文档。

  • path(属性路径表达式):要在文档中测试的属性
  • 不返回任何内容:函数的计算结果为布尔值,但不能返回此值。只能在搜索表达式中调用该函数。如果在SEARCH 操作之外使用,则会引发错误。
1
2
3
FOR doc IN viewName
SEARCH EXISTS(doc.text)
RETURN doc
测试属性类型
1
EXISTS(path, type)

匹配路径处的属性存在属于指定数据类型的文档。

  • path(属性路径表达式):要在文档中测试的属性
  • 类型(字符串):要测试的数据类型,可以是以下之一:
    • "null"
    • "bool" / "boolean"
    • "numeric"
    • "string"
    • "analyzer"(见下文)
  • 不返回任何内容:函数的计算结果为布尔值,但不能返回此值。只能在搜索表达式中调用该函数。如果在SEARCH 操作之外使用,则会引发错误。
1
2
3
FOR doc IN viewName
SEARCH EXISTS(doc.text, "string")
RETURN doc
测试分析器索引状态
1
EXISTS(path, "analyzer", analyzer)

匹配路径处的属性存在并由指定的分析器编制索引的文档。

  • path(属性路径表达式):要在文档中测试的属性
  • 类型(字符串):字符串文本"analyzer"
  • 分析器(字符串,可选):分析器的名称 。如果未指定或默认为ANALYZER()``"identity"
  • 不返回任何内容:函数的计算结果为布尔值,但不能返回此值。只能在搜索表达式中调用该函数。如果在SEARCH 操作之外使用,则会引发错误。
1
2
3
FOR doc IN viewName
SEARCH EXISTS(doc.text, "analyzer", "text_en")
RETURN doc

IN_RANGE()

1
IN_RANGE(path, low, high, includeLow, includeHigh) → included

匹配路径上的属性大于(或等于)low 且小于(或等于)high 的文档。

low 和 high 可以是数字或字符串(技术上也可以是 null、true 和 false),但两者的数据类型必须相同。

ArangoSearch 不考虑字符的字母顺序,即针对视图的 SEARCH 操作中的范围查询将不遵循定义的分析器区域设置或服务器语言(启动选项 —default-language)的语言规则! 另请参阅已知问题。

有一个相应的 IN_RANGE() Miscellaneous Function 在 SEARCH 操作之外使用。

  • path(属性路径表达式):要在文档中测试的属性的路径
  • (数字|字符串):所需范围的最小值
  • (数字|字符串):所需范围的最大值
  • includeLow (bool):最小值是否应包含在范围内(左闭区间)或不包括在内(左开区间)
  • 包括高(bool):最大值是否应包括在范围内(右闭区间)或不(右开区间)
  • 包含的返回 (布尔):是否在范围内

如果“低”和”“相同,但“包括低”和/或“包括高”设置为 ,则没有任何匹配项。如果“低“大于“高“,则任何内容都不匹配。false

示例:使用数值范围

若要将文档与该属性匹配并使用默认的分析器,请编写以下查询:value >= 3``value <= 5``"identity"

1
2
3
FOR doc IN viewName
SEARCH IN_RANGE(doc.value, 3, 5, true, true)
RETURN doc.value

这还将匹配具有数字数组作为属性的文档,其中至少有一个数字位于指定的边界内。value

示例:使用字符串范围

使用字符串边界和文本分析器允许匹配在指定字符范围内至少有一个标记的文档:

1
2
3
FOR doc IN valView
SEARCH ANALYZER(IN_RANGE(doc.value, "a","f", true, false), "text_en")
RETURN doc

这将匹配,因为b在范围 () 内,但不是因为foof被排除在外(是”f”,但includeHigh是假的)。{ "value": "bar" }``{ "value": "foo bar" }``"a" <= "b" < "f"``{ "value": "foo" }

MIN_MATCH()

1
MIN_MATCH(expr1, ... exprN, minMatchCount) → fulfilled

匹配至少满足指定搜索表达式的最小匹配计数的文档。

在操作之外使用相应的MIN_MATCH()杂项函数SEARCH

  • expr(表达式,可重复):任何有效的搜索表达式
  • minMatchCount(数字):应满足的搜索表达式的最小数量
  • 返回已满足(bool):指定表达式的minMatchCount是否至少为true
示例:匹配搜索子表达式的子集

假设使用文本分析器查看,您可以使用它来匹配属性至少包含三个标记中的两个的文档:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(MIN_MATCH(doc.text == 'quick', doc.text == 'brown', doc.text == 'fox', 2), "text_en")
RETURN doc.text

这将匹配 和 ,但不能只满足其中一个条件。{ "text": "the quick brown fox" }``{ "text": "some brown fox" }``{ "text": "snow fox" }

NGRAM_MATCH()

引入: v3.7.0

1
NGRAM_MATCH(path, target, threshold, analyzer) → fulfilled

匹配属性值与目标值相比,其属性值的n-gram 相似性高于指定阈值的文档。

相似性是通过计算匹配n-gram 的最长序列的长度除以目标的总n-gram 计数来计算的。仅计算完全匹配的n个 -克。

属性和目标的n个 -gram 由指定的分析器生成。增加n-gram长度将提高精度,但降低误差容限。在大多数情况下,2 或 3 的大小将是一个不错的选择。

使用类型与 和 等于 的分析器。否则,内部计算的相似性分数将低于预期。ngram``preserveOriginal: false``min``max

分析器必须启用 和 功能,否则该功能将找不到任何内容。"position"``"frequency"``NGRAM_MATCH()

另请参阅字符串函数NGRAM_POSITIONAL_SIMILARITY()NGRAM_SIMILARITY(),以计算视图索引无法加速的n-gram 相似性。

  • path(属性路径表达式|字符串):文档或字符串中属性的路径
  • target(字符串):要与存储的属性进行比较的字符串
  • 阈值(数字,可选):介于 和 之间。如果未指定任何内容,则默认为。0.0``1.0``0.7
  • 分析器(字符串):分析器的名称。
  • 返回已满足(bool):如果计算的n-gram 相似性值大于或等于指定的阈值,否则true``false
示例:使用自定义双面分析器

给定一个 View 索引一个属性、一个自定义n-gram 分析器 () 和一个文档,下面的查询将匹配它(阈值为 ):text``"bigram"``min: 2, max: 2, preserveOriginal: false, streamType: "utf8"``{ "text": "quick red fox" }``1.0

1
2
3
FOR doc IN viewName
SEARCH NGRAM_MATCH(doc.text, "quick fox", "bigram")
RETURN doc.text

以下各项也将匹配(请注意低阈值):

1
2
3
FOR doc IN viewName
SEARCH NGRAM_MATCH(doc.text, "quick blue fox", 0.4, "bigram")
RETURN doc.text

以下各项将不匹配(请注意高阈值):

1
2
3
FOR doc IN viewName
SEARCH NGRAM_MATCH(doc.text, "quick blue fox", 0.9, "bigram")
RETURN doc.text
示例:使用常量值
1
2
3
4
5
NGRAM_MATCH()`可以使用常量参数调用,但对于此类调用*,analyzer*参数是必需的(即使对于子句内部的调用):`SEARCH
FOR doc IN viewName
SEARCH NGRAM_MATCH("quick fox", "quick blue fox", 0.9, "bigram")
RETURN doc.text
RETURN NGRAM_MATCH("quick fox", "quick blue fox", "bigram")

PHRASE()

1
2
3
PHRASE(path, phrasePart, analyzer)
PHRASE(path, phrasePart1, skipTokens1, ... phrasePartN, skipTokensN, analyzer)
PHRASE(path, [ phrasePart1, skipTokens1, ... phrasePartN, skipTokensN ], analyzer)

在引用的属性中搜索短语。它仅匹配标记按指定顺序出现的文档。要以任何顺序搜索令牌,请改用TOKENS()。

短语可以表示为任意数量的短语Parts,skip分隔令牌数(通配符),既可以作为单独的参数,也可以作为数组作为第二个参数。

  • path(属性路径表达式):要在文档中测试的属性
  • 短语Part(字符串|数组|对象):要在标记中搜索的文本。也可以是由字符串、数组对象令牌组成的数组,或与skipTokens数交错的令牌。指定的分析器应用于字符串和数组令牌,但不应用于对象令牌。
  • skipTokens(数字,可选):要视为通配符的令牌数量(在 v3.6.0 中引入)
  • 分析器(字符串,可选):分析器的名称 。如果未指定或默认为ANALYZER()``"identity"
  • 不返回任何内容:函数的计算结果为布尔值,但不能返回此值。只能在搜索表达式中调用该函数。如果在SEARCH 操作之外使用,则会引发错误。

所选分析器必须启用 和 功能。否则,该函数将找不到任何内容。"position"``"frequency"``PHRASE()

对象令牌

v3.7.0 中引入

  • {IN_RANGE: [low, high, includeLow, includeHigh]}:请参见IN_RANGE()。只能是字符串。

  • ```plaintext
    {LEVENSHTEIN_MATCH: [token, maxDistance, transpositions, maxTerms, prefix]}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    :

    - `token`(字符串):要搜索的字符串
    - `maxDistance`(数字):最大列文什泰因/达梅劳-列文施泰因距离
    - `transpositions`(bool,*可选*):如果设置为 ,则计算 Levenshtein 距离,否则计算 Damerau-Levenshtein 距离(默认值)`false`
    - `maxTerms`(数字,*可选*):仅考虑指定数量最相关的术语。可以考虑所有匹配的术语,但它可能会对性能产生负面影响。缺省值为 。`0``64`
    - `prefix`(字符串,*可选*):如果已定义,则使用匹配项作为候选项执行搜索确切前缀。然后使用字符串的其余部分计算每个候选者的Levenshtein / Damerau-Levenshtein距离。此选项可以在存在已知公共前缀的情况下提高性能。默认值为空字符串(在 v3.7.13 和 v3.8.1 中引入)。

    - `{STARTS_WITH: [prefix]}`:请参阅[STARTS_WITH()。](https://www.arangodb.com/docs/3.10/aql/functions-arangosearch.html#starts_with)阵列支架是可选的

    - `{TERM: [token]}`:等于但不带分析器标记化。阵列支架是可选的`token`

    - `{TERMS: [token1, ..., tokenN]}`:可以在指定位置找到其中之一。在数组中,对象语法可以替换为对象字段值,例如。`token1, ..., tokenN``[..., [token1, ..., tokenN], ...]`

    - `{WILDCARD: [token]}`:请参阅[LIKE()](https://www.arangodb.com/docs/3.10/aql/functions-arangosearch.html#like)。阵列支架是可选的

    数组内的数组令牌只能在这种情况下使用。`TERMS`

    另请参阅[示例:使用对象令牌](https://www.arangodb.com/docs/3.10/aql/functions-arangosearch.html#example-using-object-tokens)。

    ##### 示例:使用文本分析器进行短语搜索

    给定一个 View 使用分析器和文档索引属性*文本*,以下查询将匹配它:`"text_en"``{ "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit" }`

    ```sql
    FOR doc IN viewName
    SEARCH PHRASE(doc.text, "lorem ipsum", "text_en")
    RETURN doc.text

但是,此搜索表达式不会因为标记和不按以下顺序出现:"ipsum"``"lorem"

1
PHRASE(doc.text, "ipsum lorem", "text_en")
示例:跳过邻近搜索的令牌

若要匹配两个标记,并使用其间的任意两个标记,可以使用以下搜索表达式:"ipsum"``"amet"

1
PHRASE(doc.text, "ipsum", 2, "amet", "text_en")

的 skipTokens值定义了ipsumamet之间必须出现多少个通配符。skipTokens值 表示令牌必须相邻。允许负值,但不是很有用。这三个搜索表达式是等效的:2``0

1
2
3
PHRASE(doc.text, "lorem ipsum", "text_en")
PHRASE(doc.text, "lorem", 0, "ipsum", "text_en")
PHRASE(doc.text, "ipsum", -1, "lorem", "text_en")
示例:与令牌数组一起使用PHRASE()

该函数还接受数组作为第二个参数,并将短语PartskipTokens参数作为元素。PHRASE()

1
2
FOR doc IN myView SEARCH PHRASE(doc.title, ["quick brown fox"], "text_en") RETURN doc
FOR doc IN myView SEARCH PHRASE(doc.title, ["quick", "brown", "fox"], "text_en") RETURN doc

此语法变体允许使用计算表达式:

1
2
3
4
5
6
LET proximityCondition = [ "foo", ROUND(RAND()*10), "bar" ]
FOR doc IN viewName
SEARCH PHRASE(doc.text, proximityCondition, "text_en")
RETURN doc
LET tokens = TOKENS("quick brown fox", "text_en") // ["quick", "brown", "fox"]
FOR doc IN myView SEARCH PHRASE(doc.title, tokens, "text_en") RETURN doc

上面的例子相当于更繁琐和静态的形式:

1
FOR doc IN myView SEARCH PHRASE(doc.title, "quick", 0, "brown", 0, "fox", "text_en") RETURN doc

您可以选择在每个字符串元素之前指定数组表单中的 skipToken 数:

1
FOR doc IN myView SEARCH PHRASE(doc.title, ["quick", 1, "fox", "jumps"], "text_en") RETURN doc

它与以下内容相同:

1
FOR doc IN myView SEARCH PHRASE(doc.title, "quick", 1, "fox", 0, "jumps", "text_en") RETURN doc
示例:处理没有成员的数组

空数组将被跳过:

1
FOR doc IN myView SEARCH PHRASE(doc.title, "quick", 1, [], 1, "jumps", "text_en") RETURN doc

该查询等效于:

1
FOR doc IN myView SEARCH PHRASE(doc.title, "quick", 2 "jumps", "text_en") RETURN doc

仅提供空数组是有效的,但不会产生任何结果。

示例:使用对象令牌

使用对象标记 、 、 和 :STARTS_WITH``WILDCARD``LEVENSHTEIN_MATCH``TERMS``IN_RANGE

1
2
3
4
5
6
7
FOR doc IN myView SEARCH PHRASE(doc.title,
{STARTS_WITH: ["qui"]}, 0,
{WILDCARD: ["b%o_n"]}, 0,
{LEVENSHTEIN_MATCH: ["foks", 2]}, 0,
{TERMS: ["jump", "run"]}, 0, // Analyzer not applied!
{IN_RANGE: ["over", "through", true, false]},
"text_en") RETURN doc

请注意,分析器已启用词干分析,但对于对象标记,不会应用分析器。 与索引(和词干化!) 属性值不匹配。因此,在示例中,将从这两个单词中手动删除将被分隔开的尾随。text_en``{TERMS: ["jumps", "runs"]}``s

以上示例等效于:

1
2
3
4
5
6
7
8
FOR doc IN myView SEARCH PHRASE(doc.title,
[
{STARTS_WITH: "qui"}, 0,
{WILDCARD: "b%o_n"}, 0,
{LEVENSHTEIN_MATCH: ["foks", 2]}, 0,
["jumps", "runs"], 0, // Analyzer is applied using this syntax
{IN_RANGE: ["over", "through", true, false]}
], "text_en") RETURN doc

STARTS_WITH()

1
STARTS_WITH(path, prefix) → startsWith

匹配以前缀开头的属性的值。如果属性由标记化分析器(类型或 )处理,或者它是数组,则以前缀开头的单个标记/元素足以匹配文档。"text"``"delimiter"

ArangoSearch不考虑字符的字母顺序,即针对视图的SEARCH操作中的范围查询将不遵循定义的分析器区域设置或服务器语言(启动选项)的语言规则!另请参阅已知问题--default-language

在操作之外使用相应的STARTS_WITH()字符串函数SEARCH

  • path(属性路径表达式):要在文档中进行比较的属性的路径
  • 前缀(字符串):要在文本开头搜索的字符串
  • 返回startsWith (bool):指定属性是否以给定前缀开头
1
STARTS_WITH(path, prefixes, minMatchCount) → startsWith

引入: v3.7.1

匹配以其中一个前缀开头的属性的值,或者至少与前缀的minMatchCount 匹配

  • path(属性路径表达式):要在文档中进行比较的属性的路径
  • 前缀(数组):要在文本开头搜索的字符串数组
  • minMatchCount(数字,可选):应满足的搜索前缀的最小数量(请参阅示例)。默认值为1
  • 返回startsWith (bool):指定的属性是否以给定前缀的minMatchCount开头
示例:搜索确切值前缀

要使用前缀和 Analyzer 匹配文档,可以按如下方式使用它:{ "text": "lorem ipsum..." }``"identity"

1
2
3
FOR doc IN viewName
SEARCH STARTS_WITH(doc.text, "lorem ip")
RETURN doc
示例:在文本中搜索前缀

此查询将匹配并给定一个视图,该视图对属性编制索引并使用分析器对其进行处理:{ "text": "lorem ipsum" }``{ "text": [ "lorem", "ipsum" ] }``text``"text_en"

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(STARTS_WITH(doc.text, "ips"), "text_en")
RETURN doc.text

请注意,如果不修改查询,它将不匹配。前缀已按原样传递到,但用于索引的内置分析器已启用词干分析。因此,索引值如下所示:{ "text": "IPS (in-plane switching)" }``STARTS_WITH()``text_en

1
2
3
4
5
6
7
8
9
RETURN TOKENS("IPS (in-plane switching)", "text_en")
[
[
"ip",
"in",
"plane",
"switch"
]
]

sips中删除,这会导致前缀ips与索引的令牌ip不匹配。您可以创建禁用词干分解的自定义文本分析器以避免此问题,也可以将词干分解应用于前缀:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(STARTS_WITH(doc.text, TOKENS("ips", "text_en")), "text_en")
RETURN doc.text
示例:搜索一个或多个前缀

该函数接受一个前缀替代数组,其中只有一个必须匹配:STARTS_WITH()

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(STARTS_WITH(doc.text, ["something", "ips"]), "text_en")
RETURN doc.text

它将匹配一个文档,但也匹配,因为至少有一个单词以给定的前缀开头。{ "text": "lorem ipsum" }``{ "text": "that is something" }

再次使用相同的查询,但具有显式:minMatchCount

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(STARTS_WITH(doc.text, ["wrong", "ips"], 1), "text_en")
RETURN doc.text

可以增加该数字,以要求至少必须存在以下许多前缀:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(STARTS_WITH(doc.text, ["lo", "ips", "something"], 2), "text_en")
RETURN doc.text

这仍然匹配,因为至少找到两个前缀 ( 和 ),但不是只包含其中一个前缀 ()。{ "text": "lorem ipsum" }``lo``ips``{ "text": "that is something" }``something

LEVENSHTEIN_MATCH()

引入: v3.7.0

1
LEVENSHTEIN_MATCH(path, target, distance, transpositions, maxTerms, prefix) → fulfilled

将文档与Damerau-Levenshtein 距离匹配,该距离小于或等于存储的属性值与目标之间的距离。它可以选择使用纯Levenshtein距离匹配文档。

如果要计算两个字符串的编辑距离,请参阅LEVENSHTEIN_DISTANCE()。

  • path(属性路径表达式|字符串):要在文档或字符串中进行比较的属性的路径
  • target(字符串):要与存储的属性进行比较的字符串
  • 距离(数字):最大编辑距离,可以是和(如果换位为 )之间,如果是0``4``false``0``3``true
  • 转置(布尔,可选):如果设置为 ,则计算列文什泰因距离,否则计算达梅劳-列文什泰因距离(默认值)false
  • max Termms(数字,可选):仅考虑指定数量的最相关术语。可以考虑所有匹配的术语,但它可能会对性能产生负面影响。缺省值为 。0``64
  • 返回满足(布尔):如果计算的距离小于或等于距离,否则true``false
  • 前缀(字符串,可选):如果已定义,则使用匹配项作为候选项执行搜索确切的前缀。然后使用值和字符串的剩余部分为每个候选项计算Levenshtein / Damerau-Levenshtein距离,这意味着需要从目标中删除前缀(请参阅示例)。此选项可以在存在已知公共前缀的情况下提高性能。默认值为空字符串(在 v3.7.13 和 v3.8.1 中引入)。target
示例:使用和不带换位进行匹配

快速quikc之间的Levenshtein距离是因为它需要两个操作从一个到另一个(删除k,在不同的位置插入k)。2

1
2
3
FOR doc IN viewName
SEARCH LEVENSHTEIN_MATCH(doc.text, "quikc", 2, false) // matches "quick"
RETURN doc.text

Damerau-Levenshtein的距离是(将k移动到末尾)。1

1
2
3
FOR doc IN viewName
SEARCH LEVENSHTEIN_MATCH(doc.text, "quikc", 1) // matches "quick"
RETURN doc.text
示例:与前缀搜索匹配

将距离为 1 的文档与前缀 匹配。编辑距离是使用搜索词(去掉前缀)和不带前缀的存储值(例如)计算的。前缀是常量。qui``kc``quikc``qui``ck``qui

1
2
3
FOR doc IN viewName
SEARCH LEVENSHTEIN_MATCH(doc.text, "kc", 1, false, 64, "qui") // matches "quick"
RETURN doc.text

您可以按如下方式从输入字符串计算前缀和后缀:

1
2
3
4
5
6
7
LET input = "quikc"
LET prefixSize = 3
LET prefix = LEFT(input, prefixSize)
LET suffix = SUBSTRING(input, prefixSize)
FOR doc IN viewName
SEARCH LEVENSHTEIN_MATCH(doc.text, suffix, 1, false, 64, prefix) // matches "quick"
RETURN doc.text
示例:根据字符串长度确定编辑距离

您可能希望根据字符串长度选择最大编辑距离。如果存储的属性是字符串quick,而目标字符串是流沙,则 Levenshtein 距离为 5,其中 50% 的字符不匹配。如果输入是qqu,那么距离只有1,尽管它也是50%的不匹配。

1
2
3
4
5
6
LET target = "input"
LET targetLength = LENGTH(target)
LET maxDistance = (targetLength > 5 ? 2 : (targetLength >= 3 ? 1 : 0))
FOR doc IN viewName
SEARCH LEVENSHTEIN_MATCH(doc.text, target, maxDistance, true)
RETURN doc.text

LIKE()

引入: v3.7.2

1
LIKE(path, search) → bool

使用通配符匹配检查模式搜索是否包含在由path表示的属性中。

  • _:单个任意字符
  • %:零、一个或多个任意字符
  • \\_:文字下划线
  • \\%:文字百分号

文字反弹需要不同的转义量,具体取决于上下文:

  • \在 Web UI 中的绑定变量(视图模式)中(自动转义为,除非值用双引号括起来并且已正确转义)\\
  • \\在绑定变量(JSON视图模式)和 Web UI 中的查询中
  • \\in 绑定变量 in arangosh
  • \\\\在 arangosh 中的查询中
  • 与使用反斜杠进行转义的 shell 中的 arangosh 相比,该数量翻了一番(在绑定变量和查询中)\\\\``\\\\\\\\

在操作上下文中使用函数进行搜索由 View 索引提供支持。String LIKE()函数用于其他上下文(如操作中),另一方面,任何类型的索引都无法加速。另一个区别是,ArangoSearch 变体不接受第三个参数来实现不区分大小写的匹配。这可以通过分析仪进行控制。LIKE()``SEARCH``FILTER

  • path(属性路径表达式):要在文档中进行比较的属性的路径
  • search(字符串):一种搜索模式,可以包含通配符(表示任何字符序列,包括无)和(任何单个字符)。字面意思,必须使用反斜杠进行转义。%``_``%``_
  • 返回bool (bool):如果模式包含在文本中,否则true``false
示例:使用通配符进行搜索
1
2
3
FOR doc IN viewName
SEARCH ANALYZER(LIKE(doc.text, "foo%b_r"), "text_en")
RETURN doc.text

LIKE也可以以运算符形式使用:

1
2
3
FOR doc IN viewName
SEARCH ANALYZER(doc.text LIKE "foo%b_r", "text_en")
RETURN doc.text

7.1.3 地理功能

视图索引可以加速以下功能。常规地理索引类型有相应的地理函数,但也有通用函数,例如可以与ArangoSearch结合使用的GeoJSON构造函数。

GEO_CONTAINS()

引入: v3.8.0

1
GEO_CONTAINS(geoJsonA, geoJsonB) → bool

检查GeoJSON 对象 geoJsonA是否完全包含geoJsonB(B中的每个点也在 A 中)。

  • geoJsonA(对象|阵列):第一个 GeoJSON 对象或坐标数组(按经度、纬度顺序)
  • geoJsonB(对象|阵列):第二个 GeoJSON 对象或坐标数组(按经度、纬度顺序排列)
  • 返回bool (bool):当 B 中的每个点也包含在 A 中时,否则true``false

GEO_DISTANCE()

引入: v3.8.0

1
GEO_DISTANCE(geoJsonA, geoJsonB) → distance

返回两个GeoJSON天体之间的距离,从每个形状的质心测量。

  • geoJsonA(对象|阵列):第一个 GeoJSON 对象或坐标数组(按经度、纬度顺序)
  • geoJsonB(对象|阵列):第二个 GeoJSON 对象或坐标数组(按经度、纬度顺序排列)
  • 返回距离(数字):参考椭圆体上两个物体的质心点之间的距离

GEO_IN_RANGE()

引入: v3.8.0

1
GEO_IN_RANGE(geoJsonA, geoJsonB, low, high, includeLow, includeHigh) → bool

检查两个GeoJSON 对象之间的距离是否在给定的时间间隔内。距离是从每个形状的质心测量的。

  • geoJsonA(对象|阵列):第一个 GeoJSON 对象或坐标数组(按经度、纬度顺序)
  • geoJsonB(对象|阵列):第二个 GeoJSON 对象或坐标数组(按经度、纬度顺序排列)
  • (数字):所需范围的最小值
  • (数字):所需范围的最大值
  • includeLow(bool,可选):最小值是否应包括在范围(左闭区间)中(左开区间)。默认值为true
  • 包括高(bool):最大值是否应包括在范围内(右闭区间)或不(右开区间)。默认值为true
  • 返回bool (bool):计算的距离是否在范围内

GEO_INTERSECTS()

引入: v3.8.0

1
GEO_INTERSECTS(geoJsonA, geoJsonB) → bool

检查GeoJSON 对象 geoJsonA是否与geoJsonB相交(即 B 的至少一个点位于 A 中,反之亦然)。

  • geoJsonA(对象|阵列):第一个 GeoJSON 对象或坐标数组(按经度、纬度顺序)
  • geoJsonB(对象|阵列):第二个 GeoJSON 对象或坐标数组(按经度、纬度顺序排列)
  • 返回布尔值(bool):如果 A 和 B 相交,否则true``false

7.1.4 评分函数

评分函数返回SEARCH 操作找到的文档的排名值。文档与搜索表达式匹配得越好,返回的数字就越高。

任何评分函数的第一个参数始终是通过 ArangoSearch 视图执行操作发出的文档。FOR

要按相关性对结果集进行排序,将更相关的文档放在最前面,请按分数降序排序(例如 )。SORT BM25(...) DESC

您可以使用文档属性和数字函数(例如)根据评分函数计算自定义分数:TFIDF(doc) * LOG(doc.value)

1
2
3
4
FOR movie IN imdbView
SEARCH PHRASE(movie.title, "Star Wars", "text_en")
SORT BM25(movie) * LOG(movie.runtime + 1) DESC
RETURN movie

允许按多个分数排序。您还可以按多个视图以及集合中的分数和属性的组合进行排序:

1
2
3
4
5
FOR a IN viewA
FOR c IN coll
FOR b IN viewB
SORT TFIDF(b), c.name, BM25(a)
...

7.1.5 BM25()

1
BM25(doc, k, b) → score

使用最佳匹配 25算法(Okapi BM25) 对文档进行排序。

  • 文档(文档):必须由FOR ... IN viewName

  • k(数字,可选):校准文本项频率缩放。缺省值为 。的 k值对应于二进制模型(无项频率),较大的值对应于使用原始项频率1.2``0

  • b(数字,可选):确定按总文本长度缩放比例。缺省值为 。在系数b的极值处,BM25 变为称为:

    1
    0.75
    • BM11 for b = (对应于按总文本长度完全缩放术语权重)1
    • BM15 for b = (对应于无长度归一化)0
  • 返回分数(数字):计算的排名值

示例:按默认分数排序BM25()

在默认设置下按 BM25 的相关性排序:

1
2
3
4
FOR doc IN viewName
SEARCH ...
SORT BM25(doc) DESC
RETURN doc

示例:使用优化的排名进行排序BM25()

按相关性排序,具有双倍加权的术语频率和全文长度规范化:

1
2
3
4
FOR doc IN viewName
SEARCH ...
SORT BM25(doc, 2.4, 1) DESC
RETURN doc

7.1.6 TFIDF()

1
TFIDF(doc, normalize) → score

使用术语”频率-反向文档频率算法“(TF-IDF)对文档进行排序。

  • 文档(文档):必须由FOR ... IN viewName
  • 规范化(布尔,可选):指定是否应对分数进行归一化。默认值为false
  • 返回分数(数字):计算的排名值

示例:按默认分数排序TFIDF()

使用 TF-IDF 分数按相关性排序:

1
2
3
4
FOR doc IN viewName
SEARCH ...
SORT TFIDF(doc) DESC
RETURN doc

示例:使用规范化按分数排序TFIDF()

使用标准化的 TF-IDF 分数按相关性排序:

1
2
3
4
FOR doc IN viewName
SEARCH ...
SORT TFIDF(doc, true) DESC
RETURN doc

示例:按值和TFIDF()

按属性值的升序排序,然后按 TFIDF 分数降序排序,其中属性值等效:text

1
2
3
4
FOR doc IN viewName
SEARCH ...
SORT doc.text, TFIDF(doc) DESC
RETURN doc

7.2 Array

AQL 为更高级别的数组操作提供了函数。另请参阅处理数字数组的函数的数字函数。如果要连接与 JavaScript 中等效的数组的元素,请参阅字符串函数一章中的CONCAT()CONCAT_SEPARATOR()。join()

除此之外,AQL还提供了几种语言结构:

  • 单个元素的简单数组访问
  • 用于阵列扩展和收缩的数组运算符,可选带内联滤波器、限制和投影,
  • 数组比较运算符,用于将数组中的每个元素与值或另一个数组的元素进行比较,
  • 使用FOR 、SORT、LIMITCOLLECT进行分组的数组上基于循环的操作,这也提供了高效的聚合。

7.2.1 APPEND()

1
APPEND(anyArray, values, unique) → newArray

将数组的所有元素添加到另一个数组。所有值都添加到数组的末尾(右侧)。

它还可用于将单个元素追加到数组。不必将其包装在数组中(除非它本身就是数组)。您也可以改用PUSH()。

  • anyArray(数组):具有任意类型元素的数组
  • (数组|任何):数组,其元素应添加到anyArray 中
  • unique (bool, optional):如果设置为true,则只会添加那些尚未包含在anyArray 中的**值。默认值为false
  • 返回newArray(数组):修改后的数组

例子

1
RETURN APPEND([ 1, 2, 3 ], [ 5, 6, 9 ])

显示查询结果

1
RETURN APPEND([ 1, 2, 3 ], [ 3, 4, 5, 2, 9 ], true)

显示查询结果

7.2.2 CONTAINS_ARRAY()

这是POSITION()的别名。

7.2.3 COUNT()

这是LENGTH()的别名。

7.2.4 COUNT_DISTINCT()

1
COUNT_DISTINCT(anyArray) → number

获取数组中不同元素的数量。

  • anyArray(数组):具有任意类型元素的数组
  • 返回编号 :anyArray中不同元素的数量。

例子

1
2
3
4
5
6
7
8
RETURN COUNT_DISTINCT([ 1, 2, 3 ])
[
3
]
RETURN COUNT_DISTINCT([ "yes", "no", "yes", "sauron", "no", "yes" ])
[
3
]

7.2.5 COUNT_UNIQUE()

这是COUNT_DISTINCT()的别名。

7.2.6 FIRST()

1
FIRST(anyArray) → firstElement

获取数组的第一个元素。它与 相同。anyArray[0]

  • anyArray(数组):具有任意类型元素的数组
  • 返回firstElement (any|null):anyArray的第一个元素,如果数组为空,则返回 null。

例子

1
2
3
4
5
6
7
8
RETURN FIRST([ 1, 2, 3 ])
[
1
]
RETURN FIRST([])
[
null
]

7.2.7 FLATTEN()

1
FLATTEN(anyArray, depth) → flatArray

将数组数组转换为平面数组。数组中的所有数组元素都将在结果数组中展开。非数组元素按原样添加。该函数将递归到子数组中,直到指定的深度。重复项将不会被删除。

另请参阅数组收缩

  • array(array):具有任意类型元素的数组,包括嵌套数组
  • 深度(数字,可选):平展到这么多级别,默认值为 1
  • 返回flatArray(数组):扁平化数组

例子

1
RETURN FLATTEN( [ 1, 2, [ 3, 4 ], 5, [ 6, 7 ], [ 8, [ 9, 10 ] ] ] )

显示查询结果

若要完全展平示例数组,请使用深度2:

1
RETURN FLATTEN( [ 1, 2, [ 3, 4 ], 5, [ 6, 7 ], [ 8, [ 9, 10 ] ] ], 2 )

显示查询结果

7.2.8 INTERLEAVE()

版本介绍: v3.7.1

1
INTERLEAVE(array1, array2, ... arrayN) → newArray

接受任意数量的数组,并生成一个元素交错的新数组。它以轮循机制方式循环访问输入数组,每次迭代从每个数组中选取一个元素,并按该序列将它们合并到结果数组中。输入数组可以具有不同数量的元素。

  • 数组(数组,可重复):任意数量的数组作为多个参数(至少 2 个)
  • 返回newArray(数组):交错数组

例子

1
RETURN INTERLEAVE( [1, 1, 1], [2, 2, 2], [3, 3, 3] )

显示查询结果

1
RETURN INTERLEAVE( [ 1 ], [2, 2], [3, 3, 3] )

显示查询结果

1
2
FOR v, e, p IN 1..3 OUTBOUND 'places/Toronto' GRAPH 'kShortestPathsGraph'
RETURN INTERLEAVE(p.vertices[*]._id, p.edges[*]._id)

显示查询结果

7.2.9 INTERSECTION()

1
INTERSECTION(array1, array2, ... arrayN) → newArray

返回指定的所有数组的交集。结果是出现在所有参数中的值数组。

其他集合运算是UNION()、MINUS()OUTERSECTION()

  • 数组(数组,可重复):任意数量的数组作为多个参数(至少 2 个)
  • 返回newArray(数组):仅包含元素的单个数组,这些元素存在于所有提供的数组中。元素顺序是随机的。重复项将被删除。

例子

1
RETURN INTERSECTION( [1,2,3,4,5], [2,3,4,5,6], [3,4,5,6,7] )

显示查询结果

1
2
3
4
RETURN INTERSECTION( [2,4,6], [8,10,12], [14,16,18] )
[
[]
]

7.2.10 JACCARD()

版本介绍: v3.7.0

1
JACCARD(array1, array2) → jaccardIndex

计算两个数组的Jaccard 索引

此相似性度量也称为“在并集上的交集”,可以按如下方式计算(效率较低且较详细):

1
2
3
COUNT(a) == 0 && COUNT(b) == 0
? 1 // two empty sets have a similarity of 1 by definition
: COUNT(INTERSECTION(array1, array2)) / COUNT(UNION_DISTINCT(array1, array2))
  • array1(数组):具有任意类型元素的数组
  • array2(数组):具有任意类型元素的数组
  • 返回jaccardIndex(数字):计算出输入数组 array1 和array2的 Jaccard 索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RETURN JACCARD( [1,2,3,4], [3,4,5,6] )
[
0.3333333333333333
]
RETURN JACCARD( [1,1,2,2,2,3], [2,2,3,4] )
[
0.5
]
RETURN JACCARD( [1,2,3], [] )
[
0
]
RETURN JACCARD( [], [] )
[
1
]

7.2.11 LAST()

1
LAST(anyArray) → lastElement

获取数组的最后一个元素。它与 相同。anyArray[-1]

  • anyArray(数组):具有任意类型元素的数组
  • 返回lastElement (any|null):anyArray 的最后一个元素,如果数组为空,则返回 null。

1
2
3
4
RETURN LAST( [1,2,3,4,5] )
[
5
]

7.2.12 LENGTH()

1
LENGTH(anyArray) → length

确定数组中元素的数量。

  • anyArray(数组):具有任意类型元素的数组
  • 返回长度(数字):anyArray中数组元素的数量。

LENGTH()还可以确定对象/文档的属性键的数量、集合中的文档量和字符串的字符长度

输入 长度
字符串 统一码字符数
表示数字的 Unicode 字符数
数组 元素数
对象 第一级元素的数量
1
0
0

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RETURN LENGTH( "🥑" )
[
1
]
RETURN LENGTH( 1234 )
[
4
]
RETURN LENGTH( [1,2,3,4,5,6,7] )
[
7
]
RETURN LENGTH( false )
[
0
]
RETURN LENGTH( {a:1, b:2, c:3, d:4, e:{f:5,g:6}} )
[
5
]

7.2.13 MINUS()

1
MINUS(array1, array2, ... arrayN) → newArray

返回指定的所有数组的差值。

其他集合运算是UNION()、INTERSECTION()OUTERSECTION()

  • 数组(数组,可重复):任意数量的数组作为多个参数(至少 2 个)
  • 返回newArray(数组):一个值数组,这些值出现在第一个数组中,但不出现在任何后续数组中。结果数组的顺序未定义,不应依赖。重复项将被删除。

1
RETURN MINUS( [1,2,3,4], [3,4,5,6], [5,6,7,8] )

显示查询结果

7.2.14 NTH()

1
NTH(anyArray, position) → nthElement

获取给定位置的数组元素。它与正面仓位相同,但不支持负仓位。anyArray[position]

  • anyArray(数组):具有任意类型元素的数组
  • 位置(数字):数组中所需元素的位置,位置从 0 开始
  • 返回nthElement (any|null):位于给定位置的数组元素。如果position为负或超出数组的上限,则返回null。

例子

1
2
3
4
5
6
7
8
9
10
11
12
RETURN NTH( [ "foo", "bar", "baz" ], 2 )
[
"baz"
]
RETURN NTH( [ "foo", "bar", "baz" ], 3 )
[
null
]
RETURN NTH( [ "foo", "bar", "baz" ], -1 )
[
null
]

7.2.15 OUTERSECTION()

1
OUTERSECTION(array1, array2, ... arrayN) → newArray

返回在所有指定数组中仅出现一次的值。

其他集合运算是UNION()、MINUS()INTERSECTION()

  • 数组(数组,可重复):任意数量的数组作为多个参数(至少 2 个)
  • 返回newArray(数组):单个数组,其中只有元素在所有提供的数组中仅存在一次。元素顺序是随机的。

1
RETURN OUTERSECTION( [ 1, 2, 3 ], [ 2, 3, 4 ], [ 3, 4, 5 ] )

显示查询结果

7.2.16 POP()

1
POP(anyArray) → newArray

删除数组的最后一个元素。

要追加元素(右侧),请参见PUSH()
若要删除第一个元素,请参 阅SHIFT()
要删除任意位置的元素,请参阅REMOVE_NTH()

  • anyArray(数组):具有任意类型元素的数组
  • 返回newArray(数组):没有最后一个元素的anyArray。如果它已经为空或只剩下一个元素,则返回一个空数组。

例子

1
RETURN POP( [ 1, 2, 3, 4 ] )

显示查询结果

1
2
3
4
RETURN POP( [ 1 ] )
[
[]
]

7.2.17 POSITION()

1
POSITION(anyArray, search, returnIndex) → position

返回搜索是否包含在数组中。(可选)返回位置。

  • anyArray(数组):干草堆,具有任意类型元素的数组
  • 搜索(任意):针,任意类型的元素
  • returnIndex (bool, optional):如果设置为true,则返回匹配项的位置,而不是布尔值。默认值为false
  • 返回位置(bool|number):如果搜索包含在anyArray中,则为 true,否则为 false。如果启用了returnIndex,则返回匹配的位置(位置从 0 开始),如果未找到,则返回-1。

要确定字符串是否出现在另一个字符串中或出现在哪个位置,请参阅CONTAINS() 字符串函数

例子

1
2
3
4
5
6
7
8
RETURN POSITION( [2,4,6,8], 4 )
[
true
]
RETURN POSITION( [2,4,6,8], 4, true )
[
1
]

7.2.18 PUSH()

1
PUSH(anyArray, value, unique) → newArray

追加到anyArray(右侧)。

若要删除最后一个元素,请参见POP()
要在前面附加值(左侧),请参阅UNSHIFT()
要追加多个元素,请参阅APPEND()

  • anyArray(数组):具有任意类型元素的数组
  • (任意):任意类型的元素
  • unique (bool):如果设置为true,则如果数组中已存在,则不添加。默认值为false
  • 返回newArray(数组):anyArray,末尾添加了(右侧)

注意:唯标志仅控制是否添加了(如果该值已存在于anyArray 中)。不会删除anyArray中已存在的重复元素。要使数组唯一,请使用UNIQUE()函数。

例子

1
RETURN PUSH([ 1, 2, 3 ], 4)

显示查询结果

1
RETURN PUSH([ 1, 2, 2, 3 ], 2, true)

显示查询结果

7.2.19 REMOVE_NTH()

1
REMOVE_NTH(anyArray, position) → newArray

anyArray中删除位于 位置处的元素。

若要删除第一个元素,请参 阅SHIFT()
若要删除最后一个元素,请参见POP()

  • anyArray(数组):具有任意类型元素的数组
  • 位置(数字):要删除的元素的位置。仓位从 0 开始。支持负位置,-1 是最后一个数组元素。如果位置超出界限,则返回未修改的数组。
  • 返回newArray(数组):anyArray,**位置处没有元素

例子

1
RETURN REMOVE_NTH( [ "a", "b", "c", "d", "e" ], 1 )

显示查询结果

1
RETURN REMOVE_NTH( [ "a", "b", "c", "d", "e" ], -2 )

显示查询结果

7.2.20 REPLACE_NTH()

版本介绍: v3.7.0

1
REPLACE_NTH(anyArray, position, replaceValue, defaultPaddingValue) → newArray

anyArray位置处的元素替换为 replaceValue

  • anyArray(数组):具有任意类型元素的数组
  • 位置(数字):要替换的元素的位置。仓位从 0 开始。支持负位置,-1 是最后一个数组元素。如果负位置超出界限,则将其设置为第一个元素 (0)
  • replaceValue要插入到位置的值
  • defaultPadding如果*位置anyArray*中最后一个元素之外的两个或多个元素,则用于填充的值
  • 返回newArray(数组):anyArray,**其中位于位置的元素被replaceValue替换,或附加到anyArray,并且可能默认填充填充

允许指定超出上部数组边界的位置:

  • 如果位置等于数组长度,则追加replace值
  • 如果较高,则默认填充值将根据需要多次追加到anyArray中,以将replaceValue放在位置
  • 如果上述情况下未提供默认值PaddingValue,则会引发查询错误

例子

1
RETURN REPLACE_NTH( [ "a", "b", "c" ], 1 , "z")

显示查询结果

1
RETURN REPLACE_NTH( [ "a", "b", "c" ], 3 , "z")

显示查询结果

1
RETURN REPLACE_NTH( [ "a", "b", "c" ], 6, "z", "y" )

显示查询结果

1
RETURN REPLACE_NTH( [ "a", "b", "c" ], -1, "z" )

显示查询结果

1
RETURN REPLACE_NTH( [ "a", "b", "c" ], -9, "z" )

显示查询结果

尝试访问越界而不提供填充值将导致错误:

1
2
arangosh> db._query('RETURN REPLACE_NTH( [ "a", "b", "c" ], 6 , "z")');
[ArangoError 1542: AQL: invalid argument type in call to function 'REPLACE_NTH()' (while optimizing ast)]

7.2.21 REMOVE_VALUE()

1
REMOVE_VALUE(anyArray, value, limit) → newArray

删除anyArray中出现的所有。(可选)限制移除次数。

  • anyArray(数组):具有任意类型元素的数组
  • (任意):任意类型的元素
  • 限制(数量,可选):将移除次数限制为此值
  • 返回newArray(数组):删除了值**的 anyArray

例子

1
RETURN REMOVE_VALUE( [ "a", "b", "b", "a", "c" ], "a" )

显示查询结果

1
RETURN REMOVE_VALUE( [ "a", "b", "b", "a", "c" ], "a", 1 )

显示查询结果

7.2.22 REMOVE_VALUES()

1
REMOVE_VALUES(anyArray, values) → newArray

anyArray中删除任何的所有匹配项。

  • anyArray(数组):具有任意类型元素的数组
  • (数组):具有任意类型元素的数组,应从anyArray 中删除
  • 返回newArray(数组):删除了所有单个anyArray

1
RETURN REMOVE_VALUES( [ "a", "a", "b", "c", "d", "e", "f" ], [ "a", "f", "d" ] )

显示查询结果

7.2.23 REVERSE()

1
REVERSE(anyArray) → reversedArray

返回一个元素反转的数组。

  • anyArray(数组):具有任意类型元素的数组
  • 返回reversedArray(数组):一个新数组,其中anyArray的所有元素都以反转顺序排列

1
RETURN REVERSE ( [2,4,6,8,10] )

显示查询结果

7.2.24 SHIFT()

1
SHIFT(anyArray) → newArray

删除anyArray的第一个元素。

要在元素前面附加(左侧),请参阅UNSHIFT()
若要删除最后一个元素,请参见POP()
要删除任意位置的元素,请参阅REMOVE_NTH()

  • anyArray(数组):包含任意类型元素的数组
  • 返回newArray(数组):不带最左侧元素的anyArray。如果anyArray已经为空或只剩下一个元素,则返回一个空数组。

例子

1
RETURN SHIFT( [ 1, 2, 3, 4 ] )

显示查询结果

1
2
3
4
RETURN SHIFT( [ 1 ] )
[
[]
]

7.2.25 SLICE()

1
SLICE(anyArray, start, length) → newArray

提取anyArray 的切片 。

  • anyArray(数组):具有任意类型元素的数组
  • start(数字):在此元素处开始提取。仓位从 0 开始。负值表示数组末尾的位置。
  • 长度(数字,可选):提取最多长度的元素,或者从开始长度的所有元素(如果为负)(不含)
  • 返回newArray(数组):anyArray的指定切片。如果未指定长度,则将返回从开头开始的所有数组元素。

例子

1
2
3
4
5
6
7
RETURN SLICE( [ 1, 2, 3, 4, 5 ], 0, 1 )
[
[
1
]
]
RETURN SLICE( [ 1, 2, 3, 4, 5 ], 1, 2 )

显示查询结果

1
RETURN SLICE( [ 1, 2, 3, 4, 5 ], 3 )

显示查询结果

1
RETURN SLICE( [ 1, 2, 3, 4, 5 ], 1, -1 )

显示查询结果

1
RETURN SLICE( [ 1, 2, 3, 4, 5 ], 0, -2 )

显示查询结果

1
RETURN SLICE( [ 1, 2, 3, 4, 5 ], -3, 2 )

显示查询结果

7.2.26 SORTED()

1
SORTED(anyArray) → newArray

anyArray中的所有元素进行排序。该函数将使用 AQL 值类型的默认比较顺序。

  • anyArray(数组):具有任意类型元素的数组
  • 返回newArray (array): anyArray,元素已排序

1
RETURN SORTED( [ 8,4,2,10,6 ] )

显示查询结果

7.2.27 SORTED_UNIQUE()

1
SORTED_UNIQUE(anyArray) → newArray

anyArray中的所有元素进行排序。该函数将使用 AQL 值类型的默认比较顺序。此外,结果数组中的值将是唯一的。

  • anyArray(数组):具有任意类型元素的数组
  • 返回newArray(数组):anyArray,对元素进行排序并删除重复项

1
RETURN SORTED_UNIQUE( [ 8,4,2,10,6,2,8,6,4 ] )

显示查询结果

7.2.28 UNION()

1
UNION(array1, array2, ... arrayN) → newArray

返回指定的所有数组的联合。

其他集合运算是MINUS()、INTERSECTION()OUTERSECTION()

  • 数组(数组,可重复):任意数量的数组作为多个参数(至少 2 个)
  • 返回newArray(数组):所有数组元素以任意顺序组合在单个数组中

例子

1
2
3
4
RETURN UNION(
[ 1, 2, 3 ],
[ 1, 2 ]
)

显示查询结果

注意:不会删除任何重复项。为了删除重复项,请使用UNION_DISTINCT()或对UNION()的结果应用UNIQUE():

1
2
3
4
5
6
RETURN UNIQUE(
UNION(
[ 1, 2, 3 ],
[ 1, 2 ]
)
)

显示查询结果

7.2.29 UNION_DISTINCT()

1
UNION_DISTINCT(array1, array2, ... arrayN) → newArray

返回指定的所有数组的不同值的并集。

  • 数组(数组,可重复):任意数量的数组作为多个参数(至少 2 个)
  • 返回newArray(数组):单个数组中所有给定数组的元素,不重复,以任何顺序

1
2
3
4
RETURN UNION_DISTINCT(
[ 1, 2, 3 ],
[ 1, 2 ]
)

显示查询结果

7.2.30 UNIQUE()

1
UNIQUE(anyArray) → newArray

返回anyArray中的所有唯一元素。为了确定唯一性,该函数将使用比较顺序。

  • anyArray(数组):具有任意类型元素的数组
  • 返回newArray(数组):不带重复项的anyArray,以任何顺序

1
RETURN UNIQUE( [ 1,2,2,3,3,3,4,4,4,4,5,5,5,5,5 ] )

显示查询结果

7.2.31 UNSHIFT()

1
UNSHIFT(anyArray, value, unique) → newArray

附加到anyArray(左侧)。

若要删除第一个元素,请参 阅SHIFT()
要追加值(右侧),请参见PUSH()

  • anyArray(数组):具有任意类型元素的数组
  • (任意):任意类型的元素
  • unique (bool):如果设置为true,则如果数组中已存在,则不添加。默认值为false
  • 返回newArray(数组):在开头添加anyArray(左侧)

注意:唯标志仅控制是否添加了(如果该值已存在于anyArray 中)。不会删除anyArray中已存在的重复元素。要使数组唯一,请使用UNIQUE()函数。

例子

1
RETURN UNSHIFT( [ 1, 2, 3 ], 4 )

显示查询结果

1
RETURN UNSHIFT( [ 1, 2, 3 ], 2, true )

7.3 Bit

版本介绍: v3.7.7

AQL 为按位算术提供了一些位操作和解释函数。

这些函数可以对介于 0 和 4294967295 (2) 之间的数字整数值进行操作32- 1),两者都包括在内。这允许将数字视为最多 32 个成员的位集。对超出支持范围的数字使用任何位函数将使该函数返回并注册警告。null

位函数的值范围保守较小,因此当位函数的输入/输出值通过线路传递或发送到精度数类型未知的客户端应用程序时,不应发生精度损失或舍入错误。

7.3.1 BIT_AND()

1
BIT_AND(numbersArray) → result

并将numbersArray中的数值合并到单个数值结果值中。

  • 数字数组(数组):具有数字输入值的数组
  • 返回结果(数字|空):和组合结果

该函数需要一个以数值作为其输入的数组。数组中的值必须是数字,不能为负数。支持的最大输入数值为 232- 1.超出允许范围的输入数字值将使函数返回并生成警告。输入数组中的任何值都将被忽略。null``null

1
BIT_AND(value1, value2) → result

如果两个数字作为单独的函数参数传递给 ,它将返回其两个操作数的按位值和值。仅限 0 到 2 范围内的数字BIT_AND()32- 允许将 1 作为输入值。

  • 值1(数字):第一个操作数
  • 值2(数字):第二个操作数
  • 返回结果(数字|空):和组合结果
1
2
3
4
5
BIT_AND([1, 4, 8, 16]) // 0
BIT_AND([3, 7, 63]) // 3
BIT_AND([255, 127, null, 63]) // 63
BIT_AND(127, 255) // 127
BIT_AND("foo") // null

7.3.2 BIT_CONSTRUCT()

1
BIT_CONSTRUCT(positionsArray) → result

构造一个数字值,其位设置在数组中给定的位置。

  • 位置阵列(数组):具有要设置的位位置的数组(从零开始)
  • 返回结果(数字|空):生成的数字

该函数需要一个以数值作为其输入的数组。数组中的值必须是数字,不能为负数。支持的最大输入数值为 31。超出允许范围的输入数字值将使函数返回并生成警告。null

1
2
3
BIT_CONSTRUCT([1, 2, 3]) // 14
BIT_CONSTRUCT([0, 4, 8]) // 273
BIT_CONSTRUCT([0, 1, 10, 31]) // 2147484675

7.3.3 BIT_DECONSTRUCT()

1
BIT_DECONSTRUCT(number) → positionsArray

将数字值解构为具有其设置位位置的数组。

  • number(number):要解构的输入值
  • 返回位置数组(数组|null):设置了位位置的数组(从零开始)

该函数将一个数值转换为一个数组,其中包含其所有设置位的位置。输出数组中的位置从零开始。输入值必须是介于 0 和 2 之间的数字32- 1(包括)。该函数将返回任何其他输入并生成警告。null

1
2
3
BIT_DECONSTRUCT(14) // [1, 2, 3]
BIT_DECONSTRUCT(273) // [0, 4, 8]
BIT_DECONSTRUCT(2147484675) // [0, 1, 10, 31]

7.3.4 BIT_FROM_STRING()

1
BIT_FROM_STRING(bitstring) → number

将位字符串(由数字和 )转换为数字。0``1

要将数字转换为位字符串,请参阅BIT_TO_STRING()

  • 位字符串(字符串):由 和 字符组成的字符串序列0``1
  • 返回数字(数字|null):解析的数字

输入值必须是位字符串,仅由 和 字符组成。位字符串最多可以包含 32 个有效位,包括任何前导零。请注意,位字符串不得以 开头。如果位字符串具有无效的格式,则此函数将返回并生成警告。0``1``0b``null

1
2
3
4
BIT_FROM_STRING("0111") // 7
BIT_FROM_STRING("000000000000010") // 2
BIT_FROM_STRING("11010111011101") // 13789
BIT_FROM_STRING("100000000000000000000") // 1048756

7.3.5 BIT_NEGATE()

1
BIT_NEGATE(number, bits) → result

按位否定位数,并保持结果中的位。

  • 数字(数字):要否定的数字
  • (数字):要保留在结果中的位数(0 到 32)
  • 返回结果(数字|null):结果数字,最多包含有效位

输入值必须是介于 0 和 2 之间的数字32- 1(包括)。位数必须介于 0 和 32 之间。该函数将返回任何其他输入并生成警告。null

1
2
3
4
BIT_NEGATE(0, 8) // 255
BIT_NEGATE(0, 10) // 1023
BIT_NEGATE(3, 4) // 12
BIT_NEGATE(446359921, 32) // 3848607374

7.3.6 BIT_OR()

1
BIT_OR(numbersArray) → result

Or 将numbersArray中的数值合并到单个数值结果值中。

  • 数字数组(数组):具有数字输入值的数组
  • 返回结果(数字|空):或组合结果

该函数需要一个以数值作为其输入的数组。数组中的值必须是数字,不能为负数。支持的最大输入数值为 232- 1.超出允许范围的输入数字值将使函数返回并生成警告。输入数组中的任何值都将被忽略。null``null

1
BIT_OR(value1, value2) → result

如果两个数字作为单独的函数参数传递给 ,它将返回其两个操作数的按位或值。仅限 0 到 2 范围内的数字BIT_OR()32- 允许将 1 作为输入值。

  • 值1(数字):第一个操作数
  • 值2(数字):第二个操作数
  • 返回结果(数字|空):或组合结果
1
2
3
4
5
BIT_OR([1, 4, 8, 16]) // 29
BIT_OR([3, 7, 63]) // 63
BIT_OR([255, 127, null, 63]) // 255
BIT_OR(255, 127) // 255
BIT_OR("foo") // null

7.3.7 BIT_POPCOUNT()

1
BIT_POPCOUNT(number) → result

计算输入值中设置的位数。

  • 数字(数字):带有数字输入值的数组
  • 返回结果(数字|ull):输入值中设置的位数

输入值必须是介于 0 和 2 之间的数字32- 1(包括)。该函数将返回任何其他输入并生成警告。null

1
2
3
4
BIT_POPCOUNT(0) // 0
BIT_POPCOUNT(255) // 8
BIT_POPCOUNT(69399252) // 12
BIT_POPCOUNT("foo") // null

7.3.8 BIT_SHIFT_LEFT()

1
BIT_SHIFT_LEFT(number, shift, bits) → result

按位将数字中的位向左移动,并在结果中保持位。当位由于偏移而溢出时,它们将被丢弃。

  • 数字(数字):要移位的数字
  • 移位(数字):要移位的位数(0 到 32)
  • (数字):要保留在结果中的位数(0 到 32)
  • 返回结果(数字|null):结果数字,最多包含有效位

输入值必须是介于 0 和 2 之间的数字32- 1(包括)。位数必须介于 0 和 32 之间。该函数将返回任何其他输入并生成警告。null

1
2
3
4
BIT_SHIFT_LEFT(0, 1, 8) // 0
BIT_SHIFT_LEFT(7, 1, 16) // 14
BIT_SHIFT_LEFT(2, 10, 16) // 2048
BIT_SHIFT_LEFT(878836, 16, 32) // 1760821248

7.3.9 BIT_SHIFT_RIGHT()

1
BIT_SHIFT_RIGHT(number, shift, bits) → result

按位将数字中的位向右移动,并在结果中保持位。当位由于偏移而溢出时,它们将被丢弃。

  • 数字(数字):要移位的数字
  • 移位(数字):要移位的位数(0 到 32)
  • (数字):要保留在结果中的位数(0 到 32)
  • 返回结果(数字|null):结果数字,最多包含有效位

输入值必须是介于 0 和 2 之间的数字32- 1(包括)。位数必须介于 0 和 32 之间。该函数将返回任何其他输入并生成警告。null

1
2
3
4
BIT_SHIFT_RIGHT(0, 1, 8) // 0
BIT_SHIFT_RIGHT(33, 1, 16) // 16
BIT_SHIFT_RIGHT(65536, 13, 16) // 8
BIT_SHIFT_RIGHT(878836, 4, 32) // 54927

7.3.10 BIT_TEST()

1
BIT_TEST(number, index) → result

测试 at 位置索引是否设置为数字

  • 数字(number):要测试的数字
  • 索引(数字):要测试的位的索引(0 到 31)
  • 返回结果(布尔|null):无论是否设置了位

输入值必须是介于 0 和 2 之间的数字32- 1(包括)。索引必须介于 0 和 31 之间。该函数将返回任何其他输入并生成警告。null

1
2
3
4
BIT_TEST(0, 3) // false
BIT_TEST(255, 0) // true
BIT_TEST(7, 2) // true
BIT_TEST(255, 8) // false

7.3.11 BIT_TO_STRING()

1
BIT_TO_STRING(number) → bitstring

将数字输入值转换为位字符串,由 和 组成。0``1

要将位字符串转换为数字,请参见BIT_FROM_STRING()

  • number(number):要字符串化的数字
  • 返回位字符串(字符串|null):从输入值生成的位字符串

输入值必须是介于 0 和 2 之间的数字32- 1(包括)。该函数将返回任何其他输入并生成警告。null

1
2
3
4
BIT_TO_STRING(7, 4) // "0111"
BIT_TO_STRING(255, 8) // "11111111"
BIT_TO_STRING(60, 8) // "00011110"
BIT_TO_STRING(1048576, 32) // "00000000000100000000000000000000"

7.3.12 BIT_XOR()

1
BIT_XOR(numbersArray) → result

numbersArray中的数值独占或组合成单个数值结果值。

  • 数字数组(数组):具有数字输入值的数组
  • 返回结果(数字|ull):异或组合结果

该函数需要一个以数值作为其输入的数组。数组中的值必须是数字,不能为负数。支持的最大输入数值为 232- 1.超出允许范围的输入数字值将使函数返回并生成警告。输入数组中的任何值都将被忽略。null``null

1
BIT_XOR(value1, value2) → result

如果将两个数字作为单独的函数参数传递给 ,它将返回其两个操作数的按位独占或值。仅限 0 到 2 范围内的数字BIT_XOR()32- 允许将 1 作为输入值。

  • 值1(数字):第一个操作数
  • 值2(数字):第二个操作数
  • 返回结果(数字|ull):异或组合结果
1
2
3
4
5
BIT_XOR([1, 4, 8, 16]) // 29
BIT_XOR([3, 7, 63]) // 59
BIT_XOR([255, 127, null, 63]) // 191
BIT_XOR(255, 257) // 510
BIT_XOR("foo") // null

7.4 Date

AQL 提供了处理日期的功能,但它没有用于日期的特殊数据类型(JSON 也没有,JSON 通常用作将数据传入和传出 ArangoDB 的格式)。相反,AQL 中的日期由数字或字符串表示。

所有日期函数操作都在Unix 时间系统中完成。Unix 时间计算所有从 1970 年 1 月 1 日 00:00:00.00 UTC 开始的非闰秒,也称为 Unix 纪元。时间点称为时间戳。时间戳在地球上的每个点上都具有相同的值。日期函数对时间戳使用毫秒级精度。

时间单位定义:

  • 毫秒:1/1000 秒
  • : 一SI 秒
  • 分钟:一分钟定义为 60 秒
  • 小时:1 小时定义为 60 分钟
  • :一天定义为24小时
  • :一周定义为7天
  • :一个月定义为一年的1/12
  • : 一年定义为 365.2425 天

所有需要日期作为参数的函数都接受以下输入值:

  • 数字时间戳,毫秒精度。

    示例时间戳值为 ,它转换为 。1399472349522``2014-05-07T14:19:09.522Z

    有效范围:.. (含)-62167219200000``253402300799999

  • ISO 8601格式的日期时间字符串

    • YYYY-MM-DDTHH:MM:SS.MMM
    • YYYY-MM-DD HH:MM:SS.MMM
    • YYYY-MM-DD

    毫秒 () 始终是可选的。小时 ()、分钟 () 和秒 () 的两位数是必需的,即值 0 到 9 需要零填充(例如 而不是 )。可以省略年 ()、月 () 和日 () 的前导零,但不鼓励这样做。.MMM``HH``MM``SS``05``5``YYYY``MM``DD

    可以选择在字符串末尾添加时间偏移量,并将需要添加或减去的小时和分钟添加到日期时间值中。例如,可用于指定一小时偏移量,并且可以指定为七个半小时的偏移量。负偏移也是可能的。或者偏移量,a 可用于指示 UTC /祖鲁时间。示例值表示 2014 年 5 月 7 日 14:19:09 和 522 毫秒,UTC /祖鲁时间。另一个不带时间分量的示例值是 。2014-05-07T14:19:09+01:00``2014-05-07T14:19:09+07:30``Z``2014-05-07T14:19:09.522Z``2014-05-07Z

    有效范围:.. (含)"0000-01-01T00:00:00.000Z"``"9999-12-31T23:59:59.999Z"

传递到 AQL 日期函数的有效范围之外的任何日期/时间值都将使该函数返回并触发查询警告,这可以选择升级为错误并中止查询。这也适用于生成无效值的操作。null

1
2
DATE_HOUR( 2 * 60 * 60 * 1000 ) // 2
DATE_HOUR("1970-01-01T02:00:00") // 2

当然,您可以自由地以不同的,更合适的方式存储标本的年龄确定,不完整或模糊的日期等。AQL 的 date 函数肯定不会对此类日期有任何帮助,但您仍然可以使用像 SORT(也支持数组排序)和索引之类的语言构造。

7.4.1 当前日期和时间

DATE_NOW()

1
DATE_NOW() → timestamp

获取当前 unix 时间作为数字时间戳。

  • 返回时间戳(数字):当前 unix 时间作为时间戳。返回值具有毫秒级精度。要将返回值转换为秒,请将其除以 1000。

请注意,此函数在每次调用时都会计算,并且在同一查询中多次调用时可能会返回不同的值。将其分配给变量以多次使用完全相同的时间戳。

Conversion

DATE_TIMESTAMP()DATE_ISO8601()可用于将 ISO 8601 日期时间字符串转换为数字时间戳,将数字时间戳转换为 ISO 8601 日期时间字符串。

两者都支持将各个日期组件作为单独的函数参数,顺序如下:

  • 小时
  • 分钟
  • 第二
  • 毫秒

二天的所有组件都是可选的,可以省略。请注意,使用单独的日期组件时,不能指定时间偏移,并且将使用 UTC/祖鲁时间。

以下对DATE_TIMESTAMP()的调用是等效的,并且都将返回1399472349522:

1
2
3
4
5
6
DATE_TIMESTAMP("2014-05-07T14:19:09.522")
DATE_TIMESTAMP("2014-05-07T14:19:09.522Z")
DATE_TIMESTAMP("2014-05-07 14:19:09.522")
DATE_TIMESTAMP("2014-05-07 14:19:09.522Z")
DATE_TIMESTAMP(2014, 5, 7, 14, 19, 9, 522)
DATE_TIMESTAMP(1399472349522)

对于也接受变量输入格式的DATE_ISO8601()的调用也是如此:

1
2
3
4
DATE_ISO8601("2014-05-07T14:19:09.522Z")
DATE_ISO8601("2014-05-07 14:19:09.522Z")
DATE_ISO8601(2014, 5, 7, 14, 19, 9, 522)
DATE_ISO8601(1399472349522)

以上函数均等效,将返回“2014-05-07T14:19:09.522Z”。

DATE_ISO8601()

1
DATE_ISO8601(date) → dateString

从日期 返回 ISO 8601日期时间字符串。日期时间字符串将始终使用 UTC/祖鲁时间,由其末尾的Z指示。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回日期字符串:根据 ISO 8601 以祖鲁时间表示的日期和时间
1
DATE_ISO8601(year, month, day, hour, minute, second, millisecond) → dateString

从 date 返回 ISO 8601日期时间字符串,但允许单独指定各个日期组件。后的所有参数都是可选的。

  • 年份(数字):通常在0..9999范围内,例如2017年
  • (数字):1 月到 12 月为 1..12
  • (数字):1..31(上限取决于月中的天数)
  • 小时(数字,可选): 0..23
  • 分钟(数字,可选):0..59
  • (数字,可选):0..59
  • 毫秒(数字,可选):0..999
  • 返回日期字符串:根据 ISO 8601 以祖鲁时间表示的日期和时间

DATE_TIMESTAMP()

1
DATE_TIMESTAMP(date) → timestamp

日期创建时间戳值。返回值具有毫秒级精度。要将返回值转换为秒,请将其除以 1000。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回时间戳(数字):数字时间戳
1
DATE_TIMESTAMP(year, month, day, hour, minute, second, millisecond) → timestamp

创建时间戳值,但允许单独指定各个日期组件。后的所有参数都是可选的。

  • 年份(数字):通常在0..9999范围内,例如2017年
  • (数字):1 月到 12 月为 1..12
  • (数字):1..31(上限取决于月中的天数)
  • 小时(数字,可选): 0..23
  • 分钟(数字,可选):0..59
  • (数字,可选):0..59
  • 毫秒(数字,可选):0..999
  • 返回时间戳(数字):数字时间戳

不允许使用负值,结果为null并导致警告。大于范围上限的值溢出到较大的分量(例如,26 小时自动转换为额外的一天和两小时):

1
2
3
DATE_TIMESTAMP(2016, 12, -1) // returns null and issues a warning
DATE_TIMESTAMP(2016, 2, 32) // returns 1456963200000, which is March 3rd, 2016
DATE_TIMESTAMP(1970, 1, 1, 26) // returns 93600000, which is January 2nd, 1970, at 2 a.m.

IS_DATESTRING()

1
IS_DATESTRING(value) → bool

检查任意字符串是否适合解释为日期时间字符串。

  • (字符串):任意字符串
  • 返回bool (bool):如果value是可在日期函数中使用的字符串,则为 true。这包括部分日期(如2015 年2015-10 年)和包含无效日期(如2015-02-31)的字符串。该函数将对所有非字符串值返回false,即使其中一些值可能在 date 函数中可用。

7.4.2 Processing

DATE_DAYOFWEEK()

1
DATE_DAYOFWEEK(date) → weekdayNumber

返回工作日日期编号 。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回weekdayNumber (number): 0..6,如下所示:
    • 0 – 周日
    • 1 – 星期一
    • 2 – 星期二
    • 3 – 星期三
    • 4 – 星期四
    • 5 – 星期五
    • 6 – 星期六

DATE_YEAR()

1
DATE_YEAR(date) → year

返回日期的年份 。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回年份(数字):日期的年份部分作为数字

DATE_MONTH()

1
DATE_MONTH(date) → month

返回月份日期

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回月份(数字):日期的月份部分作为数字

DATE_DAY()

1
DATE_DAY(date) → day

返回日期日期 。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回日期(数字):日期的日期部分作为数字

DATE_HOUR()

返回日期小时 。

1
DATE_HOUR(date) → hour
  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回小时(数字):日期的小时部分作为数字

DATE_MINUTE()

1
DATE_MINUTE(date) → minute

返回日期的分钟数。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回分钟(数字):日期的分钟部分作为数字

DATE_SECOND()

1
DATE_SECOND(date) → second

返回日期的第二个。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回(数字):日期的秒部分作为数字

DATE_MILLISECOND()

1
DATE_MILLISECOND(date) → millisecond
  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回毫秒(数字):日期的毫秒部分作为数字

DATE_DAYOFYEAR()

1
DATE_DAYOFYEAR(date) → dayOfYear

返回年份日期的日期

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回日期年份(数字):日期的年份之日。返回值的范围分别为 1 到 365,或闰年分别为 366。

DATE_ISOWEEK()

1
DATE_ISOWEEK(date) → weekDate

根据 ISO 8601 返回日期的周日期。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回weekDate(数字):日期的ISO 周日期。返回值的范围从 1 到 53。星期一被认为是一周的第一天。没有小数周,因此12月的最后几天可能属于下一年的第一周,而1月的第一天可能是前一年最后一周的一部分。

DATE_LEAPYEAR()

1
DATE_LEAPYEAR(date) → leapYear

返回日期是否在闰年中。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回leapYear (bool):如果日期在闰年中,则为 true,否则为 false

DATE_QUARTER()

1
DATE_QUARTER(date) → quarter

返回属于哪个季度的日期

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回季度(数字):给定日期的季度(基于 1):
    • 1 – 1月,2月,3月
    • 2 – 4月,5月,6月
    • 3 – 7月,8月,9月
    • 4 – 10月,11月,12月

DATE_DAYS_IN_MONTH()

返回日期月份中的天数。

1
DATE_DAYS_IN_MONTH(date) → daysInMonth
  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 返回天月(数字):日期的月份中的天数 (28..31)

DATE_TRUNC()

1
DATE_TRUNC(date, unit) → isoDate

单位后截断给定日期并返回修改后的日期。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • unit(字符串):指定时间单位(不区分大小写):满足以下任一条件:
    • y、年、年
    • m, 月, 月
    • d、天、天
    • h, 小时, 小时
    • i, 分钟, 分钟
    • s,秒,秒
    • f,毫秒,毫秒
  • 返回isoDate(字符串):截断的 ISO 8601 日期时间字符串
1
2
DATE_TRUNC('2017-02-03', 'month') // 2017-02-01T00:00:00.000Z
DATE_TRUNC('2017-02-03 04:05:06', 'hours') // 2017-02-03 04:00:00.000Z
1
2
3
4
5
RETURN MERGE(
FOR doc IN @data
COLLECT q = DATE_TRUNC(doc.date, "year") INTO bucket
RETURN { [DATE_YEAR(q)]: bucket[*].doc.value }
)

显示查询结果

DATE_ROUND()

引入: v3.6.0

1
DATE_ROUND(date, amount, unit) → isoDate

将日期/时间装箱到一组等距离存储桶中,以用于分组。

  • 日期(字符串|数字):日期字符串或时间戳
  • 数量(数量):单位数量。必须是正整数值。
  • unit(字符串):指定时间单位(不区分大小写):满足以下任一条件:
    • d、天、天
    • h, 小时, 小时
    • i, 分钟, 分钟
    • s,秒,秒
    • f,毫秒,毫秒
  • 返回isoDate(字符串):舍入的 ISO 8601 日期时间字符串
1
2
DATE_ROUND('2000-04-28T11:11:11.111Z', 1, 'day') // 2000-04-28T00:00:00.000Z
DATE_ROUND('2000-04-10T11:39:29Z', 15, 'minutes') // 2000-04-10T11:30:00.000Z
1
2
3
4
5
6
7
8
9
FOR doc IN @sensorData
COLLECT
date = DATE_ROUND(doc.timestamp, 5, "minutes")
AGGREGATE
count = COUNT(1),
avg = AVG(doc.temp),
min = MIN(doc.temp),
max = MAX(doc.temp)
RETURN { date, count, avg, min, max }

显示查询结果

DATE_FORMAT()

1
DATE_FORMAT(date, format) → str

根据给定的格式字符串设置日期格式。

  • 日期(字符串|数字):日期字符串或时间戳
  • 格式(字符串):格式字符串,见下文
  • 返回str(字符串):格式化的日期字符串

格式支持以下占位符(不区分大小写):

  • %t – 时间戳,自午夜 1970-01-01 起以毫秒为单位
  • %z – ISO 日期 (0000-00-00T00:00:00.000Z)
  • %w – 星期几 (0..6)
  • %y – 年 (0..9999)
  • %yy – 年份 (00..99),缩写(最后两位数字)
  • %yyyy – 年份 (0000..9999),填充长度为 4
  • %yyyyyy – 年份 (-009999 .. +009999),带符号前缀,填充长度为 6
  • %m – 月 (1..12)
  • %mm – 月 (01..12),填充长度为 2
  • %d – 天 (1..31)
  • %dd – 天 (01..31),填充长度为 2
  • %h – 小时 (0..23)
  • %hh – 小时 (00..23),填充长度为 2
  • %i – 分钟 (0..59)
  • %ii – 分钟 (00..59),填充长度为 2
  • %s – 秒 (0..59)
  • %ss – 秒 (00..59),填充长度为 2
  • %f – 毫秒 (0..999)
  • %fff – 毫秒 (000..999),填充长度为 3
  • %x – 一年中的某一天 (1..366)
  • %xxx – 一年中的某一天 (001..366),填充长度为 3
  • %k – ISO 周日期 (1..53)
  • %kk – ISO 周日期 (01..53),填充长度为 2
  • %l – 闰年(0 或 1)
  • %q – 季度 (1..4)
  • %a – 月中的天数 (28..31)
  • %mmm – 月份的缩写英文名称(1 月。12月)
  • %mmmm – 月份的英文名称(1 月。。十二月)
  • %www – 工作日(星期日..星期六)
  • %wwww – 工作日(星期日)的英文名称。星期六)
  • %& – 适用于极少数场合的特殊转义序列
  • %% – 文字 %
  • % – 忽略
1
%yyyy`在 0 年之前和 9999 之后的年份中不强制执行 4 的长度。将改用与 相同的格式。 将该标志保留为负年份,因此总共可能返回 3 个字符。`%yyyyyy``%yy

单个字符将被忽略。用于文字 .要解决像在 + “onth” 和 + “month” 之间的 in(未填充的月份编号 + 字符串 “month”)这样的歧义,请使用转义序列 : 。%``%%``%``%mmonth``%mm``%m``%&``%m%&month

请注意,DATE_FORMAT()是一个相当昂贵的操作,可能不适合大型数据集(如超过 100 万个日期)。如果可能,请避免在服务器端设置日期格式,并将其留给客户端执行此操作。此函数应仅用于特殊日期比较或将格式化日期存储在数据库中。为了获得更好的性能,请尽可能将基元函数与基元函数一起使用。DATE_*()``CONCAT()

例子:

1
2
3
4
5
DATE_FORMAT(DATE_NOW(), "%q/%yyyy") // quarter and year (e.g. "3/2015")
DATE_FORMAT(DATE_NOW(), "%dd.%mm.%yyyy %hh:%ii:%ss,%fff") // e.g. "18.09.2015 15:30:49,374"
DATE_FORMAT("1969", "Summer of '%yy") // "Summer of '69"
DATE_FORMAT("2016", "%%l = %l") // "%l = 1" (2016 is a leap year)
DATE_FORMAT("2016-03-01", "%xxx%") // "063", trailing % ignored

7.4.3 比较和计算

DATE_ADD()

1
DATE_ADD(date, amount, unit) → isoDate

以单位表示的金额相加至日期,并返回计算出的日期。

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 金额(数字|字符串):要添加(正值)或减去(负值)的单位数。建议仅使用正值,而改用DATE_SUBTRACT()进行减法。
  • unit(字符串):指定要加减的时间单位(不区分大小写):满足以下任一条件:
    • y、年、年
    • m, 月, 月
    • w, 周, 周
    • d、天、天
    • h, 小时, 小时
    • i, 分钟, 分钟
    • s,秒,秒
    • f,毫秒,毫秒
  • 返回isoDate(字符串):计算的 ISO 8601 日期时间字符串
1
2
3
4
5
6
7
DATE_ADD(DATE_NOW(), -1, "day") // yesterday; also see DATE_SUBTRACT()
DATE_ADD(DATE_NOW(), 3, "months") // in three months
DATE_ADD(DATE_ADD("2015-04-01", 5, "years"), 1, "month") // May 1st 2020
DATE_ADD("2015-04-01", 12*5 + 1, "months") // also May 1st 2020
DATE_ADD(DATE_TIMESTAMP(DATE_YEAR(DATE_NOW()), 12, 24), -4, "years") // Christmas four years ago
DATE_ADD(DATE_ADD("2016-02", "month", 1), -1, "day") // last day of February (29th, because 2016 is a leap year!)
DATE_ADD(date, isoDuration) → isoDate

您也可以将 ISO 持续时间字符串作为数量传递,并省略单位

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • isoDuration(字符串):要添加到日期的 ISO 8601 持续时间字符串,请参见下文
  • 返回isoDate(字符串):计算的 ISO 8601 日期时间字符串

格式为 ,其中下划线代表数字,字母代表时间间隔 - 分隔符(句点)和(时间)除外。其他字母的含义是:P_Y_M_W_DT_H_M_._S``P``T

  • Y – 年
  • M – 月(如果在 T 之前)
  • W – 周
  • D – 天
  • H – 小时
  • M – 分钟(如果在 T 之后)
  • S – 秒(可选,以 3 个小数位表示毫秒)

该字符串必须以 .仅当指定 了 和/或 时,才需要分隔。您只需要指定所需的字母和数字对。P``T``H``M``S

1
2
3
4
5
6
DATE_ADD(DATE_NOW(), "P1Y") // add 1 year
DATE_ADD(DATE_NOW(), "P3M2W") // add 3 months and 2 weeks
DATE_ADD(DATE_NOW(), "P5DT26H") // add 5 days and 26 hours (=6 days and 2 hours)
DATE_ADD("2000-01-01", "PT4H") // add 4 hours
DATE_ADD("2000-01-01", "PT30M44.4S" // add 30 minutes, 44 seconds and 400 ms
DATE_ADD("2000-01-01", "P1Y2M3W4DT5H6M7.89S" // add a bit of everything

DATE_SUBTRACT()

1
DATE_SUBTRACT(date, amount, unit) → isoDate

日期中减去以单位给出的金额,并返回计算的日期。

它的工作原理与DATE_ADD()相同,只是它减去。它等效于用负量调用DATE_ADD(),除了DATE_SUBTRACT()也可以减去 ISO 持续时间。请注意,不支持负 ISO 持续时间(即以 开头,如 )。-P``-P1Y

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 金额(数字|字符串):要减去(正值)或加(负值)的单位s的数量。建议仅使用正值,而应改用DATE_ADD()进行加法。
  • unit(字符串):指定要加减的时间单位(不区分大小写):满足以下任一条件:
    • y、年、年
    • m, 月, 月
    • w, 周, 周
    • d、天、天
    • h, 小时, 小时
    • i, 分钟, 分钟
    • s,秒,秒
    • f,毫秒,毫秒
  • 返回isoDate(字符串):计算的 ISO 8601 日期时间字符串
1
DATE_SUBTRACT(date, isoDuration) → isoDate

您也可以将 ISO 持续时间字符串作为数量传递,并省略单位

  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • isoDuration(字符串):要从日期中减去的 ISO 8601 持续时间字符串,请参见下文
  • 返回isoDate(字符串):计算的 ISO 8601 日期时间字符串

格式为 ,其中下划线代表数字,字母代表时间间隔 - 分隔符(句点)和(时间)除外。其他字母的含义是:P_Y_M_W_DT_H_M_._S``P``T

  • Y – 年
  • M – 月(如果在 T 之前)
  • W – 周
  • D – 天
  • H – 小时
  • M – 分钟(如果在 T 之后)
  • S – 秒(可选,以 3 个小数位表示毫秒)

该字符串必须以 .仅当指定 了 和/或 时,才需要分隔。您只需要指定所需的字母和数字对。P``T``H``M``S

1
2
3
4
5
DATE_SUBTRACT(DATE_NOW(), 1, "day") // yesterday
DATE_SUBTRACT(DATE_TIMESTAMP(DATE_YEAR(DATE_NOW()), 12, 24), 4, "years") // Christmas four years ago
DATE_SUBTRACT(DATE_ADD("2016-02", "month", 1), 1, "day") // last day of February (29th, because 2016 is a leap year!)
DATE_SUBTRACT(DATE_NOW(), "P4D") // four days ago
DATE_SUBTRACT(DATE_NOW(), "PT1H3M") // 1 hour and 30 minutes ago

DATE_DIFF()

1
DATE_DIFF(date1, date2, unit, asFloat) → diff

以给定的时间单位计算两个日期之间的差值,可选地使用小数位。

  • date1(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • date2(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • unit(字符串):指定用于指定要返回差值的时间单位之一(不区分大小写):
    • y、年、年
    • m, 月, 月
    • w, 周, 周
    • d、天、天
    • h, 小时, 小时
    • i, 分钟, 分钟
    • s,秒,秒
    • f,毫秒,毫秒
  • asFloat(布尔值,可选):如果设置为true,则结果中将保留小数位数。默认值为false,并返回一个整数。
  • 返回diff(数字):计算出的差值,以单位为单位的数字。如果date2早于date1,则该值将为负数。

DATE_COMPARE()

1
DATE_COMPARE(date1, date2, unitRangeStart, unitRangeEnd) → bool

检查两个部分日期是否匹配。

  • date1(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • date2(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • unitRangeStart(字符串):要从中开始的单位,见下文
  • unitRangeEnd(字符串,可选):以单位结尾,省略仅比较unitRangeStart指定的组件。如果unitRangeEnd 是 unitRangeStart之前的单元,则会引发错误。
  • 返回bool (bool):如果日期匹配,则为 true,否则为 false

要比较的部分由一系列时间单位定义。完整范围是:年、月、日、小时、分钟、秒、毫秒(按此顺序)。

将比较范围指定的date1date2的所有组成部分。您可以将这些单位称为:

  • y、年、年
  • m, 月, 月
  • d、天、天
  • h, 小时, 小时
  • i, 分钟, 分钟
  • s,秒,秒
  • f,毫秒,毫秒
1
2
3
4
5
6
7
8
9
// Compare months and days, true on birthdays if you're born on 4th of April
DATE_COMPARE("1985-04-04", DATE_NOW(), "months", "days")

// Will only match on one day if the current year is a leap year!
// You may want to add or subtract one day from date1 to match every year.
DATE_COMPARE("1984-02-29", DATE_NOW(), "months", "days")

// compare years, months and days (true, because it's the same day)
DATE_COMPARE("2001-01-01T15:30:45.678Z", "2001-01-01T08:08:08.008Z", "years", "days")

如果要查找特定日期之前或之后的日期,或者介于两个日期 (, , , ) 之间的日期,则可以直接比较 ISO 日期字符串。不需要特殊的日期功能。但是,相等性检验(和)将只匹配完全相同的日期和时间。您可以使用比较部分日期字符串,基本上是一个方便的功能。但是,将搜索限制为某一天也不需要,如下所示:>=``>``<``<=``==``!=``SUBSTRING()``DATE_COMPARE()

1
2
3
FOR doc IN coll
FILTER doc.date >= "2015-05-15" AND doc.date < "2015-05-16"
RETURN doc

该日期的每个 ISO 日期在字符串比较中大于或等于(例如 )。之前的日期较小,因此按第一个条件过滤掉。在字符串比较中,过去的每个日期都大于此日期,因此按第二个条件过滤掉。结果是,您与之比较的日期中的时间分量被”忽略”。该查询将返回日期范围为 的每个文档。它还将包括 ,但该日期实际上是并且只有在手动插入时才能发生(您可能希望将日期传递到 DATE_ISO8601()以确保正确的日期表示形式)。2015-05-15``2015-05-15T11:30:00.000Z``2015-05-15``2015-05-15``2015-05-15T00:00:00.000Z``2015-05-15T23:99:99.999Z``2015-05-15T24:00:00.000Z``2015-05-16T00:00:00.000Z

闰年(2 月 29 日)的闰日必须始终手动处理,如果需要,请这样做(例如生日检查):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LET today = DATE_NOW()
LET noLeapYear = NOT DATE_LEAPYEAR(today)

FOR user IN users
LET birthday = noLeapYear AND
DATE_MONTH(user.birthday) == 2 AND
DATE_DAY(user.birthday) == 29
? DATE_SUBTRACT(user.birthday, 1, "day") /* treat like 28th in non-leap years */
: user.birthday
FILTER DATE_COMPARE(today, birthday, "month", "day")
/* includes leaplings on the 28th of February in non-leap years,
* but excludes them in leap years which do have a 29th February.
* Replace DATE_SUBTRACT() by DATE_ADD() to include them on the 1st of March
* in non-leap years instead (depends on local jurisdiction).
*/
RETURN user

DATE_UTCTOLOCAL()

引入: v3.8.0

将以祖鲁时间 (UTC) 假定的日期转换为本地时区

它考虑了历史夏令时。

1
DATE_UTCTOLOCAL(date, timezone, zoneinfo) → date
  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 时区(字符串):IANA 时区名称,例如 ,或 。用于太平洋时间(太平洋标准时间/太平洋夏令时)。如果 ArangoDB 不知道时区,则会引发错误"America/New_York"``"Europe/Berlin"``"UTC"``"America/Los_Angeles"
  • zoneinfo(布尔值,可选):如果设置为true,则返回具有时区信息的对象。默认值为false,并返回日期字符串
  • 返回date(字符串|对象):采用非限定本地时间的 ISO 8601 日期时间字符串,或具有以下属性的对象:
    • 本地(字符串):ISO 8601 日期时间字符串(采用非限定的本地时间)
    • tzdb(字符串):所用时区数据库的版本(例如"2020f")
    • 区域信息: (对象): 时区信息
      • 名称(字符串):时区缩写(GMT、太平洋标准时间、欧洲中部时间等)
      • begin (string|null):时区效果的开始,作为 UTC 日期时间字符串
      • end (string|null):时区效果的结束,作为 UTC 日期时间字符串
      • dst(布尔值):当夏令时 (DST) 处于活动状态时为 true,否则为 false
      • 偏移量(数字):以秒为单位的UTC偏移量
1
2
3
4
5
RETURN [
DATE_UTCTOLOCAL("2020-03-15T00:00:00.000", "Europe/Berlin"),
DATE_UTCTOLOCAL("2020-03-15T00:00:00.000", "America/New_York"),
DATE_UTCTOLOCAL("2020-03-15T00:00:00.000", "UTC")
]

显示查询结果

1
2
3
4
5
6
RETURN [
DATE_UTCTOLOCAL("2020-03-15T00:00:00.000", "Asia/Shanghai"),
DATE_UTCTOLOCAL("2020-03-15T00:00:00.000Z", "Asia/Shanghai"),
DATE_UTCTOLOCAL("2020-03-15T00:00:00.000-02:00", "Asia/Shanghai"),
DATE_UTCTOLOCAL(1584230400000, "Asia/Shanghai")
]

显示查询结果

1
RETURN DATE_UTCTOLOCAL(DATE_NOW(), "Africa/Lagos", true)

显示查询结果

DATE_LOCALTOUTC()

引入: v3.8.0

将假定的本地时区**日期转换为祖鲁时间 (UTC)。

它考虑了历史夏令时。

1
DATE_LOCALTOUTC(date, timezone, zoneinfo) → date
  • 日期(数字|字符串):数字时间戳或 ISO 8601 日期时间字符串
  • 时区(字符串):IANA 时区名称,例如 ,或 。用于太平洋时间(太平洋标准时间/太平洋夏令时)。如果 ArangoDB 不知道时区,则会引发错误"America/New_York"``"Europe/Berlin"``"UTC"``"America/Los_Angeles"
  • zoneinfo(布尔值,可选):如果设置为true,则返回具有时区信息的对象。默认值为false,并返回日期字符串
  • 返回date (string|object):以祖鲁时间 (UTC) 为单位的 ISO 8601 日期时间字符串,或具有以下属性的对象:
    • utc(字符串):ISO 8601 日期时间字符串(以祖鲁时间 (UTC) 表示)
    • tzdb(字符串):所用时区数据库的版本(例如"2020f")
    • 区域信息: (对象): 时区信息
      • 名称(字符串):时区缩写(GMT、太平洋标准时间、欧洲中部时间等)
      • begin (string|null):时区效果的开始,作为 UTC 日期时间字符串
      • end (string|null):时区效果的结束,作为 UTC 日期时间字符串
      • dst(布尔值):当夏令时 (DST) 处于活动状态时为 true,否则为 false
      • 偏移量(数字):以秒为单位的UTC偏移量
1
2
3
4
5
RETURN [
DATE_LOCALTOUTC("2020-03-15T00:00:00.000", "Europe/Berlin"),
DATE_LOCALTOUTC("2020-03-15T00:00:00.000", "America/New_York"),
DATE_LOCALTOUTC("2020-03-15T00:00:00.000", "UTC")
]

显示查询结果

1
2
3
4
5
6
RETURN [
DATE_LOCALTOUTC("2020-03-15T00:00:00.000", "Asia/Shanghai"),
DATE_LOCALTOUTC("2020-03-15T00:00:00.000Z", "Asia/Shanghai"),
DATE_LOCALTOUTC("2020-03-15T00:00:00.000-02:00", "Asia/Shanghai"),
DATE_LOCALTOUTC(1584230400000, "Asia/Shanghai")
]

显示查询结果

1
RETURN DATE_LOCALTOUTC("2021-03-16T12:00:00.000", "Africa/Lagos", true)

显示查询结果

DATE_TIMEZONE()

引入: v3.8.0

返回运行 ArangoDB 的系统时区。

对于云服务器,这很可能是”Etc/UTC”。

1
DATE_TIMEZONE() → timezone
  • 返回时区(字符串):服务器时区的 IANA时区名称。

DATE_TIMEZONES()

引入: v3.8.0

返回所有有效的时区名称。

1
DATE_TIMEZONES() → timezones

7.4.3 使用日期和索引

在 ArangoDB 中存储时间戳有两种推荐的方法:

  • 字符串:使用ISO 8601的 UTC 时间戳
  • 数字:毫秒级时间

由于 ISO 日期字符串的排序属性,两者的排序顺序是相同的。但是,不能在单个属性中混合使用数字和字符串这两种类型。

您可以将持久性索引与这两种日期类型一起使用。选择字符串表示形式时,可以使用字符串比较(小于、大于等)来表示查询中的时间范围,同时仍使用持久性索引:

1
2
3
4
arangosh> db._create("exampleTime");
arangosh> var timestamps = ["2014-05-07T14:19:09.522","2014-05-07T21:19:09.522","2014-05-08T04:19:09.522","2014-05-08T11:19:09.522","2014-05-08T18:19:09.522"];
arangosh> for (i = 0; i < 5; i++) db.exampleTime.save({value:i, ts: timestamps[i]})
arangosh> db._query("FOR d IN exampleTime FILTER d.ts > '2014-05-07T14:19:09.522' and d.ts < '2014-05-08T18:19:09.522' RETURN d").toArray()

显示执行结果

数组中的第一个和最后一个时间戳由 .FILTER

7.4.4 局限性

请注意,默认情况下,ISO 8601 标准不允许 1583年之前的日期,因为它们位于公历正式引入之前,因此可能不正确或无效。所有 AQL 日期函数都根据公历系统对每个日期应用相同的规则,即使不合适也是如此。这并不构成问题,除非你处理1583年之前的日期,特别是基督之前的年份。该标准允许负年份,但如果使用负年份(例如 和)。但是,这很少使用,并且 AQL 在 ISO 字符串中 0 到 9999 之间的年份不使用 7 个字符的版本。请记住,无法将它们与该范围之外的日期正确比较。对负日期进行排序不会产生有意义的顺序,因为几年前是过去的,而是月份,天和时间部分以其他正确的顺序排列。+002015-05-15``-000753-01-01

闰秒被忽略,就像它们在JavaScript中根据ECMAScript语言规范一样。

7.5 Document / Object

AQL 提供了下面列出的函数来操作对象/文档值。另请参阅对象访问,了解其他语言构造。

7.5.1 ATTRIBUTES()

1
ATTRIBUTES(document, removeInternal, sort) → strArray

以数组形式返回文档的顶级属性键。(可选)省略系统属性并对数组进行排序。

  • 文档(对象):任意文档/对象
  • 删除内部(bool,可选):结果中是否应省略所有系统属性(_key,_id等,每个以下划线开头的属性键)。默认值为false
  • 排序(布尔,可选):可以选择按字母顺序对生成的数组进行排序。默认值为false,将按任意顺序返回属性名称。
  • 返回strArray(数组):作为字符串数组的输入文档的属性键
1
2
3
4
5
6
7
8
ATTRIBUTES( { "foo": "bar", "_key": "123", "_custom": "yes" } )
// [ "foo", "_key", "_custom" ]

ATTRIBUTES( { "foo": "bar", "_key": "123", "_custom": "yes" }, true )
// [ "foo" ]

ATTRIBUTES( { "foo": "bar", "_key": "123", "_custom": "yes" }, false, true )
// [ "_custom", "_key", "foo" ]

计算每个属性键在集合文档中出现的频率的复杂示例(大型集合的开销很大):

1
2
3
4
5
6
7
8
LET attributesPerDocument = (
FOR doc IN collection RETURN ATTRIBUTES(doc, true)
)
FOR attributeArray IN attributesPerDocument
FOR attribute IN attributeArray
COLLECT attr = attribute WITH COUNT INTO count
SORT count DESC
RETURN {attr, count}

7.5.2 COUNT()

这是LENGTH()的别名。

7.5.3 HAS()

1
HAS(document, attributeName) → isPresent

测试提供的文档中是否存在属性。

  • 文档(对象):任意文档/对象
  • 属性名称(字符串):要测试的属性键
  • 返回isPresent (bool):如果文档具有名为attributeName的属性,则为 true,否则为 false。具有假值(0,false,空字符串)或null的属性也被视为存在并返回true。""
1
2
3
HAS( { name: "Jane" }, "name" ) // true
HAS( { name: "Jane" }, "age" ) // false
HAS( { name: null }, "name" ) // true

请注意,该函数检查指定的属性是否存在。这与测试属性是否存在的类似方法不同,以防属性具有虚假值或不存在(在对象访问时隐式为 null):

1
2
3
4
5
6
7
!!{ name: "" }.name        // false
HAS( { name: "" }, "name") // true

{ name: null }.name == null // true
{ }.name == null // true
HAS( { name: null }, "name" ) // true
HAS( { }, "name" ) // false

请注意,不能使用索引。如果没有必要在查询中区分显式和隐式值,则可以使用相等性比较来测试null,并在要测试的属性上创建非稀疏索引:HAS()

1
2
3
FILTER !HAS(doc, "name")    // can not use indexes
FILTER IS_NULL(doc, "name") // can not use indexes
FILTER doc.name == null // can utilize non-sparse indexes

7.5.4 IS_SAME_COLLECTION()

1
IS_SAME_COLLECTION(collectionName, documentHandle) → bool

集合 ID 作为集合 中指定的集合文档可以是文档句柄字符串,也可以是具有_id属性的文档。该函数不验证集合是否实际包含指定的文档,而仅将指定集合的名称与指定文档的集合名称部分进行比较。如果document既不是具有id属性的对象,也不是字符串值的对象,则该函数将返回null并引发警告。

  • 集合名称(字符串):以字符串形式表示的集合的名称
  • documentHandle (string|object):文档标识符字符串(例如_users/1234) 或集合中的常规文档。传递非字符串或非文档或没有_id属性的文档将导致错误。
  • 返回bool (bool):如果documentHandle的集合与collectionName相同,则返回true,否则为 false
1
2
3
4
5
6
7
// true
IS_SAME_COLLECTION( "_users", "_users/my-user" )
IS_SAME_COLLECTION( "_users", { _id: "_users/my-user" } )

// false
IS_SAME_COLLECTION( "_users", "foobar/baz")
IS_SAME_COLLECTION( "_users", { _id: "something/else" } )

7.5.5 KEEP()

1
KEEP(document, attributeName1, attributeName2, ... attributeNameN) → doc

仅保留属性属性名称属性名称文档的属性名称N。 所有其他属性将从结果中删除。

若要执行相反操作,请参阅UNSET()

  • 文档(对象):文档/对象
  • 属性名称(字符串,可重复):任意数量的属性名称作为多个参数
  • 返回doc(对象):在顶层仅具有指定属性的文档
1
2
KEEP(doc, "firstname", "name", "likes")
KEEP(document, attributeNameArray) → doc
  • 文档(对象):文档/对象
  • 属性名称数组(数组):以字符串形式表示的属性名称数组
  • 返回doc(对象):在顶层仅具有指定属性的文档
1
KEEP(doc, [ "firstname", "name", "likes" ])

7.5.6 LENGTH()

1
LENGTH(doc) → attrCount

确定对象/文档的属性键数。

  • 文档(对象):文档/对象
  • 返回attrCount(数字):文档中属性键的数量,无论其值如何

LENGTH()还可以确定数组中的元素数、集合中的文档量以及字符串的字符长度

7.5.7 MATCHES()

1
MATCHES(document, examples, returnIndex) → match

将给定文档与提供的每个示例文档进行比较。比较将从第一个示例开始。该示例的所有属性都将与文档的属性进行比较。如果所有属性都匹配,则比较将停止并返回结果。如果存在不匹配,该函数将继续与下一个示例进行比较,直到没有更多示例。

这些示例可以是包含 1..n 个示例文档的数组,也可以是单个文档,每个文档具有任意数量的属性。

属性值 将匹配具有显式属性值 的文档以及缺少此属性的文档(隐式)。只有HAS()才能区分缺少属性和具有存储值。null``null``null``null

空对象将匹配所有文档。注意不要意外索要所有文件。例如,arangojs驱动程序跳过值为 的属性,变为 。{}``undefined``{attr: undefined}``{}

1
2
3
MATCHES()`无法利用索引。您可以改用普通条件来潜在地从现有索引中受益:`FILTER
FOR doc IN coll
FILTER (cond1 AND cond2 AND cond3) OR (cond4 AND cond5) ...
  • 文档(对象):用于确定它是否与任何示例匹配的文档
  • 示例(对象|数组):要与之进行比较的单个文档或文档数组。不允许指定空数组。
  • returnIndex (bool):通过将此标志设置为true,将返回匹配的示例的索引(从偏移量 0 开始),如果没有匹配项,则返回-1。默认值为false,并使函数返回布尔值。
  • 返回match (bool|number):如果文档与其中一个示例匹配,则返回true,否则返回 false。如果使用returnIndex,则改为返回一个数字。
1
2
3
4
5
6
LET doc = {
name: "jane",
age: 27,
active: true
}
RETURN MATCHES(doc, { age: 27, active: true } )

这将返回true,因为该示例的所有属性都存在于文档中。

1
2
3
4
5
6
7
RETURN MATCHES(
{ "test": 1 },
[
{ "test": 1, "foo": "bar" },
{ "foo": 1 },
{ "test": 1 }
], true)

这将返回2,因为第三个示例匹配,并且因为returnIndex标志设置为true

7.5.8 MERGE()

1
MERGE(document1, document2, ... documentN) → mergedDocument

将文档document1documentN合并到单个文档中。如果文档属性键不明确,则合并的结果将包含参数列表后面包含的文档的值。

  • 文档(对象,可重复):任意数量的文档作为多个参数(至少 2 个)
  • 返回合并文档(对象):合并的文档

请注意,合并将仅对顶级属性执行。如果要合并子属性,请改用MERGE_RECURSIVE()。

具有不同属性名称的两个文档可以很容易地合并为一个:

1
2
3
4
5
MERGE(
{ "user1": { "name": "Jane" } },
{ "user2": { "name": "Tom" } }
)
// { "user1": { "name": "Jane" }, "user2": { "name": "Tom" } }

合并具有相同属性名称的文档时,将在最终结果中使用后一个文档的属性值:

1
2
3
4
5
6
MERGE(
{ "users": { "name": "Jane" } },
{ "users": { "name": "Tom" } }
)
// { "users": { "name": "Tom" } }
MERGE(docArray) → mergedDocument

MERGE也适用于单个数组参数。此变体允许将数组中多个对象的属性合并到单个对象中。

  • docArray(数组):文档数组,作为唯一参数
  • 返回合并文档(对象):合并的文档
1
2
3
4
5
6
7
MERGE(
[
{ foo: "bar" },
{ quux: "quetzalcoatl", ruled: true },
{ bar: "baz", foo: "done" }
]
)

现在将返回:

1
2
3
4
5
6
{
"foo": "done",
"quux": "quetzalcoatl",
"ruled": true,
"bar": "baz"
}

7.5.9 MERGE_RECURSIVE()

1
MERGE_RECURSIVE(document1, document2, ... documentN) → mergedDocument

以递归方式将document1documentN的文档合并到单个文档中。如果文档属性键不明确,则合并的结果将包含参数列表后面包含的文档的值。

  • 文档(对象,可重复):任意数量的文档作为多个参数(至少 2 个)
  • 返回合并文档(对象):合并的文档

例如,两个具有不同属性名称的文档可以很容易地合并为一个:

1
2
3
4
5
MERGE_RECURSIVE(
{ "user-1": { "name": "Jane", "livesIn": { "city": "LA" } } },
{ "user-1": { "age": 42, "livesIn": { "state": "CA" } } }
)
// { "user-1": { "name": "Jane", "livesIn": { "city": "LA", "state": "CA" }, "age": 42 } }

MERGE_RECURSIVE()不支持MERGE提供的单个数组参数变体。

7.5.10 PARSE_IDENTIFIER()

1
PARSE_IDENTIFIER(documentHandle) → parts

分析文档句柄并将其各个部分作为单独的属性返回。

此功能可用于轻松确定给定文档的集合名称和键。

  • documentHandle (string|object):文档标识符字符串(例如_users/1234) 或集合中的常规文档。传递非字符串或非文档或没有_id属性的文档将导致错误。
  • 返回部件(对象):具有属性集合的对象
1
2
3
4
5
PARSE_IDENTIFIER("_users/my-user")
// { "collection": "_users", "key": "my-user" }

PARSE_IDENTIFIER( { "_id": "mycollection/mykey", "value": "some value" } )
// { "collection": "mycollection", "key": "mykey" }

7.5.11 TRANSLATE()

1
TRANSLATE(value, lookupDocument, defaultValue) → mappedValue

在查找文档中查找指定的。如果valuelookupDocument中的键,则value将被替换为找到的查找值。如果查找文档中不存在,则如果指定,将返回默认值。如果未指定默认值,则返回将保持不变。

  • (字符串):根据映射进行编码的值
  • 查找文档(对象):作为文档的键/值映射
  • 默认值(任何,可选):在找不到值的情况下回退
  • 返回映射值(任意):编码的值,或未更改的值默认值(如果提供),以防无法映射
1
2
3
4
5
6
7
8
TRANSLATE("FR", { US: "United States", UK: "United Kingdom", FR: "France" } )
// "France"

TRANSLATE(42, { foo: "bar", bar: "baz" } )
// 42

TRANSLATE(42, { foo: "bar", bar: "baz" }, "not found!")
// "not found!"

7.5.12 UNSET()

1
UNSET(document, attributeName1, attributeName2, ... attributeNameN) → doc

文档中删除属性属性Name1属性NameN。将保留所有其他属性。

要执行相反操作,请参阅KEEP()。

  • 文档(对象):文档/对象
  • 属性名称(字符串,可重复):任意数量的属性名称作为多个参数(至少 1 个)
  • 返回doc(对象):在顶层没有指定属性的文档
1
2
UNSET( doc, "_id", "_key", "foo", "bar" )
UNSET(document, attributeNameArray) → doc
  • 文档(对象):文档/对象
  • 属性名称数组(数组):以字符串形式表示的属性名称数组
  • 返回doc(对象):在顶层没有指定属性的文档
1
UNSET( doc, [ "_id", "_key", "foo", "bar" ] )

7.5.13 UNSET_RECURSIVE()

1
UNSET_RECURSIVE(document, attributeName1, attributeName2, ... attributeNameN) → doc

以递归方式从文档及其子文档中删除属性attributeName1属性NameN。将保留所有其他属性。

  • 文档(对象):文档/对象
  • 属性名称(字符串,可重复):任意数量的属性名称作为多个参数(至少 1 个)
  • 返回doc(对象):在所有级别(顶级对象和嵌套对象)上没有指定属性的文档
1
2
UNSET_RECURSIVE( doc, "_id", "_key", "foo", "bar" )
UNSET_RECURSIVE(document, attributeNameArray) → doc
  • 文档(对象):文档/对象
  • 属性名称数组(数组):以字符串形式表示的属性名称数组
  • 返回doc(对象):在所有级别(顶级对象和嵌套对象)上没有指定属性的文档
1
UNSET_RECURSIVE( doc, [ "_id", "_key", "foo", "bar" ] )

7.5.14 VALUES()

1
VALUES(document, removeInternal) → anyArray

文档的属性值作为数组返回。(可选)省略系统属性。

  • 文档(对象):文档/对象
  • removeInternal(bool, optional):如果设置为true,则从结果中删除所有内部属性(如_id 、_key等)
  • 返回anyArray(数组):按任意顺序返回的文档的值
1
2
3
4
5
VALUES( { "_key": "users/jane", "name": "Jane", "age": 35 } )
// [ "Jane", 35, "users/jane" ]

VALUES( { "_key": "users/jane", "name": "Jane", "age": 35 }, true )
// [ "Jane", 35 ]

7.5.15 ZIP()

1
ZIP(keys, values) → doc

返回从单独的参数组合而成的文档对象。

必须是数组并具有相同的长度。

  • 键(数组):字符串数组,用作结果中的属性名称
  • values(数组):具有任意类型元素的数组,用作属性值
  • 返回doc(对象):组合了键和值的文档
1
2
ZIP( [ "name", "active", "hobbies" ], [ "some user", true, [ "swimming", "riding" ] ] )
// { "name": "some user", "active": true, "hobbies": [ "swimming", "riding" ] }

7.6 Fulltext

AQL 提供了以下函数来根据全文索引过滤数据。

FULLTEXT()

1
FULLTEXT(coll, attribute, query, limit) → docArray

从集合coll返回所有文档,其属性属性与全文搜索短语查询匹配,可以选择限制为限制结果。

注意:FULLTEXT()函数要求集合coll属性上具有全文索引。如果没有可用的全文索引,则此函数将在运行时失败并显示错误。但是,在解释查询时,它不会失败。

  • coll(集合):集合
  • 属性(字符串):要在其中搜索的属性的属性名称
  • 查询(字符串):全文搜索表达式,如下所述
  • 限制(数量,可选):如果设置为非零值,则结果最多限制为此数量的文档
  • 返回docArray(数组):文档数组

FULLTEXT()不是用来作为 的参数,而是用来表示语句:FILTER``FOR

1
2
FOR oneMail IN FULLTEXT(emails, "body", "banana,-apple")
RETURN oneMail._id

查询是以逗号分隔的已查找单词(或所查找单词的前缀)的列表。要区分前缀搜索和完全匹配搜索,可以选择为每个单词添加 or 限定符作为前缀。可以在同一查询中混合使用不同的限定符。不为搜索词指定限定符将隐式执行对给定单词的完全匹配搜索:prefix:``complete:

  • FULLTEXT(emails, "body", "banana")
    将在集合集合的属性正文中查找单词banana。
  • FULLTEXT(emails, "body", "banana,orange")
    将在上述属性中查找香蕉橙色这两个词。将仅返回包含这两个单词的那些文档。
  • FULLTEXT(emails, "body", "prefix:head")
    将查找包含以前缀开头的任何单词的文档。
  • FULLTEXT(emails, "body", "prefix:head,complete:aspirin")
    将查找包含以前缀开头的单词的所有文档,并且还包含(完整)单词阿司匹林。注意:此处指定是可选的。complete:
  • FULLTEXT(emails, "body", "prefix:cent,prefix:subst")
    将查找包含以前缀cent开头的单词的所有文档,并且还包含以前缀subst开头的单词的所有文档。

如果给出了多个搜索词(或前缀),则默认情况下,结果将合并为AND,这意味着仅返回所有搜索的逻辑交集。也可以将部分结果与逻辑 OR 和逻辑 NOT 组合在一起:

  • FULLTEXT(emails, "body", "+this,+text,+document")
    将返回包含所有上述单词的所有文档。注意:在此处指定符号是可选的。+
  • FULLTEXT(emails, "body", "banana,|apple")
    将返回包含香蕉或苹果之一(或两者)单词的所有文档。
  • FULLTEXT(emails, "body", "banana,-apple")
    将返回所有包含单词banana,但不包含单词apple的文档。
  • FULLTEXT(emails, "body", "banana,pear,-cranberry")
    将返回所有同时包含香蕉字,但不包含蔓越莓一词的文档。

在全文查询中,不会遵循逻辑运算符的优先级。查询将仅从左到右进行计算。


其他的功能函数参见 https://www.arangodb.com/docs/3.10/aql/functions-numeric.html


八 AQL查询模式和示例

这些页面包含一些常见的查询模式和示例。 为了更好地理解,查询结果也直接包含在每个查询的下方。

通常,您会希望对存储在集合中的数据运行查询。 本节将为此提供几个示例。

下面的一些示例查询是在使用下面提供的数据的集合用户上执行的。

对集合运行查询时要考虑的事项

请注意,在任何集合中创建的所有文档都将自动获得以下服务器生成的属性:

  • _id:唯一id,由集合名和服务器端序列值组成
  • _key:服务器序列值
  • _rev:文档的修订号

每当您对集合中的文档运行查询时,如果这些附加属性也被返回,请不要感到惊讶。

另请注意,对于真实世界的数据,您可能希望为数据创建额外的索引(为简洁起见,此处省略)。 在FILTER语句中使用的属性上添加索引可能会大大加快查询速度。 此外,您可能希望使用内置的_id_from_to 属性,而不是使用 idfromto 等属性。 最后,边缘集合提供了一种在文档之间建立引用/链接的好方法。 为简洁起见,这里也省略了这些功能。

示例数据

以下一些示例查询是在具有以下初始数据的集合用户上执行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[ 
{ "id": 100, "name": "John", "age": 37, "active": true, "gender": "m" },
{ "id": 101, "name": "Fred", "age": 36, "active": true, "gender": "m" },
{ "id": 102, "name": "Jacob", "age": 35, "active": false, "gender": "m" },
{ "id": 103, "name": "Ethan", "age": 34, "active": false, "gender": "m" },
{ "id": 104, "name": "Michael", "age": 33, "active": true, "gender": "m" },
{ "id": 105, "name": "Alexander", "age": 32, "active": true, "gender": "m" },
{ "id": 106, "name": "Daniel", "age": 31, "active": true, "gender": "m" },
{ "id": 107, "name": "Anthony", "age": 30, "active": true, "gender": "m" },
{ "id": 108, "name": "Jim", "age": 29, "active": true, "gender": "m" },
{ "id": 109, "name": "Diego", "age": 28, "active": true, "gender": "m" },
{ "id": 200, "name": "Sophia", "age": 37, "active": true, "gender": "f" },
{ "id": 201, "name": "Emma", "age": 36, "active": true, "gender": "f" },
{ "id": 202, "name": "Olivia", "age": 35, "active": false, "gender": "f" },
{ "id": 203, "name": "Madison", "age": 34, "active": true, "gender": "f" },
{ "id": 204, "name": "Chloe", "age": 33, "active": true, "gender": "f" },
{ "id": 205, "name": "Eva", "age": 32, "active": false, "gender": "f" },
{ "id": 206, "name": "Abigail", "age": 31, "active": true, "gender": "f" },
{ "id": 207, "name": "Isabella", "age": 30, "active": true, "gender": "f" },
{ "id": 208, "name": "Mary", "age": 29, "active": true, "gender": "f" },
{ "id": 209, "name": "Mariah", "age": 28, "active": true, "gender": "f" }
]

对于某些示例,我们还将使用集合关系来存储用户之间的关系。 关系的示例数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[
{ "from": 209, "to": 205, "type": "friend" },
{ "from": 206, "to": 108, "type": "friend" },
{ "from": 202, "to": 204, "type": "friend" },
{ "from": 200, "to": 100, "type": "friend" },
{ "from": 205, "to": 101, "type": "friend" },
{ "from": 209, "to": 203, "type": "friend" },
{ "from": 200, "to": 203, "type": "friend" },
{ "from": 100, "to": 208, "type": "friend" },
{ "from": 101, "to": 209, "type": "friend" },
{ "from": 206, "to": 102, "type": "friend" },
{ "from": 104, "to": 100, "type": "friend" },
{ "from": 104, "to": 108, "type": "friend" },
{ "from": 108, "to": 209, "type": "friend" },
{ "from": 206, "to": 106, "type": "friend" },
{ "from": 204, "to": 105, "type": "friend" },
{ "from": 208, "to": 207, "type": "friend" },
{ "from": 102, "to": 108, "type": "friend" },
{ "from": 207, "to": 203, "type": "friend" },
{ "from": 203, "to": 106, "type": "friend" },
{ "from": 202, "to": 108, "type": "friend" },
{ "from": 201, "to": 203, "type": "friend" },
{ "from": 105, "to": 100, "type": "friend" },
{ "from": 100, "to": 109, "type": "friend" },
{ "from": 207, "to": 109, "type": "friend" },
{ "from": 103, "to": 203, "type": "friend" },
{ "from": 208, "to": 104, "type": "friend" },
{ "from": 105, "to": 104, "type": "friend" },
{ "from": 103, "to": 208, "type": "friend" },
{ "from": 203, "to": 107, "type": "boyfriend" },
{ "from": 107, "to": 203, "type": "girlfriend" },
{ "from": 208, "to": 109, "type": "boyfriend" },
{ "from": 109, "to": 208, "type": "girlfriend" },
{ "from": 106, "to": 205, "type": "girlfriend" },
{ "from": 205, "to": 106, "type": "boyfriend" },
{ "from": 103, "to": 209, "type": "girlfriend" },
{ "from": 209, "to": 103, "type": "boyfriend" },
{ "from": 201, "to": 102, "type": "boyfriend" },
{ "from": 102, "to": 201, "type": "girlfriend" },
{ "from": 206, "to": 100, "type": "boyfriend" },
{ "from": 100, "to": 206, "type": "girlfriend" }
]

8.1 使用 AQL 创建测试数据

我们假设在下面的示例查询中已经有一个名为 myCollection 的保留文档集合。

用测试数据填充集合的最简单方法之一是使用在一个范围内迭代的 AQL 查询。

运行以下 AQL 查询,例如 从 Web 界面中的 AQL 编辑器将 1,000 个文档插入到集合中:

1
2
FOR i IN 1..1000
INSERT { name: CONCAT("test", i) } IN myCollection

可以通过调整范围边界值轻松修改要创建的文档数量。

如果要立即检查结果,请在查询末尾添加 RETURN NEW。

要创建更复杂的测试数据,请调整 AQL 查询。 假设我们还需要一个状态属性,并用 1 到 5(含)之间的整数值填充它,并且分布均匀。 实现此目的的一个好方法是使用模运算符 (%):

1
2
3
4
5
FOR i IN 1..1000
INSERT {
name: CONCAT("test", i),
status: 1 + (i % 5)
} IN myCollection

要创建伪随机值,请使用 RAND() 函数。 它创建 0 到 1 之间的伪随机数。使用一些因子来缩放随机数,并使用 FLOOR() 将缩放后的数字转换回整数。

例如,以下查询使用 100 到 150(含)之间的数字填充 value 属性:

1
2
3
4
5
FOR i IN 1..1000
INSERT {
name: CONCAT("test", i),
value: 100 + FLOOR(RAND() * (150 - 100 + 1))
} IN myCollection

创建测试数据后,对其进行验证通常很有帮助。RAND() 函数也是检索集合中文档的随机样本的一个很好的候选者。 此查询将检索 10 个随机文档:

1
2
3
4
FOR doc IN myCollection
SORT RAND()
LIMIT 10
RETURN doc

COLLECT 子句是一种对某些属性运行聚合分析的简单机制。 假设我们想验证状态属性内的数据分布。 在这种情况下,我们可以运行:

1
2
3
4
5
6
FOR doc IN myCollection
COLLECT value = doc.value WITH COUNT INTO count
RETURN {
value: value,
count: count
}

上述查询将提供每个不同值的文档数。

我们可以通过将值用作属性键,将计数用作属性值并将所有内容合并为单个结果对象,从而使 JSON 结果更加紧凑。 请注意,属性键只能是字符串,但出于我们的目的,它是可以接受的。

1
2
3
4
5
6
7
RETURN MERGE(
FOR doc IN myCollection
COLLECT value = doc.value WITH COUNT INTO count
RETURN {
[value]: count
}
)

8.2 计数

集合中的文档量

要返回集合中当前存在的文档数,您可以调用 LENGTH()函数:

1
RETURN LENGTH(collection)

这种类型的调用从 2.8 开始优化(没有在内存中建立不必要的中间结果),因此它是确定计数的首选方法。 在内部,调用COLLECTION_COUNT()

COLLECT ... WITH COUNT INTO可用的早期版本中(自 2.4 起),您可以使用以下代码代替 LENGTH()以获得更好的性能:

1
2
3
FOR doc IN collection
COLLECT WITH COUNT INTO length
RETURN length

8.3 数据修改查询

以下操作可用于一次查询修改多个文档的数据。 这优于使用多个查询单独获取和更新文档。 但是,如果只需要修改单个文档,ArangoDB 专门针对单个文档的数据修改操作可能会执行得更快。

8.3.1 更新文档

要更新现有文档,我们可以使用 UPDATE 或 REPLACE 操作。 UPDATE 只更新找到的文档中的指定属性,REPLACE 用指定的值完全替换找到的文档。

我们将从重写所有文档中的性别属性的 UPDATE 查询开始:

1
2
FOR u IN users
UPDATE u WITH { gender: TRANSLATE(u.gender, { m: 'male', f: 'female' }) } IN users

要将新属性添加到现有文档,我们还可以使用 UPDATE 查询。 以下查询为状态为 active 的所有用户添加属性 numberOfLogins:

1
2
3
FOR u IN users
FILTER u.active == true
UPDATE u WITH { numberOfLogins: 0 } IN users

现有属性也可以根据它们以前的值进行更新:

1
2
3
FOR u IN users
FILTER u.active == true
UPDATE u WITH { numberOfLogins: u.numberOfLogins + 1 } IN users

仅当文档中已经存在 numberOfLogins 属性时,上述查询才有效。 如果不确定文档中是否有 numberOfLogins 属性,则必须有条件地进行增加:

1
2
3
4
5
FOR u IN users
FILTER u.active == true
UPDATE u WITH {
numberOfLogins: HAS(u, 'numberOfLogins') ? u.numberOfLogins + 1 : 1
} IN users

多个属性的更新可以组合在一个查询中:

1
2
3
4
5
6
FOR u IN users
FILTER u.active == true
UPDATE u WITH {
lastLogin: DATE_NOW(),
numberOfLogins: HAS(u, 'numberOfLogins') ? u.numberOfLogins + 1 : 1
} IN users

请注意,更新查询可能会在执行期间失败,例如因为要更新的文档不存在。 在这种情况下,查询将在第一个错误处中止。 在单服务器模式下,查询所做的所有修改都将被回滚,就好像它们从未发生过一样。

8.3.2 替换文档

要不只是部分更新,而是完全替换现有文档,请使用 REPLACE 操作。 以下查询将集合备份中的所有文档替换为集合用户中找到的文档。 两个集合共有的文档将被替换。 所有其他文件将保持不变。 文档使用它们的 _key 属性进行比较:

1
2
FOR u IN users
REPLACE u IN backup

如果集合用户中存在尚未处于集合备份中的文档,则上述查询将失败。 在这种情况下,查询将尝试替换不存在的文档。 如果在执行查询时检测到这种情况,则查询将中止。 在单服务器模式下,查询所做的所有更改也将回滚。

要使这种情况下的查询成功,请使用 ignoreErrors 查询选项:

1
2
FOR u IN users
REPLACE u IN backup OPTIONS { ignoreErrors: true }

8.3.3 删除文档

删除文档可以通过 REMOVE 操作来实现。 要删除某个年龄范围内的所有用户,我们可以使用以下查询:

1
2
3
FOR u IN users
FILTER u.active == true && u.age >= 35 && u.age <= 37
REMOVE u IN users

8.3.4 创建文档

要创建新文档,有 INSERT 操作。 它还可用于从其他集合生成现有文档的副本,或创建合成文档(例如用于测试目的)。 以下查询在集合 users 中创建了 1000 个测试用户,并设置了一些属性:

1
2
3
4
5
6
7
8
FOR i IN 1..1000
INSERT {
id: 100000 + i,
age: 18 + FLOOR(RAND() * 25),
name: CONCAT('test', TO_STRING(i)),
active: false,
gender: i % 2 == 0 ? 'male' : 'female'
} IN users

8.3.5 将数据从一个集合复制到另一个集合

要将数据从一个集合复制到另一个集合中,可以使用 INSERT 操作:

1
2
FOR u IN users
INSERT u IN backup

这会将收藏用户的所有文档复制到收藏备份中。 请注意,执行查询时,这两个集合必须已经存在。 如果备份已经包含文档,则查询可能会失败,因为执行插入可能会尝试再次插入相同的文档(由 _key 属性标识)。 这将触发唯一键约束违规并中止查询。 在单服务器模式下,查询所做的所有更改也将回滚。 为了使这种复制操作在所有情况下都有效,可以在使用 REMOVE 查询之前清空目标集合。

8.3.6 处理错误

在某些情况下,即使遇到错误(例如“找不到文档”),也可能希望继续执行查询。 要在出现错误时继续执行查询,可以使用 ignoreErrors 选项。

要使用它,请在查询的数据修改部分之后直接放置一个 OPTIONS 关键字,例如

1
2
FOR u IN users
REPLACE u IN backup OPTIONS { ignoreErrors: true }

即使在 REPLACE 操作期间发生错误,这也将继续执行查询。 它对 UPDATE、INSERT 和 REMOVE 的工作方式类似。

8.3.7 更改子结构

要修改文档中的列表,我们必须使用临时变量。 我们将在那里收集子列表并对其进行更改。 我们选择一个简单的布尔过滤条件来使查询更易于理解。

首先让我们创建一个带有样本的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
database = db._create('complexCollection')
database.save({
"topLevelAttribute" : "a",
"subList" : [
{
"attributeToAlter" : "oldValue",
"filterByMe" : true
},
{
"attributeToAlter" : "moreOldValues",
"filterByMe" : true
},
{
"attributeToAlter" : "unchangedValue",
"filterByMe" : false
}
]
})

这是将子列表保留在更改后的列表中以便稍后更新的查询:

1
2
3
4
5
6
7
8
FOR document in complexCollection
LET alteredList = (
FOR element IN document.subList
LET newItem = (! element.filterByMe ?
element :
MERGE(element, { attributeToAlter: "shiny New Value" }))
RETURN newItem)
UPDATE document WITH { subList: alteredList } IN complexCollection

虽然查询现在可以正常运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
db.complexCollection.toArray()
[
{
"_id" : "complexCollection/392671569467",
"_key" : "392671569467",
"_rev" : "392799430203",
"topLevelAttribute" : "a",
"subList" : [
{
"filterByMe" : true,
"attributeToAlter" : "shiny New Value"
},
{
"filterByMe" : true,
"attributeToAlter" : "shiny New Value"
},
{
"filterByMe" : false,
"attributeToAlter" : "unchangedValue"
}
]
}
]

它可能很快就会成为性能瓶颈,因为它会修改集合中的所有文档,无论值是否发生变化。 因此,如果我们真的改变了它们的值,我们只想更新文档。 因此我们使用第二个 FOR 来测试 subList 是否会被改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FOR document in complexCollection
LET willUpdateDocument = (
FOR element IN docToAlter.subList
FILTER element.filterByMe LIMIT 1 RETURN 1)

FILTER LENGTH(willUpdateDocument) > 0

LET alteredList = (
FOR element IN document.subList
LET newItem = (! element.filterByMe ?
element :
MERGE(element, { attributeToAlter: "shiny New Value" }))
RETURN newItem)

UPDATE document WITH { subList: alteredList } IN complexCollection

8.4 将查询与子查询组合

8.4.1 如何使用子查询

只要 AQL 中允许使用表达式,就可以放置子查询。 子查询是一个查询部分,它可以在不影响其外部范围内的变量和值的情况下引入自己的局部变量。

需要将子查询放在括号 ( 和 ) 内以明确标记它们的起点和终点:

1
2
3
4
5
6
7
8
9
FOR p IN persons
LET recommendations = ( // subquery start
FOR r IN recommendations
FILTER p.id == r.personId
SORT p.rank DESC
LIMIT 10
RETURN r
) // subquery end
RETURN { person : p, recommendations : recommendations }

子查询的结果可以用 LET 赋值给一个变量,如上所示,这样它就可以被多次引用或者只是为了提高查询的可读性。

函数调用也使用括号,如果您想使用子查询作为函数的唯一参数,AQL 允许您省略额外的一对,例如 MAX(<subquery>) 而不是 MAX((<subquery>))

1
2
3
4
5
6
7
8
9
10
FOR p IN persons
COLLECT city = p.city INTO g
RETURN {
city : city,
numPersons : LENGTH(g),
maxRating: MAX( // subquery start
FOR r IN g
RETURN r.p.rating
) // subquery end
}

但是,如果有多个函数参数,则需要额外的包装,例如 NOT_NULL((返回“确定”),“回退”)。

子查询还可以包括其他子查询。

8.4.2 子查询结果和展开

子查询总是返回一个结果数组,即使只有一个返回值:

1
2
RETURN ( RETURN 1 )
[ [ 1 ] ]

为了避免这种嵌套的数据结构,可以使用 FIRST() 例如:

1
2
RETURN FIRST( RETURN 1 )
[ 1 ]

要展开子查询的结果数组,以便每个元素作为整个查询结果中的顶级元素返回,您可以使用 FOR 循环:

1
2
3
4
5
6
7
FOR elem IN (RETURN 1..3) // [1,2,3]
RETURN elem
[
1,
2,
3
]

如果不展开,查询将是 RETURN (RETURN 1..3) 并且结果是一个嵌套数组 [ [ [ 1, 2, 3 ] ] ,其中包含一个顶级元素。

8.4.3 子查询的评估

表达式中使用的子查询从这些表达式中取出并预先执行。 这意味着子查询不参与操作数的惰性求值,例如在三元运算符中。

考虑以下查询:

1
RETURN RAND() > 0.5 ? (RETURN 1) : 0

它被转换成更像这样的东西,在评估条件之前发生子查询的计算:

1
2
3
LET temp1 = (RETURN 1)
LET temp2 = RAND() > 0.5 ? temp1 : 0
RETURN temp2

无论条件如何,都会执行子查询。 换句话说,没有短路可以避免子查询在条件评估为假的情况下运行。 您可能需要考虑到这一点,以避免查询错误,如

查询:AQL:预期作为 FOR 循环操作数的集合或数组; 您提供了一个“null”类型的值(在执行时)

1
2
3
4
5
6
LET maybe = DOCUMENT("coll/does_not_exist")
LET dependent = maybe ? (
FOR attr IN ATTRIBUTES(maybe)
RETURN attr
) : null
RETURN dependent

问题是子查询在所有情况下都会执行,尽管检查 DOCUMENT() 是否找到了文档。 它没有考虑到maybe 可以为null,不能用FOR 迭代。 一个可能的解决方案是回退到子查询中的空数组,以有效防止循环体被运行:

1
2
3
4
5
6
LET maybe = DOCUMENT("coll/does_not_exist")
LET dependent = maybe ? (
FOR attr IN NOT_NULL(ATTRIBUTES(maybe || {}), [])
RETURN attr
) : "document not found"
RETURN dependent

额外的后备可能是 || {} 防止查询警告

调用函数‘ATTRIBUTES()’时的参数类型无效

源自传递给期望对象的 ATTRIBUTES() 函数的空值。

8.5 AQL 中的动态属性名称

您可能希望 AQL 查询返回具有由函数组装的属性名称或具有可变数量的属性的结果。

这不会通过使用常规对象文字指定结果来工作,因为对象文字需要在查询编译时修复属性的名称和数量。

有两种解决方案可以使动态属性名称起作用:

  • 使用表达式作为属性名称(固定数量的属性)
  • 使用子查询和 ZIP() 函数(可变数量的属性)

8.5.1 使用表达式作为属性名称

此解决方案适用于预先知道要返回的动态属性的数量,并且只需使用表达式计算属性名称的情况。

ArangoDB 2.5 及更高版本允许在对象文字中使用表达式而不是固定属性名称。 使用表达式作为属性名称需要将表达式包含在额外的 [ 和 ] 中,以消除它们与常规、未加引号的属性名称的歧义。

让我们创建一个返回包含在动态命名属性中的原始文档数据的结果。 我们将使用表达式 doc.type 作为属性名称。 我们还将从原始文档中返回一些其他属性,但在它们前面加上文档的 _key 属性值。 为此,我们还需要属性名称表达式。

这是一个显示如何执行此操作的查询。 属性名称表达式都需要包含在 [ 和 ] 中才能使其工作:

1
2
3
4
5
6
7
8
9
10
11
12
LET documents = [
{ "_key" : "3231748397810", "gender" : "f", "status" : "active", "type" : "user" },
{ "_key" : "3231754427122", "gender" : "m", "status" : "inactive", "type" : "unknown" }
]

FOR doc IN documents
RETURN {
[ doc.type ] : {
[ CONCAT(doc._key, "_gender") ] : doc.gender,
[ CONCAT(doc._key, "_status") ] : doc.status
}
}

这将返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"user": {
"3231748397810_gender": "f",
"3231748397810_status": "active"
}
},
{
"unknown": {
"3231754427122_gender": "m",
"3231754427122_status": "inactive"
}
}
]

注意:属性名称表达式和常规的、不带引号的属性名称可以混合使用。

8.5.2 子查询解决方案

通用的解决方案是让子查询或其他函数生成动态属性名称,最后将它们传递给 ZIP() 函数以从中创建对象。

假设我们要处理以下输入文档:

1
2
{ "name": "test", "gender": "f", "status": "active", "type": "user" }
{ "name": "dummy", "gender": "m", "status": "inactive", "type": "unknown", "magicFlag": 23 }

让我们还假设我们对这些文档中的每一个的目标是仅返回包含字母 a 的属性名称及其各自的值。

要从原始文档中提取属性名称和值,我们可以使用如下子查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LET documents = [
{ "name": "test"," gender": "f", "status": "active", "type": "user" },
{ "name": "dummy", "gender": "m", "status": "inactive", "type": "unknown", "magicFlag": 23 }
]

FOR doc IN documents
RETURN (
FOR name IN ATTRIBUTES(doc)
FILTER LIKE(name, '%a%')
RETURN {
name: name,
value: doc[name]
}
)

子查询只会让包含字母 a 的属性名称通过。 然后子查询的结果可用于主查询并将返回。 但是结果中的属性名称仍然是 name 和 value,所以我们还没有。

所以让我们也使用 AQL 的 ZIP() 函数,它可以从两个数组创建一个对象:

  • ZIP() 的第一个参数是一个带有属性名称的数组

  • ZIP() 的第二个参数是一个包含属性值的数组

我们不是直接返回子查询结果,而是首先将其捕获在一个变量中,然后将变量的名称和值组件传递到 ZIP() 中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LET documents = [
{ "name" : "test"," gender" : "f", "status" : "active", "type" : "user" },
{ "name" : "dummy", "gender" : "m", "status" : "inactive", "type" : "unknown", "magicFlag" : 23 }
]

FOR doc IN documents
LET attributes = (
FOR name IN ATTRIBUTES(doc)
FILTER LIKE(name, '%a%')
RETURN {
name: name,
value: doc[name]
}
)
RETURN ZIP(attributes[*].name, attributes[*].value)

请注意,我们必须在属性上使用扩展运算符 ([*]),因为属性本身是一个数组,我们需要其每个成员的名称属性或值属性。

为了证明这是有效的,以下是上述查询的结果:

1
2
3
4
5
6
7
8
9
10
11
[
{
"name": "test",
"status": "active"
},
{
"name": "dummy",
"status": "inactive",
"magicFlag": 23
}
]

可以看出,这两个结果具有不同数量的结果属性。 我们还可以通过在每个属性前面加上 name 属性的值来使结果更加动态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LET documents = [
{ "name": "test"," gender": "f", "status": "active", "type": "user" },
{ "name": "dummy", "gender": "m", "status": "inactive", "type": "unknown", "magicFlag": 23 }
]

FOR doc IN documents
LET attributes = (
FOR name IN ATTRIBUTES(doc)
FILTER LIKE(name, '%a%')
RETURN {
name: CONCAT(doc.name, '-', name),
value: doc[name]
}
)
RETURN ZIP(attributes[*].name, attributes[*].value)

这将为我们提供文档特定的属性名称,如下所示:

1
2
3
4
5
6
7
8
9
10
11
[
{
"test-name": "test",
"test-status": "active"
},
{
"dummy-name": "dummy",
"dummy-status": "inactive",
"dummy-magicFlag": 23
}
]

8.6 投影和过滤器

8.6.1 返回未更改的文档

要从集合用户返回三个完整的文档,可以使用以下查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
FOR u IN users 
LIMIT 0, 3
RETURN u
[
{
"_id" : "users/229886047207520",
"_rev" : "229886047207520",
"_key" : "229886047207520",
"active" : true,
"id" : 206,
"age" : 31,
"gender" : "f",
"name" : "Abigail"
},
{
"_id" : "users/229886045175904",
"_rev" : "229886045175904",
"_key" : "229886045175904",
"active" : true,
"id" : 101,
"age" : 36,
"name" : "Fred",
"gender" : "m"
},
{
"_id" : "users/229886047469664",
"_rev" : "229886047469664",
"_key" : "229886047469664",
"active" : true,
"id" : 208,
"age" : 29,
"name" : "Mary",
"gender" : "f"
}
]

请注意,有一个 LIMIT 子句,但没有 SORT 子句。 在这种情况下,不能保证返回哪些用户文档。 如果未使用 SORT 子句,则实际上未指定文档返回顺序,并且您不应依赖此类查询中的顺序。

8.6.2 预测

要从集合中返回投影,用户使用修改后的 RETURN 指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FOR u IN users 
LIMIT 0, 3
RETURN {
"user" : {
"isActive" : u.active ? "yes" : "no",
"name" : u.name
}
}
[
{
"user" : {
"isActive" : "yes",
"name" : "John"
}
},
{
"user" : {
"isActive" : "yes",
"name" : "Anthony"
}
},
{
"user" : {
"isActive" : "yes",
"name" : "Fred"
}
}
]

8.6.3 过滤器

要从集合用户返回过滤后的投影,您可以使用 FILTER 关键字。 此外,使用 SORT 子句以特定顺序返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FOR u IN users 
FILTER u.active == true && u.age >= 30
SORT u.age DESC
LIMIT 0, 5
RETURN {
"age" : u.age,
"name" : u.name
}
[
{
"age" : 37,
"name" : "Sophia"
},
{
"age" : 37,
"name" : "John"
},
{
"age" : 36,
"name" : "Emma"
},
{
"age" : 36,
"name" : "Fred"
},
{
"age" : 34,
"name" : "Madison"
}
]

8.7 在 AQL 中使用联接

当你想要加入集合的文档时,两种常见的场景是:

  • 一对多:您可能有一个收藏用户和一个收藏城市。用户住在一个城市,您在查询该用户时需要城市信息。

  • 多对多:你可能有一个作者和书籍的集合。一个作者可以写很多书,一本书可以有很多作者。您想返回带有作者的书籍列表。因此,您需要加入作者和书籍。

与许多 NoSQL 数据库不同,ArangoDB 确实支持 AQL 查询中的连接。这类似于传统关系数据库处理此问题的方式。但是,因为文档允许更大的灵活性,联接也更灵活。以下部分提供了常见问题的解决方案。

到目前为止,我们一次只处理了一个集合(用户)。我们还有一个集合关系,用于存储用户之间的关系。我们现在将使用这个额外的集合从两个集合中创建一个结果。

首先,我们将查询一些用户及其朋友的 id。为此,我们将使用在其类型属性中具有朋友值的所有关系。关系是通过使用关系集合中的friendOf 和thisUser 属性建立的,它们指向用户集合中的userId 值。

8.7.1 一对多

您有一个名为 users 的集合。 用户居住在城市,城市由其主键标识。 原则上,您可以将城市文档嵌入到用户文档中并对此感到满意。

1
2
3
4
5
6
7
8
9
10
11
12
{
"_id" : "users/2151975421",
"_key" : "2151975421",
"_rev" : "2151975421",
"name" : {
"first" : "John",
"last" : "Doe"
},
"city" : {
"name" : "Metropolis"
}
}

这适用于许多用例。 现在假设您有关于该城市的其他信息,例如居住在其中的人数。 如果此数字发生变化,则更改每个用户文档是不切实际的。 因此,最好将城市信息保存在一个单独的集合中。

1
2
3
4
5
6
7
8
arangosh> db.cities.document("cities/2241300989");
{
"population" : 1000,
"name" : "Metropolis",
"_id" : "cities/2241300989",
"_rev" : "2241300989",
"_key" : "2241300989"
}

现在您无需将城市直接嵌入到用户文档中,而是可以使用城市的键。

1
2
3
4
5
6
7
8
9
10
11
arangosh> db.users.document("users/2290649597");
{
"name" : {
"first" : "John",
"last" : "Doe"
},
"city" : "cities/2241300989",
"_id" : "users/2290649597",
"_rev" : "2290649597",
"_key" : "2290649597"
}

现在,我们可以非常轻松地加入这两个集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
arangosh> db._query(
........>"FOR u IN users " +
........>" FOR c IN cities " +
........>" FILTER u.city == c._id RETURN { user: u, city: c }"
........>).toArray()
[
{
"user" : {
"name" : {
"first" : "John",
"last" : "Doe"
},
"city" : "cities/2241300989",
"_id" : "users/2290649597",
"_rev" : "2290649597",
"_key" : "2290649597"
},
"city" : {
"population" : 1000,
"name" : "Metropolis",
"_id" : "cities/2241300989",
"_rev" : "2241300989",
"_key" : "2241300989"
}
}
]

与 SQL 不同,没有特殊的 JOIN 关键字。 优化器确保在上述查询中使用主索引。

但是,对于查询的客户端而言,如果返回单个文档通常会方便得多,其中城市信息嵌入在用户文档中 - 如上面的简单示例。 使用 AQL,您无需放弃这种简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arangosh> db._query(
........>"FOR u IN users " +
........>" FOR c IN cities " +
........>" FILTER u.city == c._id RETURN merge(u, {city: c})"
........>).toArray()
[
{
"_id" : "users/2290649597",
"_key" : "2290649597",
"_rev" : "2290649597",
"name" : {
"first" : "John",
"last" : "Doe"
},
"city" : {
"_id" : "cities/2241300989",
"_key" : "2241300989",
"_rev" : "2241300989",
"population" : 1000,
"name" : "Metropolis"
}
}
]

因此,您可以兼得:为您的客户方便地表示结果,以及为您的数据模型灵活地连接。

8.7.2 多对多

在关系世界中,您需要第三个表来模拟多对多关系。 在 ArangoDB 中,您可以根据要存储的信息和要问的问题类型进行选择。

假设作者存储在一个集合中,而书籍存储在一秒钟内。 如果您只需要“哪些是一本书的作者”,那么您可以轻松地将其建模为用户中的列表属性。

如果您想存储更多信息,例如哪个作者在会议论文集中写了哪一页,或者如果您还想知道“哪些书是哪个作者写的”,您可以使用边缘集合。 这与关系世界中的“连接表”非常相似。

8.7.3 嵌入式列表

如果您只想存储一本书的作者,您可以将它们作为列表嵌入到图书文档中。 不需要单独的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arangosh> db.authors.toArray()
[
{
"_id" : "authors/2661190141",
"_key" : "2661190141",
"_rev" : "2661190141",
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
}
},
{
"_id" : "authors/2658437629",
"_key" : "2658437629",
"_rev" : "2658437629",
"name" : {
"first" : "John",
"last" : "Doe"
}
}
]

您可以查询图书

1
2
3
4
5
6
7
8
9
10
11
12
13
arangosh> db._query("FOR b IN books RETURN b").toArray();
[
{
"_id" : "books/2681506301",
"_key" : "2681506301",
"_rev" : "2681506301",
"title" : "The beauty of JOINS",
"authors" : [
"authors/2661190141",
"authors/2658437629"
]
}
]

并以一对多部分中给出的非常相似的方式加入作者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
arangosh> db._query(
........>"FOR b IN books " +
........>" LET a = (FOR x IN b.authors " +
........>" FOR a IN authors FILTER x == a._id RETURN a) " +
........>" RETURN { book: b, authors: a }"
........>).toArray();
[
{
"book" : {
"title" : "The beauty of JOINS",
"authors" : [
"authors/2661190141",
"authors/2658437629"
],
"_id" : "books/2681506301",
"_rev" : "2681506301",
"_key" : "2681506301"
},
"authors" : [
{
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
},
"_id" : "authors/2661190141",
"_rev" : "2661190141",
"_key" : "2661190141"
},
{
"name" : {
"first" : "John",
"last" : "Doe"
},
"_id" : "authors/2658437629",
"_rev" : "2658437629",
"_key" : "2658437629"
}
]
}
]

…或直接嵌入作者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
arangosh> db._query(
........>"FOR b IN books LET a = (" +
........>" FOR x IN b.authors " +
........>" FOR a IN authors FILTER x == a._id RETURN a)" +
........>" RETURN merge(b, { authors: a })"
........>).toArray();
[
{
"_id" : "books/2681506301",
"_key" : "2681506301",
"_rev" : "2681506301",
"title" : "The beauty of JOINS",
"authors" : [
{
"_id" : "authors/2661190141",
"_key" : "2661190141",
"_rev" : "2661190141",
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
}
},
{
"_id" : "authors/2658437629",
"_key" : "2658437629",
"_rev" : "2658437629",
"name" : {
"first" : "John",
"last" : "Doe"
}
}
]
}
]

8.7.4 使用边缘集合

如果您还想查询给定作者写了哪些书,可以在书文档中嵌入作者,但为了速度,使用边缘集合更有效。

或者您正在发布论文集,那么您还想存储作者编写的页面。 该信息可以存储在边缘文档中。

首先创建用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arangosh> db._create("authors");
[ArangoCollection 2926807549, "authors" (type document, status loaded)]
arangosh> db.authors.save({ name: { first: "John", last: "Doe" } })
{
"error" : false,
"_id" : "authors/2935261693",
"_rev" : "2935261693",
"_key" : "2935261693"
}
arangosh> db.authors.save({ name: { first: "Maxima", last: "Musterfrau" } })
{
"error" : false,
"_id" : "authors/2938210813",
"_rev" : "2938210813",
"_key" : "2938210813"
}

现在创建没有任何作者信息的书籍。

1
2
3
4
5
6
7
8
9
arangosh> db._create("books");
[ArangoCollection 2928380413, "books" (type document, status loaded)]
arangosh> db.books.save({ title: "The beauty of JOINS" });
{
"error" : false,
"_id" : "books/2980088317",
"_rev" : "2980088317",
"_key" : "2980088317"
}

边缘集合现在用于链接作者和书籍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arangosh> db._createEdgeCollection("written");
[ArangoCollection 2931132925, "written" (type edge, status loaded)]
arangosh> db.written.save("authors/2935261693",
........>"books/2980088317",
........>{ pages: "1-10" })
{
"error" : false,
"_id" : "written/3006237181",
"_rev" : "3006237181",
"_key" : "3006237181"
}
arangosh> db.written.save("authors/2938210813",
........>"books/2980088317",
........>{ pages: "11-20" })
{
"error" : false,
"_id" : "written/3012856317",
"_rev" : "3012856317",
"_key" : "3012856317"
}

为了获得所有书籍的作者,您可以使用图形遍历https://www.arangodb.com/docs/3.10/aql/graphs-traversals.html#working-with-collection-sets)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
arangosh> db._query(
...> "FOR b IN books " +
...> "LET authorsByBook = ( " +
...> " FOR author, writtenBy IN INBOUND b written " +
...> " RETURN { " +
...> " vertex: author, " +
...> " edge: writtenBy " +
...> " } " +
...> ") " +
...> "RETURN { " +
...> " book: b, " +
...> " authors: authorsByBook " +
...> "} "
...> ).toArray();
[
{
"book" : {
"_key" : "2980088317",
"_id" : "books/2980088317",
"_rev" : "2980088317",
"title" : "The beauty of JOINS"
},
"authors" : [
{
"vertex" : {
"_key" : "2935261693",
"_id" : "authors/2935261693",
"_rev" : "2935261693",
"name" : {
"first" : "John",
"last" : "Doe"
}
},
"edge" : {
"_key" : "2935261693",
"_id" : "written/2935261693",
"_from" : "authors/2935261693",
"_to" : "books/2980088317",
"_rev" : "3006237181",
"pages" : "1-10"
}
},
{
"vertex" : {
"_key" : "2938210813",
"_id" : "authors/2938210813",
"_rev" : "2938210813",
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
}
},
"edge" : {
"_key" : "6833274",
"_id" : "written/6833274",
"_from" : "authors/2938210813",
"_to" : "books/2980088317",
"_rev" : "3012856317",
"pages" : "11-20"
}
}
]
}
]

或者,如果您只想要存储在顶点中的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
arangosh> db._query(
...> "FOR b IN books " +
...> "LET authorsByBook = ( " +
...> " FOR author IN INBOUND b written " +
...> " OPTIONS { " +
...> " order: 'bfs', " +
...> " uniqueVertices: 'global' " +
...> " } " +
...> " RETURN author " +
...> ") " +
...> "RETURN { " +
...> " book: b, " +
...> " authors: authorsByBook " +
...> "} "
...> ).toArray();
[
{
"book" : {
"_key" : "2980088317",
"_id" : "books/2980088317",
"_rev" : "2980088317",
"title" : "The beauty of JOINS"
},
"authors" : [
{
"_key" : "2938210813",
"_id" : "authors/2938210813",
"_rev" : "2938210813",
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
}
},
{
"_key" : "2935261693",
"_id" : "authors/2935261693",
"_rev" : "2935261693",
"name" : {
"first" : "John",
"last" : "Doe"
}
}
]
}
]

或者再次将作者直接嵌入到图书文档中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
arangosh> db._query(
...> "FOR b IN books " +
...> "LET authors = ( " +
...> " FOR author IN INBOUND b written " +
...> " OPTIONS { " +
...> " order: 'bfs', " +
...> " uniqueVertices: 'global' " +
...> " } " +
...> " RETURN author " +
...> ") " +
...> "RETURN MERGE(b, {authors: authors}) "
...> ).toArray();
[
{
"_id" : "books/2980088317",
"_key" : "2980088317",
"_rev" : "2980088317",
"title" : "The beauty of JOINS",
"authors" : [
{
"_key" : "2938210813",
"_id" : "authors/2938210813",
"_rev" : "2938210813",
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
}
},
{
"_key" : "2935261693",
"_id" : "authors/2935261693",
"_rev" : "2935261693",
"name" : {
"first" : "John",
"last" : "Doe"
}
}
]
}
]

如果您需要作者及其书籍,只需反转方向即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
> db._query(
...> "FOR a IN authors " +
...> "LET booksByAuthor = ( " +
...> " FOR b IN OUTBOUND a written " +
...> " OPTIONS { " +
...> " order: 'bfs', " +
...> " uniqueVertices: 'global' " +
...> " } " +
...> " RETURN b" +
...> ") " +
...> "RETURN MERGE(a, {books: booksByAuthor}) "
...> ).toArray();
[
{
"_id" : "authors/2935261693",
"_key" : "2935261693",
"_rev" : "2935261693",
"name" : {
"first" : "John",
"last" : "Doe"
},
"books" : [
{
"_key" : "2980088317",
"_id" : "books/2980088317",
"_rev" : "2980088317",
"title" : "The beauty of JOINS"
}
]
},
{
"_id" : "authors/2938210813",
"_key" : "2938210813",
"_rev" : "2938210813",
"name" : {
"first" : "Maxima",
"last" : "Musterfrau"
},
"books" : [
{
"_key" : "2980088317",
"_id" : "books/2980088317",
"_rev" : "2980088317",
"title" : "The beauty of JOINS"
}
]
}
]

8.7.5 更多示例

8.7.5.1 联接元组

我们将从一个 SQL-ish 结果集开始,并分别返回每个元组(用户名、朋友 userId)。 生成此类结果的 AQL 查询是:

1
2
3
4
5
6
7
8
9
FOR u IN users
FILTER u.active == true
LIMIT 0, 4
FOR f IN relations
FILTER f.type == @friend && f.friendOf == u.userId
RETURN {
"user" : u.name,
"friendId" : f.thisUser
}

我们遍历集合用户。 只会检查“活跃”用户。 对于这些用户中的每一个,我们将搜索最多 4 个朋友。 我们通过将当前用户的 userId 与关系文档的friendOf 属性进行比较来定位朋友。 对于找到的每个关系,我们返回用户名和朋友的用户 ID。

8.7.5.2 水平列表

注意,在上面的结果中,一个用户可以被多次返回。 这是返回数据的 SQL 方式。 如果不需要,可以在水平列表中返回每个用户的好友 id。 这将最多返回每个用户一次。

这样做的 AQL 查询是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
FOR u IN users
FILTER u.active == true LIMIT 0, 4
RETURN {
"user" : u.name,
"friendIds" : (
FOR f IN relations
FILTER f.friendOf == u.userId && f.type == "friend"
RETURN f.thisUser
)
}
[
{
"user" : "Abigail",
"friendIds" : [
108,
102,
106
]
},
{
"user" : "Fred",
"friendIds" : [
209
]
},
{
"user" : "Mary",
"friendIds" : [
207,
104
]
},
{
"user" : "Mariah",
"friendIds" : [
203,
205
]
}
]

在这个查询中,我们仍在迭代用户集合中的用户,并且对于每个匹配的用户,我们正在执行一个子查询以创建相关用户的匹配列表。

8.7.5.3 自联接

为了不仅返回好友 id 还返回好友的名字,我们可以再次“加入”用户集合(类似于“自我加入”):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
FOR u IN users
FILTER u.active == true
LIMIT 0, 4
RETURN {
"user" : u.name,
"friendIds" : (
FOR f IN relations
FILTER f.friendOf == u.userId && f.type == "friend"
FOR u2 IN users
FILTER f.thisUser == u2.useId
RETURN u2.name
)
}
[
{
"user" : "Abigail",
"friendIds" : [
"Jim",
"Jacob",
"Daniel"
]
},
{
"user" : "Fred",
"friendIds" : [
"Mariah"
]
},
{
"user" : "Mary",
"friendIds" : [
"Isabella",
"Michael"
]
},
{
"user" : "Mariah",
"friendIds" : [
"Madison",
"Eva"
]
}
]

这个查询将再次从用户集合中获取朋友的明文名称。 所以在这里我们迭代 users 集合,并为每次命中关系集合,并为每次命中再次迭代 users 集合。

8.7.5.4 外连接

让我们在我们的数据库中找到孤独的人——那些没有朋友的人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FOR user IN users
LET friendList = (
FOR f IN relations
FILTER f.friendOf == u.userId
RETURN 1
)
FILTER LENGTH(friendList) == 0
RETURN { "user" : user.name }
[
{
"user" : "Abigail"
},
{
"user" : "Fred"
}
]

因此,对于每个用户,我们选择他们的朋友列表并计算他们。 计数为零的人是孤独的人。 在子查询中使用 RETURN 1 可以节省更多宝贵的 CPU 周期,并为优化器提供更多选择。

8.7.5.5 索引用法

尤其是在连接上,您应该确保可以使用索引来加快查询速度。 请注意,稀疏索引不符合连接条件:

在联接中,您通常还希望联接不包含所联接属性的文档。 然而,稀疏索引不包含对不包含索引属性的文档的引用——因此它们会从连接操作中丢失。 因此,您应该提供非稀疏索引。

8.7.5.6 陷阱

由于我们没有模式,默认情况下无法判断文档的格式。 因此,如果您的文档不包含属性,则默认为 null。 然而,我们可以像这样检查我们的数据的准确性:

1
2
3
4
5
6
7
8
RETURN LENGTH(FOR u IN users FILTER u.userId == null RETURN 1)
[
10000
]
RETURN LENGTH(FOR f IN relations FILTER f.friendOf == null RETURN 1)
[
10000
]

因此,如果上述查询每个返回 10k 个匹配项,Join tuples 查询的结果将变得更大 100,000,000 个项目,并使用大量内存和计算时间。 因此,重新验证联接条件的条件是否存在通常是一个好主意。

在属性上使用索引可以显着加快操作速度。 您可以使用解释助手来重新验证您的查询实际使用它们。

如果您在边缘集合上使用连接,您通常会聚合内部字段 _id、_from 和 _to(在我们的示例中,_id 等于 userId,_fromfriendOf 和 _to 将是 thisUser)。 ArangoDB 隐式地为它们创建索引。

8.8 分组

为了按任意条件对结果进行分组,AQL 提供了COLLECT关键字。COLLECT将执行分组,但不执行聚合。 如果需要,仍然可以在查询中添加聚合。

8.8.1 确保唯一性

COLLECT 可用于使结果集唯一。 以下查询将只返回每个不同的年龄属性值一次:

1
2
3
FOR u IN users
COLLECT age = u.age
RETURN age

这是不跟踪组值而仅跟踪组标准(年龄)值的分组。

也可以使用 COLLECT 在多个级别上进行分组

1
2
3
FOR u IN users
COLLECT status = u.status, age = u.age
RETURN { status, age }

或者,可以使用 RETURN DISTINCT 使结果集唯一。 RETURN DISTINCT 仅支持单个条件:

1
2
FOR u IN users
RETURN DISTINCT u.age

RETURN DISTINCT 不会改变结果的顺序。 对于上面的查询,这意味着顺序是未定义的,因为在没有显式 SORT 操作的情况下迭代集合时,不能保证特定的顺序。

8.8.2 获取组值

要按年龄对用户进行分组,并返回年龄最大的用户的姓名,我们将发出如下查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FOR u IN users
FILTER u.active == true
COLLECT age = u.age INTO usersByAge
SORT age DESC LIMIT 0, 5
RETURN {
age,
users: usersByAge[*].u.name
}
[
{ "age": 37, "users": [ "John", "Sophia" ] },
{ "age": 36, "users": [ "Fred", "Emma" ] },
{ "age": 34, "users": [ "Madison" ] },
{ "age": 33, "users": [ "Chloe", "Michael" ] },
{ "age": 32, "users": [ "Alexander" ] }
]

该查询将按年龄属性将所有用户放在一起。 每个不同的年龄值都会有一个结果文档(撇开 LIMIT 不谈)。 对于每个组,我们可以通过 COLLECT 语句中引入的 usersByAge 变量访问匹配的文档。

8.8.3 变量扩展

usersByAge 变量包含找到的完整文档,因为我们只对用户名感兴趣,我们将使用扩展运算符 [*] 来提取每个组中所有用户文档的 name 属性:

1
usersByAge[*].u.name

[*] 扩展运算符只是一个方便的捷径。 我们也可以写一个子查询:

1
( FOR temp IN usersByAge RETURN temp.u.name )

8.8.4 按多个条件分组

要按多个条件分组,我们将在 COLLECT 子句中使用多个参数。 例如,要按年龄组(我们需要首先计算的派生值)然后按性别对用户进行分组,我们将执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FOR u IN users
FILTER u.active == true
COLLECT ageGroup = FLOOR(u.age / 5) * 5,
gender = u.gender INTO group
SORT ageGroup DESC
RETURN {
ageGroup,
gender
}
[
{ "ageGroup": 35, "gender": "f" },
{ "ageGroup": 35, "gender": "m" },
{ "ageGroup": 30, "gender": "f" },
{ "ageGroup": 30, "gender": "m" },
{ "ageGroup": 25, "gender": "f" },
{ "ageGroup": 25, "gender": "m" }
]

8.8.5 计数组值

如果目标是计算每个组中值的数量,AQL 提供了特殊的COLLECT WITH COUNT INTO语法。 这是使用附加组长度计算进行分组的简单变体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FOR u IN users
FILTER u.active == true
COLLECT ageGroup = FLOOR(u.age / 5) * 5,
gender = u.gender WITH COUNT INTO numUsers
SORT ageGroup DESC
RETURN {
ageGroup,
gender,
numUsers
}
[
{ "ageGroup": 35, "gender": "f", "numUsers": 2 },
{ "ageGroup": 35, "gender": "m", "numUsers": 2 },
{ "ageGroup": 30, "gender": "f", "numUsers": 4 },
{ "ageGroup": 30, "gender": "m", "numUsers": 4 },
{ "ageGroup": 25, "gender": "f", "numUsers": 2 },
{ "ageGroup": 25, "gender": "m", "numUsers": 2 }
]

8.8.6 集合体

通过在 COLLECT 中使用 AGGREGATE 子句,在 AQL 中添加进一步的聚合也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
FOR u IN users
FILTER u.active == true
COLLECT ageGroup = FLOOR(u.age / 5) * 5,
gender = u.gender
AGGREGATE numUsers = LENGTH(1),
minAge = MIN(u.age),
maxAge = MAX(u.age)
SORT ageGroup DESC
RETURN {
ageGroup,
gender,
numUsers,
minAge,
maxAge
}
[
{
"ageGroup": 35,
"gender": "f",
"numUsers": 2,
"minAge": 36,
"maxAge": 39,
},
{
"ageGroup": 35,
"gender": "m",
"numUsers": 2,
"minAge": 35,
"maxAge": 39,
},
...
]

我们在这里使用了聚合函数 LENGTH(它返回数组的长度)。 这相当于 SQL 的 SELECT g, COUNT(*) FROM … GROUP BY g。 除了 LENGTH,AQL 还提供 MAX、MIN、SUM 和 AVERAGE、VARIANCE_POPULATION、VARIANCE_SAMPLE、STDDEV_POPULATION、STDDEV_SAMPLE、UNIQUE、SORTED_UNIQUE 和 COUNT_UNIQUE 作为基本聚合函数。

在 AQL 中,所有聚合函数只能在数组上运行。 如果在任何非数组上运行聚合函数,将产生警告并且结果将为空。

使用 AGGREGATE 子句将确保在收集操作中构建组时运行聚合。 这通常比收集所有组的所有组值然后进行后聚合更有效。

8.8.7 聚合后

也可以在使用其他 AQL 构造的 COLLECT 操作之后执行聚合,尽管在性能方面这通常不如将 COLLECT 与 AGGREGATE 一起使用。

与之前相同的查询可以转换为聚合后查询,如下所示。 请注意,此查询将构建并传递变量 g 内所有组的所有组值,并在可能的最新阶段执行聚合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FOR u IN users
FILTER u.active == true
COLLECT ageGroup = FLOOR(u.age / 5) * 5,
gender = u.gender INTO g
SORT ageGroup DESC
RETURN {
ageGroup,
gender,
numUsers: LENGTH(g[*]),
minAge: MIN(g[*].u.age),
maxAge: MAX(g[*].u.age)
}
[
{
"ageGroup": 35,
"gender": "f",
"numUsers": 2,
"minAge": 36,
"maxAge": 39,
},
{
"ageGroup": 35,
"gender": "m",
"numUsers": 2,
"minAge": 35,
"maxAge": 39,
},
...
]

这与使用 AGGREGATE 子句在收集操作期间尽早执行聚合的先前查询形成对比。

8.8.8 筛选后聚合数据

要过滤分组或聚合操作的结果(即类似于 SQL 中的 HAVING),只需在 COLLECT 语句之后添加另一个 FILTER 子句。

例如,要获取其中用户最多的 3 个年龄组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
FOR u IN users
FILTER u.active == true
COLLECT ageGroup = FLOOR(u.age / 5) * 5 INTO group
LET numUsers = LENGTH(group)
FILTER numUsers > 2 /* group must contain at least 3 users in order to qualify */
SORT numUsers DESC
LIMIT 0, 3
RETURN {
"ageGroup": ageGroup,
"numUsers": numUsers,
"users": group[*].u.name
}
[
{
"ageGroup": 30,
"numUsers": 8,
"users": [
"Abigail",
"Madison",
"Anthony",
"Alexander",
"Isabella",
"Chloe",
"Daniel",
"Michael"
]
},
{
"ageGroup": 25,
"numUsers": 4,
"users": [
"Mary",
"Mariah",
"Jim",
"Diego"
]
},
{
"ageGroup": 35,
"numUsers": 4,
"users": [
"Fred",
"John",
"Emma",
"Sophia"
]
}
]

为了提高可读性,重复的表达式 LENGTH(group) 被放入变量 numUsers 中。 numUsers 上的 FILTER 等效于 SQL HAVING 子句。

8.8.9 以本地时间聚合数据

如果您将日期时间以 UTC 格式存储在您的集合中,并且需要将本地时区中每一天的数据分组,您可以使用 DATE_UTCTOLOCAL() 和 DATE_TRUNC() 进行调整。

注意:在 2020-03-29 启用的欧洲/柏林夏令时,因此 2020-01-31T23:00:00Z 在德国是 2020-02-01 午夜,2020-03-31T22:00:00Z 是 2020- 04-01 午夜在德国。

1
2
3
4
5
6
7
8
9
10
11
12
FOR a IN @activities
COLLECT
day = DATE_TRUNC(DATE_UTCTOLOCAL(a.startDate, 'Europe/Berlin'), 'day')
AGGREGATE
hours = SUM(a.duration),
revenue = SUM(a.duration * a.rate)
SORT day ASC
RETURN {
day,
hours,
revenue
}

8.9 AQL 示例 对演员和电影数据集的查询

给定一个图 [actors] –actsIn → [movies] 有两个顶点集合 actor 和movies 以及一个边集合actsIn,边从actors 指向movie,很多有趣的查询是可能的:

  • 在“电影1”或“电影2”中演出的所有演员

  • 所有在“电影1”和“电影2”中出演的演员

  • “actor1”和“actor2”之间的所有常见电影

  • 所有出演过 3 部电影或更多电影的演员

  • 恰好有 6 位演员出演的所有电影

  • 电影演员人数

  • 演员的电影数量

  • 演员在两年内出演的电影数量

  • 带有演员姓名的演员的电影年数和数量

8.9.1 数据

我们将使用 arangosh 来创建和查询数据。 所有 AQL 查询都是字符串,可以简单地复制到 Web 界面或您最喜欢的驱动程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
var actors = db._create("actors");
var movies = db._create("movies");
var actsIn = db._createEdgeCollection("actsIn");

var TheMatrix = movies.save({ _key: "TheMatrix", title: "The Matrix", released: 1999, tagline: "Welcome to the Real World" })._id;
var Keanu = actors.save({ _key: "Keanu", name: "Keanu Reeves", born: 1964 })._id;
var Carrie = actors.save({ _key: "Carrie", name: "Carrie-Anne Moss", born: 1967 })._id;
var Laurence = actors.save({ _key: "Laurence", name: "Laurence Fishburne", born: 1961 })._id;
var Hugo = actors.save({ _key: "Hugo", name: "Hugo Weaving", born: 1960 })._id;
var Emil = actors.save({ _key: "Emil", name: "Emil Eifrem", born: 1978 });

actsIn.save(Keanu, TheMatrix, { roles: ["Neo"], year: 1999 });
actsIn.save(Carrie, TheMatrix, { roles: ["Trinity"], year: 1999 });
actsIn.save(Laurence, TheMatrix, { roles: ["Morpheus"], year: 1999 });
actsIn.save(Hugo, TheMatrix, { roles: ["Agent Smith"], year: 1999 });
actsIn.save(Emil, TheMatrix, { roles: ["Emil"], year: 1999 });

var TheMatrixReloaded = movies.save({ _key: "TheMatrixReloaded", title: "The Matrix Reloaded", released: 2003, tagline: "Free your mind" });
actsIn.save(Keanu, TheMatrixReloaded, { roles: ["Neo"], year: 2003 });
actsIn.save(Carrie, TheMatrixReloaded, { roles: ["Trinity"], year: 2003 });
actsIn.save(Laurence, TheMatrixReloaded, { roles: ["Morpheus"], year: 2003 });
actsIn.save(Hugo, TheMatrixReloaded, { roles: ["Agent Smith"], year: 2003 });

var TheMatrixRevolutions = movies.save({ _key: "TheMatrixRevolutions", title: "The Matrix Revolutions", released: 2003, tagline: "Everything that has a beginning has an end" });
actsIn.save(Keanu, TheMatrixRevolutions, { roles: ["Neo"], year: 2003 });
actsIn.save(Carrie, TheMatrixRevolutions, { roles: ["Trinity"], year: 2003 });
actsIn.save(Laurence, TheMatrixRevolutions, { roles: ["Morpheus"], year: 2003 });
actsIn.save(Hugo, TheMatrixRevolutions, { roles: ["Agent Smith"], year: 2003 });

var TheDevilsAdvocate = movies.save({ _key: "TheDevilsAdvocate", title: "The Devil's Advocate", released: 1997, tagline: "Evil has its winning ways" })._id;
var Charlize = actors.save({ _key: "Charlize", name: "Charlize Theron", born: 1975 })._id;
var Al = actors.save({ _key: "Al", name: "Al Pacino", born: 1940 })._id;
actsIn.save(Keanu, TheDevilsAdvocate, { roles: ["Kevin Lomax"], year: 1997 });
actsIn.save(Charlize, TheDevilsAdvocate, { roles: ["Mary Ann Lomax"], year: 1997 });
actsIn.save(Al, TheDevilsAdvocate, { roles: ["John Milton"], year: 1997 });

var AFewGoodMen = movies.save({ _key: "AFewGoodMen", title: "A Few Good Men", released: 1992, tagline: "In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth." })._id;
var TomC = actors.save({ _key: "TomC", name: "Tom Cruise", born: 1962 })._id;
var JackN = actors.save({ _key: "JackN", name: "Jack Nicholson", born: 1937 })._id;
var DemiM = actors.save({ _key: "DemiM", name: "Demi Moore", born: 1962 })._id;
var KevinB = actors.save({ _key: "KevinB", name: "Kevin Bacon", born: 1958 })._id;
var KieferS = actors.save({ _key: "KieferS", name: "Kiefer Sutherland", born: 1966 })._id;
var NoahW = actors.save({ _key: "NoahW", name: "Noah Wyle", born: 1971 })._id;
var CubaG = actors.save({ _key: "CubaG", name: "Cuba Gooding Jr.", born: 1968 })._id;
var KevinP = actors.save({ _key: "KevinP", name: "Kevin Pollak", born: 1957 })._id;
var JTW = actors.save({ _key: "JTW", name: "J.T. Walsh", born: 1943 })._id;
var JamesM = actors.save({ _key: "JamesM", name: "James Marshall", born: 1967 })._id;
var ChristopherG = actors.save({ _key: "ChristopherG", name: "Christopher Guest", born: 1948 })._id;
actsIn.save(TomC, AFewGoodMen, { roles: ["Lt. Daniel Kaffee"], year: 1992 });
actsIn.save(JackN, AFewGoodMen, { roles: ["Col. Nathan R. Jessup"], year: 1992 });
actsIn.save(DemiM, AFewGoodMen, { roles: ["Lt. Cdr. JoAnne Galloway"], year: 1992 });
actsIn.save(KevinB, AFewGoodMen, { roles: ["Capt. Jack Ross"], year: 1992 });
actsIn.save(KieferS, AFewGoodMen, { roles: ["Lt. Jonathan Kendrick"], year: 1992 });
actsIn.save(NoahW, AFewGoodMen, { roles: ["Cpl. Jeffrey Barnes"], year: 1992 });
actsIn.save(CubaG, AFewGoodMen, { roles: ["Cpl. Carl Hammaker"], year: 1992 });
actsIn.save(KevinP, AFewGoodMen, { roles: ["Lt. Sam Weinberg"], year: 1992 });
actsIn.save(JTW, AFewGoodMen, { roles: ["Lt. Col. Matthew Andrew Markinson"], year: 1992 });
actsIn.save(JamesM, AFewGoodMen, { roles: ["Pfc. Louden Downey"], year: 1992 });
actsIn.save(ChristopherG, AFewGoodMen, { roles: ["Dr. Stone"], year: 1992 });

var TopGun = movies.save({ _key: "TopGun", title: "Top Gun", released: 1986, tagline: "I feel the need, the need for speed." })._id;
var KellyM = actors.save({ _key: "KellyM", name: "Kelly McGillis", born: 1957 })._id;
var ValK = actors.save({ _key: "ValK", name: "Val Kilmer", born: 1959 })._id;
var AnthonyE = actors.save({ _key: "AnthonyE", name: "Anthony Edwards", born: 1962 })._id;
var TomS = actors.save({ _key: "TomS", name: "Tom Skerritt", born: 1933 })._id;
var MegR = actors.save({ _key: "MegR", name: "Meg Ryan", born: 1961 })._id;
actsIn.save(TomC, TopGun, { roles: ["Maverick"], year: 1986 });
actsIn.save(KellyM, TopGun, { roles: ["Charlie"], year: 1986 });
actsIn.save(ValK, TopGun, { roles: ["Iceman"], year: 1986 });
actsIn.save(AnthonyE, TopGun, { roles: ["Goose"], year: 1986 });
actsIn.save(TomS, TopGun, { roles: ["Viper"], year: 1986 });
actsIn.save(MegR, TopGun, { roles: ["Carole"], year: 1986 });

var JerryMaguire = movies.save({ _key: "JerryMaguire", title: "Jerry Maguire", released: 2000, tagline: "The rest of his life begins now." })._id;
var ReneeZ = actors.save({ _key: "ReneeZ", name: "Renee Zellweger", born: 1969 })._id;
var KellyP = actors.save({ _key: "KellyP", name: "Kelly Preston", born: 1962 })._id;
var JerryO = actors.save({ _key: "JerryO", name: "Jerry O'Connell", born: 1974 })._id;
var JayM = actors.save({ _key: "JayM", name: "Jay Mohr", born: 1970 })._id;
var BonnieH = actors.save({ _key: "BonnieH", name: "Bonnie Hunt", born: 1961 })._id;
var ReginaK = actors.save({ _key: "ReginaK", name: "Regina King", born: 1971 })._id;
var JonathanL = actors.save({ _key: "JonathanL", name: "Jonathan Lipnicki", born: 1996 })._id;
actsIn.save(TomC, JerryMaguire, { roles: ["Jerry Maguire"], year: 2000 });
actsIn.save(CubaG, JerryMaguire, { roles: ["Rod Tidwell"], year: 2000 });
actsIn.save(ReneeZ, JerryMaguire, { roles: ["Dorothy Boyd"], year: 2000 });
actsIn.save(KellyP, JerryMaguire, { roles: ["Avery Bishop"], year: 2000 });
actsIn.save(JerryO, JerryMaguire, { roles: ["Frank Cushman"], year: 2000 });
actsIn.save(JayM, JerryMaguire, { roles: ["Bob Sugar"], year: 2000 });
actsIn.save(BonnieH, JerryMaguire, { roles: ["Laurel Boyd"], year: 2000 });
actsIn.save(ReginaK, JerryMaguire, { roles: ["Marcee Tidwell"], year: 2000 });
actsIn.save(JonathanL, JerryMaguire, { roles: ["Ray Boyd"], year: 2000 });

var StandByMe = movies.save({ _key: "StandByMe", title: "Stand By Me", released: 1986, tagline: "For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of." })._id;
var RiverP = actors.save({ _key: "RiverP", name: "River Phoenix", born: 1970 })._id;
var CoreyF = actors.save({ _key: "CoreyF", name: "Corey Feldman", born: 1971 })._id;
var WilW = actors.save({ _key: "WilW", name: "Wil Wheaton", born: 1972 })._id;
var JohnC = actors.save({ _key: "JohnC", name: "John Cusack", born: 1966 })._id;
var MarshallB = actors.save({ _key: "MarshallB", name: "Marshall Bell", born: 1942 })._id;
actsIn.save(WilW, StandByMe, { roles: ["Gordie Lachance"], year: 1986 });
actsIn.save(RiverP, StandByMe, { roles: ["Chris Chambers"], year: 1986 });
actsIn.save(JerryO, StandByMe, { roles: ["Vern Tessio"], year: 1986 });
actsIn.save(CoreyF, StandByMe, { roles: ["Teddy Duchamp"], year: 1986 });
actsIn.save(JohnC, StandByMe, { roles: ["Denny Lachance"], year: 1986 });
actsIn.save(KieferS, StandByMe, { roles: ["Ace Merrill"], year: 1986 });
actsIn.save(MarshallB, StandByMe, { roles: ["Mr. Lachance"], year: 1986 });

var AsGoodAsItGets = movies.save({ _key: "AsGoodAsItGets", title: "As Good as It Gets", released: 1997, tagline: "A comedy from the heart that goes for the throat." })._id;
var HelenH = actors.save({ _key: "HelenH", name: "Helen Hunt", born: 1963 })._id;
var GregK = actors.save({ _key: "GregK", name: "Greg Kinnear", born: 1963 })._id;
actsIn.save(JackN, AsGoodAsItGets, { roles: ["Melvin Udall"], year: 1997 });
actsIn.save(HelenH, AsGoodAsItGets, { roles: ["Carol Connelly"], year: 1997 });
actsIn.save(GregK, AsGoodAsItGets, { roles: ["Simon Bishop"], year: 1997 });
actsIn.save(CubaG, AsGoodAsItGets, { roles: ["Frank Sachs"], year: 1997 });

var WhatDreamsMayCome = movies.save({ _key: "WhatDreamsMayCome", title: "What Dreams May Come", released: 1998, tagline: "After life there is more. The end is just the beginning." })._id;
var AnnabellaS = actors.save({ _key: "AnnabellaS", name: "Annabella Sciorra", born: 1960 })._id;
var MaxS = actors.save({ _key: "MaxS", name: "Max von Sydow", born: 1929 })._id;
var WernerH = actors.save({ _key: "WernerH", name: "Werner Herzog", born: 1942 })._id;
var Robin = actors.save({ _key: "Robin", name: "Robin Williams", born: 1951 })._id;
actsIn.save(Robin, WhatDreamsMayCome, { roles: ["Chris Nielsen"], year: 1998 });
actsIn.save(CubaG, WhatDreamsMayCome, { roles: ["Albert Lewis"], year: 1998 });
actsIn.save(AnnabellaS, WhatDreamsMayCome, { roles: ["Annie Collins-Nielsen"], year: 1998 });
actsIn.save(MaxS, WhatDreamsMayCome, { roles: ["The Tracker"], year: 1998 });
actsIn.save(WernerH, WhatDreamsMayCome, { roles: ["The Face"], year: 1998 });

var SnowFallingonCedars = movies.save({ _key: "SnowFallingonCedars", title: "Snow Falling on Cedars", released: 1999, tagline: "First loves last. Forever." })._id;
var EthanH = actors.save({ _key: "EthanH", name: "Ethan Hawke", born: 1970 })._id;
var RickY = actors.save({ _key: "RickY", name: "Rick Yune", born: 1971 })._id;
var JamesC = actors.save({ _key: "JamesC", name: "James Cromwell", born: 1940 })._id;
actsIn.save(EthanH, SnowFallingonCedars, { roles: ["Ishmael Chambers"], year: 1999 });
actsIn.save(RickY, SnowFallingonCedars, { roles: ["Kazuo Miyamoto"], year: 1999 });
actsIn.save(MaxS, SnowFallingonCedars, { roles: ["Nels Gudmundsson"], year: 1999 });
actsIn.save(JamesC, SnowFallingonCedars, { roles: ["Judge Fielding"], year: 1999 });

var YouveGotMail = movies.save({ _key: "YouveGotMail", title: "You've Got Mail", released: 1998, tagline: "At odds in life... in love on-line." })._id;
var ParkerP = actors.save({ _key: "ParkerP", name: "Parker Posey", born: 1968 })._id;
var DaveC = actors.save({ _key: "DaveC", name: "Dave Chappelle", born: 1973 })._id;
var SteveZ = actors.save({ _key: "SteveZ", name: "Steve Zahn", born: 1967 })._id;
var TomH = actors.save({ _key: "TomH", name: "Tom Hanks", born: 1956 })._id;
actsIn.save(TomH, YouveGotMail, { roles: ["Joe Fox"], year: 1998 });
actsIn.save(MegR, YouveGotMail, { roles: ["Kathleen Kelly"], year: 1998 });
actsIn.save(GregK, YouveGotMail, { roles: ["Frank Navasky"], year: 1998 });
actsIn.save(ParkerP, YouveGotMail, { roles: ["Patricia Eden"], year: 1998 });
actsIn.save(DaveC, YouveGotMail, { roles: ["Kevin Jackson"], year: 1998 });
actsIn.save(SteveZ, YouveGotMail, { roles: ["George Pappas"], year: 1998 });

var SleeplessInSeattle = movies.save({ _key: "SleeplessInSeattle", title: "Sleepless in Seattle", released: 1993, tagline: "What if someone you never met, someone you never saw, someone you never knew was the only someone for you?" })._id;
var RitaW = actors.save({ _key: "RitaW", name: "Rita Wilson", born: 1956 })._id;
var BillPull = actors.save({ _key: "BillPull", name: "Bill Pullman", born: 1953 })._id;
var VictorG = actors.save({ _key: "VictorG", name: "Victor Garber", born: 1949 })._id;
var RosieO = actors.save({ _key: "RosieO", name: "Rosie O'Donnell", born: 1962 })._id;
actsIn.save(TomH, SleeplessInSeattle, { roles: ["Sam Baldwin"], year: 1993 });
actsIn.save(MegR, SleeplessInSeattle, { roles: ["Annie Reed"], year: 1993 });
actsIn.save(RitaW, SleeplessInSeattle, { roles: ["Suzy"], year: 1993 });
actsIn.save(BillPull, SleeplessInSeattle, { roles: ["Walter"], year: 1993 });
actsIn.save(VictorG, SleeplessInSeattle, { roles: ["Greg"], year: 1993 });
actsIn.save(RosieO, SleeplessInSeattle, { roles: ["Becky"], year: 1993 });

var JoeVersustheVolcano = movies.save({ _key: "JoeVersustheVolcano", title: "Joe Versus the Volcano", released: 1990, tagline: "A story of love, lava and burning desire." })._id;
var Nathan = actors.save({ _key: "Nathan", name: "Nathan Lane", born: 1956 })._id;
actsIn.save(TomH, JoeVersustheVolcano, { roles: ["Joe Banks"], year: 1990 });
actsIn.save(MegR, JoeVersustheVolcano, { roles: ["DeDe", "Angelica Graynamore", "Patricia Graynamore"], year: 1990 });
actsIn.save(Nathan, JoeVersustheVolcano, { roles: ["Baw"], year: 1990 });

var WhenHarryMetSally = movies.save({ _key: "WhenHarryMetSally", title: "When Harry Met Sally", released: 1998, tagline: "At odds in life... in love on-line." })._id;
var BillyC = actors.save({ _key: "BillyC", name: "Billy Crystal", born: 1948 })._id;
var CarrieF = actors.save({ _key: "CarrieF", name: "Carrie Fisher", born: 1956 })._id;
var BrunoK = actors.save({ _key: "BrunoK", name: "Bruno Kirby", born: 1949 })._id;
actsIn.save(BillyC, WhenHarryMetSally, { roles: ["Harry Burns"], year: 1998 });
actsIn.save(MegR, WhenHarryMetSally, { roles: ["Sally Albright"], year: 1998 });
actsIn.save(CarrieF, WhenHarryMetSally, { roles: ["Marie"], year: 1998 });
actsIn.save(BrunoK, WhenHarryMetSally, { roles: ["Jess"], year: 1998 });

8.9.2 示例查询

8.9.2.1 所有在”movie1”或”movie2”中演出的演员

假设我们想找到所有在”TheMatrix”或”TheDevilsAdvocate”中表演的演员。首先,让我们尝试为一部电影获取所有演员:

1
2
3
4
5
db._query(`
FOR x IN ANY 'movies/TheMatrix' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN x._id
`).toArray();

结果:

1
2
3
4
5
6
7
8
9
[
[
"actors/Keanu",
"actors/Hugo",
"actors/Emil",
"actors/Carrie",
"actors/Laurence"
]
]

现在我们继续形成两个邻居查询的 UNION_DISTINCT这将是解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
db._query(`
FOR x IN UNION_DISTINCT(
(FOR y IN ANY 'movies/TheMatrix' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN y._id),
(FOR y IN ANY 'movies/TheDevilsAdvocate' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN y._id)
) RETURN x
`).toArray();
[
[
"actors/Emil",
"actors/Hugo",
"actors/Carrie",
"actors/Laurence",
"actors/Keanu",
"actors/Al",
"actors/Charlize"
]
]

8.9.2.2 所有同时出演”movie1”和”movie2”的演员

这几乎与上面的问题相同。 但是这次我们对 UNION不感兴趣,而是对 INTERSECTION 感兴趣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db._query(`
FOR x IN INTERSECTION(
(FOR y IN ANY 'movies/TheMatrix' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN y._id),
(FOR y IN ANY 'movies/TheDevilsAdvocate' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN y._id)
) RETURN x
`).toArray();
[
[
"actors/Keanu"
]
]

8.9.2.3 “actor1”和“actor2”之间的所有共同电影

这其实和关于movie1和movie2中共同演员的问题是一样的。 我们只需要改变起始顶点。 例如,让我们找出雨果·维文和基努·里维斯共同主演的所有电影::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
db._query(`
FOR x IN INTERSECTION(
(FOR y IN ANY 'actors/Hugo' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN y._id),
(FOR y IN ANY 'actors/Keanu' actsIn
OPTIONS { order: 'bfs', uniqueVertices: 'global' }
RETURN y._id)
) RETURN x
`).toArray();
[
[
"movies/TheMatrixRevolutions",
"movies/TheMatrixReloaded",
"movies/TheMatrix"
]
]

8.9.2.4 所有出演过 3 部或更多电影的演员

将利用 AQL 的边缘索引和 COLLECT 语句进行分组。 基本思想是按起始顶点(在此数据集中始终是参与者)对所有边进行分组。 然后我们从结果中删除所有少于 3 部电影的演员。 下面的查询还返回计算出的演员演过的电影数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
db._query(`
FOR x IN actsIn
COLLECT actor = x._from WITH COUNT INTO counter
FILTER counter >= 3
RETURN { actor: actor, movies: counter }
`).toArray();
[
{
"actor" : "actors/Carrie",
"movies" : 3
},
{
"actor" : "actors/CubaG",
"movies" : 4
},
{
"actor" : "actors/Hugo",
"movies" : 3
},
{
"actor" : "actors/Keanu",
"movies" : 4
},
{
"actor" : "actors/Laurence",
"movies" : 3
},
{
"actor" : "actors/MegR",
"movies" : 5
},
{
"actor" : "actors/TomC",
"movies" : 3
},
{
"actor" : "actors/TomH",
"movies" : 3
}
]

8.9.2.5 恰好有 6 位演员出演的所有电影

与之前查询中的想法相同,但使用相等过滤器,但是现在我们需要电影而不是演员,因此我们返回_to 属性:

1
2
3
4
5
6
7
8
9
10
11
db._query(`
FOR x IN actsIn
COLLECT movie = x._to WITH COUNT INTO counter
FILTER counter == 6
RETURN movie
`).toArray();
[
"movies/SleeplessInSeattle",
"movies/TopGun",
"movies/YouveGotMail"
]

8.9.2.6 电影演员人数

我们记得在我们的数据集中_to 边缘对应于电影,因此我们计算相同 _to 出现的频率。 这是演员的数量。 该查询与之前的查询几乎相同,但在COLLECT之后没有FILTER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
db._query(`
FOR x IN actsIn
COLLECT movie = x._to WITH COUNT INTO counter
RETURN { movie: movie, actors: counter }
`).toArray();
[
{
"movie" : "movies/AFewGoodMen",
"actors" : 11
},
{
"movie" : "movies/AsGoodAsItGets",
"actors" : 4
},
{
"movie" : "movies/JerryMaguire",
"actors" : 9
},
{
"movie" : "movies/JoeVersustheVolcano",
"actors" : 3
},
{
"movie" : "movies/SleeplessInSeattle",
"actors" : 6
},
{
"movie" : "movies/SnowFallingonCedars",
"actors" : 4
},
{
"movie" : "movies/StandByMe",
"actors" : 7
},
{
"movie" : "movies/TheDevilsAdvocate",
"actors" : 3
},
{
"movie" : "movies/TheMatrix",
"actors" : 5
},
{
"movie" : "movies/TheMatrixReloaded",
"actors" : 4
},
{
"movie" : "movies/TheMatrixRevolutions",
"actors" : 4
},
{
"movie" : "movies/TopGun",
"actors" : 6
},
{
"movie" : "movies/WhatDreamsMayCome",
"actors" : 5
},
{
"movie" : "movies/WhenHarryMetSally",
"actors" : 4
},
{
"movie" : "movies/YouveGotMail",
"actors" : 6
}
]

8.9.2.7 演员的电影数量

边上的_to属性对应的是actor,所以我们按它分组,用COLLECT计数。 作为奖励,我们可以添加排序以首先返回拥有最多电影的演员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
db._query(`
FOR x IN actsIn
COLLECT actor = x._from WITH COUNT INTO counter
SORT counter DESC
RETURN { actor: actor, movies: counter }
`).toArray();
[
{
"actor" : "actors/MegR",
"movies" : 5
},
{
"actor" : "actors/Keanu",
"movies" : 4
},
{
"actor" : "actors/CubaG",
"movies" : 4
},
{
"actor" : "actors/Carrie",
"movies" : 3
},
{
"actor" : "actors/Laurence",
"movies" : 3
},
{
"actor" : "actors/Hugo",
"movies" : 3
},
{
"actor" : "actors/TomC",
"movies" : 3
},
{
"actor" : "actors/TomH",
"movies" : 3
},
{
"actor" : "actors/JerryO",
"movies" : 2
},
{
"actor" : "actors/GregK",
"movies" : 2
},
{
"actor" : "actors/MaxS",
"movies" : 2
},
{
"actor" : "actors/JackN",
"movies" : 2
},
{
"actor" : "actors/KieferS",
"movies" : 2
},
{
"actor" : "actors/JamesM",
"movies" : 1
},
{
"actor" : "actors/JayM",
"movies" : 1
},
{
"actor" : "actors/ReneeZ",
"movies" : 1
},
{
"actor" : "actors/JamesC",
"movies" : 1
},
{
"actor" : "actors/TomS",
"movies" : 1
},
{
"actor" : "actors/AnthonyE",
"movies" : 1
},
{
"actor" : "actors/ValK",
"movies" : 1
},
{
"actor" : "actors/KellyM",
"movies" : 1
},
{
"actor" : "actors/ChristopherG",
"movies" : 1
},
{
"actor" : "actors/Al",
"movies" : 1
},
{
"actor" : "actors/JTW",
"movies" : 1
},
{
"actor" : "actors/KevinP",
"movies" : 1
},
{
"actor" : "actors/Emil",
"movies" : 1
},
{
"actor" : "actors/NoahW",
"movies" : 1
},
{
"actor" : "actors/Charlize",
"movies" : 1
},
{
"actor" : "actors/KevinB",
"movies" : 1
},
{
"actor" : "actors/DemiM",
"movies" : 1
},
{
"actor" : "actors/WernerH",
"movies" : 1
},
{
"actor" : "actors/CarrieF",
"movies" : 1
},
{
"actor" : "actors/BillyC",
"movies" : 1
},
{
"actor" : "actors/Nathan",
"movies" : 1
},
{
"actor" : "actors/RosieO",
"movies" : 1
},
{
"actor" : "actors/VictorG",
"movies" : 1
},
{
"actor" : "actors/BillPull",
"movies" : 1
},
{
"actor" : "actors/RitaW",
"movies" : 1
},
{
"actor" : "actors/SteveZ",
"movies" : 1
},
{
"actor" : "actors/DaveC",
"movies" : 1
},
{
"actor" : "actors/ParkerP",
"movies" : 1
},
{
"actor" : "actors/RickY",
"movies" : 1
},
{
"actor" : "actors/EthanH",
"movies" : 1
},
{
"actor" : "actors/KellyP",
"movies" : 1
},
{
"actor" : "actors/AnnabellaS",
"movies" : 1
},
{
"actor" : "actors/Robin",
"movies" : 1
},
{
"actor" : "actors/HelenH",
"movies" : 1
},
{
"actor" : "actors/MarshallB",
"movies" : 1
},
{
"actor" : "actors/JohnC",
"movies" : 1
},
{
"actor" : "actors/CoreyF",
"movies" : 1
},
{
"actor" : "actors/RiverP",
"movies" : 1
},
{
"actor" : "actors/WilW",
"movies" : 1
},
{
"actor" : "actors/JonathanL",
"movies" : 1
},
{
"actor" : "actors/ReginaK",
"movies" : 1
},
{
"actor" : "actors/BonnieH",
"movies" : 1
},
{
"actor" : "actors/BrunoK",
"movies" : 1
}
]

8.9.2.8 两年间由演员出演的电影数量

这个查询是多模型数据库真正发挥作用的地方。 首先我们想在生产中使用它,所以我们在 year 上设置了一个持久性索引。 这允许执行诸如 1990 和 1995 之间的快速范围查询。

1
db.actsIn.ensureIndex({ type: "persistent", fields: ["year"] });

现在我们通过演员查询稍微修改我们的电影。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
db._query(`
FOR x IN actsIn
FILTER x.year >= 1990 && x.year <= 1995
COLLECT actor = x._from WITH COUNT INTO counter
RETURN { actor: actor, movies: counter }
`).toArray();
[
{
"actor" : "actors/BillPull",
"movies" : 1
},
{
"actor" : "actors/ChristopherG",
"movies" : 1
},
{
"actor" : "actors/CubaG",
"movies" : 1
},
{
"actor" : "actors/DemiM",
"movies" : 1
},
{
"actor" : "actors/JackN",
"movies" : 1
},
{
"actor" : "actors/JamesM",
"movies" : 1
},
{
"actor" : "actors/JTW",
"movies" : 1
},
{
"actor" : "actors/KevinB",
"movies" : 1
},
{
"actor" : "actors/KevinP",
"movies" : 1
},
{
"actor" : "actors/KieferS",
"movies" : 1
},
{
"actor" : "actors/MegR",
"movies" : 2
},
{
"actor" : "actors/Nathan",
"movies" : 1
},
{
"actor" : "actors/NoahW",
"movies" : 1
},
{
"actor" : "actors/RitaW",
"movies" : 1
},
{
"actor" : "actors/RosieO",
"movies" : 1
},
{
"actor" : "actors/TomC",
"movies" : 1
},
{
"actor" : "actors/TomH",
"movies" : 2
},
{
"actor" : "actors/VictorG",
"movies" : 1
}
]

8.9.2.9 带有演员姓名的演员的电影年数和数量

如果我们想返回一个年份列表,而不仅仅是演员出演的电影数量,那么我们不能使用 COLLECT WITH COUNT INTO,因为我们只能在分组后访问演员和计数器。 相反,我们可以使用 COLLECT ... INTO来跟踪每个演员的电影年数。 年数等于电影的数量。

为简单起见,示例查询仅限于两个参与者。 作为额外的附加功能,它使用 DOCUMENT()函数查找演员姓名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
db._query(`
FOR x IN actsIn
FILTER x._from IN [ "actors/TomH", "actors/Keanu" ]
COLLECT actor = x._from INTO years = x.year
RETURN {
name: DOCUMENT(actor).name,
movies: COUNT(years),
years
}`
).toArray();
[
{
"name" : "Keanu Reeves",
"movies" : 4,
"years" : [
1999,
2003,
2003,
1997
]
},
{
"name" : "Tom Hanks",
"movies" : 3,
"years" : [
1998,
1993,
1990
]
}
]

8.10 组合图形遍历

通过地理查询查找起始顶点

我们的第一个示例将通过地理索引定位图遍历的起始顶点。 我们使用城市图及其地理索引:

城市示例图

1
2
arangosh> var examples = require("@arangodb/graph-examples/example-graph.js");
arangosh> var g = examples.loadGraph("routeplanner");

我们搜索前首都波恩周围 400 公里范围内的所有德国城市:汉堡和科隆。 我们找不到巴黎,因为它在 frenchCity 收藏中。

1
2
3
FOR startCity IN germanCity
FILTER GEO_DISTANCE(@bonn, startCity.geometry) < @radius
RETURN startCity._key

绑定参数:

1
2
3
4
5
6
7
8
9
10
11
{
"bonn": [
7.0998,
50.734
],
"radius": 400000
}
[
"Cologne",
"Hamburg"
]

让我们重新验证实际使用了地理索引:

1
2
3
FOR startCity IN germanCity
FILTER GEO_DISTANCE(@bonn, startCity.geometry) < @radius
RETURN startCity._key

现在将其与图遍历结合起来:

1
2
3
4
5
FOR startCity IN germanCity
FILTER GEO_DISTANCE(@bonn, startCity.geometry) < @radius
FOR v, e, p IN 1..1 OUTBOUND startCity
GRAPH 'routeplanner'
RETURN {startcity: startCity._key, traversedCity: v._key}

地理索引查询返回我们的 startCity(科隆和汉堡),然后我们将其用作图遍历的起点。 为简单起见,我们只返回它们的直接邻居。 我们格式化返回结果,以便我们可以看到遍历来自哪个 startCity。

或者,我们可以使用带有子查询的 LET 语句来有效地按 startCity 对遍历进行分组:

1
2
3
4
5
6
7
FOR startCity IN germanCity
FILTER GEO_DISTANCE(@bonn, startCity.geometry) < @radius
LET oneCity = (
FOR v, e, p IN 1..1 OUTBOUND startCity
GRAPH 'routeplanner' RETURN v._key
)
RETURN {startCity: startCity._key, connectedCities: oneCity}

最后,我们再次清理:

1
arangosh> examples.dropGraph("routeplanner");

8.11 移除顶点

删除具有关联边的顶点当前不通过 AQL 处理,而图形管理接口和图形模块的 REST API 提供了顶点删除功能。 但是,如本示例所示,基于 know_graph,可以创建此用例的查询。

示例图形

从图中删除顶点 eve 时,我们还希望删除边 eve -> alice 和 eve -> bob。 必须知道所涉及的图及其唯一的边集合。 在这种情况下,它是图知道_图,边集合知道。

此查询将删除 eve 及其相邻边:

1
2
3
4
LET edgeKeys = (FOR v, e IN 1..1 ANY 'persons/eve' GRAPH 'knows_graph' RETURN e._key)
LET r = (FOR key IN edgeKeys REMOVE key IN knows)
REMOVE 'eve' IN persons
[]

此查询执行了几个操作:

  • 使用深度为1的图遍历得到eve相邻边的_key
  • 从知道集合中删除所有这些边
  • 从人集合中删除顶点前夕

以下查询显示了实现相同结果的不同设计:

1
2
3
4
LET edgeKeys = (FOR v, e IN 1..1 ANY 'persons/eve' GRAPH 'knows_graph'
REMOVE e._key IN knows)
REMOVE 'eve' IN persons
[]

注意:必须调整查询以匹配具有多个顶点/边集合的图形。

例如,城市图包含几个顶点集合 - GermanCity 和 frenchCity 和几个边集合 - french / German / International Highway。

示例图2

要删除城市柏林,必须考虑法国/德国/国际高速公路的所有边缘集合。 REMOVE 操作必须应用于所有带有 OPTIONS { ignoreErrors: true } 的边集合。 每当应在集合中删除不存在的键时,不使用此选项将停止查询。https://www.arangodb.com/docs/3.10/aql/examples-remove-vertex.html#aql-GRAPHTRAV-removeVertex3)

1
2
3
4
5
6
7
LET edgeKeys = (FOR v, e IN 1..1 ANY 'germanCity/Berlin' GRAPH 'routeplanner' RETURN e._key)
LET r = (FOR key IN edgeKeys REMOVE key IN internationalHighway
OPTIONS { ignoreErrors: true } REMOVE key IN germanHighway
OPTIONS { ignoreErrors: true } REMOVE key IN frenchHighway
OPTIONS { ignoreErrors: true })
REMOVE 'Berlin' IN germanCity
[]

8.12 多路径搜索

最短路径算法只能确定一条最短路径。 例如,如果这是完整图(基于 mps_graph):

Example Graph

那么从 A 到 C 的最短路径查询可能会返回路径 A -> B -> C 或 A -> D -> C,但不确定是哪一个(此处不考虑边权重)。

但是,您可以使用有效的最短路径算法来确定最短路径长度:

1
2
3
4
5
6
7
8
RETURN LENGTH(
FOR v IN OUTBOUND
SHORTEST_PATH "mps_verts/A" TO "mps_verts/C" mps_edges
RETURN v
)
[
3
]

示例图的结果为 3(包括起始顶点)。 现在,减去 1 以获得边缘计数/遍历深度。 您可以运行模式匹配遍历来查找具有此长度的所有路径(或通过增加最小和最大深度来找到更长的路径)。 起点再次是 A,并且对 v(或 p.vertices[-1])的文档 ID 的过滤器确保我们只检索在点 C 处结束的路径。

以下查询返回所有长度为 2 的部分,起始顶点 A 和目标顶点 C:

1
2
3
4
5
6
7
FOR v, e, p IN 2..2 OUTBOUND "mps_verts/A" mps_edges
FILTER v._id == "mps_verts/C"
RETURN CONCAT_SEPARATOR(" -> ", p.vertices[*]._key)
[
"A -> B -> C",
"A -> D -> C"
]

3..3 的遍历深度将返回 A -> E -> F -> C 和 2..3 所有三个路径。

请注意,计算最短路径长度并根据最短路径长度(减去 1)进行模式匹配需要两个单独的查询,因为 min 和 max depth 不能是表达式(它们必须提前知道,所以 要么是数字文字要么是绑定参数)。

8.13 没有集合的查询

AQL 查询通常访问一个或多个集合以从文档中读取或修改它们。 然而,查询不一定必须涉及集合。 下面是一些例子。

以下是返回字符串值的查询。 结果字符串包含在一个数组中,因为每个有效查询的结果都是一个数组:

1
2
3
4
RETURN "this will be returned"
[
"this will be returned"
]

您可以使用变量、调用函数并返回任意结构的结果:

1
2
LET array = [1, 2, 3, 4]
RETURN { array, sum: SUM(array) }

也可以使用诸如 FOR 循环之类的语言结构。 下面的查询创建两个数组的笛卡尔积并连接值对:https://www.arangodb.com/docs/3.10/aql/examples-queries-no-collections.html#aql-aqlWithoutCollections-3)

1
2
3
4
5
6
7
FOR year IN [ 2011, 2012, 2013 ]
FOR quarter IN [ 1, 2, 3, 4 ]
RETURN {
year,
quarter,
formatted: CONCAT(quarter, " / ", year)
}

8.14 在 AQL 中比较两个文档

没有内置的 AQL 函数来比较两个文档的属性,但是很容易构建一个查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// input document 1
LET doc1 = {
"foo": "bar",
"a": 1,
"b": 2
}

// input document 2
LET doc2 = {
"foo": "baz",
"a": 2,
"c": 3
}

// collect attributes present in doc1, but missing in doc2
LET missing = (
FOR key IN ATTRIBUTES(doc1)
FILTER ! HAS(doc2, key)
RETURN {
[ key ]: doc1[key]
}
)

// collect attributes present in both docs, but that have different values
LET changed = (
FOR key IN ATTRIBUTES(doc1)
FILTER HAS(doc2, key) && doc1[key] != doc2[key]
RETURN {
[ key ] : {
old: doc1[key],
new: doc2[key]
}
}
)

// collect attributes present in doc2, but missing in doc1
LET added = (
FOR key IN ATTRIBUTES(doc2)
FILTER ! HAS(doc1, key)
RETURN {
[ key ]: doc2[key]
}
)

// return final result
RETURN {
"missing": missing,
"changed": changed,
"added": added
}

查询可能看起来有点冗长,但其中大部分是由于格式问题。 可以在下面找到更简洁的版本。

上面的查询将返回一个具有三个属性的文档:

  • missing:包含仅在第一个文档中存在的所有属性(即在第二个文档中缺失)

  • changed:包含两个文档中存在的具有不同值的所有属性

  • added:包含仅存在于第二个文档中的所有属性(即在第一个文档中缺失)

对于两个示例文档,它将返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
{
"missing" : [
{
"b" : 2
}
],
"changed" : [
{
"foo" : {
"old" : "bar",
"new" : "baz"
}
},
{
"a" : {
"old" : 1,
"new" : 2
}
}
],
"added" : [
{
"c" : 3
}
]
}
]

您可以调整查询以生成不同的输出格式。

以下是可以从 JavaScript 轻松调用的相同查询的版本。 它将两个文档作为绑定参数传递并调用 db._query。 查询现在是单行的(可读性较差但更易于复制和粘贴):

1
2
3
4
5
6
7
8
bindVariables = {
doc1 : { "foo" : "bar", "a" : 1, "b" : 2 },
doc2 : { "foo" : "baz", "a" : 2, "c" : 3 }
};

query = "LET doc1 = @doc1, doc2 = @doc2, missing = (FOR key IN ATTRIBUTES(doc1) FILTER ! HAS(doc2, key) RETURN { [ key ]: doc1[key] }), changed = (FOR key IN ATTRIBUTES(doc1) FILTER HAS(doc2, key) && doc1[key] != doc2[key] RETURN { [ key ] : { old: doc1[key], new: doc2[key] } }), added = (FOR key IN ATTRIBUTES(doc2) FILTER ! HAS(doc1, key) RETURN { [ key ] : doc2[key] }) RETURN { missing : missing, changed : changed, added : added }";

result = db._query(query, bindVariables).toArray();

8.15 有条件地插入和修改文档

接收数据时的一个常见要求是确保集合中存在某些文档。通常,在运行命令时,不清楚目标文档是否已存在于集合中,或者是否需要先插入。

无条件插入操作在这里不起作用,因为如果目标文档已经存在,它们可能会出错。这将触发“唯一约束冲突”错误。无条件更新或替换操作也将失败,因为它们要求目标文档已经存在。如果不是这样,操作将遇到“未找到文档”错误。

因此,需要运行的是条件插入/更新/替换,也称为upserts或repserts。此类操作的行为是:

  • 根据一些标准检查文档是否存在
  • 如果不存在,则创建文档
  • 如果存在,请更新或用新版本替换它

ArangoDB在AQL中提供了以下选项来实现这一点

  • UPSERT AQL操作
  • 用overwriteMode插入AQL操作
  • 插入操作不使用AQL,而是使用Document REST API

8.15.1 UPSERTAQL 操作

让我们从UPSERT AQL操作开始,它非常通用和灵活。

UPSERT AQL操作的目的是确保在操作完成后存在特定的文档。

UPSERT将根据用户可配置的属性/值查找特定的文档,如果该文档还不存在,则创建该文档。如果UPSERT找到这样的文档,它可以对其进行部分调整(UPDATE)或完全替换(replace)。

总结一下,AQL UPSERT的语法是,取决于你是否想要更新替换文档:

1
2
3
4
UPSERT <search-expression>
INSERT <insert-expression>
UPDATE <update-expression>
IN <collection> OPTIONS <options>

1
2
3
4
UPSERT <search-expression>
INSERT <insert-expression>
REPLACE <replace-expression>
IN <collection> OPTIONS <options>

OPTIONS部分是可选的。

一个UPSERT操作示例如下:

1
2
3
4
UPSERT { page: "index.html" }
INSERT { page: "index.html", status: "inserted" }
UPDATE { status: "updated" }
IN pages

这将在pages集合中查找page属性值为index.html的文档。如果找不到这样的文档,将执行INSERT部分,这将创建一个具有页面和状态属性的文档。如果操作发现页面为index.html的现有文档,它将执行UPDATE部分,该部分将文档的status属性设置为updated

8.15.1.1 跟踪修改日期

UPSERT AQL操作有时与日期/时间记录结合使用。例如,下面的查询记录文档首次创建的时间和最后一次更新的时间:

1
2
3
4
UPSERT { page: "index.html" } 
INSERT { page: "index.html", created: DATE_NOW() }
UPDATE { updated: DATE_NOW() }
IN pages

8.15.1.2 OLD变量

UPSERT AQL操作还提供一个名为OLD的伪变量,用于引用UPDATE/REPLACE部分中的现有文档及其值。下面是一个每当执行UPSERT操作时就增加一个文档上的计数器的例子:

1
2
3
4
UPSERT { page: "index.html" }
INSERT { page: "index.html", hits: 1 }
UPDATE { hits: OLD.value + 1 }
IN pages

8.15.2 UPSERT警告

UPSERT是一种非常灵活的操作,因此为了有效地使用它,应该记住一些事情

8.15.2.1 重复搜索属性

首先,UPSERT操作的INSERT部分应该包含搜索表达式中使用的所有属性。考虑以下反例:

1
2
3
4
UPSERT { page: "index.html" }
INSERT { status: "inserted" } /* page attribute missing here! */
UPDATE { status: "updated" }
IN pages

忘记在INSERT部分中指定搜索属性会带来一个问题:第一次执行UPSERT时,如果没有找到页面为index.html的文档,它将按照预期分支到INSERT部分。但是,INSERT部分将创建一个只有状态属性集的文档。这里缺少page属性,所以当INSERT完成时,仍然没有pageindex.html的文档。这意味着无论何时执行这个UPSERT语句,它都将分支到INSERT部分,而永远不会到达UPDATE部分。这可能是无意的。

通过向INSERT部分添加搜索属性,可以很容易地避免这个问题:

1
2
3
4
UPSERT { page: "index.html" }
INSERT { page: "index.html", status: "inserted" }
UPDATE { status: "updated" }
IN pages

注意,没有必要在UPDATE部分中重复搜索属性,因为UPDATE是部分更新。它将只设置UPDATE部分中指定的属性,而不设置所有其他现有属性。但是,有必要在REPLACE部分中重复搜索属性,因为REPLACE将使用REPLACE部分中指定的内容完全覆盖现有文档。

这意味着当使用REPLACE操作时,查询应该看起来像:

1
2
3
4
UPSERT { page: "index.html" }
INSERT { page: "index.html", status: "inserted" }
REPLACE { page: "index.html", status: "updated" }
IN pages

8.15.2.2 对搜索属性使用索引

UPSERT灵活性的一个缺点是,它可以用于任意集合属性,即使这些属性没有被索引。

当UPSERT查找现有文档时,如果存在索引,它将使用索引,但如果不存在索引,它也将继续。在后一种情况下,UPSERT将执行一个完整的收集扫描,这对于大型收集来说可能代价高昂。因此建议在UPSERT中使用的搜索属性上创建一个索引。

8.15.2.3 UPSERT是非原子的

整个UPSERT操作不会为单个文档以原子方式执行。它基本上是一个文档查找,然后是一个文档插入、更新或替换操作。

这意味着,如果多个UPSERT操作同时运行相同的搜索值,它们可能都确定目标文档不存在——然后都决定创建这样的文档。这将意味着一个人最终会得到目标文档的多个实例。

为了避免这种并发性问题,可以在搜索属性上创建唯一的索引。这样的索引将防止并发的UPSERT操作创建相同的文档。相反,只有一个并发upsert将成功,而其他的将失败,并出现“违反惟一约束”的错误。在这种情况下,客户端应用程序可以重试操作(然后应该进入UPDATE/REPLACE分支),或者忽略错误(如果目标只是确保目标文档存在)。

因此,在搜索属性上使用唯一索引将提高查找性能并避免重复。

8.15.2.4 使用分片键进行查找

在集群设置中,搜索表达式应该包含分片键,因为这允许只将查找发送到单个分片。这将比必须在集合的所有碎片上执行查找更有效。

在搜索表达式中使用shard键的另一个好处是,只有在包含shard键的情况下才支持唯一索引。

8.15.3 INSERTAQL 操作overwriteMode

虽然UPSERT AQL操作非常强大和灵活,但它通常不是大容量摄入的理想选择。

一个比UPSERT AQL操作更有效的替代方法是带有overwriteMode属性集的INSERT AQL操作。该操作不是UPSERT的临时替换,而是在执行操作时已知文档键(_key属性)的情况下的快速替代,并且不需要引用任何旧值。

INSERT AQL操作的一般语法是:

1
2
INSERT <insert-expression>
IN <collection> OPTIONS <options>

因为我们将在这里处理overwriteMode选项,所以我们主要关注设置了该选项的INSERT操作,例如:

1
2
INSERT { _key: "index.html", status: "created" }
IN pages OPTIONS { overwriteMode: "ignore" }

不管选择的overwriteMode是什么,如果集合中不存在具有指定_key的文档,INSERT操作将插入文档。在这方面,它表现为常规的INSERT操作。

但是,如果指定_key的文档已经存在于集合中,INSERT行为将如下所示,这取决于所选的overwriteMode:

  • conflict(默认值):如果存在具有指定约束的文档,则返回”唯一约束冲突”_key
  • ignore:如果存在具有指定文档的文档,则不执行任何操作。特别是不要报告”唯一约束冲突”错误。_key
  • update:如果存在具有指定属性的文档,则(部分)使用指定的属性更新文档。_key
  • replace:如果存在具有指定属性的文档,请将该文档完全替换为指定的属性。_key

如果没有指定overwriteMode, INSERT操作的行为就好像overwriteMode被设置为冲突。

使用INSERT并将overwriteMode设置为忽略、更新或替换的好处是,INSERT操作将非常快,尤其是与UPSERT操作相比。此外,INSERT将使用_key属性进行查找,该属性总是被索引。因此,它将始终使用主索引,而从不进行完整的收集扫描。它也不需要设置额外的索引,因为所有集合都会自动显示主索引。

在使用INSERT AQL操作时也有一些注意事项:

  • 只有在插入时属性的值已知时,才能使用它们。这意味着客户端应用程序必须能够以确定性方式提供文档密钥。_key
  • 可用于属性的值具有一些字符和长度限制,但字母数字键工作良好。_key
  • 在群集设置中,基础集合必须由 分片。但是,这是默认的分片键。_key
  • 进入 or 模式时,无法访问现有文档的数据以进行任意计算。update``replace

请注意,即使INSERT AQL操作不能引用现有文档来计算更新/替换的值,如果文档已经存在,它仍然可以返回文档的以前版本。这可以通过在INSERT操作中附加一个RETURN OLD来实现,例如。

1
2
3
INSERT { _key: "index.html", status: "created" }
IN pages OPTIONS { overwriteMode: "replace" }
RETURN OLD

还可以使用return new返回文档的新版本(如果以前没有文档存在,则返回插入的文档;如果已经存在文档,则返回更新/替换的版本):

1
2
3
INSERT { _key: "index.html", status: "created" }
IN pages OPTIONS { overwriteMode: "replace" }
RETURN NEW

8.15.4 不使用 AQL 的插入操作

可以在AQL之外使用overwriteMode执行插入操作。POST /_api/document/{collection}端点是用于插入操作的专用REST API,它可以处理一个文档,也可以同时处理多个文档。

从概念上讲,这个API的行为类似于INSERT AQL操作,但是可以用一批文档一次调用它。这是最有效的解决方案,如果可能,应该首选它。

大多数ArangoDB驱动程序还提供了一次插入多个文档的方法,这将在内部调用相同的REST API。

REST API提供了returnOld和returnNew选项,以使其返回以前版本的文档或插入/更新/替换的文档,与insert AQL操作的方式相同。

8.15.5 总结

UPSERT AQL操作是在ArangoDB中有条件地插入或更新/替换文档的最灵活的方法,但它也是效率最低的变体。

带有overwriteMode设置的INSERT AQL操作将优于UPSERT,但它只能在某些情况下使用。

使用专用的REST API进行文档插入将更加有效,因此是批量文档插入的首选选项。

九 使用用户函数扩展 AQL

AQL自带一组内置函数,但它并不是一种功能齐全的编程语言。

为了添加缺失的功能或简化查询,用户可以向所选数据库中的AQL添加自己的功能。这些函数是用JavaScript编写的,并通过API进行部署;看到注册功能。

为了避免与现有或将来的内置函数名冲突,所有用户定义的函数(UDF)都必须放在单独的名称空间中。然后可以通过引用全限定函数名调用UDF,其中也包括命名空间;看到约定。

9.1 技术细节

9.1.1 已知限制

udf会对查询的性能和ArangoDB中的资源使用产生严重影响。特别是在集群设置中,不应该对大量数据使用它们,因为这些数据需要通过网络在db - server和Coordinators之间来回发送,这可能会增加很多延迟。这可以通过在调用udf之前使用非常有选择性的过滤器来缓解。

因为优化器不知道函数的性质,所以优化器不能为udf使用索引。因此,决不应该依靠UDF作为FILTER语句的主要条件来减少查询结果集。相反,在它前面放置另一个FILTER语句。您应该确保FILTER语句在将查询结果传递给UDF之前能够有效地减少查询结果。

经验法则是,UDF越接近最终的RETURN语句(或者甚至在其中),效果就越好。

当在集群中使用udf时,udf总是在协调器上执行。

由于UDF是用JavaScript编写的,每个执行UDF的查询都将获得一个V8上下文来执行其中的UDF。V8上下文可以在后续查询中重用,但是当udf调用的查询并行运行时,它们都需要一个专用的V8上下文。

因此,在集群中使用udf可能会导致使用V8上下文和服务器线程方面的更高资源分配。如果耗尽了这些资源,查询可能会中止,并出现集群后端不可用错误。

要克服上述限制,您可能需要增加可用V8上下文的数量(以增加内存使用为代价)和可用服务器线程的数量。

此外,不支持从udf内部修改全局JavaScript变量,也不支持从AQL用户函数内部读取或更改任何集合的数据或运行查询。

9.1.2 部署详细信息

在内部,udf存储在所选数据库的一个名为_aqlfunctions的系统集合中。当AQL语句引用这样的UDF时,它将从该集合中加载。udf将只用于该特定数据库中的查询。

由于协调器没有自己的本地集合,所以_aqlfunctions集合将跨集群分片。因此(像往常一样),它必须通过协调器访问—您不能直接与碎片对话。一旦它在_aqlfunctions集合中,它就可以在所有coordinator上使用,无需额外的工作。

请记住,系统集合在默认情况下不会出现在使用arangodump创建的转储文件中。要在转储中包含AQL UDF,需要使用选项——include-system-collections true启动转储

9.2 约定

9.2.1 命名

与ArangoDB一起提供的内置AQL函数驻留在名称空间_aql中,如果找到不合格的函数名,这也是查找的默认名称空间。

要引用用户定义的AQL函数,函数名必须完全限定,以包含用户定义的命名空间。符号::用作名称空间分隔符。用户可以根据需要创建多层次的功能组:

1
2
MYGROUP::MYFUNC()
MYFUNCTIONS::MATH::RANDOM()

注意:将用户函数添加到_aql命名空间是不允许的,并且会失败。

与AQL中的所有函数名一样,用户函数名不区分大小写。

9.2.2 变量和副作用

用户函数可以接受任意数量的输入参数,并且应该通过return语句提供一个结果。用户函数应该保持纯粹的功能性,从而避免副作用和状态以及状态修改。

不支持修改全局变量,也不支持从AQL用户函数中读取或更改任何集合的数据或运行查询。

用户函数代码是后期绑定的,因此可能不依赖于声明时存在的任何变量。如果用户功能代码需要访问任何外部数据,它必须自己设置数据。

所有AQL用户函数特定的变量都应该使用var关键字来引入,以避免意外地从外部范围访问已经定义的变量。在执行函数时,不为自己的变量使用var关键字可能会导致副作用。

下面是一个可以修改外部作用域变量i和name的例子,使函数没有副作用:

1
2
3
4
5
6
7
8
9
function (values) {
for (i = 0; i < values.length; ++i) {
name = values[i];
if (name === "foo") {
return i;
}
}
return null;
}

上面的函数可以通过使用var或let关键字来避免副作用,因此这些变量成为函数局部变量:

1
2
3
4
5
6
7
8
9
function (values) {
for (var i = 0; i < values.length; ++i) {
var name = values[i];
if (name === "foo") {
return i;
}
}
return null;
}

9.2.3 输入参数

为了返回结果,用户函数应该使用返回指令而不是修改其输入参数。

AQL用户函数允许修改其输入参数为空值、布尔值、数字或字符串值。在用户函数中修改这些输入参数类型应该没有副作用。但是,如果参数是数组或对象,并且是通过引用传递的,那么用户函数不应该修改输入参数,因为这样可能会在用户函数本身之外修改变量和状态。

9.2.4 返回值

用户函数必须只返回原始类型(即空值、布尔值、数字值、字符串值)或由这些类型组成的聚合类型(数组或对象)。从用户函数返回任何其他JavaScript对象类型(Function, Date, RegExp等)可能会导致未定义的行为,应该避免。

9.2.5 强制执行严格模式

默认情况下,任何用户函数代码将在草率模式下执行,而不是严格或强模式。为了使用户函数在strict模式下运行,在用户函数中显式地使用”use strict”,例如:

1
2
3
4
5
6
7
8
9
10
11
function (values) {
"use strict"

for (var i = 0; i < values.length; ++i) {
var name = values[i];
if (name === "foo") {
return i;
}
}
return null;
}

任何违反严格模式的行为都将触发运行时错误。

9.3 注册和注销用户函数

用户定义函数(udf)可以通过aqlfunctions对象注册到所选数据库中,如下所示:

1
var aqlfunctions = require("@arangodb/aql/functions");

要注册一个函数,必须指定完全限定函数名加上函数代码。这在阿兰格里很容易做到。HTTP接口还提供用户功能管理。

在集群设置中,确保连接到一个协调器来管理udf。

不应该直接访问_aqlfunctions集合(或任何其他系统集合)中的文档,而只能通过专用接口访问。否则,您可能会看到缓存问题或意外破坏某些东西。这些接口将确保文档的正确格式,并使UDF缓存无效。

9.3.1 注册 AQL 用户函数

对于测试,直接在shell中键入函数代码可能就足够了。要管理更复杂的代码,您可以在您选择的代码编辑器中编写它,并将其保存为文件。例如:

1
2
3
4
5
6
7
8
9
10
11
/* path/to/file.js */
'use strict';

function greeting(name) {
if (name === undefined) {
name = "World";
}
return `Hello ${name}!`;
}

module.exports = greeting;

然后在 shell 中需要它,以便注册用户定义的函数:

1
2
arangosh> var func = require("path/to/file.js");
arangosh> aqlfunctions.register("HUMAN::GREETING", func, true);

请注意,返回值为false表示函数HUMAN::GREETING是新创建的,而不是它注册失败。如果之前存在该名称的函数并刚刚更新,则返回True。

1
aqlfunctions.register(name, code, isDeterministic)

注册一个AQL用户函数,用一个完全限定的函数名标识。code中的函数代码必须指定为JavaScript函数或JavaScript函数的字符串表示形式。如果code中的函数代码以字符串形式传递,则要求该字符串的计算结果为JavaScript函数定义。

如果以名称标识的函数已经存在,则更新前面的函数定义。还请确保功能代码不违反AQL功能公约。

isDeterministic属性可用于指定函数结果是否完全确定(即仅依赖于输入,并且对于具有相同输入值的重复调用是相同的)。它目前不使用,但可能用于以后的优化。

注册函数存储在所选数据库的系统集合_aqlfunctions中。

当更新/替换同名的现有AQL函数时,该函数返回true,否则返回false。当检测到语法无效的函数代码时,它将抛出异常。

例子

1
2
3
4
require("@arangodb/aql/functions").register("MYFUNCTIONS::TEMPERATURE::CELSIUSTOFAHRENHEIT",
function (celsius) {
return celsius * 1.8 + 32;
});

函数代码默认不会在严格模式或强模式下执行。为了使一个用户函数在strict模式下运行,可以显式地使用strict,例如:

1
2
3
4
5
require("@arangodb/aql/functions").register("MYFUNCTIONS::TEMPERATURE::CELSIUSTOFAHRENHEIT",
function (celsius) {
"use strict";
return celsius * 1.8 + 32;
});

你可以通过访问JavaScript代码中this的name属性来访问AQL函数注册的名称:

1
2
3
4
5
6
7
8
9
require("@arangodb/aql/functions").register("MYFUNCTIONS::TEMPERATURE::CELSIUSTOFAHRENHEIT",
function (celsius) {
"use strict";
if (typeof celsius === "undefined") {
const error = require("@arangodb").errors.ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH;
AQL_WARNING(error.code, require("util").format(error.message, this.name, 1, 1));
}
return celsius * 1.8 + 32;
});

AQL_WARNING()对用户定义函数的代码自动可用。错误代码和消息通过@arangodb模块检索。参数号不匹配消息有占位符,我们可以使用format()替换:

1
invalid number of arguments for function '%s()', expected number of arguments: minimum: %d, maximum: %d

在上面的例子中,%s被this.name (AQL函数名)替换,同时%d占位符被1(预期参数的数量)替换。如果你不带参数调用这个函数,你会看到:

1
2
3
4
5
6
7
8
arangosh> db._query("RETURN MYFUNCTIONS::TEMPERATURE::CELSIUSTOFAHRENHEIT()")
[object ArangoQueryCursor, count: 1, hasMore: false, warning: 1541 - invalid
number of arguments for function 'MYFUNCTIONS::TEMPERATURE::CELSIUSTOFAHRENHEIT()',
expected number of arguments: minimum: 1, maximum: 1]

[
null
]

9.3.2 删除现有的 AQL 用户函数

1
aqlfunctions.unregister(name)

取消注册一个现有的AQL用户函数,该函数由完全限定函数名标识。

试图注销不存在的函数将导致异常。

例子

1
require("@arangodb/aql/functions").unregister("MYFUNCTIONS::TEMPERATURE::CELSIUSTOFAHRENHEIT");

9.3.3 注销组

删除一组AQL用户函数aqlfunctions.unregisterGroup(prefix)

注销一组AQL用户功能,该功能由一个公共功能组前缀标识。

这将返回未注册函数的数量。

例子

1
2
3
require("@arangodb/aql/functions").unregisterGroup("MYFUNCTIONS::TEMPERATURE");

require("@arangodb/aql/functions").unregisterGroup("MYFUNCTIONS");

9.3.4 列出所有 AQL 用户函数

1
aqlfunctions.toArray()

返回所有以前注册的AQL用户函数,以及它们的完全限定名称和函数代码。

通过指定一个组前缀,可以选择将结果限制到指定的一组函数:

1
aqlfunctions.toArray(prefix)

例子

列出所有可用的用户函数:

1
require("@arangodb/aql/functions").toArray();

列出MYFUNCTIONS命名空间中的所有可用用户函数:

1
require("@arangodb/aql/functions").toArray("MYFUNCTIONS");

要列出MYFUNCTIONS::TEMPERATURE命名空间中的所有可用用户函数,请执行以下操作:

1
require("@arangodb/aql/functions").toArray("MYFUNCTIONS::TEMPERATURE");

十 AQL 执行和性能

本章描述与查询执行和查询性能相关的AQL特性。

  • 执行统计信息:已执行的查询还返回关于其执行的统计信息。

  • 查询解析:客户端可以使用ArangoDB检查给定的AQL查询在语法上是否有效。

  • 查询执行计划:如果不清楚某个查询将如何执行,客户端可以从AQL查询优化器中检索查询的执行计划,而无需实际执行查询;这叫做解释。

  • AQL查询优化器:AQL查询在执行前通过优化器发送。优化器的任务是为查询创建一个初始执行计划,寻找优化机会并应用它们。

  • 查询分析:有时查询不执行,但不清楚计划的哪个部分应对此负责。查询分析器可以向您显示查询执行的每个阶段的执行统计信息。

  • AQL查询结果缓存:可选的查询结果缓存,避免重复计算相同的查询结果。

10.1 查询统计信息

已执行的查询将始终返回执行统计信息。可以通过调用游标上的getExtra()来检索执行统计信息。统计信息在返回值的stats属性中返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arangosh> db._query(`
........> FOR i IN 1..@count INSERT
........> { _key: CONCAT('anothertest', TO_STRING(i)) }
........> INTO mycollection`,
........> {count: 100},
........> {},
........> {fullCount: true}
........> ).getExtra();
arangosh> db._query({
........> "query": `FOR i IN 200..@count INSERT
........> { _key: CONCAT('anothertest', TO_STRING(i)) }
........> INTO mycollection`,
........> "bindVars": {count: 300},
........> "options": { fullCount: true}
........> }).getExtra();

统计属性含义如下:

  • writesExecuted:成功执行数据修改操作的总数。这相当于通过INSERT、UPDATE、REPLACE或REMOVE操作创建、更新或删除的文档数量。
  • writesIgnored:不成功的数据修改操作的总数,但由于查询选项ignoreErrors而被忽略。
  • scannedFull:在扫描没有索引的集合时遍历的文档总数。子查询扫描的文档将包含在结果中,但内置或用户定义的AQL函数不会触发任何操作。
  • scannedIndex:使用索引扫描集合时遍历的文档总数。子查询扫描的文档将包含在结果中,但内置或用户定义的AQL函数不会触发任何操作。
  • filtered:在FilterNode中执行过滤条件后被删除的文档总数。注意,IndexRangeNodes还可以通过只从集合中选择所需的索引范围来筛选文档,而筛选的值仅指示FilterNodes进行了多少筛选。
  • fullCount:如果查询的最终顶级LIMIT语句不存在,那么匹配搜索条件的文档总数。只有在启动查询时设置了fullCount选项,才会返回该属性;如果查询在顶层包含了LIMIT操作,则只会包含有意义的值。
  • peakMemoryUsage:查询运行时的最大内存使用量。在集群中,内存核算是按每个分片进行的,所报告的内存使用量是各个分片的峰值内存使用量。请注意,为了保持轻量级,每个查询的内存使用在一个相对较高的水平上被跟踪,不包括任何内存分配器开销,也不包括任何用于临时结果计算的内存(例如,在AQL表达式和函数调用中分配/释放的内存)。属性peakMemoryUsage在3.4.3版本中可用。
  • 节点:(可选)当选项配置文件设置为至少2时执行查询,则此值包含每个查询执行节点的运行时统计信息。该字段包含节点id(以id表示)、对该节点调用的次数以及该节点项返回的项数(项是在此阶段返回的临时结果)。您可以将此统计数据与在extra中返回的计划关联起来。对于人类可读的输出,你可以执行db。_profileQuery(<query>, <bind-vars>)。</bind-vars></query>

10.2 分析查询

客户端可以使用ArangoDB检查给定的AQL查询在语法上是否有效。aranggodb为此提供了一个HTTP REST API。

也可以使用ArangoStatement的解析方法从ArangoShell解析查询。如果查询语法无效,解析方法将抛出异常。否则,它将返回关于查询的一些信息。

返回值是一个对象,该对象具有collections属性中列出的查询中使用的集合名称,以及bindVars属性中列出的所有绑定参数。此外,查询的内部表示,查询的抽象语法树,将在结果的AST属性中返回。请注意,抽象语法树将在没有对其进行任何优化的情况下返回。

1
2
3
arangosh> var stmt = db._createStatement(
........> "FOR doc IN @@collection FILTER doc.foo == @bar RETURN doc");
arangosh> stmt.parse();

10.3 解释查询

如果不清楚给定查询将如何执行,客户机可以从AQL查询优化器检索查询的执行计划,而无需实际执行查询。从优化器获取查询执行计划称为解释。

如果给定查询在语法上无效,解释将抛出错误。否则,它将返回执行计划和一些关于可以对查询应用哪些优化的信息。查询将不会执行。

解释查询可以通过调用HTTP REST API或通过aranggosh来实现。一个查询也可以从ArangoShell使用ArangoDatabase的解释方法或通过ArangoStatement的解释方法进行详细解释。

10.3.1 检查查询计划

下一章中显示的ArangoStatement的解释方法创建了非常详细的输出。要获得人类可读的查询计划输出,可以在arangod中对数据库对象使用explain方法。你可以这样使用它:(我们在这里禁用语法高亮)

1
arangosh> db._explain("LET s = SLEEP(0.25) LET t = SLEEP(0.5) RETURN 1", {}, {colors: false});

该计划包含查询期间使用的所有执行节点。这些节点表示查询中的不同阶段。每个阶段从直接上一级(它的依赖项)获得输入。该计划将向您显示每个查询阶段(在Est下)的项目(结果)的估计数量。每个查询阶段大致等同于原始查询中的一行,您可以在Comment下看到。

10.3.2 分析查询

有时,当您有一个复杂的查询时,可能不清楚在执行过程中花费了什么时间,即使对于中间的ArangoDB用户也是如此。

通过分析查询,它可以在启用特殊工具代码的情况下执行。它提供了所有常见的信息,比如解释查询时的信息,但是您还可以获得查询概要文件、运行时统计信息和每个节点的统计信息。

要在shell中以交互方式使用它,你可以在ArangoDatabase对象上使用_profileQuery()方法或使用web界面。

有关更多信息,请参阅分析查询。

1
arangosh> db._profileQuery("LET s = SLEEP(0.25) LET t = SLEEP(0.5) RETURN 1", {}, {colors: false});

10.3.3 详细执行计划

默认情况下,查询优化器将返回它认为的最佳计划。最佳计划将在结果的plan属性中返回。如果在调用explain时将选项allPlans设置为true,则所有计划将在plans属性中返回。结果对象还将包含一个属性警告,这是在优化或执行计划创建期间发生的警告数组。

结果中的每个计划都是一个具有以下属性的对象:

  • 节点:计划的执行节点数组。参见执行节点列表
  • estimatedCost:计划的总估计成本。如果有多个计划,优化器将选择总成本最低的计划。
  • 集合:查询中使用的集合数组
  • 规则:优化器应用的规则数组。请参阅优化器规则列表
  • 变量:查询中使用的变量数组(注意:它可能包含优化器创建的内部变量)

下面是检索简单查询的执行计划的示例:

1
2
3
arangosh> var stmt = db._createStatement(
........> "FOR user IN _users RETURN user");
arangosh> stmt.explain();

由于explain的输出非常详细,因此建议使用一些脚本来减少输出的详细信息

1
2
3
4
5
arangosh> var formatPlan = function (plan) {
........> return { estimatedCost: plan.estimatedCost,
........> nodes: plan.nodes.map(function(node) {
........> return node.type; }) }; };
arangosh> formatPlan(stmt.explain().plan);

如果一个查询包含bind参数,它们必须在explain被调用之前被添加到语句中:

1
2
3
4
5
arangosh> var stmt = db._createStatement(
........> `FOR doc IN @@collection FILTER doc.user == @user RETURN doc`
........> );
arangosh> stmt.bind({ "@collection" : "_users", "user" : "root" });
arangosh> stmt.explain();

在某些情况下,AQL优化器会为单个查询创建多个计划。默认情况下,只保留估计总成本最低的计划,而放弃其他计划。要检索优化器生成的所有计划,可以调用explain,并将选项allPlans设置为true。

在以下示例中,优化器创建了两个计划:

1
2
3
4
arangosh> var stmt = db._createStatement(
........> "FOR user IN _users FILTER user.user == 'root' RETURN user");
arangosh> stmt.explain({ allPlans: true }).plans.length;
1

若要查看计划的略微更紧凑的版本,可以应用以下转换:

1
2
arangosh> stmt.explain({ allPlans: true }).plans.map(
........> function(plan) { return formatPlan(plan); });

Explain也将接受以下附加选项:

  • maxPlans:限制AQL查询优化器创建的计划的最大数量

  • rules:可以将一个包含或排除优化器规则的数组放入该属性中,告诉优化器包含或排除特定的规则。如果要禁用一个规则,请在其名称前加上“-”前缀;如果要启用一个规则,请在名称前加上“+”前缀。还有一个伪规则all,它将匹配所有优化器规则。

    下面的示例禁用所有优化器规则,但删除冗余计算

1
2
arangosh> stmt.explain({ optimizer: {
........> rules: [ "-all", "+remove-redundant-calculations" ] } });

执行计划的内容应该是机器可读的。要获取人类可读的查询执行计划版本,可以使用以下命令:

1
2
arangosh> var query = "FOR doc IN mycollection FILTER doc.value > 42 RETURN doc";
arangosh> require("@arangodb/aql/explainer").explain(query, {colors:false});

上面的命令直接在ArangoShell中打印查询的执行计划,重点是最重要的信息。

10.3.4 收集有关查询的调试信息

如果解释没有提供合适的见解来解释为什么查询没有按照预期执行,那么可能会报告给ArangoDB支持。为了使这尽可能简单,在ArangoShell中有一个内置的命令,用于打包查询、它的绑定参数和在其他地方执行查询所需的所有数据。

该命令会将所有数据存储在一个文件名可配置的文件中:

1
2
arangosh> var query = "FOR doc IN mycollection FILTER doc.value > 42 RETURN doc";
arangosh> require("@arangodb/aql/explainer").debugDump("/tmp/query-debug-info", query);

用户可以将生成的文件发送到aranggodb支持,方便复制和调试。

如果查询包含bind参数,则需要在查询字符串中指定它们:

1
2
3
arangosh> var query = "FOR doc IN @@collection FILTER doc.value > @value RETURN doc";
arangosh> var bind = { value: 42, "@collection": "mycollection" };
arangosh> require("@arangodb/aql/explainer").debugDump("/tmp/query-debug-info", query, bind);

还可以包括来自底层集合的示例文档,以便使复制更加容易。示例文档可以按原样发送,也可以以匿名形式发送。示例文档的数量可以在examples options属性中指定,通常应该保持较低的数量。anonymize选项将用“XXX”替换示例中字符串属性的内容。然而,它不会取代任何其他类型的数据(例如数字值)或属性名称。示例中的属性名称将始终保留,因为它们可能被索引并在查询中使用:

1
2
3
4
arangosh> var query = "FOR doc IN @@collection FILTER doc.value > @value RETURN doc";
arangosh> var bind = { value: 42, "@collection": "mycollection" };
arangosh> var options = { examples: 10, anonymize: true };
arangosh> require("@arangodb/aql/explainer").debugDump("/tmp/query-debug-info", query, bind, options);

10.4 分析和手动优化 AQL 查询

为了让您更深入地了解您的查询,ArangoDB允许执行您的查询与特殊的仪器代码启用。然后,这将打印一个包含详细执行统计信息的查询计划。

要在shell中以交互式方式使用它,可以在aranggosh中使用db._profileQuery(..)。或者,在web界面的Query选项卡中有一个Profile按钮。

然后打印的执行计划包含三个额外的列:

  • 调用:执行此查询阶段的次数
  • 项目: 此阶段的临时结果行数
  • 运行时间:在此阶段花费的总时间

在执行计划下方,还有用于整体运行时统计信息和查询配置文件的其他部分。

10.4.1 示例:简单 AQL 查询

假设我们得到了一个名为collection的集合,并通过for插入10000个文档for (let i=0; i < 10000;i++) db.acollection.insert({value:i}),然后对值< 10的简单查询过滤将返回10个结果:

1
2
3
4
5
arangosh> db._profileQuery(`
........> FOR doc IN acollection
........> FILTER doc.value < 10
........> RETURN doc`, {}, {colors: false}
........> );

AQL查询本质上是在一个管道中执行的,该管道将不同的功能执行块链接在一起。每个块从它上面的父块获取输入行,进行一些处理,然后输出一定数量的输出行。

如果没有对查询执行的详细了解,就不可能知道每个管道块必须处理多少结果以及这需要多长时间。通过使用查询分析器(db._profileQuery()或通过web界面中的Profile按钮)执行查询,您可以准确地检查每个阶段需要做多少工作。

如果没有任何索引,这个查询应该执行以下操作:

  • 通过EnumerateCollectionNode执行一个完整的集合扫描,并输出文档中包含文档的行。
  • 计算布尔表达式LET #1 = doc。通过CalculationNode的所有输入的值< 10
  • 通过FilterNode过滤掉#1为false的所有输入行
  • 通过ResultNode将剩余行的doc变量放入结果集中

EnumerateCollectionNode处理并返回所有10k行(文档),CalculationNode也是如此。因为AQL执行引擎也使用1000的内部批处理大小,这些块也被每个调用100次。然而,FilterNode和ReturnNode只返回10行,并且只需要调用一次,因为结果大小适合单个批处理。

让我们在value上添加一个持久索引来加速查询:

1
db.acollection.ensureIndex({type:"persistent", fields:["value"]});
1
2
3
4
5
arangosh> db._profileQuery(`
........> FOR doc IN acollection
........> FILTER doc.value < 10
........> RETURN doc`, {}, {colors: false}
........> );

这导致将集合扫描和筛选块替换为IndexNode。AQL查询的执行管道变得更短了。而且,每个管道块所处理的行数仅为10,因为我们不再需要查看所有文档。

10.4.2 示例:带有子查询的 AQL

让我们考虑一个包含子查询的查询:

1
2
3
4
5
6
arangosh> db._profileQuery(`
........> LET list = (FOR doc in acollection FILTER doc.value > 90 RETURN doc)
........> FOR a IN list
........> FILTER a.value < 91
........> RETURN a`, {}, {colors: false, optimizer:{rules:["-all"]}}
........> );

结果查询配置文件包含一个SubqueryNode,它将其所有子节点的运行时组合在一起。

实际上,我们作弊了。如果子查询没有被停用,优化器就会完全删除它(规则:[“-all”])。优化后的版本在“优化计划”阶段会花费更长的时间,但应该会执行得更好,并产生很多结果。

10.4.3 示例:具有聚合的 AQL

让我们尝试一个更高级的查询,使用COLLECT语句。假设我们有一个用户集合,每个文档都有一个城市、一个用户名和一个年龄属性。

下面的查询将我们所有年龄组按桶(0-9,10-19,20-29,…)

1
2
3
4
5
6
7
8
9
10
11
arangosh> db._profileQuery(`
........> FOR u IN myusers
........> COLLECT ageGroup = FLOOR(u.age / 10) * 10
........> AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age), len = LENGTH(u)
........> RETURN {
........> ageGroup,
........> minAge,
........> maxAge,
........> len
........> }`, {}, {colors: false}
........> );

如果没有任何索引,则此查询应必须执行以下操作:

  1. 通过EnumerateCollectionNode执行一个完整的集合扫描,并输出文档中包含文档的行。
  2. 计算表达式LET #1 = FLOOR(u。年龄/ 10)* 10通过一个CalculationNode的所有输入
  3. 通过CollectNode执行聚合
  4. 通过SortNode对结果聚合行进行排序
  5. 通过另一个CalculationNode构建一个结果值
  6. 通过ResultNode将结果变量放入结果集

与上面的示例一样,您可以看到在CalculationNode阶段之后,最初的20行中只剩下少数行。

10.4.4 典型的 AQL 性能错误

使用新的查询探查器,您应该能够发现我们经常看到的典型性能错误:使用新的查询分析器,你应该能够发现我们经常看到的典型的性能错误:

  • 不使用索引来加速使用通用筛选器表达式的查询
  • 在filter语句中不使用shard键,当它是已知的(只有集群问题)
  • 使用子查询计算中间结果,但只使用少数结果

不好的例子:

1
2
3
4
5
6
7
8
LET vertices = (
FOR v IN 1..2 ANY @startVertex GRAPH 'my_graph'
// <-- add a LIMIT 1 here
RETURN v
)
FOR doc IN collection
FILTER doc.value == vertices[0].value
RETURN doc

在子查询中添加LIMIT 1应该会带来更好的性能,因为可以在第一个结果之后停止遍历,而不是计算所有路径。

另一个错误是从错误的一端开始图遍历(如果两端都是已知的)。

假设我们有两个顶点集合,用户和产品以及一个购买的边集合。图模型看起来是这样的:(users) <--[purchased]--> (products),也就是说,每个用户都与购买的零或更多产品的边缘相连。

如果我们想知道所有购买了playstation产品以及legwarmer产品的用户,我们可以使用这个查询:

1
2
3
4
5
FOR prod IN products
FILTER prod.type == 'legwarmer'
FOR v,e,p IN 2..2 OUTBOUND prod purchased
FILTER v._key == 'playstation' // <-- last vertex of the path
RETURN p.vertices[1] // <-- the user

该查询首先查找所有legwarmer产品,然后对每个产品执行遍历。但我们也可以通过从已知的playstation产品开始逆遍历。这样我们只需要一次遍历就可以得到相同的结果:

1
2
3
FOR v,e,p IN 2..2 OUTBOUND 'product/playstation' purchased
FILTER v.type == 'legwarmer' // <-- last vertex of the path
RETURN p.vertices[1] // <-- the user

10.5 AQL 查询结果缓存

AQL提供一个可选的查询结果缓存。

查询结果缓存的目的是避免对相同的查询结果进行重复计算。如果数据读取查询重复很多,而写查询很少,那么它就很有用。

查询结果缓存是透明的,所以当底层集合数据被修改时,用户不需要手动使其中的结果无效。

10.5.1 模式

缓存可以在以下模式下运行:

  • off:已禁用缓存。查询结果将不存储
  • on:缓存将存储所有AQL查询的结果,除非它们的缓存属性标志被设置为false
  • demand:缓存将存储缓存属性设置为true的AQL查询的结果,但将忽略其他所有查询

该模式可以在服务器启动时设置,稍后在运行时更改。

10.5.2 查询资格

如果两个查询具有完全相同的查询字符串和相同的绑定变量,则查询结果缓存将认为它们是相同的。任何空格、大小写等方面的偏差都将被视为差异。查询字符串将被散列并用作缓存查找键。如果查询使用绑定参数,这些参数也将被散列并作为缓存查找键的一部分使用。

这意味着即使两个查询的查询字符串是相同的,如果它们具有不同的绑定参数值,查询结果缓存将把它们视为不同的查询。将成为查询缓存键的一部分的其他组件是count、fullCount和优化器属性。

如果缓存是打开的,那么缓存将在执行一开始就检查是否为这个特定的查询准备好了结果。如果是这样,查询结果将直接从缓存中提供,这通常是非常有效的。如果在缓存中找不到查询,它将像往常一样执行。

如果查询符合缓存条件,并且打开了缓存,则查询结果将存储在查询结果缓存中,以便用于相同查询的后续执行。

只有满足以下所有条件,一个查询才有资格缓存:

  • 执行查询的服务器是单个服务器(即不属于集群)
  • 查询字符串至少8个字符
  • 该查询是一个只读查询,不会修改任何集合中的数据
  • 在执行查询时没有产生警告
  • 查询是确定性的,并且只使用结果被标记为可缓存的确定性函数
  • 查询结果的大小不会超过为单个缓存结果或累积结果配置的最大缓存大小
  • 查询不是使用流游标执行的

使用非确定性函数会导致查询不可缓存。这是有意避免缓存函数结果,而函数结果应该在每次调用查询时计算(例如RAND()或DATE_NOW())。

查询结果缓存认为所有用户定义的AQL函数都是非确定性的,因为它没有深入了解这些函数。

10.5.3 缓存失效

如果查询修改了在缓存查询结果的计算过程中使用的集合的数据,缓存的结果将自动完全或部分失效。这是为了防止用户从查询结果缓存中获得陈旧的结果。

这也意味着,如果缓存是打开的,那么每个数据修改操作(例如插入、更新、删除、截断操作以及AQL数据修改查询)都会有一个额外的缓存失效检查。

如果以下查询的结果出现在查询结果缓存中,那么修改集合用户或集合组织中的数据将从缓存中删除已经计算的结果:

1
2
3
4
FOR user IN users
FOR organization IN organizations
FILTER user.organization == organization._key
RETURN { user: user, organization: organization }

修改命名的两个集合以外的其他集合中的数据不会导致该查询结果从缓存中删除。。

10.5.4 性能注意事项

查询结果缓存被组织为一个哈希表,因此查找缓存中是否存在查询结果相对比较快。尽管如此,查询中使用的查询字符串和绑定参数仍需要进行散列。如果关闭缓存或将查询标记为不可缓存,则这是一个很小的开销。

此外,在缓存中存储查询结果并从缓存中获取结果需要通过R/W锁进行锁定。虽然许多线程可以并行地从缓存中读取数据,但在任何给定的时间内只能有一个修改线程。当查询结果存储在缓存中或数据修改后缓存失效时,需要修改查询缓存内容。缓存失效所需的时间与需要失效的缓存项的数量成比例。

在某些工作负载中,启用查询结果缓存可能会导致性能下降。在只修改数据或修改数据多于读取数据的工作负载中,不建议打开查询结果缓存。如果查询非常多样化且不经常重复,那么打开缓存也不会带来任何好处。在只读或读为主的工作负载中,如果相同的查询重复很多次,那么缓存将是有益的。

一般来说,查询结果缓存将为具有小结果集且需要很长时间计算的查询提供最大的改进。如果查询结果非常大,并且大部分查询时间都花在将结果从缓存复制到客户机上,那么缓存将不会提供太多好处。

105.5 全局配置

查询结果缓存可以在服务器启动时使用配置参数——query.cache-mode配置。这将根据上面的描述设置缓存模式。

服务器启动后,可以在运行时修改缓存模式:

1
require("@arangodb/aql/cache").properties({ mode: "on" });

在服务器启动时,可以使用以下配置参数配置每个数据库的缓存中缓存结果的最大数量:

  • --query.cache-entries:每个数据库的查询结果缓存中的最大结果数
  • --query.cache-entries-max-size:每个数据库的查询结果缓存中结果的最大累积大小
  • --query.cache-entry-max-size:查询结果缓存中单个结果条目的最大大小
  • --query.cache-include-system-collections:是否在查询结果缓存中包含系统集合查询

这些参数可用于对每个数据库的查询缓存中查询结果的数量和大小设置上限,从而限制缓存的内存消耗。

这些值也可以在运行时调整如下:

1
2
3
4
5
6
require("@arangodb/aql/cache").properties({ 
maxResults: 200,
maxResultsSize: 8 * 1024 * 1024,
maxEntrySize: 1024 * 1024,
includeSystem: false
});

上述方法将每个数据库查询结果缓存中的缓存结果数量限制为200个结果,每个数据库的累积查询结果大小为8 MB。每个查询缓存条目的最大大小被限制为8MB。涉及系统集合的查询被排除在缓存之外。

10.5.6 每个查询配置

当一个查询被发送到服务器执行,并且缓存被设置为on或demand时,查询执行器将查看查询的缓存属性。如果查询缓存模式是开启的,那么不设置该属性或将其设置为false以外的任何值都将使查询执行器查询查询缓存。如果查询缓存模式是需要的,那么将缓存属性设置为true将使执行程序在查询缓存中查找查询。当查询缓存模式关闭时,执行器将不会在缓存中查找查询。

缓存属性可以通过db._createStatement()函数如下设置:

1
2
3
4
5
6
var stmt = db._createStatement({ 
query: "FOR doc IN users LIMIT 5 RETURN doc",
cache: true /* cache attribute set here */
});

stmt.execute();

使用db._query()函数时,缓存属性可以如下设置:

1
2
3
4
db._query({ 
query: "FOR doc IN users LIMIT 5 RETURN doc",
cache: true /* cache attribute set here */
});

缓存属性也可以通过HTTP REST API POST /_api/游标设置。

返回的每个查询结果将包含一个缓存的属性。如果结果是从查询缓存中检索的,则设置为true,否则设置为false。客户端可以使用此属性检查是否从缓存中提供了特定的查询。

10.5.7 查询结果缓存检查

查询结果缓存的内容可以在运行时使用缓存的toArray()函数检查:

1
require("@arangodb/aql/cache").toArray();

这将返回存储在当前数据库的查询结果缓存中的所有查询结果的列表。

当前数据库的查询结果缓存可以在运行时使用缓存的clear函数清除:

1
require("@arangodb/aql/cache").clear();

10.5.8 限制

从查询结果缓存返回的查询结果可能包含初始的、未缓存的查询执行产生的执行统计信息。这意味着对于缓存的查询结果,额外的。stats属性可能包含过时的数据,特别是在executionTime和profile属性值方面。

十一 常见错误

11.1 查询字符串中的尾随分号

许多SQL数据库允许一次发送多个查询。在这种情况下,多个查询使用分号分隔。通常还支持执行在末尾有分号的单个查询。

AQL不支持这个,并且在AQL查询字符串的末尾使用分号是一个解析错误。

11.2 字符串串联

在AQL中,字符串必须使用CONCAT()函数连接。不支持使用+操作符将它们连接在一起。特别是作为JavaScript程序员,很容易陷入这个陷阱:

1
2
3
RETURN "foo" + "bar" // [ 0 ]
RETURN "foo" + 123 // [ 123 ]
RETURN "123" + 200 // [ 323 ]

算术加号操作符期望将数字作为操作数,并尝试将它们隐式转换为不同类型的数字。“foo”和“bar”被转换为0,然后相加在一起(仍然是0)。如果添加一个实际的数字,将返回该数字(添加零不会改变结果)。如果字符串是数字的有效字符串表示形式,则将其强制转换为数字。因此,将“123”和“200”相加,结果两个数字相加等于323。

要连接元素(对于非字符串值使用隐式强制转换为字符串),执行:

1
2
3
RETURN CONCAT("foo", "bar") // [ "foobar" ]
RETURN CONCAT("foo", 123) // [ "foo123" ]
RETURN CONCAT("123", 200) // [ "123200" ]

11.3 参数注入漏洞

参数注入意味着在查询中插入可能会改变其含义的潜在恶意内容。这是一个安全问题,可能允许攻击者对数据库数据执行任意查询。

如果应用程序可靠地将用户提供的输入插入到查询字符串中,但没有完全或不正确地过滤这些输入,就会发生这种情况。当应用程序天真地构建查询,而不使用数据库软件通常提供的安全机制或查询机制时,也经常会发生这种情况。

AQL本身不容易受到参数注入的影响,但查询可以在客户端、应用服务器或Foxx服务中构造。使用简单的字符串连接组装查询字符串看起来很简单,但可能不安全。如果可能的话,你应该使用绑定参数,如果驱动程序提供了查询构建功能(参见arangojs AQL Helpers),或者至少小心翼翼地对用户输入进行净化。

11.3.1 参数注入示例

下面是一个使用JavaScript API的简单查询,它提供了一些动态输入值,假装它来自一个web表单。这可能是Foxx服务的情况。路由愉快地获取输入值,并将其放入一个查询:

1
2
3
4
5
// evil!
var what = req.params("searchValue"); // user input value from web form
// ...
var query = "FOR doc IN collection FILTER doc.value == " + what + " RETURN doc";
db._query(query, params).toArray();

上面的方法对于数字输入值可能很有效。

攻击者可以对这个查询做什么?这里有一些用于searchValue参数的建议:

  • 用于返回集合中的所有文档:
    1 || true
  • 用于删除所有文档:
    1 || true REMOVE doc IN collection //
  • 用于插入新文档:
    1 || true INSERT { foo: "bar" } IN collection //

很明显,这是非常不安全的,应该避免。通常看到的解决这一问题的模式是,在将可能不安全的输入值放入查询字符串之前,尝试引用和转义它们。这在某些情况下可能是有效的,但很容易忽略某些东西或让它微妙地出错:

1
2
3
4
5
// We are sanitizing now, but it is still evil!
var value = req.params("searchValue").replace(/'/g, '');
// ...
var query = "FOR doc IN collection FILTER doc.value == '" + value + "' RETURN doc";
db._query(query, params).toArray();

上面的示例使用单引号来封装可能不安全的用户输入,并且预先替换了输入值中的所有单引号。这不仅可能改变用户的输入(导致细微的错误,比如“为什么我搜索O’Brien没有返回任何结果?”),而且仍然是不安全的。如果用户输入在末尾包含一个反斜杠(例如foo bar),该反斜杠将转义结束的单引号,允许用户输入再次跳出字符串栅栏。

如果用户输入在多个位置插入到查询中,情况会变得更糟。让我们假设我们有一个具有两个动态值的查询:

1
2
query = "FOR doc IN collection FILTER doc.value == '" + value +
"' && doc.type == '" + type + "' RETURN doc";

如果攻击者插入\for参数 value和’ || true REMOVE doc IN集合// for参数类型’,那么有效的查询将变成:

1
2
3
FOR doc IN collection
FILTER doc.value == '\' && doc.type == ' || true
REMOVE doc IN collection //' RETURN doc

这是非常不可取的。反斜杠转义结束的单引号,转向文档。将condition类型输入到字符串中,该字符串将与doc.value进行比较。此外,还注入了一个始终为真或条件以及一个删除操作,从而完全改变了查询的目的。原始的返回操作被注释掉,查询将截断集合,而不是返回几个文档。

11.3.2 避免参数注入

与其通过字符串连接天真地将查询字符串片段与用户输入混合在一起,不如使用绑定参数或查询生成器。两者都有助于避免注入问题,因为它们允许将实际的查询操作(如FOR、INSERT、REMOVE)与(用户输入)值分离开来。

下面的重点是绑定参数。这并不是说不应该使用查询构建器。为了简单起见,这里只是略去了它们。

11.3.3 什么是绑定参数

AQL查询中的绑定参数是作为实际值占位符的特殊标记。这里有一个例子:

1
2
3
FOR doc IN collection
FILTER doc.value == @what
RETURN doc

在上面的查询中,@what是一个bind参数。为了执行该查询,必须指定绑定参数@what的值。否则查询执行将失败,错误为1551(声明的绑定参数没有指定值)。如果指定了@what的值,就可以执行查询。但是,查询字符串和绑定参数值(即@what bind参数的内容)将分别处理。bind参数中的内容将始终被视为一个值,它不能离开它的沙箱并更改查询的语义含义。

11.3.4 如何使用绑定参数

要执行带有bind参数的查询,查询字符串(包含bind参数)和bind参数值是单独指定的(注意,当bind参数值被分配时,需要省略前缀@):

1
2
3
4
5
6
7
8
// query string with bind parameter
var query = "FOR doc IN collection FILTER doc.value == @what RETURN doc";

// actual value for bind parameter
var params = { what: 42 };

// run query, specifying query string and bind parameter separately
db._query(query, params).toArray();

如果恶意用户将@what设置为1 ||为真,这不会造成任何伤害。AQL将@what的内容视为单个字符串标记,而查询的含义将保持不变。实际执行的查询将是:

1
2
3
FOR doc IN collection
FILTER doc.value == "1 || true"
RETURN doc

由于绑定参数,也不可能将选择(即只读)查询转换为数据删除查询。

11.3.5 使用 JavaScript 变量作为绑定参数

还有一个模板字符串生成器函数aql,可以使用JavaScript变量和表达式安全地(和方便地)构建aql查询。它可以被调用如下:

1
2
3
4
5
6
7
const aql = require('@arangodb').aql; // not needed in arangosh

var value = "some input value";
var query = aql`FOR doc IN collection
FILTER doc.value == ${value}
RETURN doc`;
var result = db._query(query).toArray();

注意,ES6模板字符串用于填充查询变量。该字符串是使用与ArangoDB绑定的aql生成器函数组装的。模板字符串可以通过${…}包含对JavaScript变量或表达式的引用。在上面的示例中,查询引用了一个名为value的变量。aql函数生成一个具有两个单独属性的对象:包含绑定参数引用的查询字符串和实际绑定参数值。

绑定参数名是由aql函数自动生成的:

1
2
3
4
5
6
7
8
9
var value = "some input value";
aql`FOR doc IN collection FILTER doc.value == ${value} RETURN doc`;

{
"query" : "FOR doc IN collection FILTER doc.value == @value0 RETURN doc",
"bindVars" : {
"value0" : "some input value"
}
}

11.3.6 在动态查询中使用绑定参数

绑定参数很有帮助,因此使用它们来处理动态值是有意义的。您甚至可以将它们用于本身高度动态的查询,例如有条件的FILTER和LIMIT部分。以下是如何做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Note: this example has a slight issue... hang on reading
var query = "FOR doc IN collection";
var params = { };

if (useFilter) {
query += " FILTER doc.value == @what";
params.what = req.params("searchValue");
}

if (useLimit) {
// not quite right, see below
query += " LIMIT @offset, @count";
params.offset = req.params("offset");
params.count = req.params("count");
}

query += " RETURN doc";
db._query(query, params).toArray();

注意,在这个示例中,我们回到了字符串连接,但是没有查询易受任意修改影响的问题。

11.3.7 输入值验证和卫生

尽管如此,您还是应该保持偏执的态度,尽可能早地检测无效的输入值,至少在使用这些值执行查询之前。这是因为一些输入参数可能会对查询的运行时行为产生负面影响,或者在修改时可能导致查询抛出运行时错误,而不是返回有效结果。这不是一个攻击者应该得到的。

LIMIT就是一个很好的例子:如果与单个参数一起使用,则参数应该是数字。当LIMIT给出一个字符串值时,执行查询将失败。您可能希望尽早检测到这一点,而不要返回HTTP 500(因为这将表明攻击者已经成功破坏了您的应用程序)。

LIMIT的另一个问题是,高LIMIT值可能比低LIMIT值更昂贵,您可能希望禁止使用超过某个阈值的LIMIT值。

以下是在这种情况下你可以做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var query = "FOR doc IN collection LIMIT @count RETURN doc";

// some default value for limit
var params = { count: 100 };

if (useLimit) {
var count = req.params("count");

// abort if value does not look like an integer
if (! preg_match(/^d+$/, count)) {
throw "invalid count value!";
}

// actually turn it into an integer
params.count = parseInt(count, 10); // turn into numeric value
}

if (params.count < 1 || params.count > 1000) {
// value is outside of accepted thresholds
throw "invalid count value!";
}

db._query(query, params).toArray();

这有点复杂,但这是您可能愿意为一点额外的安全付出的代价。实际上,你可能想要使用一个框架来进行验证(比如与ArangoDB捆绑在一起的joi),而不是在各处写自己的支票。

11.3.8 绑定参数类型

AQL 中有两种类型的绑定参数:

  • 为值绑定参数:
    它们在AQL查询中使用单个@作为前缀,并且在赋值时不使用前缀。这些绑定参数可以包含任何有效的JSON值。

    例子:@what @searchValue

  • 为集合绑定参数:
    在AQL查询中,它们以@@作为前缀,并用集合的名称代替。当bind参数值被赋值时,参数本身必须使用单个@前缀指定。这种类型的绑定参数只允许使用字符串值。

    例子:@@collection @@edgeColl

后一种类型的绑定参数可能不经常使用,它不应该与用户输入一起使用。否则,用户可以自由决定您的AQL查询将在哪个集合上操作(这可能是一个有效的用例,但通常是非常不希望的)。

11.4 意外的长时间运行查询

慢速查询可能有各种原因,对于计算复杂度高的查询或者涉及大量数据的查询是合理的。使用Explain特性检查执行计划,并验证是否使用了适当的索引。还要检查错误,比如引用了错误的变量。

文字集合名称,它不是FOR、UPDATE…等构造的一部分。IN等,表示该集合的所有文档的数组,可以在进一步处理之前使整个集合物化。因此应该避免这种情况。

检查/所有收集文件/的执行计划,并验证其意图。如果你执行这样的查询,你也应该看到一个警告:

集合’ coll ‘用作表达式操作数

例如,而不是:

1
RETURN coll[* LIMIT 1]

…与执行计划…

1
2
3
4
5
Execution plan:
Id NodeType Est. Comment
1 SingletonNode 1 * ROOT
2 CalculationNode 1 - LET #2 = coll /* all collection documents */[* LIMIT 0, 1] /* v8 expression */
3 ReturnNode 1 - RETURN #2

…您可以使用以下等效查询:

1
2
3
FOR doc IN coll
LIMIT 1
RETURN doc

…使用(更好的)执行计划:

1
2
3
4
5
6
Execution plan:
Id NodeType Est. Comment
1 SingletonNode 1 * ROOT
2 EnumerateCollectionNode 44 - FOR doc IN Characters /* full collection scan */
3 LimitNode 1 - LIMIT 0, 1
4 ReturnNode 1 - RETURN doc

同样,请确保您没有意外地将任何变量名称与集合名称混淆:

1
2
3
4
LET names = ["John", "Mary", ...]
// supposed to refer to variable "names", not collection "Names"
FOR name IN Names
...

您可以设置启动选项——query。allow-collections-in-expression设为false,禁止在AQL表达式的任意位置使用集合名称,以防止此类错误。参见ArangoDB服务器查询选项

十二 ArangoDB安装与配置

12.1 基于docker的安装

官方链接参见 https://hub.docker.com/_/arangodb

ArangoDB是一个多模型数据库,支持key/value,graph,document文档,并且提供了统一的数据库查询语言,并且支持ACID。更多介绍可以参见官方文档,并且还有与MongoDB、Neo4j对比。

本文使用Docker的方式安装Arangodb,目前最新版本是3.8.4 使用下面命令pull镜像

1
docker pull arangodb/arangodb:3.8.4

下载完镜像后,运行下面命令启动一个容器

1
docker run -e ARANGO_RANDOM_ROOT_PASSWORD=1 -p 8529:8529 -d arangodb/arangodb:3.4.2

ArangoDB 安全验证模式有三种:

1、ARANGO_RANDOM_ROOT_PASSWORD=1 随机生成一个管理员密码,使用docker logs 命令查看日志文件可以得到密码。

2、ARANGO_NO_AUTH=1 不进行验证密码,一般用于测试模式。

3、ARANGO_ROOT_PASSWORD=somepassword 指定一个自己的密码。

启动容器,可以使用下面命令:

1
docker run -e ARANGO_ROOT_PASSWORD=somepassword -p 8529:8529 -d arangodb/arangodb:3.4.2

这样启动的容器,会把数据也保存到容器内部,可以通过docker -v 参数映射到宿主机上的目录。

1
2
3
4
5
mkdir /tmp/arangodb

docker run -e ARANGO_ROOT_PASSWORD=somepassword -p 8529:8529 -d \
-v /tmp/arangodb:/var/lib/arangodb3 \
arangodb/arangodb:3.4.2

通过上面命令启动容器后数据将保存宿主机的/tmp/arangodb目录下。

然后可以通过http://{ip}:8529 来访问控制台。 用户是root,登录密码就是启动时指定的密码。

12.2 基于linux的安装

在CentOS 7执行以下命令:

3.启动 ArangoDB

  • 启动命令:systemctl start arangodb3
  • 停止命令:systemctl stop arangodb3
  • 重启命令:systemctl restart arangodb3
  • 查看状态:systemctl status arangodb3
  • 在终端修改密码:arango-secure-installatio

4.ArangoDB 的配置

  • 修改arangod.conf: vim /var/lib/arangodb3
  • 修改访问路径:endpoint = tcp://0.0.0.0:8529
  • 重启数据库生效

此时会有主机访问不了虚拟机的情况。因为centos主机默认的防火墙策略为不开放任何端口

  • # 查询端口是否开放
  • firewall-cmd —query-port=8529/tcp
  • # 开放8529端口
  • firewall-cmd —permanent —add-port=8529/tcp
  • # 移除端口
  • firewall-cmd —permanent —remove-port=8529/tcp
  • #重启防火墙(修改配置后要重启防火墙)
  • firewall-cmd —reload