MongoDB实战

基础

概念

文档(document)

文档也就是传统数据库中的行,但是没有确定的键模式,而是由不确定的键值对组成。在MongoDB中文档一般组织和表示为JavaScript的对象形式。

{"greeting" : "Hello, world!", "foo" : 3}

  • 文档中的键/值对是有序的,上面的文档和下面的文档是不同的。
    {"foo" : 3, "greeting" : "Hello, world!}"

  • 文档中的值有几种数据类型,甚至是整个嵌入的文档。

  • 文档的键一般为字符串。
  • 文档不能有重复的键。

集合(collection)

集合是一组文档的集合。
集合是无模式的,也就是一个集合里的文档可以是各式各样的,但是一般根据应用和效率的需要,一般还是把相同类型文档划分为不同集合。

组织集合的一种习惯是使用”.”字符分开的按命名空间划分的子集合。例如,一个带有博客功能的应用可能包含两个集合,分别是blog.posts和blog.authors。这样做的目的只是为组织结构更好些,也就是blog这个集合只是逻辑上的存在,和其子集合没有只是层次上的逻辑关系。

数据库

MongoDB中多个文档组成集合,同样多个集合可以组成数据库。一个MongoDB实例可运行多个数据库。

启动MongoDB

  • Download MongoDB
  • Decompress
  • create directory /data/db with sudo
  • 进入MonogoDB解压目录bin目录,./mongod启动mongoDB本地实例,在没有参数情况下使用默认数据目录/data/db和27017端口。
  • 可以通过访问http://localhost:28017来获取数据库的管理信息。

MongoDB shell

在mongoDB实例启动后,可以执行./mongo启动shell,会自动连接MongoDB服务器。这个shell同时是一个完全的javascript解释器,javascript所有语法都可使用。
默认shell会连接mongodb服务器的test数据库,并将这个数据库连接复制给全局变量db。这个变量是通过shell访问MongoDB的主要入口点。

shell中的基本操作

创建

1
2
3
4
5
6
7
8
9
10
11
 post = {"title" : "My Blog Post",
"content" : "Here's my blog post.","date" : new Date()}

db.blog.insert(post)
db.blog.find()
{
"_id" : ObjectId("5037ee4a1084eb3ffeef7228"),
"title" : "My Blog Post",
"content" : "Here's my blog post.",
"date" : ISODate("2012-08-24T21:12:09.982Z")
}

读取

1
2
3
4
5
6
7
db.blog.findOne()
{
"_id" : ObjectId("5037ee4a1084eb3ffeef7228"),
"title" : "My Blog Post",
"content" : "Here's my blog post.",
"date" : ISODate("2012-08-24T21:12:09.982Z")
}

更新

1
2
3
 post.comments = []
[]
db.blog.update({title : "My Blog Post"}, post)

更新所有键title的值为”My Blog Post”的文档为修改过后的post。

删除

1
db.blog.remove({title : "My Blog Post"})

常用shell命令

可以通过help查看常用命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
help
db.help() help on db methods
db.mycoll.help() help on collection methods
sh.help() sharding helpers
rs.help() replica set helpers
help admin administrative help
help connect connecting to a db help
help keys key shortcuts
help misc misc things to know
help mr mapreduce

show dbs show database names
show collections show collections in current database
show users show users in current database
show profile show most recent system.profile entries with time = 1ms
show logs show the accessible logger names
show log [name] prints out the last segment of log in memory, 'global' is default
use <db_name set current database
db.foo.find() list objects in collection foo
db.foo.find( { a : 1 } ) list objects in foo where a == 1
it result of the last line evaluated; use to further iterate
DBQuery.shellBatchSize = x set default number of items to display on shell
exit quit the mongo shell

另外可以使用db.help()查看数据库级别的命令的帮助,集合的相关帮助可以通过db.foo.help()查看。

另外有个了解函数功能的技巧,就是输入函数但不要输入参数和括号,这样就会现实该函数的JavaScript源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
 db.foo.update
function ( query , obj , upsert , multi ){
var parsed = this._parseUpdate(query, obj, upsert, multi);
var query = parsed.query;
var obj = parsed.obj;
var upsert = parsed.upsert;
var multi = parsed.multi;
var wc = parsed.wc;

var result = undefined;
var startTime = (typeof(_verboseShell) === 'undefined' ||
!_verboseShell) ? 0 : new Date().getTime();
...

数据类型

基本类型

类型 meaning example
null {“x” : null}
Boolean {“x” : true}
Integer 分为32位和64位 {“x” : NumberInt(“3”)}、{“x” : NumberLong(“3”)}
Double {“x” : 3.14}
String {“x” : “foobar”}
Object ID 对象ID。用于创建文档的 ID。 {“x” : ObjectId()}
Date {“x” : new Date()}
Regular expression 正则表达式类型。用于存储正则表达式(javascript语法)。 {“x” : /foobar/i}
JavaScript Code {“x” : function(){//}}
Binary Data 二进制数据。用于存储二进制数据。
Min/Max keys 表示可能的最小/最大值
Arrays 用于将数组或列表或多个值存储为一个键 {“x” : [“a”, 2, 3.14]}
内嵌文档 文档内可以包含别的文档 {“x” : {“foo” : “bar”}} }
  • 数组

如果需要经常对某个数组进行查询,可以其对其构建索引,比如需要快速查询x数组中包含3.14的文档。

1
2
3
4
5
6
7
{
"name" : "John Doe",
"address" : {
"street" : "123 Park Street",
"city" : "Anytown",
"state" : "NY"
} }
  • 内嵌文档

同数组一样,mongoDB能够理解内嵌文档的结构,并能够深入其中构建索引、执行查询,或者更新。可以看出mongoDB的数组组织形式不同于关系型数据库,一般在关系型数据库中,上面的数据需要分开存在两个表中,然后通过第三个表或者外键连接。

mongodb这种内嵌文档会使信息表示更加自然,效率也更好,但是会储存更多的重复数据。

  • _id和ObjectId

MongoDB中存储的文档必须有一个"_id"键。这个键的值可以是任意类型的,默认是个ObjectId对象。在一个集合里,每篇文档都有唯一的"_id"值,来确保集合里面的每个文档都能被唯一标识。

ObjectId是"_id"的默认类型。ObjectId被设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。这是MongoDB采用ObjectId,,而不是其他常规的方法(如自增主键),因为在多个服务器上同步自动增加主键更加费时费力。

ObjectId使用12字节的存储空间,每个字节两位16位进制数字,是一个24位的字符串。

ObjectId的生成方式与结构

1
|时间戳|机器ID|PID|计数器|

如果插入文档时没有设置"_id"键,系统会自动帮我们创建一个(通常不是有mongodb服务器完成,而是在客户端有驱动程序完成)。

创建、更新及删除文档

插入并保存文档

db.foo.insert({"bar", "baz"})

如果需要插入多个文档,使用批量插入会提高效率,因为以此批量插入只是一个TCP请求,而单个单个文档插入时每个文档插入都需要提交一个TCP请求。批量插入能传递一个由文档构成的数组给数据库。另外批量插入只能对于同一个集合,不能批量插入多个集合。

1
db.foo.batchInsert([{"_id" : 0}, {"_id" : 1}, {"_id" : 2}])

如果只是导入原始数据(如从mysql),可以使用命令行工具,如mongoimport,而不是批量插入。当前MongoDB消息最大长度为48MB.

在执行插入的时候,使用的驱动程序会讲数据转换BSON的形式,然后将其送入数据库。数据库只会检查是否包含_id键并且文档不超过16MB,不做其他检查,就只是简单地的文档原样存入数据库中,所以可以避免注入式攻击。但是一般主流语言的驱动程序都会做一定检查,确认是否是有效文档。

删除文档

1
2
db.users.remove() // 删除users集合中所有文档,但不会删除集合本身
// 原有的索引也会保留

remove函数可以接受一个查询作为可选参数。给定这个参数后,只有符合条件的文档才会被删除。例如,删除mailing.list集合中所有”output”为true的人:

1
db.mailing.list.remove({"opt-out" : true})

删除数据是永久性的,不能撤销,也不能恢复。

删除速度
删除文档通常会很快,如果要删除整个集合,直接删除集合(然后重建索引)会更快

1
2
3
db.bar.remove()  //删除所有文档

db.drop_collection("bar") // 删除集合,比上一种方式要快得多

更新文档

文档存入数据库后,就可以使用update方法来修改。update有两个参数,一个用来查询文档,用来找出要更新的文档;另一个是修改器(modifier)文档,描述对找到的文档做哪些更改。

更新操作是原子的:如果两个更新同时发生,先到达的执行,接着执行另外一个。

文档替换

1
2
3
4
5
6
 {
"_id" : ObjectId("4b2b9f67a1f631733d917a7a"),
"name" : "joe",
"friends" : 32,
"enemies" : 2
}

我们要删除部分字段并更新部分字段,可以执行下面命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
 var joe = db.users.findOne({"name" : "joe"});
joe.relationships = {"friends" : joe.friends, "enemies" : joe.enemies}; {
"friends" : 32,
"enemies" : 2
} joe.username = joe.name;
"joe"
delete joe.friends;
true
delete joe.enemies;
true
delete joe.name;
true
db.users.update({"name" : "joe"}, joe);

注意在一个集合中_id必须唯一的,也就是如果有多个匹配的文档,update的文档会被插入多次,会导致重复_id,导致失败。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 db.people.find()
{"_id" : ObjectId("4b2b9f67a1f631733d917a7b"), "name" : "joe", "age" : 65},
{"_id" : ObjectId("4b2b9f67a1f631733d917a7c"), "name" : "joe", "age" : 20},
{"_id" : ObjectId("4b2b9f67a1f631733d917a7d"), "name" : "joe", "age" : 49},

joe = db.people.findOne({"name" : "joe", "age" : 20});
{
"_id" : ObjectId("4b2b9f67a1f631733d917a7c"),
"name" : "joe",
"age" : 20
}
joe.age++;
db.people.update({"name" : "joe"}, joe);
E11001 duplicate key on update

最好是使用ObjectId指定要更新的文档:

1
db.people.update({"_id" : ObjectId("4b2b9f67a1f631733d917a7c")}, joe)

使用修改器

通常文档只有一部分要更新。利用原子的更新修改器,可以使得这个操作非常快。更新修改器是种特殊的键,用来指定复杂的更新操作,比如调整、增加或删除键,还可能是操作数组或者内嵌文档。

target doc:

1
2
3
4
5
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"url" : "www.example.com",
"pageviews" : 52
}

增加pageviews:

1
2
3
4
5
6
7
8
9
 db.analytics.update({"url" : "www.example.com"},
... {"$inc" : {"pageviews" : 1}})

db.analytics.find()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"url" : "www.example.com",
"pageviews" : 53
}

$set修改器

$set用来指定一个键的值,如果这个键不存在,则创建它。

1
2
3
4
5
6
7
8
db.users.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin"
}

