我是一只小白兔~
分类: NOSQL
2017-01-05 22:14:05
文档是MongoDB中数据的基本单元。
每个文档都有一个特殊的键“_id”,它在文档所处的集合中是唯一的。
2.1 文档
l 文档中的键值对是有序的;
l 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型;
l 键不能含有\0(空字符)。该字符用来表示键的结尾;
l .和$有特别的含义,只有在特定环境下才能使用;
l 以下划线”_”开头的键是保留的
MongoDB不但区分类型,还区分大小写。
MongoDB的文档不能有重复的键。
2.2 集合
集合就是一组文档。
2.2.1 无模式
集合是无模式的。
2.2.2 命名
集合名可以是满足下列条件的任意UTF-8字符串。
l 集合名不能是空字符串””.
l 集合名不能含有\0字符(空字符),这个字符表示集合名的结尾;
l 集合名不能以“system”开头,这是为系统集合保留的前缀。
l 用户创建的集合名字不能含有保留字符$.
子集合:组织集合的一种惯例是使用”.”字符分开的按命名空间划分的子集合。
2.3 数据库
一个MongoDB实例可以承载多个数据库。
数据库名可以是满足以下条件的任意UTF-8字符串。
l 不能是空字符串””。
l 不得含有’ ’(空格)、.(点)、$、/、\、\0(空字符)。
l 应全部小写。
l 最多64个字节。
有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。具体如下:
l Admin
从权限角度看,它是root数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。
l Local
这个数据永远不会被复制。可以用来存储限于本地单台服务器的任意集合。
l Config
当Mongodb用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
把数据库的名字放到集合名前面,得到就是集合的完全限定名,称为命名空间。命名空间的长度不得超过121字节,在实际使用中应该小于100字节。
2.4 启动MongoDB
MongoDB一般作为网络服务器来运行,客户端可以连接到该服务器并执行操作。要启动服务器,需要运行mongod可执行文件:
$ ./mongod
Mongod在没有参数的情况下会使用默认数据目录/data/db,并使用27017端口。如果数据目录不存在或者不可写,服务器会启动失败。如果端口被占用,服务器也会启动失败。
默认情况下,Mongodb监听27017端口。
Mongod还会启动一个非常基本的HTTP服务器,监听数字比主端口高1000的端口28017端口。
访问数据库管理信息地址:http://localhost:28017
在启动服务器的shell下可以使用Ctrl+C来完全停止Mongodb的运行。
2.5 MongoDB shell
2.5.1 运行shell
运行Mongodb启动shell:
$ ./mongo
Shell是功能完备的JavaScript解释器,可以运行任何JavaScript程序。
2.5.2 MongoDB客户端
Shell还是一个独立的Mongodb客户端。
2.5.3 shell中的基本操作
在shell中操作数据会用到4个基本操作:创建、读取、更新和删除(CRUD).
(1) 创建
Insert函数添加一个文档到集合里面。
>post={“title”: “My Blog Post”,
…”content” : ”Here’s my blog post.”,
…”date” : new Date() }
{
“title” : “My Blog Post”,
“content” : “Here’s my blog post”,
“date” : “Sat Dec 12 2009 21:33:30 GMT-0500 (EST)”
}
#将文档插入到集合中:
>db.blog.insert(post)
#使用find查看集合
>db.blog.find()
(2)读取
find会返回集合里面所有的文档。若只是想查看一个文档,可以用findOne.
>db.blog.findOne()
使用find时,shell自动显示最多20个匹配的文档,但可以获取更多文档。
(3)更新
Update接受(至少)两个参数:第一个是要更新文档的限定条件,第二个是新的文档。
l 第一步修改变量post,增加”comments”键:
>post.comments= []
l 执行update操作,用新版本的文档替换标题为“My Blog Post”的文章
>db.blog.update( { title : “My Blog Post”}, post)
(4)删除
Remove用来从数据库中永久性的删除文档。在不使用任何参数进行调用的情况下,它会删除一个集合内的所有文档。它也可以接受一个文档以指定限定条件。
>db.blog.remove( { title : “My Blog Post”})
2.5.4 使用shell的窍门
使用db.help()可以查看数据库级别的命令的帮助,集合的相关帮助可以通过db.foo.help()来查看。
当有属性与目标集合同名时,可以使用getCollection函数:
>db.getCollection(“Version”);
要查看名称中含有无效JavaScript字符的集合,例如foo-bar
>db.getCollection(“foo-bar”);
2.6 数据类型
2.6.1 基本数据类型
l null
null用于表示空值或者不存在的字段
{“x” : null}
l 布尔
布尔类型有两个值‘true’和’false’
{“x” : true}
l 32位整数
Shell中该类型不可用。32位整数都会自动转换
l 64位整数
Shell也不支持这个类型。
l 64位浮点数
Shell中的数字都是这种类型。以下都是浮点数
{“x”: 3.14}
{”x”: 3}
l 字符串
UTF-8字符串都可表示为字符串类型的数据:
{“x”: “foobar”}
l 符号
Shell不支持这种类型。Shell将数据库里的符号类型转换成字符串。
l 对象id
对象id是文档的12字节的唯一ID。
{“x”: ObjectId()}
l 日期
日期类型存储的是从标准纪元开始的毫秒数。不存储时区。
{“x”: new Date()}
l 正则表达式
文档中可以包含正则表达式,采用JavaScript的正则表达式语法。
{“x”: /foobar/i}
l 代码
文档中还可以包含JavaScript代码:
{“x”: function() { /* … */ }}
l 二进制数据
二进制数据可以由任意字节的串组成。但shell中无法使用。
l 最大值
BSON包括一个特殊类型,表示可能的最大值。Shell中没有这个类型。
l 最小值
BSON包括一个特殊类型,表示可能的最小值。Shell中没有这个类型。
l 未定义
文档中也可以使用未定义类型。
{”x”: undefined}
l 数组
值的集合或者列表可以表示成数组:
{“x”: [“a”,”b”,”c”]}
l 内嵌文档
文档可以包含别的文档,也可以作为值嵌入到父文档中。
{“x”: {“foo”: “bar”}}
2.6.2 数字
默认情况下,shell中的数字被MongoDB当做是双精度数。
如果插入的64位整数不能精确的作为双精度数显示,shell会添加两个键,“top”和”bottom”.分别表示高32位和低32位。
32位整数都能用64位的浮点数精确表示。
2.6.3 日期
在JavaScript中,Date对象用做MongoDB的日期类型,创建一个新的Date对象时,通常会调用new Date(…).
2.6.4 数组
数组是一组值,既可以作为有序对象(例如:列表、栈或队列)来操作,也可以作为无序对象(例如:集合)来操作。
在如下文档中,”things”这个键就是一个数组:
{“things” : [“pie”, 3.14]}
2.6.5 内嵌文档
内嵌文档就是把整个MongoDB文档作为另一个文档中的键的一个值。例如:
{
”name“ : “John Doe”,
“address” : {
“street”: “123 Park Street”,
“city”: “Anytown”,
“state” : “NY”
}
}
2.6.6 _id和ObjectId
MongoDB中存储的文档必须有一个”_id”键。这个键可以是任何类型的,默认是个ObjectId对象。在一个集合里面,每个文档都有唯一的”_id”值,来确保集合里面的每个文档都能被唯一标识。
l ObjectId
ObjectId是”_id”的默认类型。
ObjectId使用12字节的存储空间,每个字节两位十六进制数字,是一个24位的字符串。
0 1 2 3 4 5 6 7 8 9 10 11
时间戳 机器 PID 计数器
时间戳:与随后的5个字节组合起来,提供了秒级别的唯一性。
l 自动生成_id
如果插入文档的时候没有”_id”键,系统会自动帮你创建一个。通过在客户端由驱动程序完成。
第3章 创建、更新及删除文档
本章主要介绍如下内容:
l 向集合添加新文档
l 从集合里删除文档
l 更新现有文档
l 位这些操作选择合适的安全级别和速度
3.1 插入并保存文档
使用insert方法,插入一个文档:
>db.foo.insert( {“bar” : “ baz” } )
该操作会给文档增加一个”_id”键,然后将其保存到MongoDB中。
3.1.2 插入:原理和作用
当执行插入的时候,使用的驱动程序会将数据转换成BSON的形式,然后将其送入数据库。数据库解析BSON,检验是否包含”_id”键并且文档不超过4MB. 除此之外,不做别的验证。
3.2 删除文档
删除数据库中的所有数据,但不删除集合,原有的索引也会保留:
>db.users.remove()
Remove函数可以接受一个查询文档作为可选参数,给定这个参数后,只有符合条件的文档才会删除。
例如:要删除mailing.list集合中所有”optout”为true的人:
>db.mailing.list.remove( { “opt-out” : true })
删除数据是永久的,不能撤销,也不能恢复。
3.3 更新文档
可使用update来更新文档。Update有2个参数,一个是查询文档,用来找出要更新的文档,另一个是修改器文档,描述对找到的文档做哪些修改。
更新操作是原子的:若是两个更新同时发生,先达到服务器的先执行,接着执行另外一个。
3.3.1 文档替换
l 新创建集合foo,并插入如下数据:
>db.foo.insert( { "name": "joe","friends": 32,"enemies" : 2} )
l 原始文档:
> db.foo.find();
{
"_id" : ObjectId("583760fee993075b312d058e"),
"name" : "joe",
"friends" : 32,
"enemies" : 2
}
l 目标文档:
{
"_id" : ObjectId("583760fee993075b312d058e"),
"relationships" :
{
"friends" : 32,
"enemies" : 2
},
"username" : "joe"
}
l 替换操作如下:
#查找name为joe的值,并复制为变量joe.
var joe=db.foo.findOne( { "name" : "joe" });
#设置relaitionships的值
joe.relationships={"friends": joe.friends,"enemies":joe.enemies};
#修改集合name键的值为username
joe.username=joe.name;
#删除集合中的键和值
delete joe.friends;
delete joe.enemies;
delete joe.name;
#更新foo集合
>db.foo.update( {"name":"joe"},joe );
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
3.3.2 使用修改器
更新修改器是一种特殊键,用来指定复杂的更新操作,比如调整、增加或删除键,还可能是操作数组或者内嵌文档。
#创建集合analytics
db.analytics.insert({"url": "www.example.com","pageviews": 52});
#使用修改器
db.analytics.update( {"url":"www.example.com" },{"$inc": {"pageviews":1}});
>db.analytics.find()
{ "_id" : ObjectId("58376f22e993075b312d058f"), "url" : "www.example.com", "pageviews" : 53 }
1.”$set”修改器入门
“$set”用来指定一个键的值。如果这个键不存在,则创建它。
#创建集合Jerry
db.Jerry.insert( {"name":"joe","age":30,"sex": "male","location":"Wisconsin" });
#使用$set更新集合
db.Jerry.update({"_id" : ObjectId("58377c1e1cddb4cc5c3448b6")},{"$set": {"favorite book": "war and peace"}});
#查看更新后的集合
> db.Jerry.findOne();
{
"_id" : ObjectId("58377c1e1cddb4cc5c3448b6"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin",
"favorite book" : "war and peace"
}
#对favorite book的值进行更新
db.Jerry.update( {"name": "joe"},{"$set":{"favorite book": "green eggs and ham"} } );
> db.Jerry.findOne();
{
"_id" : ObjectId("58377c1e1cddb4cc5c3448b6"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin",
"favorite book" : "green eggs and ham"
}
用$set可以修改键的数据类型,例如将favorite book键的值变成一个数组:
> db.Jerry.update({"name":"joe"},{"$set": {"favorite book": ["cat's cradle","foundation trilogy", "ender's game"]}});
> db.Jerry.findOne();
{
"_id" : ObjectId("58377c1e1cddb4cc5c3448b6"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin",
"favorite book" : [
"cat's cradle",
"foundation trilogy",
"ender's game"
]
}
当然,也可以使用”$unset”将键完全删除:
> db.Jerry.update( {"name":"joe"},{"$unset": {"favorite book":1}});
> db.Jerry.findOne();
{
"_id" : ObjectId("58377c1e1cddb4cc5c3448b6"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin"
}
可以用”$set”修改内嵌文档:
#创建集合
db.blog.posts.insert({"title": "A Blog Post","content": "...","author": { "name": "joe","email": "joe@example.com"}});
#修改集合中name键的值
db.blog.posts.update( {"author.name":"joe"},{"$set": {"author.name":"joe scheme"}});
#查看修改后的结果
> db.blog.posts.findOne();
{
"_id" : ObjectId("583785181cddb4cc5c3448b7"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe scheme",
"email" : "joe@example.com"
}
}
增加、修改或删除键的时候,应该使用$修改器。
一定要使用以$开头的修改器来修改键值对。
2.增加和减少
“$inc”修改器用来增加已有键的值,或者在键不存在时创建一个键。
#创建集合games
db.games.insert({"game": "pinball","user": "joe"});
#新增键score
db.games.update({"game":"pinball","user":"joe"},{"$inc": {"score": 50}});
#查看更新后的集合
> db.games.findOne();
{
"_id" : ObjectId("583792cb1cddb4cc5c3448b8"),
"game" : "pinball",
"user" : "joe",
"score" : 50
}
#如果要对score增加10000
db.games.update({"game":"pinball","user":"joe"},{"$inc": {"score": 10000}});
#查看更新后的集合
> db.games.findOne();
{
"_id" : ObjectId("583792cb1cddb4cc5c3448b8"),
"game" : "pinball",
"user" : "joe",
"score" : 10050
}
“$inc”和”$set”就是专门来增加(或减少)数字的。”$inc”只能用于整数、长整数或双精度浮点数。
3.数组修改器
如果指定的键已经存在,”$push“会向已有的数组末尾加入一个元素,要是没有就会创建一个新的数组。
#创建一个不存在的键comments
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("583785181cddb4cc5c3448b7"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe scheme",
"email" : "joe@example.com"
},
"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("583785181cddb4cc5c3448b7"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe scheme",
"email" : "joe@example.com"
},
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
},
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post."
}
]
}
如果一个值不在数组里就把它加进去,可以在查询文档中用“$ne”来实现。
>db.papers.update( {"authors cited": {"$ne": "Richie"}},{"$push": {"authors cited": "Rechie"}});
也可以用“$addToSet”完成同样的事。
例如:有一个用户,已经有了电子邮件的地址信息:
db.tom.insert({"username":"joe","emails":["joe@example.com","joe@gmail.com","joe@yahoo.com"]});
> db.tom.findOne();
{
"_id" : ObjectId("5837d2fae98eeef4419fb7cd"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com"
]
}
当添加新的地址时,用”$addToSet”可以避免重复:
db.tom.update({"_id" : ObjectId("5837d2fae98eeef4419fb7cd")},{"$addToSet": {"emails": "joe@gmail.com"}});
#新增一个不重复的email
db.tom.update({"_id" : ObjectId("5837d2fae98eeef4419fb7cd")},{"$addToSet": {"emails": "joe@hotmail.com"}});
> db.tom.findOne();
{
"_id" : ObjectId("5837d2fae98eeef4419fb7cd"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com",
"joe@hotmail.com"
]
}
将”$addToSet”和“$each”组合起来,可以添加多个不同的值,而用”$ne”和”$push”组合就不能实现。例如:一次添加多个电子邮件地址:
db.tom.update({"_id" : ObjectId("5837d2fae98eeef4419fb7cd")},{"$addToSet": {"emails": {"$each": ["joe@php.net","joe@example.com","joe@python.org"]}}});
> db.tom.findOne();
{
"_id" : ObjectId("5837d2fae98eeef4419fb7cd"),
"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“.
db.lists.insert({"todo": ["dishes","laundry","dry cleaning"]});
例如:想把laundry放到第1位,可以从列表中先删除:
db.lists.update({},{"$pull": {"todo": "laundry"}});
> db.lists.findOne();
{
"_id" : ObjectId("5837d821e98eeef4419fb7ce"),
"todo" : [
"dishes",
"dry cleaning"
]
}
“$pull”会将所有匹配的部分删掉。对数组[1,1,2,1]执行pull 1,得到结果就是只有一个元素的数组[2].
4.数组的定位修改器
有两种方法操作数组中的值:通过位置或者定位操作符(“$”).
数组都是以0开头的,可以将下标直接作为键来选择元素。
#创建集合
db.blog.posts1.insert({"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}]});
> db.blog.posts1.findOne();
{
"_id" : ObjectId("5837eb28a0482336cdd7b337"),
"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
}
]
}
例如:想增加第一个评论的投票数量(未执行成功):
db.blog.posts1.update( {"post":post_id}, {"$inc": {"comments.0.votes": -1}});
MongoDB提供了定位操作符”$”,用来定位查询文档已经匹配的元素,并进行更新。定位符只更新第一个匹配的元素。
例如:把用户John的名字改成Jim.(居然改差了)
db.blog.posts1.update({"comments.author": "John"},{"$set": {"coments.$.author": "Jim"}});
> db.blog.posts1.findOne();
{
"_id" : ObjectId("5837eb28a0482336cdd7b337"),
"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
}
],
"coments" : {
"0" : {
"author" : "Jim"
}
}
}
5.修改器速度
$inc能就地修改,因为不需要改变文档的大小,只需要将键的值修改一下,所以比较快。
数组修改器更改了文档的大小,就会慢一些。
3.3.3 upsert
Upsert是一种特殊的更新。要是没有文档符合更新条件,就会以这个条件和更新文档为基础创建一个新的文档。如果找到了匹配的文档,则正常更新。Upsert不必预置集合,同一套代码既可以创建又可以更新文档。
例如:要是执行一个匹配键并增加对应键值的upsert操作,会在匹配的基础上进行增加:
#创建一个键为count,值为25的集合,并在count上+3
db.math.update({"count":25}, {"$inc": {"count":3}},true);
> db.math.findOne();
{ "_id" : ObjectId("583801096a07a045beacc77d"), "count" : 28 }
Save Shell帮助程序
Save是一个shell函数,可以在文档不存在时插入,存在时更新。它只有一个参数:文档。
如果这个文档存在”_id”键,save会调用upsert. 否则,会调用插入。
var x =db.foo.findOne();
x.num=42
db.foo.save(x)
db.foo.findOne();
{
"_id" : ObjectId("583760fee993075b312d058e"),
"relationships" : {
"friends" : 32,
"enemies" : 2
},
"username" : "joe",
"num" : 42
}
3.3.4 更新多个文档
默认情况下,更新只能对符合匹配条件的第一个文档执行操作。要使所有匹配到的文档都得到更新,可以设置update的第4个参数为true.
例如:要给所有在特定日期过生日的用户一份礼物,就可以使用多文档更新。将“gift”增加到他们的帐号。
#创建集合
db.users1.insert({birthday: "10/13/1978"});
db.users1.update({birthday: "10/13/1978"},{$set : {gift: "Happy Birthday!"}},false,true);
> db.users1.findOne();
{
"_id" : ObjectId("583805d754ab78af61540ce5"),
"birthday" : "10/13/1978",
"gift" : "Happy Birthday!"
}
要知道多文档更新到底更新了多少文档,可以运行getLastError命令。键”n”的值就是要的数字。
db.count.insert({x:1},{$inc: {x:1}},false,true);
db.runComand({getLastError:1})
3.3.5 返回已更新的文档
用getLastError仅能获得有限的信息,并不能返回已更新的文档。这个可以通过findAndModify命令来做到。
findAndModify既有“update”键也有“remove”键。“remove”键表示将匹配到的文档从集合里面删除。
findAndModify命令中每个键对应的值如下所示:
l findAndModify :字符串,集合名
l query : 查询文档,用来检索文档的条件
l sort : 排序结果的条件
l update : 修改器文档,对所找到的文档执行的更新
l remove : 布尔类型,表示是否删除文档
l new:布尔类型,表示返回的是更新前的文档还是更新后的文档。默认是更新前的文档。
“update”和”remove”必须有一个,也只能有一个。
该命令一次只能处理一个文档,也不能执行upsert操作,只能更新已有文档。
3.4 瞬间完成
插入、删除和更新操作都是瞬间完成。
3.5 请求和连接
数据库会为每一个Mongodb数据库连接创建一个队列,存放这个连接的请求。当客户端发送一个请求,会被放到队列的末尾。
第4章 查询
本章会详细介绍查询,主要包括如下几个方面:
l 使用find或者findOne函数和查询文档对数据库执行查询
l 使用$条件查询实现范围、集合包含、不等式和其他查询
l 有些查询用查询文档,甚至$条件语句都不能表达。这时可以使用$where子句。
l 查询将会返回一个数据库游标,游标只有在你需要的时候才会惰性的批量返回文档
4.1 find简介
Mongodb使用find来进行查询。查询就是返回一个集合中文档的子集,子集合的范围从0个文档到整个集合。Find的第一个参数决定要返回哪些文档,其形式也是一个文档。
空的查询文档{ }会匹配集合的全部内容。如果不指定查询文档,默认就是{ }.
例如:要查找所有“age”的值为27的文档
db.foo.find({“age”:27})
也可以通过向查询文档加入多个键值对的方式来将多个查询组合在一起:
db.foo.find({“username”: “joe”,”age”: 27});
4.1.1 指定返回的键
有时不需要返回所有的键值对。可以使用find的第二个参数来指定想要的键。
例如:只返回集合中的“username”和”email“键感兴趣。
db.foo.find({},{“username”: 1, “email”:1})
也可以用第二个参数来剔除查询结果中的某个键值对。
例如:文档中有很多键,但不希望结果中含有”fatal_weakness”键。
db.users.find({}, {“fatal_weakness”: 0});
也可以用来防止“_id”返回
db.users.find({}, {“_id”:0});
4.1.2 限制
数据库关心的查询文档的值必须是常量。也就是不能引用文档中其他键的值。
4.2 查询条件
4.2.1 查询条件
“$lt”、“$lte”、“$gt”和”$gte”就是全部的比较操作符。分别对应<、<=、>、>=。
例如:查找一个18~30岁的用户:
db.users.find({“age”: {“gte“:18, “$lte”: 30}});
例如:要查找2016年1月1日前注册的用户
start=new Date(“01/01/2016”)
db.users.find({“registered”: {“$lt”:start}});
对于文档的键值不等于某个特定值的情况,就要使用条件操作符”$ne”.
例如:若要查找所有名字不为”joe”的人
db.users.find({“username”: {“$ne”: “joe”}})
4.2.2 OR查询
Mongodb中有两种方式进行OR查询。”$in“可以用来查询一个键的多个值。”$or”用来完成多个键值的任意给定值。
对于单一键要是有多个值与其匹配的话,就要用”$in”加一个条件数组。
例如:抽奖活动的中奖号码是725、542和390.要找出全部这些中奖数据。
db.raffle.find({“ticket_no”: {“$in” : [725,542,390]}})
“$in“可以指定不同的类型的条件和值。
例如:在逐步将用户的ID号迁移成用户名的过程中,要做两者兼顾的查询:
db.users.find({“user_id”: {“$in”: [12345,”joe”] }})
要是”$in”对应的数组只有一个值,那么和直接匹配这个值效果是一样的。
{ticket_no: {$in: [725]}} 和 {ticket_no: 725} 是一样的。
与”$in“相对应的是”$nin”,将返回与数组中所有条件都不匹配的文档。
例如:找出所有没有中奖的人
db.raffle.find({“ticket_no”: {“$nin”: [725,542,390]}})
“$or“接受一个包含所有可能条件的数组作为参数。
例如:要找到”ticket_no”为725或者”winter”为true的文档
db.raffle.find({“$or”: [{“ticket_no”: 725}, {“winter”: true}]})
“$or”可以含有其他条件句。
例如:要想将”ticket_no“与那3个值都匹配上,外加”winter“键
db.raffle.find({ “$or”: [{“ticket_no”: {“$in” : [725,542,390]}},{”winter”: true } ]})
4.2.3 $not
“$not”是元条件句,即可以用在任何其他条件之上。
“$mod”会将查询的值除以第一个给定值,若余数等于第二个给定值则返回该结果:
db.users.find( {“id_num”: {“$mod”: [5,1] } })
上面返回id_num的值为1、6、11、16. 若返回id_num为2、3、4、5、7、8、9、12、13、14、15时需要用到”$not”.
db.users.find( {“id_num”: {“$not”: {“$mod”: [5,1] }}})
4.2.4 条件句的规则
“$lt”在内层文档,”$inc”是外层文档的键。
条件句是内层文档的键,修改器则是外层文档的键。
可对一个键应用多个条件,但是一个键不能对应多个更新修改器。
4.3 特定于类型的查询
4.3.1 null
Null不仅可以匹配自身,而且匹配“不存在的”。 这种匹配会返回缺少这个键的所有文档
#创建集合c
db.c.insert({"y": null});
db.c.insert({"y": 1});
db.c.insert({"y": 2});
#查询键为y且值为null的记录
> db.c.find({"y":null});
{ "_id" : ObjectId("583841868b555139af48dc2c"), "y" : null }
#查询键为z且值为null的记录,如果找不到就会返回缺少这个键的所有文档
> db.c.find({"z":null});
{ "_id" : ObjectId("5838416a8b555139af48dc2b"), "y" : 2 }
{ "_id" : ObjectId("583841868b555139af48dc2c"), "y" : null }
{ "_id" : ObjectId("5838418a8b555139af48dc2d"), "y" : 1 }
如果要匹配键值为null的文档,既要检查键的值是否为null,还要通过”$exists”条件判定值已经已存在:
db.c.find( {"z": {"$in": [null], "$exists": true }});
4.3.2 正则表达式
例如:需要查找所有名为JOE或joe的用户,可以使用正则表达式忽略大小写。
db.users.find( {“name”: /joe/i } )
如果要匹配各种大小写组合形式的joey.
db.users.find( {“name”: /joey?/i })
正则表达式也可以匹配自身。
db.foo.insert({“bar”: /baz/})
db.foo.find( {“bar”: /baz/ })
4.3.3 查询数组
例如:如果水果清单是一个数组
db.food.insert( { "fruit": ["apple","banana","peach"]});
db.food.find({"fruit": "banana"});
1.$all
使用多个元素来匹配数组。可以使用$all
#创建集合food1
db.food1.insert( {"_id": 1, "fruit": ["apple","banana","peach"] });
db.food1.insert( {"_id": 2, "fruit": ["apple","kumquat","orange"] });
db.food1.insert( {"_id": 3, "fruit": ["cherry","banana","apple"] });
例如:要找出既有apple又有banana的文档。
> db.food1.find( {"fruit": {$all : ["apple","banana"]}});
{ "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
{ "_id" : 3, "fruit" : [ "cherry", "banana", "apple" ] }
若想查询数组指定位置的元素,则需使用key.index语法指定下标。下标从0开始:
db.food1.find( {“fruit.2”: “peach”})
2.$size
可以用来查询指定长度的数组。
> db.food1.find( {"fruit": {"$size": 3}})
{ "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
{ "_id" : 2, "fruit" : [ "apple", "kumquat", "orange" ] }
{ "_id" : 3, "fruit" : [ "cherry", "banana", "apple" ] }
db.food1.update( {“$push”: {“fruit”: “strawberry”}, “$inc”: {“$size”: 1}});
3.$slice操作符
$slice返回数组的一个子集合。
例如:有一个博客文章的文档,要想返回前10条评论。
db.blog.posts.findOne( criteria, {“comments”: {“$slice”: 10}});
返回后10条评论:
db.blog.posts.findOne( criteria, {“comments”: {“$slice”: -10}});
$slice也可接受偏移值和要返回的元素数量,来返回中间的结果:
db.blog.posts.findOne(criteria, {“comments”: {“$slice”: [23,10] } })
返回最后一条评论
db.blog.posts.findOne( criteria, {“comments”: {“$slice”: -1}});
4.3.4 查询内嵌文档
有2种方法查询内嵌文档:查询整个文档,或者只针对键值对进行查询。
如果允许的话,通常只针对内嵌文档的特定键值进行查询。
#创建集合
db.people.insert({"name": {"first": "Joe","last": "Schmoe"},"age": 45});
#使用点表示法查询内嵌键
> db.people.find({"name.first": "Joe","name.last": "Schmoe"});
{ "_id" : ObjectId("583862d0f0076b1462ca4dbd"), "name" : { "first" : "Joe", "last" : "Schmoe" }, "age" : 45 }
要正确的指定一组条件,而不用指定每个键,要使用”$elemMatch”.
#创建集合blog1
db.blog1.insert({"content": "...","coments": [ {"author": "joe","score": 3,"comment": "nice post"},{"author": "mary","score": 6,"coment": "terrible post"}]});
#查询mary发表的5分以上的评论
db.blog1.find({"coments": {"$elemMatch": {"author": "mary","score": {"$gte": 5}}}});
4.4 $where查询
$where典型的应用就是比较文档中的两个键的值是否相等。
#创建集合foo2
db.foo2.insert({"apple":1,"banana": 6,"peach": 3});
db.foo2.insert({"apple": 8,"spinach": 4, "watermelon": 4});
#查找文档中两个键的值是否相等
db.foo2.find({"$where": function() {
for (var current in this ) {
for (var other in this ) {
if (current != other && this[current] == this[other]) {
return true;
}
}
}
return false;
}});
结果:
{ "_id" : ObjectId("5838685bf0076b1462ca4dc0"), "apple" : 8, "spinach" : 4, "watermelon" : 4 }
建议:非必要时,不要使用$where,它在速度上比常规查询慢很多。
4.5 游标
数据库使用游标来返回find的执行结果。
#创建集合a
for(i=0;i<100;i++){db.a.insert({x: i});}
> db.a.find();
{ "_id" : ObjectId("5838729e647f29906eaaa13a"), "x" : 0 }
{ "_id" : ObjectId("5838729f647f29906eaaa13b"), "x" : 1 }
{ "_id" : ObjectId("5838729f647f29906eaaa13c"), "x" : 2 }
{ "_id" : ObjectId("5838729f647f29906eaaa13d"), "x" : 3 }
{ "_id" : ObjectId("5838729f647f29906eaaa13e"), "x" : 4 }
{ "_id" : ObjectId("5838729f647f29906eaaa13f"), "x" : 5 }
{ "_id" : ObjectId("5838729f647f29906eaaa140"), "x" : 6 }
{ "_id" : ObjectId("5838729f647f29906eaaa141"), "x" : 7 }
{ "_id" : ObjectId("5838729f647f29906eaaa142"), "x" : 8 }
{ "_id" : ObjectId("5838729f647f29906eaaa143"), "x" : 9 }
{ "_id" : ObjectId("5838729f647f29906eaaa144"), "x" : 10 }
{ "_id" : ObjectId("5838729f647f29906eaaa145"), "x" : 11 }
{ "_id" : ObjectId("5838729f647f29906eaaa146"), "x" : 12 }
{ "_id" : ObjectId("5838729f647f29906eaaa147"), "x" : 13 }
{ "_id" : ObjectId("5838729f647f29906eaaa148"), "x" : 14 }
{ "_id" : ObjectId("5838729f647f29906eaaa149"), "x" : 15 }
{ "_id" : ObjectId("5838729f647f29906eaaa14a"), "x" : 16 }
{ "_id" : ObjectId("5838729f647f29906eaaa14b"), "x" : 17 }
{ "_id" : ObjectId("5838729f647f29906eaaa14c"), "x" : 18 }
{ "_id" : ObjectId("5838729f647f29906eaaa14d"), "x" : 19 }
Type "it" for more
var cursor=db.collection.find();
while(cursor.hasNext()) { obj=cursor.next();};
cursor.hasNext()检查是否有后续结果存在,然后用cursor.next()将其获得。
游标类还实现了迭代接口,所以可以在foreach循环中使用。
> var cursor=db.people.find();
> cursor.forEach(function(x) {print (x.name);});
[object BSON]
> cursor.hasNext();
false
4.5.1 limit、skip和sort
最常用的查询选项就是限制返回结果的数量,忽略一定数量的结果并排序。所有这些选项一定要在查询被派发到服务器之前添加。
要限制结果数量,可在find后使用limit函数。只返回3个结果:
db.c.find().limit(3)
如果匹配的结果不到3个,则返回匹配数量的结果。Limit指定的是上限,而非下限。
Skip与limit类似:
#略过前三个匹配的文档,然后返回余下的文档。如果集合里面能匹配的文档少于3个,则不会返回任何文档
db.c.find().skip(3)
Sort用一个对象作为参数:一组键值对,键对应文档的键名,值代表排序的方向。排序方向可以是1(升序)或者-1(降序)。如果指定了多个键,则按照多个键的顺序逐个排序。
例如:按照”username”升序及”age”降序排序。
db.c.find().sort({username: 1, age:-1})
例如:如果想每页返回50个结果,而且按照价格从高到低排序:
db.stock.find({“desc”: “mp3”}).limit(50).sort({“price”: -1});
点击下一页,可以看到更多结果:
db.stock.find({“desc”: “mp3”}).limit(50).skip(50).sort({“price”: -1})
比较顺序
Mongodb处理不同类型的数据是有一个顺序的。从小到大,顺序如下:
(1) 最小值
(2) Null
(3) 数字(整型、长整型、双精度)
(4) 字符串
(5) 对象、文档
(6) 数组
(7) 二进制数据
(8) 对象ID
(9) 布尔型
(10) 日期型
(11) 时间戳
(12) 正则表达式
(13) 最大值
4.5.2 避免使用skip略过大量结果
1. 不用skip对结果分页
最简单的分页方法就是用limit返回结果的第一页,然后将每个后续页面作为相对于开始的偏移量返回。
Var page1=db.foo.find(criteria).limit(100)
要按照“date”降序显示文档。可用如下方式获取结果的第一页:
Var page1=db.foo.find().sort({“date”: -1}).limit(100)
然后可以利用最后一个文档中“date”的值作为查询条件,来获取下一页:
var latest=null;
while (page1.hasNext()) {
latest =page1.next();
display(latest);
}
var page2=db.foo.find({“date”: {“$gt”: latest.date}});
page2.sort({“date”: -1}).limit(100)
2. 随机选取文档
要随机选取文档,可以从集合里面查找一个随机元素。这需要在插入文档时给每个文档都添加一个额外的随机键。
例如:在shell中,可以用Math.random() (产生一个0~1的随机数):
>db.people.insert({“name”: “joe”,”random”: Math.random()});
要从集合中查找一个随机文档,只要计算个随机数并将其作为查询条件就好了。
>var random=Math.random();
>result=db.foo.findOne({“random”: {“$gt” : random}})
如果遇到的随机数比所有集合里面存着的随机值大,这时可能没有结果返回,可以这样做:
>if (result ==null ) { result=db.foo.findOne({“random”: {“$lt”: random}})};
4.5.3 高级查询选项
查询分为包装的和普通的。
【普通查询】
var cursor=db.foo.find({“foo”: “bar”})
【包装查询】
假设我们执行一个排序:
>var cursor=db.foo.find({“foo”: “bar”}).sort({“x”: 1})
它将查询包装在一个更大的文档中。Shell会把查询从{“foo”: “bar”}转换成{“$query”: {“foo”: “bar”}, “$orderby”: {“x”: 1}}.
下面列举了一些常用选项:
l $maxscan: integer 指定查询最多扫描的文档数量
l $min:document 查询的开始条件
l $max:document 查询的结束条件
l $hint:document 指定服务器使用哪个索引进行查询
l $explain:Boolean 获取查询执行的细节(用到的索引、结果数量、耗时等),而并
非真正执行查询。
l $snapshot:Boolean 确保查询的结果是在查询执行的那一刻的一致快照。
4.5.4 获取一致结果
数据处理通常做法就是:先把数据从Mongodb中取出来,然后经过转换再存回去。
但当结果很大时,就会出现问题了。机器的内存不足以存取大量数据。针对该问题,可以对查询进行快照。如果使用了”$snapshot”选项,查询就是针对不变的集合视图运行的。所有返回一组结果的查询实际上都进行了快照。不一致只在游标等待结果时集合内容被改变的情况下发生。
第5章 索引
索引就是来加速查询的。
5.1 索引简介
当查询中仅使用一个键时,可以对该键建立索引,以提高查询速度。创建索引要使用ensureIndex方法。
例如:对username添加索引:
db.people.ensureIndex({“username”: 1})
对于同一个集合,同样的索引只需要创建一次。
对某个键创建的索引会加速对该键的查询。但对于其他查询可能没有帮助,即便是查询包含了被索引的键。
实践证明,一定要创建查询中用到的所有键的索引。
例如:建立日期和用户名的索引
db.ensureIndex({“date”:1,”username”: 1})
一组值为1和-1的键,表示索引创建的方向。若索引只有一个键,方向无关紧要。
一般来说,如果索引包含N个键,则对于前几个键的查询都会有帮助。
MongoDB的查询优化器会重排查询项的顺序,以便利用索引。
每个集合默认的最大索引个数为64个。
5.1.1 扩展索引
建立索引时,要考虑如下问题:
(1) 会做什么样的查询?其中哪些键需要索引?
(2) 每个键的索引方向是怎样的?
(3) 如何应对扩展?有没有种不同的键的排列可以使常用数据更多的保留在内存中?
5.1.2 索引内嵌文档中的键
例如:想按日期搜索博客文章的评论,可以在内嵌的”coments”文档组成的数组中对”date”键创建索引:
>db.blog.ensureIndex({“coments.date”: 1})
5.1.3 为排序创建索引
5.1. 4 索引名称
默认情况下,索引名类似keyname1_dir1_keyname2_dir2…keynameN_dirN的形式。
其中keynameX表示索引的键,dirX表示索引的方向(1或-1)。
当然也可以通过ensureIndex来自定义索引的名字。
getLastError可用于检查索引未创建成功的原因。
5.2 唯一索引
唯一索引可以保证集合的每一个文档的指定键都有唯一值。
例如:想保证文档的“username”键都有不一样的值。
>db.people.ensureIndex({“username”: 1},{“unique”: true})
默认情况下,insert并不检查文档是否插入过了。
5.2.1 消除重复
dropDups选项可以保留发现的第一个文档,而删除接下来的有重复值的文档。
>db.people.ensureIndex({“username”: 1},{“unique”: true,”dropDups”: true})
5.2.2 复合唯一索引
创建复合唯一索引时,单个键的值可以相同,只要所有键的值组合起来不同就好。
GirdFS是MongoDB中存储大文件的标准方式。
5.3 使用explain和hint
只要对游标调用该方法,就能得到查询细节。Explain会返回一个文档,而不是游标本身。
>db.foo.find().explain();
Explain会返回查询使用的索引情况,耗时及扫描文档数的统计信息。
“cursor”: “BasicCursor” 说明查询没有用到索引
“nsanned” :64 这个数字代表数据库查找了多少个文档
”n“:64 代表返回文档的数量
”millis” : 0 这个毫秒数表示数据库执行查询的时间。0是非常理想的成绩
如果发现Mongodb用了非预期的索引,可以用hint强制使用某个索引。
例如:在Mongodb中使用{”username”: 1,”age”: 1} 索引,则需要:
>db.c.find({“age”: 14,”username”: /.*/ }).hint({“username”: 1,”age”: 1})
5.4 索引管理
索引的元信息存储在每个数据库的system.indexes集合中。这是一个保留集合,不能对其插入或者删除文档。操作只能通过ensureIndex或者dropIndexes进行。
System.indexes包含每个索引的详细信息,同时system.namespaces集合也含有索引的名字。
查看该集合,会发现每个集合至少有两个文档与之对应,一个对应集合本身,一个对应集合包含的索引。
“_id”索引的命名空间需要额外的6个字节。
集合名和索引名加起来不能超过127字节。
修改索引
使用{“backgroud”: true}选项可以使建立索引的过程在后台完成。
使用dropIndexes加上索引名可以删除索引。
#删除集合foo中的索引alphabet
>db.runCommand({“dropIndexes”: “foo”, “index”: “alphabet”})
要删除所有索引,可以将index的值赋为*
>db.runComand({“dropIndexes”: “foo”, “index”: “*”})
删除集合也能删除索引。
删除集合中的所有文档不影响索引,当有新文档插入时还会再生的。
5.5 地理空间索引
Mongodb为坐标平面查询提供了专门的索引,叫地理空间索引。
地理空间索引可以由ensureIndex来创建,不过参数变成了”2d”.
>db.map.ensureIndex({“gps”: “2d” })
“gps”键的值必须是某种形式的一对值:一个包含两个元素的数组或者包含两个键的内嵌文档。例如:
{”gps”: [0,100] }
{“gps”: {“x”: -30, “y”: 30 }}
{“gps”: {“latitude”: -180, “longitude”: 180 }}
默认情况下,地理空间索引假设值的范围是-180~180. 如果想用其他值,可以通过ensureIndex的选项来指定最大最小值:
>db.star.trek.ensureIndex({“light-years”: “2d”}, {“min”: -1000,”max”: 1000})
这样就创建了一个2000光年见方的空间索引。
地理空间索引的查询:使用find或者数据库命令。
Find查询时,使用了“$near”,需要两个目标值的数组作为参数:
>db.map.find({“gps”: {“$near”: [40,-73 ]}})
这会按照离点(40,-73)由近及远的方式将map集合的所有文档都返回。在没有指定limit值时,默认是100个文档。例如,可设置返回10个文档。
>db.map.find({“gps”: {“$near”: [40,-73 ]}}).limit(10)
也可以用geoNear完成相同的操作:
>db.runCommand({geoNear: “map”,near: [40,-73],num: 10})
geoNear还会返回每个文档到查询点的距离。这个距离以你插入的数据为单位,如果按照经纬度的角度插入,则距离就是经纬度。
Mongodb还能找到指定形状内的文档,就是将原来的”$near”换成”$within”. “$within”获取数量不断增加的形状作为参数。例如:可以查询矩形和圆形内的所有点。
对于矩形,使用”$box”选项:
>db.map.find({“gps”: {“$within”: {“$box”: [ [10,20], [15,30] ] }}})
$box参数是两个元素的数组,第一个元素指定了左下角的图标,第二个指定右上角的图标。
可以使用”$center”来找到圆形内部的所有点,参数:圆心和半径。
>db.map.find({“gps”: {“$within”: {“$center”:[ [12,25],5 ] } } })
5.5.1 复合地理空间索引
可以普通索引和地理空间索引结合起来,就是复合地理空间索引。
例如:要查询”location”和”desc”, 可以创建索引:
>db.ensureIndex( {“location”: “2d”, “desc”: 1})
查找附近的咖啡馆:
>db.map.find({“location”: {“$near”: [-70,30]},”desc”: “coffeeshop”}).limit(1)
第6章 聚合
6.1 count
Count用来返回集合中的文档数量:
>db.foo.count()
Count也可以传递查询,MongoDB则会计算查询结果的数量:
>db.foo.count({“x”: 1})
6.2 DISTINCT
distinct用来找出给定键的所有不同的值。使用时必须指定集合和键。
>db.runCommand({“distinct”: “people”,”key”:”age”})
6.3 group
Group首先选定分组所依据的键,然后MongoDB就会将集合依据选定值的不同分成若干组。然后通过聚合每一组内的文档,产生一个结果文档。
例如:想获取每天股票的收盘价列表。
可以先把集合按照天分组,然后在每一组里取包含最新时间戳的文档,将其放置到结果中就OK了。过程如下:
>db.runComand({
“group”: {“ns”: “stocks”,
”key”: “day”,
”initial”: {“time”:0},
”$reduce”: function(doc,prev) {
if (doc.time>prev.time) {
prev.price=doc.price;
prev.time=doc.time;
}
} } } )
“ns”: “stocks” 指定要分组的集合
“key”: “day” 指定文档分组依据的键。这里就是day。所有day的值相同的文档划分到一组
“initial”: {“time”:0} 每一组reduce函数调用的初始时间。会作为初始文档传递给后续过程。每一组的所有成员都会使用这个累加器,所以改变会保留住。
“$reduce”:function(doc,prev) {…} 每个文档都对应一次这个调用。系统会传递两个参数:当前文档和累加器文档。
6.3.1 使用完成器
完成器(finalizer)用以精简从数据库传到用户的数据。
Finalize附带一个函数,在每组结果传递到客户端之前被调用一次。可以用其修剪结果中的“残枝败叶”:
6.3.2 将函数作为键使用
为了消除大小写的影响,就要定义一个函数来确定文档分组所依据的键。
定义分组函数要用到$keyf键。有了”$keyf”就能依据各种复杂的条件进行分组了。
>db.posts.group({“ns”: “posts”,
“$keyf”: function(x) { return x.category.toLowerCase(); },
“initializer”: … } )
6.4 MapReduce
MapReduce是一个可以轻松并行化到多个服务器的聚合方法。它会拆分问题,再将各个部分发送到不同的机器上,让每台机器都完成一部分。当所有机器都完成的时候,再把结果汇集起来形成最终完整的结果。
MapReduce需要几个步骤:
l 映射
将操作映射到集合中的每个文档。该操作要么“无作为”,要么“产生一些键和X个值”。
l 洗牌
按照键分组,并将产生的键值组成列表放到对应的键中。
l 化简
将列表中的值化简成一个单值。
l 再洗牌
化简后的值被返回,然后继续洗牌,直到每个键的列表只有一个值为止,这个值就是最终结果。
使用MapReduce的代价就是速度:MapRecude很慢,绝不要在实时环境中。要作为后台任务来运行MapReduce,将创建一个保存结果的集合,可以对这个集合进行实时查询。
6.4.1 例1:找出集合中的所有键
在映射环节,要得到文档中的每个键。Map函数使用函数emit“返回”要处理的值。
Emit会给MapReduce一个键和一个值。用emit将文档某个键的计数(count)返回({count:1}).
要为每个键单独计数,所以为文档中的每一个键调用一次emit. This就是当前映射文档的引用:
>map=function() {
For (var key in this) {
Emit (key,{ count :1} );
}};
这样就有了许多{count:1}文档,每一个都与集合中的一个键相关。这种由一个或多个{count:1}文档组成的数组,会传递给reduce函数。Reduce函数有两个参数:一个是key,就是emit返回的第一个值;一个是数组,由一个或者多个对应于键的{count:1}文档组成。
>reduce=function(key,emits) {
total=0
for (var i in emits) {
total+=emits(i).count;
}
Return {“count”: total};
}
Reduce一定能被反复调用,不论是映射环节还是前一个简化环节。所以Reduce返回的文档必须能作为reduce的第二个参数的一个元素。
例如:x键映射到了3个文档{count:1, id:1}、{count:1, id:2}和{count:1, id:3},其中id键用于区别。MongoDB调用reduce:
>r1=reduce(“x”, [{count:1,id:1},{“count”:1,id:2}] )
{count:2}
>r2=reduce(“x”, [{count:1,id:3}])
{count:1}
>reduce({“x”, [r1,r2]})
{count:3}
Reduce应该能处理emit文档和其他reduce结果的各种组合。
MapReduce函数:
>mr=db.runCommand({“mapreduce”:”foo”,”map”:map,”reduce”:reduce})
{
“result”: “tmp.mr.mapreduce_1266787811_1”,
“timeMilis”: 12,
“counts”: {
“input”: 6
“emit”: 14
“output”: 5
},
“ok”: true
}
MapReduce返回的文档包含很多与操作有关的元信息:
“result”: “tmp.mr.mapreduce_1266787811_1” 存放MapReduce结果的集合名。这是个临时集合,MapReduce的连接关闭后自动就删除了。
“timeMilis”: 12 操作花费的时间,单位是毫秒
“counts” : {…} 这个内嵌文档包含3个键
“input”: 6 发送到map函数的文档个数
“emit”: 14 在map函数中emit被调用的次数
“output”: 5 结果集合中创建的文档数量
对结果集合进行查询发现原有集合的所有键及其计数:
>db[mr.result].find()
每个键都变成”_id”,最终简化步骤的结果变为”value”.
6.4.2 例2 :网页分类
例如:可以用MapReduce找出某个网站哪个主题最热门,热门与否由最近的投票决定。
(1) 首先创建一个map函数,发出(emit)标签和一个基于流行度和新近程度的值;
map=function() {
for (var i in this.tags ) {
var recency=1/(new Date() – this.date);
var score=recency * this.score;
emit(this.tags[i], {“urls”: [this.url], “score”: score});
}
}
(2) 化简同一个标签的所有值,形成这个标签的分数;
Reduce=function(key,emits) {
var total = {urls: []. score:0}
for (var I in emits) {
emits[i].urls.forEach(function(url)) {
total.urls.push(url);
}
total.score += emits[i].score;
}
return total;
};
最终的集合包含每个标签的URL列表和表示该标签流行程度的分数。
6.4.3 MongoDB和MapReduce
Mapreduce、map和reduce这3个键是必需的,但MapReduce命令还有很多可选的键。
l “finalize”: 函数
将reduce的结果发送给这个键,这是处理过程的最后一步。
l “keeptemp”: 布尔
连接关闭时临时结果集合是否保存。
l “output”: 字符串
结果集合的名字。设定该项则隐含着keeptemp:true.
l “query”: 文档
会在发往map函数前,先用指定条件过滤文档。
l “sort”: 文档
在发送map前先给文档排序(与limit一同使用非常有用)
l “limit”: 整数
发往map函数的文档数量的上限
l “scope”: 文档
JavaScript代码中要用到的变量
l “verbose”: 布尔
是否产生更加详尽的服务器日志
1. finalize函数
MapReduce可以使用finalize函数作为参数。它会在最后reduce得到输出后执行,然后将结果存到临时集合中。
2. 保留结果集合
默认情况下,Mongo在执行MapReduce时创建一个临时集合,集合名是系统选的不常用名字,其中含有mr,执行MapReduce的集合名,时间戳,数据库作业ID,将这些用“.”连成一个字符串。MongoDB会在调用的连接关闭时自动销毁这个集合。如果想保留这个集合,就要指定keeptemp为true.
利用out选项可以指定临时集合的名字。
3. 对文档子集合执行MapReduce
有时需要对集合的一部分执行MapReduce。只需要在传给map函数前添加一个查询来过滤文档即可。
每个传递给map函数的文档都要事先反序列化,从BSON转换成JavaScript对象。
“query”键的值是一个查询文档。通常查询返回的结果就传递给了map函数。
例如:一个应用程序做跟踪分析,需要上周的概要,
>db.runCommand({“mapreduce”: “analytics”,”map”:map,”reduce”:reduce,”query”: {“date”: {“$gt”: week_ago}}})
Sort选项一般和limit一同使用。Limit也可单独使用,用来截取一部分文档发送给map函数。
4. 使用作用域
MapReduce可以为map、reduce、finalize函数都采用一种代码类型。
Reduce有自己的作用域键”scope”,如果想在MapReduce中使用客户端的值,就必须使用这个参数。可以用”变量名:值”这样的普通文档来设置该选项,然后在map、reduce和finalize函数中使用。
5. 获得更多的输出
如果想查看MapReduce的运行过程,可以用”verbose”: true.
可用print打印信息输出到服务器日志上。
第7章 进阶指南
本章内容包含:
l 通过数据库命令使用高级特性
l 使用一种特殊的集合—固定大小的集合
l 使用GridFS存储大文件
l 利用MongoDB对服务端JavaScript的支持
l 理解数据库引用,何时使用
7.1 数据库命令
7.1.1 命令的工作原理
db.test.drop(); #删除一个集合
db.runCommand({“drop”: “test”}); #等同于上条命令
7.1.2 命令参考
获取Mongo中所有命令的2种方式:
l 在shell中运行db.listCommands(); 或者从驱动程序中运行list-Commands;
l 浏览管理员接口
MongoDB常用的命令:
l buildInfo
{“buildInfo”: 1}
管理专用命令,返回MongoDB服务器的版本号和主机的操作系统
l collStats
{“collStats”: collection}
返回指定集合的统计信息,包括数据大小,已分配的存储空间和索引的大小
l distinct
{"distinct”: collection, “key”: key,”query”: query}
列出指定集合中满足查询条件的文档的指定键的所有不同值
l drop
{"drop”: collection}
删除集合的所有数据
l dropDatabase
{"dropDatabase”: 1}
删除当前数据库的所有数据
l dropIndexes
{"dropIndexes”: collection,”index”: name}
删除集合里面名称为name的索引,如果名称为”*”,则删除全部索引
l findAndModify
见第3章
l getLastError
{“getLastError”: 1 [, “w”: w [, “wtimeout”: timeout ] ] }
查看本集合执行的最后一次操作的错误信息或其他状态信息。在w台服务器上复制集合的最后操作之前,这个命令会阻塞(超时的毫秒数到了)。
l isMaster
{"isMaster”: 1}
检查本服务器是主服务器还是从服务器。
l ListCommands
{"ListCommands”: 1}
返回所有可以在服务器上运行的命令及相关信息
l listDatabases
{“listDatabses” :1 }
管理专用命令,列出服务器上的所有数据库。
l Ping
{“ping”: 1}
检查服务器链接是否正常。即便服务器锁定,该命令也会立刻返回。
l renameCollection
{"renameCollection”: a, “to”: b}
将集合a重命名为b,其中a和b都必须是完整的集合命名空间
l repairDatabase
{“repairDatabases”:1}
修复并压缩当前数据库,该操作比较耗时。
l serverStatus
{“serverStatus”: 1}
返回这台服务器的管理统计信息
7.2 固定集合
固定集合要事先创建,而且大小固定。
固定集合很像一个环状队列,如果空间不足,最早的文档就会被删除,为新的文档腾出空间。
固定集合中的文档以插入的顺序存储,而且不必维护一个已删除的文档的释放空间列表。
默认情况下,固定集合没有索引,即便是”_id”也没有索引。
7.2.1 属性及用法
l 对固定集合进行插入速度极快。做插入操作时,无需额外分配空间,服务器也不必查找空闲列表来放置文档。直接将文档插入集合末尾即可。插入也无需更新索引。
l 按照插入顺序输出的查询速度极快。返回结果的顺序就是文档在磁盘上的顺序。默认情况下,对固定集合进行查找都会以插入顺序返回结果。
l 固定集合能够在新数据插入时,自动淘汰最早的数据。
7.2.2 创建固定集合
固定集合必须要在使用前显式的创建。使用create命令创建。在Shell中,可以使用createCollection来创建:
>db.createCollection(“my_collections”,{capped: true, size: 10000});
createCollection还能指定文档数量的上限:
>db.createCollection(“my_collections”, {capped: true, size:10000, max: 100});
还可通过转换已有的普通集合的方式来创建固定集合。使用convertTocapped命令。
例如:将test集合转换成大小为10000字节的固定集合:
>db.runCommand( {convertTocapped: “test”, size:10000);
7.2.3 自然排序
自然排序就是文档在磁盘上的顺序。
默认情况下,查询固定集合后就是按照插入顺序返回文档。也可以使用自然排序按照反向插入的顺序查询:
>db.my_collections.find().sort( {“$natural” : -1 } )
使用{“$natural”: 1}表示与默认顺序相同。
7.2.4 尾部游标
尾部游标是一种特殊的持久游标,该类游标不会在没有结果后销毁。
因为该类游标在没有结果后也不销毁,所以一旦有新文档添加到集合里面就会被取回并输出。
尾部游标只能用在固定集合上。
Mongo Shell不支持尾部游标。
7.3 GridFS:存储文件
GridFS是一种在MongoDB中存储大二进制文件的机制。使用GridFS存文件有如下原因:
l 利用GridFS可以简化需求。要是已经用了MongoDB,GridFS就可以不需要使用独立文件存储架构。
l GridFS会直接利用业已建立的复制或分片机制,所以对文件存储来说故障恢复和扩展都很容易。
l GridFS可以避免用于存储用户上传内容的文件系统出现的某些问题。
l GridFS不产生磁盘碎片,因为MongoDB分配数据文件空间时以2GB为一块。
7.3.1 开始使用GridFS: mongofiles
Mongofiles内置在MongoDB发布版中,可以用来在GridFS中上传、下载、列举、查找或删除文件。
例如:利用mongofiles从文件系统向GridFS上传文件,列出GridFS中的所有文件,下载刚上传的文件。
./mongofiles put foo.txt #上传文件
./mongofiles list #查看已上传的文件
./mongofiles get foo.txt #下载已上传的文件
put: 将文件系统中的一个文件添加到GridFS中;
list: 将所有添加到GridFS中的文件列出来;
get:将GridFS中的文件写入到文件系统中。
Search : 用来按文件名查找GridFS中的文件;
Delete : 从GridFS中删除一个文件
7.3.2 通过MongoDB驱动程序操作GridFS
略
7.3.3 内部原理
GridFS的原理就是将大文件分成很多块,每块作为一个单独的文档存储,这样就能存储大文件了。由于MongoDB支持在文档中存储二进制数据,可以最大限度减小块的存储开销。
除了存储文件本身的块,还有一个单独的文档用来存储分块的信息和文件的元数据。
GridFS的块有个单独的集合。默认情况下,块将使用fs.chunk集合,如有需要可以覆盖。
这个块集合里面文档结构比较简单:
{
“_id”: ObjectId(“…”),
“n” : 0,
“data” : BinData(“…”),
“files_id” : ObjectId(“…”)
}
块有自己唯一的”_id”.
“files_id”是包含这个块元数据的文件文档的”_id”.
“n”表示块编号,即这个块在原文件中的顺序编号。
“data”包含组成文件块的二进制数据。
文件的元数据放在另一个集合中,默认为fs.files.
GridFS规范定义了一些键:
l _id
文件唯一的id,在块中作为”files_id”键的值存储。
l length
文件内容总的字节数
l chunksize
每块的大小,以字节为单位。默认是256k。
l uploadDate
文件存入GridFS的时间戳。
l md5
文件内容的MD5校验和,由服务器端生成。
md5的值是由服务器端用filemd5命令生成的,用于计算上传块的md5校验和。用户可以通过检验md5键的这个值,确保文件正确上传了。
例如:可使用distinct命令获取GridFS中不重复的文件名列表
>db.fs.files.distinct(“filename”)
7.4 服务器端脚本
在服务器端可通过db.eval函数来执行JavaScript脚本。也可以将JavaScript脚本保存在数据库中,然后在别的数据库命令中调用。
7.4.1 db.eval
该函数先将给定的JavaScript字符串传送给MongoDB,然后返回结果。
db.eval可以用来模拟多文档事务:db.eval锁住数据库,然后执行JavaScript再解锁。
发送代码有2种选择,或者封装进一个函数,或者不封装。以下2种方法是等价的:
>db.eval(“return 1;”)
>db.eval(“function() {“return 1;”})
只有传递参数时,才必须要封装成一个函数。参数通过db.eval的第二个参数传递,要写成一个数组的形式。
例如:想传递一个参数给username:
>db.eval(“function(u) { print (‘Hello ,’+u+’!’); }”, [username] )
传递多个参数。例如:要计算3个数的和:
>db.eval(“function(x,y,z) { return x+y+z;}”, [num1,num2,num3 ] )
num1对应x, num2对应y, num3对应z.
调试的一个好方法就是将调试信息写进数据库日志中,可通过print来完成:
>db.eval(“print (‘Hello,world’);”);
7.4.2 存储JavaScript
每个MongoDB数据库中都有一个特殊的集合,叫做system.js,用来存放JavaScript变量。
这些变量可以在任何MongoDB的JavaScript上下文调用,包括”$where”子句,db.eval调用,MapReduce作业。
例如:用insert将变量加入system.js中
>db.system.js.insert( {“_id”: “x”, “value”: 1 })
>db.system.js.insert( {“_id”: “y”, “value”: 2 })
>db.system.js.insert( {“_id”: “z”, “value”: 3 })
上例在全局作用域定义了x,y,z.现在对其求和:
>db.eval(“return x+y+z;”)
system.js也能用来存放JavaScript。
例如:要用JavaScript写一个日志函数,就可以将其存放到system.js中
>db.system.js.insert( {“_id”: “log”, “value”:
function(msg,level) {
var levels= [“DEBUG”,”WARN”,”ERROR”,”FATAL”];
level=level ? level : 0; //check if level is defined
var now=new Date();
print (now + “ “ + levels[level] + msg);
}})
使用任意的JavaScript程序中调用这个函数:
>db.eval(“x=1; log(‘x is ’ +x); x=2; log(‘x is greater than 1’, 1); “);
使用存储的JavaScript缺点是代码会与常规的源代码控制脱离,会搅乱客户端发送来的JavaScript.
7.4.3 安全性
略
7.5 数据库引用
数据库引用称作DBRef. 它就像URL,唯一确定一个到文档的引用。
7.5.1 什么是DBRef
DBRef是个内嵌文档。
DBRef指向一个集合,还有一个id_value用来在集合里面根据”_id”确定唯一的文档。
这两条信息使得DBRef能唯一标识MongoDB数据库内的任何一个文档。
若想引用另一个数据库中的文档,DBRef中有个可选键”$db”.
{“$ref”: collections, “$id”: id_value, “$db”: database}
DBRef中的键的顺序不能改变,第一个必须是”$ref”,接着是”$id”,然后是”$db”.
7.5.2 示例模式
略
第8章 管理
本章内容包括:
l MongoDB是一个普通的命令行程序,用mongod调用;
l MongoDB提供了内置的管理接口和监控功能,易于与第三方监控包集成;
l MongoDB支持基本的、数据库级别的用户认证,包括只读用户,以及独立的管理员权限。
l 有多种方式备份MongoDB,主要取决于实际的情况该用哪种。
8.1 启动和停止MongoDB
8.1.1 从命令行启动
执行mongod,启动MongoDB服务器。一些主要选项如下:
l --dbpath
指定数据目录;默认值是/data/db.每个mongod进程都需要独立的数据目录,所以要是有3个mongod实例,必须要有3个独立的数据目录。当mongod启动时,会在数据目录中创建mongod.lock文件,该文件用于防止其他mongod进程使用该数据目录。
l --port
指定服务器监听的端口号。默认端口号是27017。要是运行多个mongod进程,则要给每个指定不同的端口号。
l --fork
以守护进程的方式运行MongoDB,创建服务器进程。
l --logpath
指定日志输出路径,而不是输出到命令行。如果对文件夹有写权限,系统会在文件不存在时创建它。它会将已有文件覆盖掉,清除所有原来的日志记录。如果想保留原来的日志,还需要使用—logappend选项。
l --config
指定配置文件,加载命令行未指定的各种选项。
8.1.2 配置文件
指定配置文件可使用-f或—config选项。
配置文件的特点:
l 以#开头的行是注释;
l 指定选项的语法就是这种“选项=值”的形式,其中选项是区分大小写的;
l 命令行中的那些如—fork的开关选项,其值要设为true.
8.1.3 停止MongoDB
最基本的方法就是向MongoDB服务器发送一个SIGINT或者SIGTERM信号。
另一种方法就是使用shutdown命令。这是管理命令,要在admin数据库下使用。
Shell提供了辅助函数,来简化这一过程:
>use admin
>db.shutdownServer();
8.2 监控
8.2.1 使用管理接口
默认情况下,启动mongod时还会启动一个基本的HTTP服务器,该服务器监听的端口号比主服务器的端口号大1000。该服务器提供了HTTP接口,用于查看MongoDB的一些基本信息。
要想利用好管理接口,需要用—rest选项开启REST支持。也可以在启动mongod时使用
--nohttpinterface关闭管理接口。
8.2.2 serverStatus
获取运行中的MongoDB服务器的统计信息,可使用工具serverStatus.
>db.runCommand({“serverStatus”: 1})
serverStatus呈现了比如当前服务器版本、运行时间、当前连接数等。
“globalLock”: 表示全局写入锁占用了服务器多少时间。
“men”: 包含服务器内存映射了多少数据,服务器进程的虚拟内存和常驻内存的占用情况
“indexCounters”: 表示B树在磁盘检索和内存检索的次数
“backgroundFlushing”: 表示后台做了多少次fsync以及用了多少时间。
“opcounters”: 包含了每种主要操作的次数
“asserts”: 统计断言的次数
8.2.3 mongostat
Mongostat输出一些serverStatus提供的重要信息。
8.3 安全和认证
8.3.1 认证的基础知识
如果开启了安全检查,则只有数据库认证用户才能执行读或写操作。
在认证的上下文,MongoDB会将普通的数据作为admin数据库处理。admin数据库中的用户被视为超级用户。在认证之后,管理员可以读写所有数据库,执行特定的管理命令。
在shell中创建只读用户只要将addUSer的第3个参数设为true即可。
调用addUSer必须有相应数据库的写权限。
加入—auth命令行选项,即可开启安全检查。
8.3.2 认证的工作原理
数据库的用户账号以文档的形式存储在system.users集合中。
文档的结构是{“user”:username,”readOnly” : true,”pwd”: password hash}
Password hash是根据用户名和密码生成的散列。
例如:在system.users集合中删掉用户账号文档,就可以删除用户
>db.auth(“test_user”, “efgh”)
>db.system.users.remove( {“user”: “test_user”} );
8.3.3 其他安全考虑
建议将MongoDB服务器布置在防火墙后或者布置在只有应用服务器能访问的网络中。
MongoDB想被外部访问到的话,可以使用—bindip选项。
使用—noscripting可完全禁止服务端JavaScript的运行。
8.4 备份和修复
8.4.1 数据文件备份
数据文件目录默认是/data/db.
所以备份MongoDB,只要创建数据目录的副本即可。
备份时,先关闭服务器,然后进行备份。
8.4.2 mongodump和mongorestore
Mongodump是一种能在运行时备份的方法。
Mongodump对运行的MongoDB做查询,然后将所有查到的文档写入磁盘。
Mongorestore获取mongodump的输出结果,并将备份的数据插入到运行的MongoDB实例中。
例如:从数据库test到backup的热备份,然后调用mongorestore进行恢复
./mongodump –d test –o backup
./mongorestore –d foo –drop backup/test/
-d指定要恢复的数据库,这里是foo.
--drop 代表在恢复前删除集合。否则,数据就会与现有集合数据合并。
8.4.3 fsync和锁
MongoDB的fsync命令能在MongoDB运行时复制数据目录还不会损毁数据。
Fsync命令强制服务器将所有缓冲区写入磁盘。还可以选择上锁阻止对数据库的进一步写入,知道释放锁为止。写入锁是让fsync在备份时发挥作用的关键。
>use admin
>db.runCommand({“fsync”: 1, “lock”: 1});
解锁:
>db.$cmd.sys.unlock.findOne();
>db.currentOp(); #确保已经解锁
8.4.4 从属备份
略
8.4.5 修复
修复所有数据库最简单的方式就是加上—repair来启动服务器。mongo –repair
修复数据库的过程:将所有的文档导出然后马上导入,忽略那些无效的文档。
在shell中使用repairDatabase修复运行中的数据库:
>use test
>db.repairDatabase();
第9章 复制
9.1 主从复制
最基本的设置方式就是建立一个主节点和一个或多个从节点,每个从节点要知道主节点的地址。
Mongod --master #启动主服务器
Mongod --slave --source master_address #启动从服务器,master_address为主服务器地址
9.1.1 选项
主从复制一些有用的选项:
l --only
在从节点上指定只复制特定某个数据库(默认复制所有数据库)
l --slavedelay
用在从节点上,当应用主节点的操作时增加延时(单位:秒)。这种节点对用户无意删除重要文档或者插入垃圾数据等事故有很好的防护作用。
l --fastsync
以主节点的数据快照为基础启动从节点。
l --autoresync
如果从节点与主节点不同步了,则自动重新同步。
l --oplogsize
主节点oplog的大小(单位:MB)。
9.1.2 添加及删除源
启动从节点时,可以用—source指定主节点,也可以在shell中配置这个源。
例如:假设主节点绑定了localhost:27017.
#将localhost:27017作为源添加到从节点上:
>use local
>db.source.insert({“host”: “localhost:27017”})
如果想更改从节点的配置,改用prod.example.com为源,可以用insert和remove来完成:
>db.source.insert( {“host”: “prod.example.com:27017”})
>db.source.remove({“host”: “localhost:27017”})
9.2 副本集
副本集就是有自动故障恢复功能的主从集群。主从集群和副本集最明显的区别就是副本集没有固定的“主节点”:整个集群会选举出一个“主节点”,当其不能工作时则变更到其他节点。
9.2.1 初始化副本集
1.首先为每个服务器创建数据目录,选择端口:
mkdir -p ~/dbs/node1 ~/dbs/node2
2.命名副本集为blort
3.启动服务器。--replSet选项的作用是让服务器知晓这个blort副本集中还有别的同伴
./mongod –dbpath ~/dbs/node1 --port 10001 –replSet blort/morton:10002
4.启动另一台服务器
./mongod –dbpath ~/dbs/node2 --port 10002 --replSet blort/morton:10001
如果想添加第3台服务器,有2种方式:
./mongod --dbpath ~/dbs/node3 –port 10003 --replSet blort/morton:10003
./mongod --dbpath ~/dbs/node3 –port 10003 --replSet blort/morton:10001,morton:10002
副本集有自检功能:在其中指定单台服务器后,MongoDB就会自动搜索并连接其余的节点。
5.初始化副本集
在shell中,连接其中一台服务器:
>./mongo morton:10001/admin
>db.runCommand( {“replSetInitiate”: {
“_id”: “blort”,
“members”: [
{
“_id”:1,
“host”: “morton:10001”
},
{
“_id”: 2,
“host”: “Morton:10002”
}
] } } )
“_id”: “blort” 副本集的名字
“members”: […] 副本集中的服务器列表。每个服务器文档至少有两个键
“_id”: N 每个服务器的唯一ID
“host”:hostname 这个键指定服务器主机
9.2.2 副本集中的节点
任何时间,集群只有一个活跃节点,其他的都为备份节点。
指定的活跃节点可以随时间而改变。
有几种不同类型的节点可以存在于副本集中:
l standard
常规节点,它存储一份完整的数据副本,参与投票选举,有可能成为活跃节点。
l passive
存储了完整的数据副本,参与投票,不能成为活跃节点。
l arbiter
仲裁者只参与投票,不接收复制的数据,也不能成为活跃节点。
标准节点和被动节点之间的区别就是数量的差别:每个参与节点(非仲裁者)有个优先权。
优先值为0则是被动的,不能成为活跃节点。优先值不为0,则按照由大到小选出活跃节点,优先值一样的话则看谁的数据比较新。所以,要是有两个优先值为1和一个优先值为0.5的节点,最后一个节点只有在前两个节点都不可用时才能成为活跃节点。
在节点配置中修改priority键,来配置成标准节点或者被动节点。
>members.push({
“_Id”: 3,
“host”: “morton:10003”,
“priority”: 40
});
默认优先级为1,可以是0~1000.
“arbiterOnly”键可以指定仲裁节点:
>members.push( {
“_id”: 4,
“hosts”: “morton:10004”,
“arbiterOnly”: true
});
备份节点会从活跃节点抽取oplog,并执行操作。
活跃节点会写操作到自己的本地oplog,这样就能成为活跃节点了。
9.2.3 故障切换和活跃节点选举
如果活跃节点坏了,其余节点会选一个新的活跃节点出来。选举过程可以由任何非活跃节点发起。新的活跃节点由副本集中的大多数选举产生。仲裁节点也会参与投票。
新的活跃节点将是优先级最高的节点,优先级相同则数据较新的节点获胜。
活跃节点使用心跳来跟踪集群中有多少节点对其可见。如果不够半数,活跃节点会自动降为备份节点。这样能防止活跃节点一直不放权。
9.3在从服务器上执行操作
9.2.4 读扩展
用MongoDB扩展读取的一种方式就是将查询放在从节点上。
9.3.2 用从节点做数据处理
从节点的另一个用途就是作为一种机制来减轻密集型处理的负载,或作为聚合,避免影响主节点的性能。
用—master参数启动一个普通的从节点。
9.4工作原理
MongoDB的复制至少需要两个服务器或节点。
其中一个是主节点,负责处理客户端请求,其他的都是从节点,负责映射主节点的数据。
主节点记录在其上执行的所有操作。
从节点定期轮询主节点获得这些操作,然后对自己的数据副本执行这些操作。
由于和主节点执行了相同的操作,从节点就能保持与主节点的数据同步。
9.4.1 oplog
主节点的操作记录称为oplog. Oplog存储在一个特殊的数据库中,叫做local.
oplog就在其中的oplog.$main集合里面。Oplog中的每个文档都代表主节点上执行的一个操作。文档包含的键如下:
l ts
操作的时间戳。时间戳是一种内部类型,用于跟踪操作执行的时间。由4字节的时间戳和4字节的递增计数器构成。
l op
操作类型,只有1字节代码。(例如:i代表插入)
l ns
执行操作的命名空间(集合名)。
l o
进一步指定要执行的操作的文档。对插入来说,就是要插入的文档。
Oplog只记录改变数据库状态的操作。例如:查询就不再存储在oplog中。
存储在oplog中的操作也不是完全和主节点的操作一模一样。
Oplog存储在固定集合中。
启动服务器时可以用—oplogSize指定大小,单位是MB.
默认情况下,64位的实例将使用oplog 5%的可用空间。这个空间将在local数据库中分配,并在服务器启动时预先分配。
9.4.2 同步
从节点第一次启动时,会对主节点数据进行完整的同步。同步完成后,从节点开始查询主节点的oplog并执行这些操作,以保证数据是最新的。
为避免从节点跟不上主节点,一定要确保主节点的oplog足够大。能存放相当长时间的操作记录。
9.4.3 复制状态和本地数据库
本地数据库用来存放所有内部复制状态,主节点和从节点都有。
本地数据库的名字就是local,其内容不会被复制。这样能确保一个MongoDB服务器只有一个本地数据库。
主节点上的复制状态还包括从节点的列表(从节点连接主节点时会执行handshake命令进行握手)。这个列表存放在slave集合中:
>db.slave.find();
从节点也在本地数据库中存放状态。在me集合中存放从节点的唯一标识符,在sources集合中存放源或节点列表。
>db.sources.find()
主节点和从节点都跟踪从节点的更新状况,这是通过存放在”syncedTo”中的时间戳来完成的。每次从节点查询主节点的oplog时,都会用”syncedTo”来确定哪些操作需要执行,或者查看是否已经跟不上同步了。
9.4.4 阻塞复制
开发者可以用getLastError的w参数来确保数据的同步性。这里运行getLastError会进入阻塞状态,直到N个服务器复制了最新的写入操作为止。
>db.runCommand( {getLastError:1, w:N})
如果没有N,或者小于2,命令就会立刻返回。如果N等于2,主节点要等到至少一个从节点复制了上个操作才会响应命令。
主节点使用local.slaves中存放的”syncedTo”信息跟踪从节点的更新情况。
当执行w选项后,还可以使用wtimeout选项,表示以毫秒为单位的超时。
getLastError就能在上一个操作复制到N个节点超时时返回错误。
阻塞复制会导致写操作明显变慢,尤其是w的值比较大时。
9.5管理
9.5.1 诊断
查看复制的状态,可以使用db.printReplicationInfo函数:
>db.printReplicationInfo()
上述函数查询出的信息是oplog的大小和oplog中操作的时间范围。
当连接到从节点时,用db.printSlaveReplicationInfo()函数,能得到从节点的一些信息:
>db.printSlaveReplicationInfo()
显示的是从节点的数据源列表,其中有数据滞后时间。
9.5.2 变更oplog的大小
若发现oplog大小不合适,可以先停掉主节点,删除local数据库的文件,用新的设置重新启动。过程如下:
rm /data/db/local.*
./mongod --master –oplogSize size
重启主节点之后,所有从节点得用—autoresync重启,否则需要手动重新同步。
9.5.3 复制的认证问题
如果在复制中使用了认证,还需要做配置,使得从节点能够访问主节点的数据。
在主节点和从节点上都需要在本地数据库添加用户,每个节点的用户名和密码都是相同的。
本地数据库的用户类似admin中的用户,能够读写整个服务器。
从节点连接主节点时,会用存储在local.system.users中的用户进行认证。最先尝试repl用户,若没有此用户,则用local.system.users中的第一个可用用户。
按如下步骤配置主节点和从节点,就能配置认证复制:
>use local
>db.addUser(“repl”,password);
第10章 分片
10.1 分片简介
分片(sharing)是指将数据拆分,将其分散存在不同的机器上的过程。
MongoDB支持自动分片。集群自动切分数据,做负载均衡。
10.2 MongoDB中的自动分片
MongoDB分片就是将集合切分成小块。这些块分散到若干片里面,每个片只负责总数据的一部分。应用程序不必知道哪片对应哪些数据,甚至不需要知道数据已经被拆分了。
所以在分片之前要运行一个路由进程,进程名为mongos. 该路由知道所有数据的存放位置,所以应用可以连接它来正常发送请求。对应用来说,它仅知道连接了一个普通的mongod.
路由器知道数据和片的对应关系,能够转发请求到正确的片上。如果请求有了回应,路由器将其收集起来返回给应用。
未分片时,客户端连接mongod进程;分片时客户端连接mongos进程。
何时分片
l 机器的磁盘不够用了
l 单个mongod已经不能满足写数据的性能需要了
l 想将大量数据存放在内存中提高性能
10.3 片键
设置分片时,需要从集合里选一个键,用该键的值作为数据拆分的依据,这个键称为片键。
10.3.1 将已有的集合分片
根据片键对集合进行拆分,每个拆分的部分称为块。每个块中包含片键值在一定范围内的所有文档。
10.3.2 递增片键还是随机片键
片键的选择决定了插入操作在片之间的分布。
如果写入负载比较高,想均匀分散负载到各个片,就得选择分布均匀的片键。
如果键的变化太少,但又想让其成为片键,可以将这个键与一个变化较大的键组合起来,创建一个复合片键。
选择片键并创建片键很像建索引。
10.3.3 片键对操作的影响
略
10.4 建立分片
建立分片有2步:启动实际的服务器,然后决定怎么切分数据。
分片一般有3个组成部分:
l 片
片就是保存子集合数据的容器。片可以是单个的mongod服务器,也可以是副本集。
l Mongos
Mongos是MongoDB中的路由器进程。它路由所有请求,然后将结果聚合。它本身并不存储数据或者配置信息。
l 配置服务器
配置服务器存储了集群的配置信息:数据和片的对应关系。Mongos不永久存放数据,所以需要配置服务器存放分片配置。它会从配置服务器获取同步数据。
10.4.1 启动服务器
首先要最新启动配置服务器,然后再是mongos.
#启动配置服务器
$ mkdir –p ~/dbs/config
$ ./mongod –dbpath ~/dbs/config --port 20000
#启动mongos
$ ./mongos –port 30000 –configdb localhost:20000
分片管理是通过mongos完成的。
添加片
片就是普通的mongod实例(或者副本集):
$ mkdir –p ~/dbs/shard1
$ ./mongod –dbpath ~/dbs/shard1 –port 10000
连接已启动的mongos,为集群添加一个片。连接mongos:
$ ./mongo localhost:30000/admin
通过addshard命令添加片:
>db.runCommand( {addshard: “localhost:10000”, allowLocal: true})
10.4.2 切分数据
例如:需要以”_id”为基准切分foo数据库的bar集合。
#1.首先开启foo的分片功能
>db.runCommand( {“enablesharding”: “foo”})
#2.使用shardcollection命令对集合进行分片
>db.runCommand( {“shardCollection”: “foo.bar”, “key”: {“_id”: 1}})
这样集合就按照”_id”进行分片了。再添加数据,就会依据”_id”的值自动分散到各个片上。
10.5 生产配置
成功构建分片需要如下条件:
l 多个配置服务器
l 多个mongos服务器
l 每个片都是副本集
l 正确设置w
10.5.1 健壮的配置
例如:设置多个配置服务器
$ mkdir -p ~/dbs/config1 ~/dbs/config2 ~/dbs/config3
$ ./mongod –dbpath ~/dbs/config1 –port 20001
$ ./mongod –dbpath ~/dbs/config2 –port 20002
$ ./mongod –dbpath ~/dbs/config3 –port 20003
启动mongos服务器
$ ./mongos –configdb localhost:20001,localhost:20002,localhost:20003
10.5.2 多个mongos
略
10.5.3 健壮的片
生产环境,每个片都应是副本集。
用addshard命令可以将副本集作为片添加。
例如: 要添加副本集foo,其中包括一个服务器prod.example.com:27017. 将其添加到副本集中。
>db.runCommand( {“addshard”: “foo/prod.example.com:27017”})
10.5.4 物理服务器
略
10.6 管理分片
分片信息主要存放在config数据库上,这样就能被任何连接到mongos的进程访问了。
10.6.1 配置集合
l 片
可在shards集合中查到所有的片:
>db.shards.find();
l 数据库
Databases集合包含已经在片上的数据库列表和一些相关信息:
>db.databases.find();
“_id” , 字符串 #”_id”表示数据库名
“partitioned” 布尔型 #如果为true,则表示已经启动分片功能
“primary” 字符串 #这个值与”_id”相对应,表明这个数据库的大本营,基片
l 块
块信息保存在chunks集合中。
>db.chunks.find();
单块的集合的范围:从负无穷大到正无穷大。
10.6.2 分片命令
1.获得概要
PrintShardingStatus:获取集合的概要信息
>db.printShardingStatus();
2.删除片
Removeshard : 从集群中删除片。RemoveShard会把给定片上的所有块都挪到其他片上。
>db.runCommand( {“removeShard”: “localhost:10000});
在挪动过程中,removedshard会显示进程。
挪动完毕后,removeshard会提示片已被成功删除。
如果删除的片是数据库的大本营(基片),必须手动移动数据库:
>db.runCommand({“moveprimary”: “test”, “to”: “localhost:10001”})