MongoDB学习笔记
参考资料:
?
《MongoDB:The Definitive Guide》?http://book.douban.com/subject/4746684/
?
《MongoDB官方文档》?http://www.mongodb.org/display/DOCS/Home
?
什么是MongoDB:
?
MongoDB是10gen公司开发的一个NoSQL产品,它的主要特点是:
非关系式,面向文档形的数据库(document-oriented database)。schema-free——无需为数据库建模,可以随时改变数据的形式。JSON-like文档——可以用类似JavaScript的方式操作文档对象。MapReduce——支持大规模数据聚合(Aggregation)。速度非常快——索引支持,内存映射引擎(memory-mapped engine),查询动态优化。易于扩展——允许自动划分数据到多台服务器,动态扩展。实际应用:
?
MongoDB已经用在很多实际的生产环境当中了,比如以下
?
http://www.foursquare.com/
http://sourceforge.net/
http://bit.ly/
?
更多案例可以看?http://www.mongodb.org/display/DOCS/Production+Deployments
?
安装:
?
访问?http://www.mongodb.org/display/DOCS/Quickstart?
选择你的操作系统来安装MongoDB
?
MongoDB安装完成之后,默认会有一个MongoShell可供交互使用
?
huangz@pad:~$ mongoMongoDB shell version: 1.4.4url: testconnecting to: testtype "exit" to exittype "help" for help>
?
可以使用JavaScript的语法来在MongoShell中进行操作。
使用help可以查看常用命令
?
> helpHELPshow dbs show database namesshow collections show collections in current databaseshow users show users in current databaseshow profile show most recent system.profile entries with time >= 1msuse <db name> set curent database to <db name>db.help() help on DB methodsdb.foo.help() help on collection methodsdb.foo.find() list objects in collection foodb.foo.find( { a : 1 } ) list objects in foo where a == 1it result of the last line evaluated; use to further iterate?
数据:文档、集合和数据库
?
文档(document)是MongoDB中最基本的数据组织形式,每个文档以Key-Value(键-值对)的方式组织起来。
?
{"greeting" : "Hello World!"}?
?一个文档可以有多个Key-Value组合,每个Value可以是不同的类型,比如String、Integer、List等等。
?
{ "name" : "huangz", "sex" : "male", "age" : 20 }?
?Key的遵循以下规则:
"\0"不能使用带有"."号,"_"号和"$"号前缀的Key被保留大小写有区别,Age不同于age同一个文档不能有相同的Key除了上面几条规则外,其他所有UTF-8字符都可以使用将多个文档组织起来,就形成了集合(collection)。如果将文档比作关系数据库中的行(row)的话,那么集合就是数据库中的表(table)。
?
在关系数据库(如MySQL)中,在同一个数据库表里面,总是有相同的行(row),比如你有一个student表,里面有id,name,age,class,grade几个row,那么整个student只能有相同的几个行。
?
但是在MongoDB当中,你可以利用schema-free特性,在一个集合中,储存多个有不同Key、不同类型的文档,比如你可以在一个student集合里面,有如下格式的文档:
?
{ "name" : "huangz", "age" : 20, "sex" : "male"}{ "name" : "jack", "class" : 3, "grade" : 3}?
?在这个student集合里面,并不要求每个文档都要有同样的Key和同样的类型,一切随意。
?
集合的命名规则和文档的命名规则大概相似,另外要记住的是
system集合是被保留的另外,“.”号的使用在集合当中是允许的,它们被成为子集合(Subcollection);比如你有一个blog集合,你可以使用blog.title,blog.content或者blog.author来帮组你更好地组织集合。
?
将多个集合组织起来,就形成了数据库(database)。单个MongoDB实例可以使用多个数据库,每个数据库都是独立运作的,可以有单独的权限,每个数据库的数据被分开保存在不同的文件里。
?
数据库也有命名规则:
不能使用空字符""或者空格" "、点号"."、美元符"$"、斜线"/"、反斜线"\"和"\0"只能用小写必须少于64个字节(byte)总结起来,MongoDB组织数据的方式如下:Key-Value对 > 文档 > 集合 > 数据库
另外,在MongoDB中(不包括GridFS),单个文档大小不得超过4mb(版本>=1.7则是16MB)。
MongoDB不对数据做任何处理(它只是单纯的把文档保存起来),所以你可以淡定面对SQL Injection之类的攻击。
?
数据类型:
?
上面说过,MongoDB的基本数据是以key-value为单位的,key只能是字符串,但value的数据类型则多种多样,这些类型基本从BSON格式的基础上进行一些添加和修改而来,以下是一些常见类型。
?
/* 空值 null */{ "name" : null }/* 布尔值 boolean */{ "flag" : true }/* 数值 包括32bit、64bit整数integer类型和64bit浮点floating point类型 */{ "copies" : 300, "price" : 60.8}/* 字符串 string */{ "dbname" : "MongoDB" }/* 日期 date */{ "post_time" : new Date() }/* 正则表达式 regular expression */{ "name_match" : /huangz/i }/* 数组类型 array */{ "tags" : ["mongodb", "nosql"] }/* 嵌套文档 embedded document 一个文档里面Key的值可以是另外一个文档 */{ "name" : "huangz", "phone" : { "home" : 123321, "mobile" : 15820123123}}/* id 每个MongoDB文档都必须有一个叫作"_id"的Key, 同一集合里面的文档_id必须各不相同。 id可以在创建文档的时候指定,可以是任何类型,无指定时,默认使用ObjectId对象,自动生成不同的id。 ObjectId对象生成id(12位)的规则如下: 0-3 : Timestamp,时间撮 4-6 : Machine,机器代码 7-8 : PID,MongoDB的程序id 9-11: Increment,一个自增量*/{ "_id" : ObjectId("4da025ac5149e6d915098c59"), "name" : "huangz", "phone" : { "home" : 33123123, "mobile" : 15820123123 } }?
不必担心自动生成id会重复,因为它足够大。
如果想模仿关系数据库,生成一个连续自增的id,可以使用类似如下的代码来实现:
?
/* 生成连续自增id */> db.person.id = 00> db.person.insert({... "_id" : db.person.id += 1,... "name" : "huangz" ... })> db.person.insert({... "_id" : db.person.id += 1,... "name" : "jack",... })> db.person.find(){ "_id" : 1, "name" : "huangz" }{ "_id" : 2, "name" : "jack" }> db.person.find({"_id" : 1}){ "_id" : 1, "name" : "huangz" }更详细的类型资料请参考?http://www.mongodb.org/display/DOCS/Data+Types+and+Conventions?。
?
基本语法:
?
了解MongoDB的基本数据类型和组织方式后,我们可以测试以下常见的CRUD操作。
?
/* 选择数据库和集合,这里使用test数据库 */> use testswitched to db test/* CREATE 创建一个post文档,并将post添加到test.blog集合里面 */> post = {... "title" : "First day with MongoDB",... "author" : "huangz",... "content" : "Today, i try MongoDB, it's great!",... "date" : new Date()... }{"title" : "First day with MongoDB","author" : "huangz","content" : "Today, i try MongoDB, it's great!","date" : "Sat Apr 09 2011 16:21:51 GMT+0800 (CST)"}> db.blog.insert(post) /*这里的db是一个指向test数据库的变量 *//* READ 查询创立的post文档,MongoDB提供了包括find和findOne等多种查询方式 find函数的参数如果为空字典,则是查询整个集合,显示最先20条记录(record), find函数也可以使用参数,作为查询条件, findOne函数和find类似,但是只返回符合条件的第一条记录 */> db.blog.find(){ "_id" : ObjectId("4da017c55149e6d915098c57"), "title" : "First day with MongoDB", "author" : "huangz", "content" : "Today, i try MongoDB, it's great!", "date" : "Sat Apr 09 2011 16:24:26 GMT+0800 (CST)" }> db.blog.find({"author" : "huangz"}){ "_id" : ObjectId("4da0177a0fcb4b53ae3ba9fc"), "title" : "First day with MongoDB", "author" : "huangz", "content" : "Today, i try MongoDB, it's great!", "date" : "Sat Apr 09 2011 16:21:51 GMT+0800 (CST)" }> db.blog.findOne(){"_id" : ObjectId("4da0177a0fcb4b53ae3ba9fc"),"title" : "First day with MongoDB","author" : "huangz","content" : "Today, i try MongoDB, it's great!","date" : "Sat Apr 09 2011 16:21:51 GMT+0800 (CST)"}/* UPDATE 更新post记录,为它增加一个Key名字为comments的列表。*/> post {"title" : "First day with MongoDB","author" : "huangz","content" : "Today, i try MongoDB, it's great!","date" : "Sat Apr 09 2011 16:24:26 GMT+0800 (CST)"}> post.comments = [][ ]> db.blog.update({"title" : "First day with MongoDB"}, post)> post{"title" : "First day with MongoDB","author" : "huangz","content" : "Today, i try MongoDB, it's great!","date" : "Sat Apr 09 2011 16:24:26 GMT+0800 (CST)","comments" : [ ]}/* DELETE 删除post,下面两种方法等效 */db.blog.remove(post)db.blog.remove({"title" : "First day with MongoDB"})?
深入查询:
1.条件查询
?
除了使用
?
db.collection.find()
?
查询集合里所有文档外,我们可以指定特定的查询条件,比如以下语句只查询age为20的人。
?
> db.user.find({"age": 20}){ "_id" : ObjectId("4d7aff454dca002afb000000"), "age" : 20, "name" : "huangz" }?
还可以使用多个查询条件,比如以下语句不但查询age为20的人,还查询name为huangz的人。
?
> db.user.find({"age": 20, "name" : "huangz"}){ "_id" : ObjectId("4d7aff454dca002afb000000"), "age" : 20, "name" : "huangz" }?
还可以指定返回文档的数目
?
> db.user.find().limit(1){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }?
以及跳过指定数目的文档
?
/* 所有文档 */> db.user.find(){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }/* 跳过第一个文档 */> db.user.find().skip(1){ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }?
?排序指定文档的key,国际惯例,1为升序,-1为降序。
?
/* 以name项的降序排列文档 */> db.user.find().sort({"name": -1}){ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }2.指定返回项
?
如果你只是对文档的某个(或数个)key感兴趣,那么你可以在查询时,指定要返回的key(注意:"_id"项默认总是被返回的,除非你明确指定不返回"_id"项)。
?
/* 所有结果 */> db.user.find(){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }/* 只显示age为20岁的人所喜欢的水果,其他不显示。(比如一个水果贩子它只关心水果的受欢迎程度,不管你姓甚名谁。) */> db.user.find({"age": 20}, {"favorite fruit": 1}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "favorite fruit" : "apple" }/* 除了favorite fruit项之外,都显示 */> db.user.find({"age": 20}, {"favorite fruit": 0}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20 }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20 }/* 但是似乎没有办法同时指定显示和不显示。。。 > db.user.find({"age": 20}, {"favorite fruit": 0, "name": 1})error: {"$err" : "You cannot currently mix including and excluding fields. Contact us if this is an issue."}*/3.条件查询(使用条件操作符)
?
可以使用MongoDB提供的查询条件操作符来进行查询,比如"$lt", "$lte", "$gt", "$gte"分别代表 "<", "<=", ">", ">="。
?
/* 只查询age <= 20的人 */> db.user.find({"age": {"$lte" : 20}}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }?
?"$ne"操作符代表"!=",不等关系。
?
/* 只查询age != 20的人 */> db.user.find({"age": {"$ne" : 20}}){ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }?
"$in"操作符表示,只要符合条件数组中的任何一个,就能被选中。
?
/* 选中喜欢吃苹果apple或者香蕉banana的人 */> db.user.find({"favorite fruit": {"$in" : ["apple", "banana"]}}){ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }?
?"$nin"操作符表示,只要不符合条件数组的任何一个,就能被选中。
?
/* 选中不喜欢吃苹果或香蕉的任何一种的人 */> db.user.find({"favorite fruit": {"$nin" : ["apple", "banana"]}}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }?
"$all"操作符表示,必须符合条件数组中的每一个,才能被选中。
?
/* 所有文档 */> db.post.find(){ "_id" : ObjectId("4da079ee78d367d0af2ded04"), "title" : "today, a new database release", "content" : "long long ago...", "tags" : [ "nosql", "mongodb", "database" ] }{ "_id" : ObjectId("4da07aee78d367d0af2ded06"), "title" : "MySQL sucks", "content" : "i use MySQL 10 year ago, is a long time...", "tags" : [ "database", "mysql" ] }/* 只选中tags项符合["nosql", "mongodb", "database"]的文档(注意条件的顺序这里不重要) */> db.post.find({"tags": {$all: ["nosql", "mongodb", "database"]}}){ "_id" : ObjectId("4da079ee78d367d0af2ded04"), "title" : "today, a new database release", "content" : "long long ago...", "tags" : [ "nosql", "mongodb", "database" ] }?
"$not"操作符表示,对操作取反。
?
/* 只选中age不小于或等于20的人 */> db.user.find({"age": {"$not": {"$lte": 20}}}){ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }?
"$or"操作符表示,至少符合条件数组的其中一个,就能被选中。
?
/* 下面包含or的语句本该返回2个文档,但在我的测试中,1.8.1版本一个结果也不返回。。。真诡异 */> db.user.find(){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }/* WTF */> db.user.find({"$or": [{"age": 20}, {"age": 21}]})>/* 本该是{ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }*/?
"$and"操作符表示。。。嗯。。。MongoDB没有$and,你在写查询的时候提供多个条件就是了。
?
> db.user.find({"name":"huangz", "age": 20}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }??"$size"操作符搜索符合指定大小的数组的文档。/* 所有文档,分别有size为2和3的文档各一个 */> db.post.find(){ "_id" : ObjectId("4da079ee78d367d0af2ded04"), "title" : "today, a new database release", "content" : "long long ago...", "tags" : [ "nosql", "mongodb", "database" ] }{ "_id" : ObjectId("4da07aee78d367d0af2ded06"), "title" : "MySQL sucks", "content" : "i use MySQL 10 year ago, is a long time...", "tags" : [ "database", "mysql" ] }/* 查找所有tags的size为3的文档 */> db.post.find({"tags": {"$size": 3}}){ "_id" : ObjectId("4da079ee78d367d0af2ded04"), "title" : "today, a new database release", "content" : "long long ago...", "tags" : [ "nosql", "mongodb", "database" ] }?"$slice"操作符返回符合查询条件的文档的,指定大小的数组分割。/* 返回所有post集合文档, 其中只返回大小为2的tags数组 */> db.post.find({}, {"tags": {"$slice": 2}}){ "_id" : ObjectId("4da079ee78d367d0af2ded04"), "tags" : [ "nosql", "mongodb", "database" ] }{ "_id" : ObjectId("4da07aee78d367d0af2ded06"), "tags" : [ "database", "mysql" ] }/* $slice操作怎么又错了,悲剧阿。。。 */?"$elemMatch"操作符用来处理嵌套文档的查询。// Document 1{ "foo" : [ { "shape" : "square", "color" : "purple", "thick" : false }, { "shape" : "circle", "color" : "red", "thick" : true }] }// Document 2{ "foo" : [ { "shape" : "square", "color" : "red", "thick" : true }, { "shape" : "circle", "color" : "purple", "thick" : false }] }/* 失败查询 */db.foo.find({"foo.shape": "square", "foo.color": "purple"})db.foo.find({foo: {"shape": "square", "color": "purple"} } )/* 正确方法 */db.foo.find({foo: {"$elemMatch": {shape: "square", color: "purple"}}})??4.指针指针用来记住一个文档的位置,它是惰性的,不会马上求值,适合用来做一些迭代形的操作,比如翻页。
/* 翻页程序 */var record_per_page = 50var pages = db.post.find().sort({"date": -1}).limit(record_per_page)latest = loop_and_print_pages(pages)var pages = db.post.find({"date": {"$gt": latest.date}}).sort({"date": 1}).limit(record_per_page)latest = loop_and_print_pages(pages)?为什么要用指针代替skip?因为skip的值变得越来越大的时候,也会越来越慢,而用一个指针代替庞大的skip,可以节省时间。
5.随机
经常会遇到问题,需要随机获取任意一个文档,你可以这样做。
>db.user.find().skip(Math.floor(Math.random()*db.user.find().count())).limit(1) { "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }>db.user.find().skip(Math.floor(Math.random()*db.user.find().count())).limit(1){ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }>db.user.find().skip(Math.floor(Math.random()*db.user.find().count())).limit(1){ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }>db.user.find().skip(Math.floor(Math.random()*db.user.find().count())).limit(1){ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }?因为每次随机都要读取一次count(),你可以用随机id或者其他办法优化速度。MongoDB的官方cookbook有一个相关实例:?http://cookbook.mongodb.org/patterns/random-attribute/
?
6.快照查询当你对一系列文档同时进行查询-修改-保存的操作哦时,文档有可能在过程中被意外修改或者因为有新的文档被插入而导致数据错误,这时,你需要使用快照查询,对当前数据做一个快照,确保在查询的整个过程中数据(暂时)不变。
> db.user.find({"$query": {}, "$snapshot": true}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }更详细和全面的查询语法请参考:Query?, Advanced Query?。
?
索引:MongoDB的索引和关系数据库的索引差不多,都是加速查询用的,个人通常的做法是,对常用的查询key添加索引。
/* 比如你经常要按升序查询名字,可以给名字(name)项加上索引 */> db.user.ensureIndex({"name":1})> db.user.find().sort({"name":1}){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }?可以用explain()函数对文档的索引情况进行详细调查。/* 使用了索引的查询 */> db.user.find({"name": "huangz", "age": 20}).explain(){"cursor" : "BtreeCursor name_1_age_1", // 索引的类型,索引成功一般为BasicCursor,name_1_age_1显示索引的名字"indexBounds" : [ // 索引查询的条件限制[{"name" : "huangz","age" : 20},{"name" : "huangz","age" : 20}]],"nscanned" : 1, // 扫描项目数量,项目可以是Object也可以是索引关键字"nscannedObjects" : 1, // 扫描过的对象数量"n" : 1, // 实际返回文档数量"millis" : 0 // 查询使用时间}/* 没有使用索引的查询 */> db.post.find({"title": "feak title"}).explain(){"cursor" : "BasicCursor","indexBounds" : [ ],"nscanned" : 2,"nscannedObjects" : 2,"n" : 0,"millis" : 0}?删除索引/* 删除name项的升序索引 */db.user.dropIndexes({"name": 1})?需要注意的一些地方是:1.MongoDB的索引是包含性的,位置无关的。? ?比如你已经索引了{"name": 1, "age": 1},那么就不必单独索引{"name": 1}了;? ?另外,{"name": 1, "age": 1}和{"age": 1, "name": 1}基本相同,说基本是因为当查询的时候,key的顺序是没有关系的,但是当遇上sort操作的时候,索引顺序的不同决定了能否使用索引。
/* 比如你以下两个查询都可以被索引 */db.user.ensureIndex({"name": 1, "age": 1})db.user.find({"name": "huangz", "age": 20})db.user.find({"age": 20, "name": "huangz"})/* 以下两个就不一样了,只有第一个查询被索引,第二个查询没有用到索引 */db.user.find().sort({"name": 1, "age": 1})db.user.find().sort({"age": 1, "name": 1})?2.MongoDB不单是为快速查询而设计,其他update、delete、insert操作也是相当重要,但是索引会减慢除查询外其他操作的速度,这个要注意,如果你集合里面文档增加的速度相当快,比如你要实现一个tweeter,那么对tweet项索引可能反而会增加系统插入新tweet的负担。另外MongoDB的索引是全部放进内存当中的,如果你的内存不大,要慎重使用索引。聚合(aggregation):
MongoDB提供了包括MapReduce在内的多种工具,帮助你进行数据聚合操作。
count:最简单的聚合工具,也非常常用——计算符合查询条件的文档数量。
> db.user.find().count()3> db.user.find({"name":"huangz"}).count()1?distinct: 计算给定key内的不同值的数量。/* 用distinct计算出,集合里只有20岁和30岁的人。 */> db.user.find(){ "_id" : ObjectId("4da070040d03918e09fe7dac"), "name" : "huangz", "age" : 20, "favorite fruit" : "pear" }{ "_id" : ObjectId("4da070180d03918e09fe7dad"), "name" : "jack", "age" : 20, "favorite fruit" : "apple" }{ "_id" : ObjectId("4da0702d0d03918e09fe7dae"), "name" : "peter", "age" : 30, "favorite fruit" : "banana" }> db.user.distinct("age")[ 20, 30 ]?group:类似关系数据库,对数据进行group操作。(如果MongoDB设置为sharded环境,或者要处理的key数量在10 000以上,则目前必须用MapReduce代替group。)/* group的一般参数如下: ns: 要处理的集合名称 key: 需要group by处理的项 reduce: reduce函数,对集合内的每个文档使用一次,接受两个函数,一个是目前迭代的文档,另一个是聚合计数器。 initial: 聚合的初始值 keyf: 可选,用函数对key中的值进行修改然后作为key参数传入到group。 cond: 可选,相当于文档的filter,符合条件的文档才会被group处理。 finalize: 可选,每个group处理之后的结果都传入到这个函数,可以用于修改最终group结果。 参数有点复杂,但整体还是很清晰的。 最好的例子还是参考mongodb主页的例子: http://www.mongodb.org/display/DOCS/Aggregation*/db.test.group( { cond: {"invoked_at.d": {$gte: "2009-11", $lt: "2009-12"}} , key: {http_action: true} , initial: {count: 0, total_time:0} , reduce: function(doc, out){ out.count++; out.total_time+=doc.response_time } , finalize: function(out){ out.avg_time = out.total_time / out.count } } );?MapReduce: MongoDB数据处理的终极武器,可以自动将任务分配到多台服务器上计算,然后将结果合并。主要用于后台大规模数据集处理,不要用于实时计算。/* MapReduce参数 db.runCommand( { mapreduce : <collection>, map : <mapfunction>, reduce : <reducefunction> [, query : <query filter object>] [, sort : <sort the query. useful for optimization>] [, limit : <number of objects to return from collection>] [, out : <see output options below>] [, keeptemp: <true|false>] // 如果为true,则计算的结果作为数据保存到out参数指定的地方 [, finalize : <finalizefunction>] // 处理所有计算结果的函数 [, scope : <object where fields go into javascript global scope >] // 指定计算时候可以访问的外部变量 [, verbose : true] // 提供计算时候的统计数据 });关于out选项,当使用版本<=1.7.3时,可以指定变量或者集合名字作为参数。如果使用版本>=1.7.4,参数名可以是以下: 1.collectionName: 结果覆盖同名集合 2.{replace: collectionName}: 同上 3.{merge: collectionName}: 将结果和集合里的数据合并,如果有同名的key,新的数据会覆盖旧的数据。 4.{reduce: collectionName}: 如果计算结果和集合里的数据有相同key的情况出现,将调用指定的reduce function。 5.{inline: 1}: 所有计算结果保存在内存当中。详细参考MongoDB网站: http://www.mongodb.org/display/DOCS/MapReduce*/> db.things.insert( { _id : 1, tags : ['dog', 'cat'] } );> db.things.insert( { _id : 2, tags : ['cat'] } );> db.things.insert( { _id : 3, tags : ['mouse', 'cat', 'dog'] } );> db.things.insert( { _id : 4, tags : [] } );> // map function> m = function(){... this.tags.forEach(... function(z){... emit( z , { count : 1 } );... }... );...};> // reduce function> r = function( key , values ){... var total = 0;... for ( var i=0; i<values.length; i++ )... total += values[i].count;... return { count : total };...};> res = db.things.mapReduce(m,r);> res{"timeMillis.emit" : 9 , "result" : "mr.things.1254430454.3" , "numObjects" : 4 , "timeMillis" : 9 , "errmsg" : "" , "ok" : 0}> db[res.result].find(){"_id" : "cat" , "value" : {"count" : 3}}{"_id" : "dog" , "value" : {"count" : 2}}{"_id" : "mouse" , "value" : {"count" : 1}}> db[res.result].drop()其他功能特性: ?
1.Capped Collection
Capped Collection(CC)其实就是MongoDB实现的一个定长(fix size)的FIFO结构。
在CC结构当中的文档,没有索引(连_id项也是),没有办法进行update、remove,不能使用普通的sort函数。CC结构的文档以插入先后排序。另外CC结构是定长的,所以会自动清理过期文档。CC结构的优点是它的insert非常快,因为没有索引碍事,数据放在内存中,插入几乎等于一个memcpy;CC结构的插入序的查询也非常块。(个人猜测具体实现是双链表,插入总是O(1),查询O(N)。)
综合CC结构的特点,它非常适合类似于logging、caching的场合。
/* 创建一个名为"fifo"的capped collection, 最大占用空间为100 000byte, 最大文档数量为100个 */> db.createCollection("fifo", {capped: true, size: 100000, max:100}){ "ok" : 1 }> db.fifo.stats(){"ns" : "test.fifo","count" : 0,"size" : 0,"storageSize" : 100096,"numExtents" : 1,"nindexes" : 0,"lastExtentSize" : 100096,"paddingFactor" : 1,"flags" : 0,"totalIndexSize" : 0,"indexSizes" : { // capped collection的索引总是空的,因为它不允许使用索引 },"capped" : 1, // 说明这是个capped collection"max" : 100, // 最大文档数量"ok" : 1}/* 注意只是指定一个集合的size,而不指定capped:true参数,那只是创建了一个限制大小的普通集合,不是capped collection。 */> db.createCollection("collection_with_preallocating_space", {size: 100000}){ "ok" : 1 }> db.collection_with_preallocating_space.stats(){"ns" : "test.collection_with_preallocating_space","count" : 0,"size" : 0,"storageSize" : 131072,"numExtents" : 1,"nindexes" : 1,"lastExtentSize" : 131072,"paddingFactor" : 1,"flags" : 1,"totalIndexSize" : 8192,"indexSizes" : {"_id_" : 8192},"ok" : 1}?CC结构只有两种排序,$natural:1和$natural-1,其中$natural: 1是文档插入顺序,-1是逆插入顺序。/* 对一个CC结构进行插入 */> db.fifo.insert({"name":"huangz"})> db.fifo.insert({"name":"jack"})> db.fifo.insert({"name":"peter"})/* 默认的插入序 */> db.fifo.find() { "_id" : ObjectId("4da26e03fd2b46bafddb950f"), "name" : "huangz" }{ "_id" : ObjectId("4da26e0cfd2b46bafddb9510"), "name" : "jack" }{ "_id" : ObjectId("4da26e16fd2b46bafddb9511"), "name" : "peter" }> db.fifo.find().sort({"$natural": 1}){ "_id" : ObjectId("4da26e03fd2b46bafddb950f"), "name" : "huangz" }{ "_id" : ObjectId("4da26e0cfd2b46bafddb9510"), "name" : "jack" }{ "_id" : ObjectId("4da26e16fd2b46bafddb9511"), "name" : "peter" }/* 逆插入序 */> db.fifo.find().sort({"$natural": -1}){ "_id" : ObjectId("4da26e16fd2b46bafddb9511"), "name" : "peter" }{ "_id" : ObjectId("4da26e0cfd2b46bafddb9510"), "name" : "jack" }{ "_id" : ObjectId("4da26e03fd2b46bafddb950f"), "name" : "huangz" }?CC结构的详细文档参见: Capped Collection
?
2.DBRef
?
DBRef文档,它不同于嵌套文档,不直接保存文档本身,它只是保存对另外文档的引用,类似关系数据库中的外键引用。
?
当数据修改时,嵌套数据要更新文档,但DBRef的文档总是最新的。
?
/* 创建一个ref到user的profile文档 */> var huangz = db.user.find({"name":"huangz"})> db.phone.insert({"number": 15820123123, "profile": {"$ref": "user", "$id": huangz._id}}) > db.phone.find(){ "_id" : ObjectId("4da1e59bfadbe09a1a9a324f"), "number" : 15820123123, "profile" : { "$ref" : "user", "$id" : null } }// 引用ref文档的方法各个驱动都稍有不同,请参见各自的文档?
DBRef的详细文档参见: Database Reference
?
3.GridFS
?
MongoDB内建了一个称为GridFS的存储机制,可以用于保存比普通MongoDB文档更大的文件(最大为2GB)。
?
/* MongoDB的Python驱动PyMongo手册的例子 */>>> from pymongo import Connection>>> import gridfs>>>>>> db = Connection().gridfs_example>>> fs = gridfs.GridFS(db)>>> a = fs.put("hello world") // 保存>>> fs.get(a).read() // 读取'hello world'?
GridFS的相信文档参见: GridFS
?
更多MongoDB的基本操作列举得差不多了,你并不需要30课时的SQL课程来学习怎么使用MongoDB,这也是它受欢迎的原因之一。
更详细和深入学习可以参考MongoDB的文档:?http://www.mongodb.org/display/DOCS/Home
MongoDB的各个语言的驱动可以在这里找到:?http://www.mongodb.org/display/DOCS/Drivers
《MongoDB:The Definitive Guide》也不错,不过MongoDB正在快速发展中,书中缺少一些最新的特性,而且有些命令已经修改了,可以配合文档来看。另外,这本书的中文版也快出了。
?
再分享两个使用的MongoDB使用经验的视频:
MongoDB在foursquare的应用: 视频? 幻灯
MongoDB在sourceforge的应用:?http://us.pycon.org/2010/conference/schedule/event/110/