添加一个字段进去:

1
2
3
4
5
6
7
8
9
10
11
12
db.users.update({"_id" : ObjectId("4b253b067525f35f94b60a31")},
... {"$set" : {"favorite book" : "War and Peace"}})

db.users.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin",
"favorite book" : "War and Peace"
}

也可以修改键的值,甚至修改键的类型数据:

1
2
3
4
5
6
db.users.update({"name" : "joe"},
... {"$set" : {"favorite book" : "Green Eggs and Ham"}})

db.users.update({"name" : "joe"},
... {"$set" : {"favorite book" :
... ["Cat's Cradle", "Foundation Trilogy", "Ender's Game"]}})

另外可以通过$unset删除一个字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
db.users.update({"name" : "joe"},
... {"$unset" : {"favorite book" : 1}})

db.blog.posts.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe",
"email" : "joe@example.com"
}
}

$set可以修改内嵌文档

1
2
3
4
5
6
7
8
9
10
11
12
13
 db.blog.posts.update({"author.name" : "joe"},
... {"$set" : {"author.name" : "joe schmoe"}})

db.blog.posts.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe schmoe",
"email" : "joe@example.com"
}
}

增加和减少

$inc修改器用来增加已有键的值,或者在键不存在时创建一个键。

1
2
3
4
5
6
7
8
9
10
11
12
 db.games.insert({"game" : "pinball", "user" : "joe"})

