接上篇,本篇专门整理 MongoDB 查询方法。
4 基本查询
- 你可以在数据库中使用 find 或者 findOne 函数来执行专门的查询;
- 你可以查询范围、集合、不等式,也可以使用 $-条件 执行更多的操作;
- 查询结果是一个数据库游标(cursor),当需要的时候返回你需要的文档。
- 你可以在 cursor 上执行许多元操作(metaoperations),包括 skipping 一定数量的结果,limiting 返回结果的数量,和 sorting 结果。
4.1 find
# find 的第一个参数指定了查询准则
db.users.find() # 匹配集合中的所有文档
db.users.find({"age": 27})
# 多条件查询可以通过增加更多的 key/value 对,可以解释为 *condition1* AND *condition2* AND ... AND *conditionN*
db.users.find({"username": "joe", "age": 27)
指定返回的键
find(或者 findOne)的第二个参数指定返回的键,虽然 "_id" 键没有被指定,但是默认返回。也可以指定需要排除的 key/value 对。
# "_id" 键默认返回
> db.users.find({}, {"username": 1, "email": 1})
# 结果
{
"_id" : ObjectId("4ba0f0dfd22aa494fd523620"),
"username" : "joe",
"email" : "[email protected]"
}
# 阻止 "_id" 键返回
> db.users.find({}, {"username": 1, "email": 1, "_id": 0})
# 结果
{
"username" : "joe",
"email" : "[email protected]"
}
限制(Limitations)
数据库所关心的查询文档的值必须是常量,也就是不能引用文档中其他键的值。例如,要想保持库存,有原库存 "in_stock" 和 "num_sold" 两个键,先通过比较两者来查询:
> db.stock.find({"in_stock" : "this.num_sold"}) // doesn't work
4.2 查询准则(Criteria)
查询条件
比较操作:"\(\$\)lt","\(\$\)lte","\(\$\)gt","\(\$\)gte","\(\$\)ne"
> db.users.find({"age" : {"$gte" : 18, "$lte" : 30}})
> db.users.find({"username" : {"$ne" : "joe"}})
OR 查询
MongoDB 中有两种 OR 查询。"\(\$\)in" 用作对一个 key 查询多个值;"\(\$\)or" 用作查询多个 keys 给定的值。
# 单个键,一种类型的值
> db.raffle.find({"ticket_no" : {"$in" : [725, 542, 390]}})
# 单个键,多种类型的值
> db.users.find({"user_id" : {"$in" : [12345, "joe"]})
# "$nin"
> db.raffle.find({"ticket_no" : {"$nin" : [725, 542, 390]}})
# 多个键
> db.raffle.find({"$or" : [{"ticket_no" : 725}, {"winner" : true}]})
# 多个键,带有条件
> db.raffle.find({"$or" : [{"ticket_no" : {"$in" : [725, 542, 390]}}, {"winner" : true}]})
$not
"\(\$\)not" 是元条件句,可以用在任何条件之上。例如,取模运算符 "\(\$\)mod" 会将查询的值除以第一个给定的值,若余数等于第二个给定值,则返回该结果:
db.users.find({"id_num" : {"$mod" : [5, 1]}})
上面的查询会返回 "id_num" 值为 1、6、11、16 等的用户,但要返回 "id_num" 为 2、3、4、6、7、8 等的用户,就要用 "$not" 了:
> db.users.find({"id_num" : {"$not" : {"$mod" : [5, 1]}}})
"$not" 与正则表达式联合使用的时候极为有用,用来查找那些与特定模式不符的文档。
条件句的规则
比较更新修改器和查询文档,会发现以 $ 开头的键处在不同的位置。条件句是内层文档的键,而修改器是外层文档的键。可以对一个键应用多个条件,但是一个键不能对应多个修改器。
> db.users.find({"age" : {"$lt" : 30, "$gt" : 20}})
# 修改了年龄两次,错误
> db.users.find({"$inc" : {"age" : 1}, "$set" : {age : 40}})
但是,也有一些 元操作 可以用在外层文档:"\(\$\)and","\(\$\)or",和 "\(\$\)nor":
> db.users.find({"$and" : [{"x" : {"$lt" : 1}}, {"x" : 4}]})
看上去这个条件相矛盾,但是如果 x 是一个数组的话:{"x" : [0, 4]} 是符合的。
4.3 特定类型的查询
null
null 表现起来有些奇怪,它不但匹配它自己,而且能匹配 "does not exist",所以查询一个值为 null 的键,会返回缺乏那个键的所有文档。
> db.c.find()
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }
> db.c.find({"y" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
> db.c.find({"z" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }
如果我们想要找到值为 null 的键,我们可以使用 "\(\$\)exists" 检查键是 null,并且存在。如下,不幸的是,没有 "\(\$\)eq"操作,但是带有一个元素的 "\(\$\)in" 和它等价。
> db.c.find({"z" : {"$in" : [null], "$exists" : true}})
正则表达式
- MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式。
- MongoDB 使用 Perl Compatible Regular Expression (PCRE) 库匹配正则表达式;任何在 PCRE 中可以使用的正则表达式语法都可以在 MongoDB 中使用。在使用正则表达式之前可以先在 JavaScript shell 中检查语法,看是否是你想要匹配的。
# 文档结构
{
"post_text": "enjoy the mongodb articles on runoob",
"tags": [
"mongodb",
"runoob"
]
}
# 使用正则表达式查找包含 runoob 字符串的文章
> db.posts.find({post_text:{$regex:"runoob"}})
# 以上操作还可以表示如下
> db.posts.find({post_text:/runoob/})
# 不区分大小写的正则表达式
# 如果为 $options: "$s",表示允许点字符(dot character,即 .)匹配包括换行字符(newline characters)在内的所有字符
> db.posts.find({post_text:{$regex:"runoob", $options:"$i"}})
# 或者
> db.posts.find({post_text: /runoob/i})
查询内嵌文档
有两种方式查询内嵌文档:查询整个文档,或者只针对它的键/值对进行查询。
{
"name" : {
"first" : "Joe",
"last" : "Schmoe"
},
"age" : 45
}
可以使用如下方式进行查:
> db.people.find({"name" : {"first" : "Joe", "last" : "Schmoe"}})
但是,对于子文档的查询必须精确匹配子文档。如果 Joe 决定去添加一个中间的名字,这个查询就将不起作用,这类查询也是 order-sensitive 的,{"last" : "Schmoe", "first" : "Joe"} 将不能匹配。
仅仅查询内嵌文档特定的键通常是一个好主意。这样的话,如果你的数据模式改变,也不会导致所有查询突然失效,因为他们不再是精确匹配。可以通过使用 dot-记号 查询内嵌的键:
> db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})
当文档更加复杂的时候,内嵌文档的匹配有些技巧。例如,假设有博客文章若干,要找到由 Joe 发表的 5 分以上的评论。博客文章的结构如下所示:
> db.blog.find()
{
"content" : "...",
"comments" : [
{
"author" : "joe",
"score" : 3,
"comment" : "nice post"
},
{
"author" : "mary",
"score" : 6,
"comment" : "terrible post"
}
]
}
查询的方式如下:
# 错误,内嵌的文档必须匹配整个文档,这个没有匹配 "comment" 键
> db.blog.find({"comments" : {"author" : "joe", "score" : {"$gte" : 5}}})
# 错误,因为符合 author 条件的评论和符合 score 条件的评论可能不是同一条评论
> db.blog.find({"comments.author" : "joe", "comments.score" : {"$gte" : 5}})
# 正确,"$elemMatch" 将限定条件进行分组,仅当对一个内嵌文档的多个键进行操作时才会用到
> db.blog.find({"comments" : {"$elemMatch" : {"author" : "joe", "score" : {"$gte" : 5}}}})
5 聚合(aggregation)
将数据存储在 MongoDB 中后,我们就可以进行检索,然而,我们可能想在它上面做更多的分析工作。
5.1 聚合框架(The aggregation framework)
聚合框架可以在一个集合中转化(transform)和混合(combine)文档。基本的,你可以通过几个创建模块(filtering, projecting, grouping, sorting, limiting, and skipping)来建立处理一批文档的管道。
例如,如果有一个杂志文章的集合,你可能想找出谁是最多产的作者。假设每一篇文章都作为一个文档存储在 MongoDB 中,你可以通过以下几步来创建一个管道:
# 1. 将每篇文章文档的作者映射出来
{"$project" : {"author" : 1}}
# 2. 通过名字将作者分组,统计文档的数量
{"$group" : {"_id" : "$author", "count" : {"$sum" : 1}}}
# 3. 通过文章数量,降序排列作者
{"$sort" : {"count" : -1}}
# 4. 限制前5个结果
{"$limit" : 5}
# 在 MonoDB 中,将每个操作传递给 aggregate() 函数
> db.articles.aggregate( {"$project" : {"author" : 1}},
{"$group" : {"_id" : "$author", "count" : {"$sum" : 1}}},
{"$sort" : {"count" : -1}},
{"$limit" : 5}
)
# 输出结果,返回一个结果文档数组
{
"result" : [
{
"_id" : "R. L. Stine",
"count" : 430
},
{
"_id" : "Edgar Wallace",
"count" : 175
},
{
"_id" : "Nora Roberts",
"count" : 145
},
{
"_id" : "Erle Stanley Gardner",
"count" : 140
},
{
"_id" : "Agatha Christie",
"count" : 85
}
],
"ok" : 1
}
注:aggregate 框架不会写入到集合,所以所有的结果必须返回客户端。因此,aggregation 返回的数据结果限制在 16MB。
5.2 管道操作(Pipeline Operations)
$match
\(\$\)match 过滤文档,以致于你可以在文档子集上运行聚合操作。通常,尽可能的将 "\(\$\)match" 操作放到管道操作的前面。这样做主要有两个优点:1. 可以快速过滤掉不需要的文档(留下管道操作需要执行的文档),2. 可以在 projections 和 groupings 之前使用 indexes 查询。
$project
映射在管道中操作比在“标准的”查询语言中(find函数的第二个参数)更加强有力。
# 映射,"_id" 总是默认返回,此处指定不返回
> db.articles.aggregate({"$project" : {"author" : 1, "_id" : 0}})
# 重命名被映射的域 "_id"
> db.users.aggregate({"$project" : {"userId" : "$_id", "_id" : 0}})
# 如果 originalFieldname 是索引,则在重命名之后就不再默认为索引了
> db.articles.aggregate({"$project" : {"newFieldname" : "$originalFieldname"}},
{"$sort" : {"newFieldname" : 1}})
"\(\$\)fieldname" 语法被用来在 aggregation framework 中引用 fieldname 的值。比如上面例子中,"\(\$\)_id" 将会被 _id 域的内容取代。当然,如果重命名了,则就不要返回两次了,正如上例所示,当 "_id" 被重命名之后就不再返回。
管道表达式
最简单的 "$project" 表达式是包含、排除和域名重命名。也可以使用其它的表达式。
数学表达式
"\(\$\)add", "\(\$\)subtract", "\(\$\)multiply", "\(\$\)divide", "\(\$\)mod"
# 域 "salary" 和域 "bonus" 相加
> db.employees.aggregate(
{
"$project" : {
"totalPay" : {
"$add" : ["$salary", "$bonus"]
}
}
})
# "$subtract" 表达式,减掉 401k
> db.employees.aggregate(
{
"$project" : {
"totalPay" : {
"$subtract" : [{"$add" : ["$salary", "$bonus"]}, "$401k"]
}
}
})
日期表达式
aggregation 有一个可以提取日期信息的表达式集合: "\(\$\)year", "\(\$\)month", "\(\$\)week","\(\$\)dayOfMonth", "\(\$\)dayOfWeek", "\(\$\)dayOfYear", "\(\$\)hour", "\(\$\)minute" 和 "\(\$\)second"。
# 返回每个员工被雇佣的月
> db.employees.aggregate(
{
"$project" : {
"hiredIn" : {"$month" : "$hireDate"}
}
})
# 计算员工在公司工作的年数
> db.employees.aggregate(
{
"$project" : {
"tenure" : {
"$subtract" : [{"$year" : new Date()}, {"$year" : "$hireDate"}] }
}
}
}
字符串表达式
"$substr" : [expr, startOffset, numToReturn] 返回第一个参数的子串,起始于第 startOffset 个字节,包含 numToReturn 个字节(注意,这个以字节测量,而不是字符,所以多字节编码需要小心)。
"$concat" : [expr1[, expr2, ..., exprN]] 连接每一个给定的字符串。
"$toLower" : expr 以小写的形式返回字符串。
"$toUpper" : expr 以大写的形式返回字符串。
> db.employees.aggregate(
{
"$project" : {
"email" : {
"$concat" : [
{"$substr" : ["$firstName", 0, 1]},
".",
"$lastName",
"@example.com"
]
}
}
})
逻辑表达式
比较表达式
"$cmp" : [expr1, expr2] 比较表达式 expr1 和 expr2,如果相等返回 0,如果 expr1 小于 expr2 返回负值,如果 expr2 小于 expr1 返回正值。
"$strcasecmp" : [string1, string2] 比较 string1 和 string2,必须为罗马字符。
"\(\$\)eq","\(\$\)ne", "\(\$\)gt", "\(\$\)gte", "\(\$\)lt", "\(\$\)lte" : [expr1, expr2]
布尔表达式:
"$and" : [expr1[, expr2, ..., exprN]]
"$or" : [expr1[, expr2, ..., exprN]]
"$not" : expr
控制语句:
"$cond" : [booleanExpr, trueExpr, falseExpr] booleanExpr 为 true 时返回 trueExpr,否则返回 falseExpr。
"$ifNull" : [expr, replacementExpr] 如果 expr 为空返回 replacementExpr,否则返回 expr。
一个例子
> db.students.aggregate(
{
"$project" : {
"grade" : {
"$cond" : [
"$teachersPet",
100, // if
{ // else
"$add" : [
{"$multiply" : [.1, "$attendanceAvg"]},
{"$multiply" : [.3, "$quizzAvg"]},
{"$multiply" : [.6, "$testAvg"]}
]
}
]
}
}
})
$group
算数操作符
# 在多个国家销售数据的集合,计算每个国家的总收入
> db.sales.aggregate(
{
"$group" : {
"_id" : "$country",
"totalRevenue" : {"$sum" : "$revenue"}
}
})
# 返回每个国家的平均收入和销售的数量
> db.sales.aggregate(
{
"$group" : {
"_id" : "$country",
"totalRevenue" : {"$average" : "$revenue"},
"numSales" : {"$sum" : 1}
}
})
极端操作符(Extreme operators)
如果你的数据已经排序好了,使用 \(\$\)first 和 \(\$\)last 比 \(\$\)min 和 \(\$\)max 更有效率。如果数据事先没有排序,则使用 \(\$\)min 和 \(\$\)max 比先排序然后 \(\$\)first 和 \(\$\)last 更有效率。
# 在一次测验中学生分数的集合,找出每个年级的局外点
> db.scores.aggregate(
{
"$group" : {
"_id" : "$grade",
"lowestScore" : {"$min" : "$score"},
"highestScore" : {"$max" : "$score"}
}
}
# 或者
> db.scores.aggregate(
{
"$sort" : {"score" : 1}
},
{
"$group" : {
"_id" : "$grade",
"lowestScore" : {"$first" : "$score"},
"highestScore" : {"$last" : "$score"}
}
})
数组操作符(Array operators)
"$addToSet": expr 保持一个数组,如果 expr 不在数组中,添加它。每一个值在数组中最多出现一次,不一定按照顺序。
"$push": expr 不加区分的将每一个看到的值添加到数组,返回包含所有值得数组。
$unwind(展开)
unwind 将数组的每个域转化为一个单独的文档。例如,如果我们有一个有多条评论的博客,我们可以使用 unwind 将每个评论转化为自己的文档。
> db.blog.findOne()
{
"_id" : ObjectId("50eeffc4c82a5271290530be"),
"author" : "k",
"post" : "Hello, world!",
"comments" : [
{
"author" : "mark",
"date" : ISODate("2013-01-10T17:52:04.148Z"),
"text" : "Nice post"
},
{
"author" : "bill",
"date" : ISODate("2013-01-10T17:52:04.148Z"),
"text" : "I agree"
}
]
}
# unwind
> db.blog.aggregate({"$unwind" : "$comments"})
{
"results" :
{
"_id" : ObjectId("50eeffc4c82a5271290530be"),
"author" : "k",
"post" : "Hello, world!",
"comments" : {
"author" : "mark",
"date" : ISODate("2013-01-10T17:52:04.148Z"),
"text" : "Nice post"
}
},
{
"_id" : ObjectId("50eeffc4c82a5271290530be"),
"author" : "k",
"post" : "Hello, world!",
"comments" : {
"author" : "bill",
"date" : ISODate("2013-01-10T17:52:04.148Z"),
"text" : "I agree"
}
}
"ok" : 1
}
$sort
# 1 是 ascending,-1 是 descending
> db.employees.aggregate(
{
"$project" : {
"compensation" : {
"$add" : ["$salary", "$bonus"]
},
"name" : 1
}
},
{
"$sort" : {"compensation" : -1, "name" : 1}
}
)
$limit
$limit 接收数值 n,然后返回前 n 个结果文档。
$skip
$limit 接收数值 n,然后从结果集中剔除前 n 个文档。对于标准查询,一个大的 skips 效率比较低,因为它必须找出所有匹配被 skipped 的文档,然后剔除它们。
使用管道
在使用 "\(\$\)project"、"\(\$\)group" 或者 "\(\$\)unwind" 操作之前,最好尽可能过滤出更多的文档(和更多的域)。一旦管道不使用直接来自集合中的数据,索引(index)就不再能够帮助取过滤(filter)和排序(sort)。如果可能的话,聚合管道试图为你重新排序这些操作,以便能使用索引。
MongoDB 不允许单一聚合操作使用超过一定比例的系统内存:如果它计算得到一个聚合操作占用超过 20% 的内存,聚合就会出错。允许输出被输送到一个集合中(这样可以最小化所需内存的数量)是为将来作计划。
参考资料
MongoDB: The Definitive Guide, Second Edition
MongoDB 正则表达式
dateToString