db.games.update({"game" : "pinball", "user" : "joe"},
... {"$inc" : {"score" : 50}})

db.games.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"game" : "pinball",
"user" : "joe",
"score" : 50
}

数组修改器

$push

数组操作只能用于数组的键上。如果指定的键已经存在,$push会向已有的数组末尾添加一个元素,否则则会创建一个新的数组。

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
 db.blog.posts.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "..."
}
db.blog.posts.update({"title" : "A blog post"},
... {"$push" : {"comments" :
... {"name" : "joe", "email" : "joe@example.com",
... "content" : "nice post."}}})
db.blog.posts.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
}
] }

db.blog.posts.update({"title" : "A blog post"},
... {"$push" : {"comments" :
... {"name" : "bob", "email" : "bob@example.com",
... "content" : "good post."}}})
db.blog.posts.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
},
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post."
}
] }

如果需要同时添加多个元素,可以使用$each,以数组作为参数.

1
2
db.stock.ticker.update({"_id" : "GOOG"},
... {"$push" : {"hourly" : {"$each" : [562.776, 562.790, 559.123]}}})

如果需要限制数组的大小,我们可以只用$slice,使得数组只保留有top N items(-10指只保留10个元素,参数必须是负值).

1
2
3
4
db.movies.find({"genre" : "horror"},
... {"$push" : {"top10" : {
... "$each" : ["Nightmare on Elm Street", "Saw"],
... "$slice" : -10}}})

如果需要加入数组,并且希望将这个数组中元素排序后再加入:

1
2
3
4
5
6
7
 db.movies.find({"genre" : "horror"},
... {"$push" : {"top10" : {
... "$each" : [{"name" : "Nightmare on Elm Street", "rating" : 6.6},
... {"name" : "Saw", "rating" : 4.3}],
... "$slice" : -10,
... "$sort" : {"rating" : -1}}}})
// 按照rating排序

注意$slice$sort必须要结合$each使用,不能单独使用。

using arrays as sets

有时我们需要需要判断只有当元素不在该文档的数组里面,才将该元素加入该文档的数组中,则可以在查询文档中使用$ne

1
2
db.papers.update({"authors cited" : {"$ne" : "Richie"}},
... {$push : {"authors cited" : "Richie"}})

同样可以用$addToSet实现,可以避免重复

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
 db.users.findOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com"
] }

db.users.update({"_id" : ObjectId("4b2d75476cc613d5ee930164")},
... {"$addToSet" : {"emails" : "joe@gmail.com"}})
db.users.findOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com",
] }
db.users.update({"_id" : ObjectId("4b2d75476cc613d5ee930164")},
... {"$addToSet" : {"emails" : "joe@hotmail.com"}})
db.users.findOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com",
"joe@hotmail.com"
] }

$addToSet$each结合起来,就可以添加多个不同的值,而使用$ne$push组合则不能实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.users.update({"_id" : ObjectId("4b2d75476cc613d5ee930164")}, {"$addToSet" :
... {"emails" : {"$each" :
... ["joe@php.net", "joe@example.com", "joe@python.org"]}}})
db.users.findOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com",
"joe@hotmail.com"
"joe@php.net"
"joe@python.org"
] }

删除数组元素

可以将数组看做一个队列或栈,使用$pop可以移除开头或结尾的一个元素。
{"$pop" : {"key" : 1}}移除末尾一个元素,{"$pop" : {"key" : -1}}移除开头一个元素。

有时我们需要一些要求删除元素,这时可以使用$pull

1
2
3
4
5
6
7
8
9
10
 db.lists.insert({"todo" : ["dishes", "laundry", "dry cleaning"]})
db.lists.update({}, {"$pull" : {"todo" : "laundry"}})

db.lists.find()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"todo" : [
"dishes",
"dry cleaning"
] }

pulling会移除所有匹配的元素。

数组的定位修改器

如果数组有多个值,而我们只想修改其中一部分,这时我们需要通过位置或者定位操作符$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
db.blog.posts.findOne()
{
"_id" : ObjectId("4b329a216cc613d5ee930192"),
"content" : "...",
"comments" : [
{
"comment" : "good post",
"author" : "John",
"votes" : 0
},
{
"comment" : "i thought it was too short",
"author" : "Claire",
"votes" : 3
},
{
"comment" : "free watches",
"author" : "Alice",
"votes" : -1
}
]
}
1
2
db.blog.update({"post" : post_id},
... {"$inc" : {"comments.0.votes" : 1}})

在很多情况下,不预先查询文档就不能知道所有修改数组的下标,我们需要$用来定位查询文档已经匹配的元素在数组中的位置,并进行更新。

1
2
3
db.blog.update({"comments.author" : "John"},
... {"$set" : {"comments.$.author" : "Jim"}})
// 56 67

upsert

upsert是一种特殊的更新。如果没有文档符合更新条件,就会以这个条件和更新文档内容为基础创建一个新的文档。如果找到了文档,则正常更新。

1
2
3
4
5
db.users.update({"rep" : 25}, {"$inc" : {"rep" : 3}}, true)
db.users.findOne()
{
"_id" : ObjectId("4b3295f26cc613d5ee93018f"),
"rep" : 28 }

有时有些键在文档插入时需要被设置,但是在随后的修改中不再改变,可以使用$setOnInsert

1
2
3
4
5
6
db.users.update({}, {"$setOnInsert" : {"createdAt" : new Date()}}, true)
db.users.findOne()
{
"_id" : ObjectId("512b8aefae74c67969e404ca"),
"createdAt" : ISODate("2013-02-25T16:01:50.742Z")
}

再执行一遍时,由于文档已经被插入,修改不再执行。

1
2
3
4
5
6
db.users.update({}, {"$setOnInsert" : {"createdAt" : new Date()}}, true) 
db.users.findOne()
{
"_id" : ObjectId("512b8aefae74c67969e404ca"),
"createdAt" : ISODate("2013-02-25T16:01:50.742Z")
}

save

save可以在文档不存在时插入,存在时更新。save只有一个参数:文档。要是这个文档含有_id键,save则会调用upsert(以_id作为匹配参数);否则会调用插入。可以非常方便的使用这个函数在shell中快速修改文档。

1
2
3
4
var x = db.foo.findOne() 
x.num = 42
42
> db.foo.save(x)

更新多个文档

默认情况下,更新只能对符合条件匹配的第一个文档执行操作。要是有多个文档符合条件,其余的文档不会发生变化。如果要使所有匹配的文档都得到更新,可以设置update的第4个参数为true。

1
2
db.users.update({"birthday" : "10/13/1978"},
... {"$set" : {"gift" : "Happy Birthday!"}}, false, true)

可以通过getLastError查看有多少文档被update。

1
2
3
4
5
db.count.update({x : 1}, {$inc : {x : 1}}, false, true) db.runCommand({getLastError : 1})
{
"err" : null, "updatedExisting" : true, "n" : 5,
"ok" : true
}

返回已更新的文档

用getLastError仅能获得有限的信息,并不能反悔已更新的文档。这个可以使用findAndModify命令实现。

findAndModify可以使一系列操作:查询,更新(remove),以原子性操作顺序一起执行完成,而不会发生读取某个值之后该值已经被其他客户端访问,同时满足的文档会被返回,实现在一个操作中返回结果并更新。

1
2
3
4
5
6
7
8
9
10
11
ps = db.runCommand({"findAndModify" : "processes",
... "query" : {"status" : "READY"},
... "sort" : {"priority" : -1},
... "update" : {"$set" : {"status" : "RUNNING"}})
{
"ok" : 1,
"value" : {
"_id" : ObjectId("4b3e7a18005cab32be6291f7"),
"priority" : 1,
"status" : "READY"
} }

注意,是先返回结果,然后再更新,之后再看文档:

1
2
3
4
5
6
db.processes.findOne({"_id" : ps.value._id})
{
"_id" : ObjectId("4b3e7a18005cab32be6291f7"),
"priority" : 1,
"status" : "RUNNING"
}

findAndModify也可以删除文档,也就是返回结果后,不需要更新,而是直接删除文档。

1
2
3
4
ps = db.runCommand({"findAndModify" : "processes", 	"query" : {"status" : "READY"},
"sort" : {"priority" : -1},
"remove" : true}).value
do_something(ps)

findAndModify命令中每个键对应的值如下所示:

键名 意义
findAndModify 字符串,集合名
query 查询文档,用来检索文档的条件
sort 排序结果文件的条件
update 修改器文档,对所找到的文档执行的更新
remove 布尔类型,表示是否删除文档

uodate和remove中有且只能有一个,并且findAndModify只能处理一个文档,不能执行upsert操作,只能更新已有文档。

查询

find

查询返回一个集合中符合条件的子集,其参数也是一个文档,成为query selector,说明选择文档的条件。

空的文档或不指定查询文档会匹配集合所有的内容。

db.c.find()

查询所有age为27的文档:
db.users.find({"age" : 27})

如果需要匹配一个字符串,比如username键值位joe

多个条件AND的关系:

1
2
db.users.find({"username" : "joe"})
db.users.find({"username" : "joe", "age" : 27})

指定返回的键

find可以设置第二个参数来指定想要的键,一般_id键总是会被返回,也可以设置为不返回。

1
2
3
4
5
6
7
8
9
10
11
db.users.find({}, {"username" : 1, "email" : 1})
{
"_id" : ObjectId("4ba0f0dfd22aa494fd523620"),
"username" : "joe",
"email" : "joe@example.com"
}

db.users.find({}, {"username" : 1, "_id" : 0})
{
"username" : "joe",
}

限制

查询文档中的键值对的值必须是常量或者是代码中的变量,但是不能是其他文档中的键的值。

查询条件

前面的查询都是精准匹配,更复杂的有范围,OR,子句,取反等。

查询条件

比较操作符包括$lt, $lte, $gt, $gte,$ne

1
2
3
4
5
6
db.users.find({"age" : {"$gte" : 18, "$lte" : 30}})

start = new Date("01/01/2007")
db.users.find({"registered" : {"$lt" : start}})

db.users.find({"username" : {"$ne" : "joe"}})

OR查询

MongoDB中有两种方式进行OR查询。 1. $in, $nin可以用来匹配一个键的多个值。 2. $or则更加通用,可以完成多个键值的OR查询。

1
2
3
4
db.raffle.find({"ticket_no" : {"$in" : [725, 542, 390]}})
db.users.find({"user_id" : {"$in" : [12345, "joe"]})

db.raffle.find({"ticket_no" : {"$nin" : [725, 542, 390]}})

使用$in, $nin只能实现对于单个键的OR查询,如果需要多个键的OR查询:

1
2
3
db.raffle.find({"$or" : [{"ticket_no" : 725}, {"winner" : true}]})

db.raffle.find({"$or" : [{"ticket_no" : {"$in" : [725, 542, 390]}}, {"winner" : true}]})

$not

$not是元条件语句,可以用在其他任何条件之上。$mod会将查询的值除以第一个给定值,若余数等于第二个给定值则返回该结果。

1
2
3
db.users.find({"id_num" : {"$mod" : [5, 1]}})

db.users.find({"id_num" : {"$not" : {"$mod" : [5, 1]}}})

$not和正则表达式结合会非常有用。

条件句的规则

条件句是内层文档的键,而修改器则是外层文档的键。
一个键可以有多个条件句,但是不能对应多个更新修改器。

特定类型的查询

null

null不仅能匹配键值为null的文档,而且也能匹配不存在该键值的文档。

1
2
3
4
5
6
7
8
9
db.c.find()
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

db.c.find({"z" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

如果仅仅想要匹配键值为null的文档,既要检查该键的值是否为null,还要通过$exists条件判定键值是否已经存在。

1
db.c.find({"z" : {"$in" : [null], "$exists" : true}})

正则表达式

使用Perl兼容的正则表达式(PCRE)库来匹配正则表达式。

1
db.users.find({"name" : /joe/i})

查询数组

参数只有一个元素时,默认数组中有一个元素匹配就算作匹配。

1
2
db.food.insert({"fruit" : ["apple", "banana", "peach"]})
db.food.find({"fruit" : "banana"})

$all

如果需要通过多个元素来匹配数组,就要使用$all。顺序无关紧要。

1
2
3
4
5
6
7
db.food.insert({"_id" : 1, "fruit" : ["apple", "banana", "peach"]})
db.food.insert({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]})
db.food.insert({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})

db.food.find({fruit : {$all : ["apple", "banana"]}})
{"_id" : 1, "fruit" : ["apple", "banana", "peach"]}
{"_id" : 3, "fruit" : ["cherry", "banana", "apple"]}

如果直接以一个完整的数组作为参数,则是完全的精准匹配,包括顺序,元素个数,即是冗余也会被当做不匹配。

1
2
db.food.find({"fruit" : ["apple", "banana"]}) 
% 无法匹配

如果需要匹配指定位置的元素,可以使用key.index语法指定下标(下标从0开始)。

1
db.food.find({"fruit.2" : "peach"})

$size

用来指定查询指定长度的数组。

1
db.food.find({"fruit" : {"$size" : 3}})

一种需求是查询一个长度范围的数组,但是$size并不能与其他查询子句组合(如$gt),我们可以通过在文档中添加一个size键的方式实现,每次插入元素时同时更新size。

$slice

find的第二个参数是可选的,用来指定返回所需要的键。$slice可以指定返回数组的一个子集合。

返回前10条评论:

1
db.blog.posts.findOne(criteria, {"comments" : {"$slice" : 10}})

返回后10条评论:

1
db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -10}})

同时也可以指定偏移量和需要的元素数量:

1
db.blog.posts.findOne(criteria, {"comments" : {"$slice" : [23, 10]}})

使用$slice时,如果没有具体声明其他键的返回需要,默认会返回所有其他的键。

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
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
},
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post."
}
] }

db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post."
}
] }

查询内嵌文档

有两种方法可以查询内嵌文档:1. 查询整个文档 2. 只针对其键/值进行查询。

查询整个内嵌文档与普通查询完全相同。

1
2
3
4
5
6
7
8
9
10
{
"name" : {
"first" : "Joe",
"last" : "Schmoe"
},
"age" : 45
}

% 完全匹配整个文档
db.people.find({"name" : {"first" : "Joe", "last" : "Schmoe"}})

如果joe加入一个middle name,上面将无法匹配。更好的方式是针对内嵌文档的特定键值进行查询。

1
db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})

.表示深入内嵌文档内部。点表示法也是待插入文档不能包含.的原因。

如果是有一个数组包含若干内嵌文档,我们需要找到有满足某组合条件内嵌文档的文档,上面是无法实现的,因为满足各个组合条件的因素并不在同一个内嵌文档中。这时我们需要使用$elemMatch,对于数组的当个文档,需要实现所有的match条件。

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
db.blog.find()
{
"content" : "...",
"comments" : [
{
"author" : "joe",
"score" : 3,
"comment" : "nice post"
},
{
"author" : "mary",
"score" : 6,
"comment" : "terrible post"
}
]
}

b.blog.find({"comments" : {"author" : "joe", "score" : {"$gte" : 5}}})
% failed

db.blog.find({"comments.author" : "joe", "comments.score" : {"$gte" : 5}})
% failed, because the author criteria could match a different comment
% than the score criteria. That is, it would return the document
% shown above: it would match "author" : "joe" in the first comment
% and "score" : 6 in the second comment.

db.blog.find({"comments" : {"$elemMatch" : {"author" : "joe",
"score" : {"$gte" : 5}}}})
% successful

$where查询

键/值对是很有表现力的查询方式,但是仍不能处理所有情况,这是我们也许需要使用$where,用来指定一些更特殊的条件,它可以执行任意javascript作为查询条件判断的一部分。

如比较文档中的存在两个键值是否相等。

1
2
db.foo.insert({"apple" : 1, "banana" : 6, "peach" : 3})
db.foo.insert({"apple" : 8, "spinach" : 4, "watermelon" : 4})

第二文档存在”spinach”键的值和”watermelon”相等,是符合要求的文档,但是前面的条件句都无法实现这个功能,这时需要使用$where.

1
2
3
4
5
6
7
8
9
10
db.foo.find({"$where" : function () {
for (var current in this) {
for (var other in this) {
if(current != other && this[current] == this[other]) {
return true;
}
}
}
return false;
}}};

对于每个文档(用this表示)都使用函数进行判断,如果函数函数返回true,则这个文档将作为结果set的一个文档。

前面用的是一个函数,也可以用一个字符串来指定$where查询。下面两种表达式是完全等价的:

1
2
db.foo.find("$where" : "this.x + this.y == 10")
db.foo.find("$where" : "function() {return this.x + this.y == 10};")

不是非常必要时,一定要避免使用$where,因为速度要比常规查询慢的多。每篇文档要从BSON转换为javascript对象,然后通过$where的表达式来判断。

游标

数据库使用游标来返回find的执行结果。客户端对游标的实现通常能够对最终结果进行有效的控制。可以限制结果的数量,略过部分就诶过,根据任意方向任意键的组合对结果进行各种排序,或者是执行其他一些功能强大的操作。

1
2
3
for(i=0; i<100; i++) {
db.collection.insert({x : i}); ... }
var cursor = db.collection.find();

这样mongobd不会自动迭代输出所有结果,可以一次查看一条结果。

1
2
3
4
while (cursor.hasNext()) {
obj = cursor.next();
// do stuff ...
}

游标类还实现了迭代器接口,所以可以在foreach循环中使用。

1
2
3
4
5
6
7
var cursor = db.people.find();
cursor.forEach(function(x) {
print(x.name);
... });
adam
matt
zak

当我们调用find的时候,shell并不立即查询数据库,而是等待真正开始要求获得结果的时候才发送查询,这样可以在执行之前可以给查询附加额外的选项。几乎所有游标对象的方法都返回游标本身,这样就可以按任意顺序组成方法链。例如,下面几种表达是等价的:

1
2
3
var cursor = db.foo.find().sort({"x" : 1}).limit(1).skip(10); 
var cursor = db.foo.find().limit(1).sort({"x" : 1}).skip(10);
var cursor = db.foo.find().skip(10).limit(1).sort({"x" : 1});

此时查询还没有被执行,这些函数都只是构造查询。当我们执行

cursor.hasNext()

查询被发往服务器。shell立即获得前100个结果或者前4mb数据(两者中较小者),这样下次调用next或hasNext()就不必再去服务器上去数据。一旦客户端用完第一组结果,shell会再一次请求数据库得到后面的结果。这个过程会一直持续到游标耗尽或者结果全部返回。

limit, skip和sort

1
2
db.c.find().limit(3)
db.c.find().skip(3)

sort用一个文档作为参数:一组键/值对。键对应用于排序的键名,值代表排序的方向,1表示升序,-1表示降序。

1
db.c.find().sort({username : 1, age : -1})

注意有时一个键的值可能是多种类型的,多种类型数据的排序是预先定义好的,从小到大顺序如下:

  1. Minimum value
  2. null
  3. Numbers (integers, longs, doubles)
  4. Strings
  5. Object/document
  6. Array
  7. Binary data
  8. Object ID
  9. Boolean
  10. Date
  11. Timestamp
  12. Regular expression
  13. Maximum value

避免使用skip略过大量结果

skip大量结果速度很很慢,所以要尽量避免。通常可以向文档本身内置查询条件,来避免大的skip,或者利用上次的结果计算下一次查询。

  • 不用skip对结果分页
1
2
3
4
5
6
7
8
9
10
11
12
13
// do not use: slow for large skips
var page1 = db.foo.find(criteria).limit(100)
var page2 = db.foo.find(criteria).skip(100).limit(100)
var page3 = db.foo.find(criteria).skip(200).limit(100)

// use:
var page1 = db.foo.find().sort({"date" : -1}).limit(100)
var latest = null;
// display first page
while (page1.hasNext()) { latest = page1.next(); display(latest);
}
// get next page
var page2 = db.foo.find({"date" : {"$gt" : latest.date}}); page2.sort({"date" : -1}).limit(100);
  • 随机选取文档

最慢的方法是计算文档总数,然后选取一个随机数利用find做一次查询。
另外一种方法是给每个文档添加一个额外的随机键。

高级查询选项

查询分为包装的和普通的两类。

普通查询:
var cursor = db.foo.find({"foo" : "bar"})

有几个选项用于包装查询:
var cursor = db.foo.find({"foo" : "bar"}).sort({"x" : 1})

实际上发送的查询文档不是{"foo" : "bar"},而是将原始查询文档包装在一个更大的文档中{"$query" : {"foo" : "bar"}, "$orderby" : {"x" : 1}}作为发送的查询文档。

查询的各种选项:

  • $maxscan : integer
    Specify the maximum number of documents that should be scanned for the query.
    db.foo.find(criteria)._addSpecial("$maxscan", 20)

This can be useful if you want a query to not to take too long but are not sure how much of a collection will need to be scanned. This will limit your results to whatever was found in the part of the collection that was scanned (i.e., you may miss other documents that match).

  • $min : document
    Start criteria for querying. document must exactly match the keys of an index used for the query. This forces the given index to be used for the query.
    This is used internally and you should generally use "$gt" instead of "$min". You can use "$min" to force the lower bound on an index scan, which may be helpful for complex queries.

  • $max : document
    End criteria for querying. document must exactly match the keys of an index used for the query. This forces the given index to be used for the query.
    If this is used internally, you should generally use "$lt" instead of "$max". You can use "$max" to force bounds on an index scan, which may be helpful for complex queries.

  • $showDiskLoc : true
    Adds a "$diskLoc" field to the results that shows where on disk that particular result lives. For example:

    1
    2
    db.foo.find()._addSpecial('$showDiskLoc',true)
    { "_id" : 0, "$diskLoc" : { "file" : 2, "offset" : 154812592 } } { "_id" : 1, "$diskLoc" : { "file" : 2, "offset" : 154812628 } }

The file number shows which file the document is in. In this case, if we’re using the test database, the document is in test.2. The second field gives the byte offset of each document within the file.

获取一致结果

数据处理通常的一种做法就是先把数据从MongoDB中取出来,然后经过某种变换,,然后存回去。

1
2
3
4
5
6
cursor = db.foo.find();
while (cursor.hasNext()) {
var doc = cursor.next();
doc = process(doc);
db.foo.save(doc);
}

但是如果文档的改动比较大,尤其是增加了很多数量量,会导致预留空间不够,这时mongodb会将文档移动至集合的末尾处。导致的后果是游标往后面移动又会返回已经被处理但是被移动过的文档。

解决方法是对查询进行快照,如果使用了$snapshot选项,查询就是针对不变的集合视图运行的,确保查询的结果就是查询执行的那一刻的一致快照。

索引

索引介绍

如果经常需要基于某个键进行检索,可以对该键建立索引,以提高查询速度。使用explain()可以看到mongodb的执行信息。

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
for (i=0; i<1000000; i++) { ... db.users.insert(
... {
... "i" : i,
"username" : "user"+i,
"age" : Math.floor(Math.random()*120),
"created" : new Date()
}
);
}

db.users.find({username: "user101"}).explain()
{
"cursor" : "BasicCursor",
"nscanned" : 1000000,
"nscannedObjects" : 1000000,
"n" : 1,
"millis" : 721,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
}
}

% 建立索引
db.users.ensureIndex({"username" : 1})

db.users.find({"username" : "user101"}).explain()
{
"cursor" : "BtreeCursor username_1",
"nscanned" : 1,
"nscannedObjects" : 1,
"n" : 1,
"millis" : 3,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
"username" : [
[
"user101",
"user101"
] ]
} }

建立索引:

1
db.users.ensureIndex({"username" : 1})

-1, 1指名创建索引的方向,如果索引只有一个键,则方向无关紧要。如果有个键则需要考虑索引的方向问题。

如果索引包含N个键,则对于前几个键的查询都会有帮助,比如索引{"a" : 1, "b" : 1, "c" : 1}实际上是有了{“a” : 1}, {“a” : 1, “b” : 1}, {“a” : 1, “b” : 1, “c” : 1}索引,但是没有{“b” : 1}, {“a” : 1, “c” : 1}等索引。

创建索引是每次插入,更新删除时都会产生额外的开销,即使是查询,如果查询要返回集合中一半以上的结果,表扫描效率要高于查索引。

扩展索引

在建立索引时我们需要考虑以下问题:

  • 会做怎样的查询?其中哪些键需要索引?
  • 每个键的索引方向是怎样的?
  • 如何应对扩展?有没有种不同的键的排列可以使常用数据更多的保留在内存中,减少内存交换次数。

假设我们有个集合,保存了用户的状态信息。现在想要查询用户的日期,取出某一用户最近的状态,我们可以创建:

1
db.status.ensureIndex({user : 1, date : -1})

组织形式:

1
2
3
4
5
user 1, 2015_11_11_2
user 1, 2015_11_11_1...and
user 2, 2015_11_11_6
user 2, 2015_11_10_1
...

这样会对用户的日期的查询非常快,但是并不是最好的方式。因为每个用户每天可能有数十条状态更新,如果每个用户的状态索引值都占用类似一页纸一样的空间,但是其中大部分并不是最新值,如果用户数很多,那么内存将无法放下所有索引,发生频繁内存交换。

如果改变索引顺序为{data : -1, user : 1},则数据库可以按照时间将更多用户最近的状态保存在内存中,可以有效减少内存交换。

索引内嵌文档中的键

为内嵌文档的键建立所有那个和位普通的键创建索引没有什么区别。

1
db.blog.ensureIndex({"comments.date" : 1})

内嵌文档的键索引与普通键索引并无差异,两者也可以联合组成符合索引。

为排序创建索引

对于经常需要排序的键,最好建立索引。如果对没有索引的键调用sort,mongodb需要将所有数据提取到内存来做排序,代价非常高且有上限。

索引名称

集合中每个索引都有一个字符串类型的名字,来唯一标识索引,如果没有指定名字,默认情况下会将索引的键名连在一起作为该索引的名字。如果索引的键特别多,就不太合适,可以使用下面的方式:

1
2
db.foo.ensureIndex({"a" : 1, "b" : 1, "c" : 1, ..., "z" : 1},
... {"name" : "alphabet"})

唯一索引

唯一索引可以确保集合的每一个文档的指定键都有唯一值。

1
db.people.ensureIndex({"username" : 1}, {"unique" : true})

消除重复

如果创建唯一索引时已经有数据重复,那么唯一索引的创建会失败。如果希望将所有包含重复值的文档只保留第一个,而删除后面重复的文档,可以使用:

1
db.people.ensureIndex({"username" : 1}, {"unique" : true, "dropDups" : true})

复合唯一索引

创建复合唯一索引时,单个键的值可以相同,只要所有键的组合不同就可以。

explain和hint

索引管理

索引的元信息存储在每个数据库的system.indexes集合中。这是一个保留集合,不能对其插入或者删除文档。操作只能通过ensureIndex或者dropIndexes进行。

system.indexes集合包含每个索引的详细信息,同时system.namespaces集合也包含有索引的名字。在system.namespaces集合中哦该,每个集合至少有两个文档与之对应,集合本身名字,以及其_id唯一索引名字,如果还有其他索引,也会有对应信息在system.namespaces中。

修改索引

后台建立新的索引(否则建立索引期间会阻塞所有索引期间的请求)。

1
db.people.ensureIndex({"username" : 1}, {"background" : true})

删除索引

查看system.indexes找到索引名字,然后删除:

1
2
3
db.people.dropIndex("x_1_y_1")
{ "nIndexesWas" : 3, "ok" : 1 }
% 99 130

可以使用通配符删除所有索引(除了_id索引,这个索引只有删除集合时才会被删除)

另外删除集合也会删除这个集合的所有索引,remove方式不会影响索引。