MongoDB 关系
MongoDB 的关系表示多个文档之间在逻辑上的相互联系。文档间可以通过嵌入和引用来建立联系。MongoDB 中的关系可以是:
1:1 (1对1)
1:N (1对多)
N:1 (多对1)
N:N (多对多)
例如用户与用户地址的关系:一个用户可以有多个地址,所以是一对多的关系。以下是 user 文档的简单结构:
db.user.insert({"_id": ObjectId("52ffc33cd85242f436000001"),"name": "admin1","phone": "86-15212341111","birthday": "1991-10-20","email": "admin1@gmail.com"})
以下是 address 文档的简单结构:
db.address.insert({"_id": ObjectId("52ffc4a5d85242602e000000"),"province": "四川省","city": "成都市","area": "锦江区","street": "红星路三段99号"})
嵌入式关系使用嵌入式方法,可以把用户地址嵌入到用户的文档中:
db.user.insert({"_id": ObjectId("52ffc33cd85242f436000002"),"name": "admin2","phone": "86-15212342222","birthday": "1992-10-20","email": "admin2@gmail.com","address": [{ "province": "四川省", "city": "成都市", "area": "锦江区", "street": "红星路三段99号"},{ "province": "广东省", "city": "深圳市", "area": "罗湖区", "street": "建设路1003号"}]})
以上数据保存一个文档中,数据容易获取、维护,查询用户的地址:
db.users.findOne({"name" : "admin2"}, {"address" : 1})
这种数据结构的缺点是:如果用户和用户地址在不断增加,数据量不断变大,会影响读写性能。
引用式关系引用式关系是设计数据库时经常用到的方法,这种方法把用户数据文档和用户地址数据文档分开,通过引用文档的 id 字段来建立关系。
db.user.insert({"id": ObjectId("52ffc33cd85242f436000003"),"name": "admin3","phone": "86-15212343333","birthday": "1993-10-20","email": "admin3@gmail.com","address_ids": [ ObjectId("52ffc4a5d85242602e000000"), ObjectId("52ffc4a5d85242602e000001")]})db.address.insert({"id": ObjectId("52ffc4a5d85242602e000000"),"province": "四川省","city": "成都市","area": "锦江区","street": "红星路三段99号"})db.address.insert({"_id": ObjectId("52ffc4a5d85242602e000001"),"province": "广东省","city": "深圳市","area": "罗湖区","street": "建设路1003号"})
以上示例中,用户文档的 address_ids 字段包含用户地址的对象 id 数组。此时可以读取这些用户地址的对象 id 来获取用户地址信息。这种方式需要两次查询,第一次查询用户地址的对象 id 数组,第二次通过查询到的 id 数组获取用户地址信息。如下:
var user = db.user.findOne({"name" : "admin3"}, {"address_ids" : 1})db.address.find({"_id" : {$in : user["address_ids"]}}).pretty()
注意:var user = db.user.findOne({"name" : "admin3"}, {"address_ids" : 1})中的 findOne() 不能写成 find(),因为 find() 返回的是一个数组,而 findOne() 返回的是一个对象。如果这句写成 find(),即:
var user = db.user.find({"name" : "admin3"}, {"address_ids" : 1})
那么查询用户地址的语句应改为:
db.address.find({"_id" : {$in : user0}}).pretty()
MongoDB 数据库引用
MongoDB 引用有两种:手动引用(Manual References)、DBRefs。考虑这样的一个场景:在不同集合中(如:address_home、address_office、address_mailing等)存储不同地址(住址、办公室地址、邮件地址等)。在调用不同地址时,也需要指定集合,一个文档从多个集合引用文档,就应该使用 DBRefs 了。使用 DBRefs 的形式:{$ref : , $id : , $db : }其中,$ref 表示集合名称;$id 表示引用的 id;$db 表示数据库名称(可选参数)。下例中用户数据文档使用了 DBRef 字段 address:
db.user.insert({"_id": ObjectId("52ffc33cd85242f436000004"),"name": "admin4","phone": "86-15212344444","birthday": "1994-10-20","email": "admin4@gmail.com","address": { "$ref": "address_home", "$id": ObjectId("52ffc4a5d85242602e000000"), "$db": "test"}})
address 字段指明引用的地址文档是 test 数据库下的 address_home 集合,且其 id 为 52ffc4a5d85242602e000000。下面通过指定 $ref 参数(address_home 集合)来查找集合中指定 id 的用户地址信息:
var user = db.user.findOne({"name" : "admin4"})var dbRef = user.addressdb[dbRef.$ref].findOne({"_id" : (dbRef.$id)})
上例中的第2、3行也可以合并成一行,如下:
var user = db.user.findOne({"name" : "admin4"})db[user.address.$ref].findOne({"_id" : (user.address.$id)})
注:有人说 db[dbRef.$ref].findOne({"id" : (dbRef.$id)})在 MongoDB4.0 版本应该这样写:db[dbRef.$ref].findOne({"id":ObjectId(dbRef.$id)}),我在4.2.0版本上测试会报错!
MongoDB 覆盖索引查询
MongoDB 官方文档中说覆盖查询是指以下的查询:
所有的查询字段是索引的一部分。
所有的查询返回字段在同一个索引中。
由于所有查询字段是索引的一部分,所以 MongoDB 无需在整个文档中检索匹配查询条件和返回使用相同索引的查询结果。因为索引存在于 RAM 中,从索引中获取数据比通过扫描文档读取数据要快得多。
使用覆盖索引查询为了测试覆盖索引查询,使用以下 user 集合:
db.user.insert({"_id": ObjectId("53402597d852426020000005"), "name": "admin5", "gender": "M", "phone": "86-15212345555", "birthday": "1995-10-20", "email": "admin5@gmail.com"})
然后在 user 集合中创建联合索引,字段为 name 和 gende:
db.user.createIndex({"name" : 1, "gender" : 1}, {"name" : "name_gender", "background" : true})
现在,该索引会覆盖以下查询:
db.user.find({"name" : "admin5"}, {"gender" : 1, "_id" : 0})
也就是说:对于上述查询,MongoDB 不会去数据库文件中查找,而是从索引中提取数据,这是非常快速的数据查询。由于新建的索引中不包括 _id 字段,而 _id 在查询时默认会返回,所以可以在查询结果集中排除它。
下例中没有排除 _id 字段,查询就不会被索引覆盖:
db.user.find({"name" : "admin5"}, {"gender" : 1})
以下查询将不能使用覆盖索引
所有索引字段是一个数组。
所有索引字段是一个子文档。
MongoDB 查询分析
MongoDB 查询分析可以确保我们所建立的索引是否有效,是查询语句性能分析的重要工具。常用的查询分析函数有:explain() 和 hint()。
使用 explain()explain() 操作提供了查询信息、使用索引及查询统计等,有利于对索引的优化。MongoDB 3.0+ 的 explain() 有三种模式:queryPlanner、executionStats、allPlansExecution。现实开发中最常用的是 executionStats 模式。
通过设置 explain() 不同参数可以查看更详细的查询计划:
queryPlanner:默认参数。
executionStats:会返回最佳执行计划的一些统计信息。
allPlansExecution:用来获取所有执行计划。
在 user 集合中创建 name 和 gender 的索引:
db.user.createIndex({"name" : 1, "gender" : 1}, {"name" : "name_gender", "background" : true})
1、在查询语句中使用 explain("queryPlanner")
db.user.find({"name" : "admin5"}, {"gender" : 1}).explain()
等价于:
db.user.find({"name" : "admin5"}, {"gender" : 1}).explain("queryPlanner")
以上 explain() 查询返回如下结果:
{
"queryPlanner": {
"plannerVersion":1,// 查询计划版本
"namespace":"test.user",// 要查询的集合(返回值是该查询所涉及的集合)
"indexFilterSet":false,// 是否使用索引(针对该查询是否有 indexfilter)
"parsedQuery": {// 查询条件
"name": {"$eq":"admin5"}
},
"queryHash":"438FDCFD",
"planCacheKey":"849EB436",
"winningPlan": {// 最佳执行计划
"stage":"PROJECTION_SIMPLE",// 最优执行计划的 stage(查询方式),常见的有:COLLSCAN(全表扫描)、IXSCAN(索引扫描)、FETCH(根据索引去检索文档)、SHARD_MERGE-(合并分片结果)、IDHACK(针对 _id 进行查询)
"transformBy": {"gender":1},
"inputStage": {
"stage":"FETCH",
"inputStage": {// 用来描述子 stage,并且为其父 stage 提供文档和索引关键字
"stage":"IXSCAN",// 此处是 IXSCAN,表示进行的是索引扫描
"keyPattern": {// 所扫描的 index 内容
"name":1,
"gender":1
},
"indexName":"name_gender",// winningPlan 所选用的索引的名称
"isMultiKey":false,// 是否是 Multikey。如果索引建立在 array 上,此处将是 true
"multiKeyPaths": {
"name": [ ],
"gender": [ ]
},
"isUnique":false,
"isSparse":false,
"isPartial":false,
"indexVersion":2,
"direction":"forward",// 此查询的查询顺序
"indexBounds": {// 所扫描的索引范围,如果没有指定则范围就是[MaxKey, MinKey],这主要是直接定位到 MongoDB 的 chunck 中去查找数据,加快数据读取
"name": ["[\"admin5\", \"admin5\"]"],
"gender": ["[MinKey, MaxKey]"]
}
}
}
},
"rejectedPlans": [ ]// 拒绝的执行计划(其他执行计划,非最优而被查询优化器 reject 的)的详细返回,其具体信息与 winningPlan 相同
},
"serverInfo": {// MongoDB 服务器信息
"host":"LAPTOP-TAR7TI27",
"port":27017,
"version":"4.2.0",
"gitVersion":"a4b751dcf51dd249c5865812b390cfd1c0129c30"
},
"ok":1
}
返回结果包含两大块信息:queryPlanner(即查询计划) 和 serverInfo(即 MongoDB 服务器的一些信息)。
2、在查询语句中使用 explain("executionStats")executionStats 会返回最佳执行计划的一些统计信息,即:
db.user.find({"name" : "admin5"}, {"gender" : 1}).explain("executionStats")
以上 explain() 查询返回如下结果:
{
"queryPlanner": {
"plannerVersion":1,
"namespace":"test.user",
"indexFilterSet":false,
"parsedQuery": {"name": {"$eq":"admin5"}},
"queryHash":"438FDCFD",
"planCacheKey":"849EB436",
"winningPlan": {
"stage":"PROJECTION_SIMPLE",
"transformBy": {"gender":1},
"inputStage": {
"stage":"FETCH",
"inputStage": {
"stage":"IXSCAN",
"keyPattern": {
"name":1,
"gender":1
},
"indexName":"name_gender",
"isMultiKey":false,
"multiKeyPaths": {
"name": [ ],
"gender": [ ]
},
"isUnique":false,
"isSparse":false,
"isPartial":false,
"indexVersion":2,
"direction":"forward",
"indexBounds": {
"name": ["[\"admin5\", \"admin5\"]"],
"gender": ["[MinKey, MaxKey]"]
}
}
}
},
"rejectedPlans": [ ]
},
"executionStats": {
"executionSuccess":true,// 是否执行成功
"nReturned":1,// 返回的结果数
"executionTimeMillis":0,// 执行耗时
"totalKeysExamined":1,// 索引扫描次数
"totalDocsExamined":1,// 文档扫描次数
"executionStages": {// 这个分类下描述执行的状态
"stage":"PROJECTION_SIMPLE",// 扫描方式,具体可选值与上文的相同
"nReturned":1,// 查询结果数量
"executionTimeMillisEstimate":0,// 预估耗时
"works":2,// 工作单元数,一个查询会分解成小的工作单元
"advanced":1,// 优先返回的结果数
"needTime":0,
"needYield":0,
"saveState":0,
"restoreState":0,
"isEOF":1,
"transformBy": {"gender":1},
"inputStage": {
"stage":"FETCH",
"nReturned":1,
"executionTimeMillisEstimate":0,
"works":2,
"advanced":1,
"needTime":0,
"needYield":0,
"saveState":0,
"restoreState":0,
"isEOF":1,
"docsExamined":1,// 文档检查数目,与totalDocsExamined一致。检查了总共的个documents,而从返回上面的nReturne数量
"alreadyHasObj":0,
"inputStage": {
"stage":"IXSCAN",
"nReturned":1,
"executionTimeMillisEstimate":0,
"works":2,
"advanced":1,
"needTime":0,
"needYield":0,
"saveState":0,
"restoreState":0,
"isEOF":1,
"keyPattern": {
"name":1,
"gender":1
},
"indexName":"name_gender",
"isMultiKey":false,
"multiKeyPaths": {
"name": [ ],
"gender": [ ]
},
"isUnique":false,
"isSparse":false,
"isPartial":false,
"indexVersion":2,
"direction":"forward",
"indexBounds": {
"name": ["[\"admin5\", \"admin5\"]"],
"gender": ["[MinKey, MaxKey]"]
},
"keysExamined":1,
"seeks":1,
"dupsTested":0,
"dupsDropped":0
}
}
}
},
"serverInfo": {
"host":"LAPTOP-TAR7TI27",
"port":27017,
"version":"4.2.0",
"gitVersion":"a4b751dcf51dd249c5865812b390cfd1c0129c30"
},
"ok":1
}
注:nReturned-该条查询返回的条目、totalKeysExamined-索引扫描条目、totalDocsExamined-文档扫描条目。这些都直观地影响到 executionTimeMillis,因此扫描的越少速度越快。对于一个查询,理想状态是:nReturned=totalKeysExamined=totalDocsExamined
stage 又影响到 totalKeysExamined 和 totalDocsExamined,其类型列举如下:
COLLSCAN:全表扫描
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_FILTER:通过mongos对分片数据进行查询
COUNT:利用db.coll.explain().count()之类进行count运算
COUNTSCAN:count不使用Index进行count时的stage返回
COUNT_SCAN:count使用了Index进行count时的stage返回
SUBPLA:未使用到索引的$or查询的stage返回
TEXT:使用全文索引进行查询时候的stage返回
PROJECTION:限定返回字段时候stage的返回
对于普通查询,希望的 stage 组合(查询的时候尽可能用上索引):Fetch + IDHACK、Fetch + ixscan、Limit + (Fetch + ixscan)、PROJECTION + ixscan、SHARDING_FITER + ixscan、COUNT_SCAN、SORT_KEY_GENERATOR
不希望看到的 stage:COLLSCAN(全表扫描)、SORT(使用sort但是无index)、不合理的SKIP、SUBPLA(未用到index的$or)、COUNTSCAN(不使用index进行count)
3、在查询语句中使用 explain("allPlansExecution")allPlansExecution:用来获取所有执行计划,即:
db.user.find({"name" : "admin5"}, {"gender" : 1}).explain("allPlansExecution")
使用 hint()虽然 MongoDB 查询优化器一般工作的很不错,但是也可以使用 hint 来强制 MongoDB 使用一个指定的索引,这种方法某些情形下会提升性能。
如下查询指定了使用 name 和 gender 索引字段来查询:
db.user.find({"name" : "admin5"}, {"name" : 1, "_id" : 0}).hint({"name" : 1, "gender" : 1})
可以使用 explain() 函数来分析以上查询:
db.user.find({"name" : "admin5"}, {"name" : 1, "_id" : 0}).hint({"name" : 1, "gender" : 1}).explain()
MongoDB 原子操作
MongoDB 不支持事务,因此在实际项目中应用时要注意这点。无论什么设计,都不要要求 MongoDB 保证数据的完整性。但 MongoDB 提供了许多原子操作(如文档的保存、修改、删除等)。所谓的原子操作,就是这个文档要么完全保存到 MongoDB,要么完全没有保存到 MongoDB,不会出现查询到的文档没有保存完整的情况。
原子操作数据模型考虑下面的例子,图书馆的书籍及结账信息。该实例说明了在一个相同的文档中如何确保嵌入字段关联原子操作(update:更新)的字段是同步的。book = { "_id": 123456789, "title": "MongoDB: The Definitive Guide", "author": ["Kristina Chodorow", "Mike Dirolf"], "published_date": ISODate("2010-09-24"), "pages": 216, "language": "English", "publisher_id": "oreilly", "available": 3, "checkout": [{"by": "joe", "date": ISODate("2012-10-15")}]}
接下来可以使用 db.book.findAndModify() 方法来判断书籍是否可结算并更新新的结算信息。在同一个文档中嵌入的 available 和 checkout 字段来确保这些字段是同步更新的:
db.book.findAndModify({query: { "_id": 123456789, "available": {$gt: 0}},update: { $inc: {"available": -1}, $push: {"checkout": {"by": "abc", "date": new Date()}}}})
原子操作常用命令
$set:用来指定一个键并更新键值,若键不存在并创建。{$set : {field : value}}
$unset:用来删除一个键。{$unset : {field : 1}}
$inc:可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作。{$inc : {field : value}}
$push:把 value 追加到 field 里面去,field 必须是数组类型,如果 field 不存在,则会新增一个数组类型加进去。{$push : {field : value}}
$pushAll:同 $push,只是一次可以追加多个值到一个数组字段内。{$pushAll : {field : value_array}}
$pull:从数组 field 内删除一个等于 value 值。{$pull : {field : value}}
$addToSet:增加一个值到数组内,而且只有当这个值不在数组内才增加。{$addToSet : {field : value}}
$pop:删除数组的第一个或最后一个元素{$pop : {field : 1}}
$rename:修改字段名称{$rename : {old_field_name : new_field_name}}
$bit:位操作,integer 类型{$bit : {field : {and : 5}}}
偏移操作符
db.mycol.find(){"_id" : ObjectId("4b97e62bf1d8c7152c9ccb74"),"title" : "ABC","comments" : [ {"by" : "joe", "votes" : 3}, {"by" : "jane", "votes" : 7}] }
db.mycol.update({'comments.by' : 'joe'}, {$inc : {'comments.$.votes' : 1}}, false, true)
db.mycol.find(){"_id" : ObjectId("4b97e62bf1d8c7152c9ccb74"),"title" : "ABC","comments" : [ {"by" : "joe", "votes" : 4}, {"by" : "jane", "votes" : 7}]}
MongoDB 高级索引
db.user.insert({ "address": { "city": "Los Angeles", "state": "California", "pincode": "123" }, "tags": ["music", "cricket", "blogs"], "name": "Tom Benzamin"})
以上文档包含了 address 子文档和 tags 数组。
索引数组字段假设需要基于标签来检索用户,则需对集合中的数组 tags 建立索引。在数组中创建索引时需要对数组中的每个字段依次建立索引。因此在为数组 tags 创建索引时,会为 music、cricket、blogs 三个值建立单独的索引。使用以下命令创建数组索引:
db.user.createIndex({"tags" : 1})
创建索引后就可以这样检索集合的 tags 字段了:
db.user.find({"tags" : "cricket"})
为了验证是否使用了索引,可以使用 explain() 方法:
db.user.find({"tags" : "cricket"}).explain()
索引子文档字段假设需要通过 city、state、pincode 字段来检索文档,由于这些字段是子文档的字段,因此需要对子文档建立索引。使用以下命令为子文档的三个字段创建索引:
db.user.createIndex({"address.city" : 1, "address.state" : 1, "address.pincode" : 1})
创建索引后就可以使用子文档的字段来检索数据:
db.user.find({"address.city" : "Los Angeles"})
查询表达不一定遵循指定的索引顺序,MongoDB 会自动优化。所以上面创建的索引将支持以下查询:
db.user.find({"address.state" : "California", "address.city" : "Los Angeles"})
同样也支持以下查询:
db.user.find({"address.city" : "Los Angeles", "address.state" : "California", "address.pincode" : "123"})
MongoDB 索引限制
1、额外开销每个索引都要占据一定的存储空间,在进行插入、更新、删除等操作时也需要对索引进行操作。如果很少对集合进行读取操作,建议不使用索引。
2、内存(RAM)使用由于索引是存储在内存(RAM)中的,所以需要确保该索引的大小不超过内存的限制。如果索引的大小大于内存的限制,MongoDB 就会删除一些索引,这将导致性能下降。
3、查询限制索引不能被以下查询使用:
正则表达式及非操作符(如:$nin、$not 等)
算术运算符(如:$mod 等)
$where 子句
所以,检测 MongoDB 查询语句是否使用索引是一个好习惯,可以用 explain() 来查看。
4、索引键限制从 2.6 版本开始,如果现有的索引字段的值超过索引键的限制,MongoDB 中就不会创建索引。
5、插入文档超过索引键限制如果文档的索引字段值超过了索引键的限制,MongoDB 不会将任何文档转换成索引的集合。与 mongorestore 和 mongoimport 工具类似。
6、最大范围
集合中索引不能超过 64 个
索引名的长度不能超过 128 个字符
一个复合索引最多可以有 31 个字段
MongoDB ObjectId
MongoDB 的对象 Id(ObjectId) 是一个12字节 BSON 类型数据,具体格式为:
前4个字节表示时间戳
接下来的3个字节是机器标识码
紧接的两个字节由进程 id 组成(PID)
最后三个字节是随机数
MongoDB 中存储的文档必须有一个 _id 键,这个键的值可以是任何类型的,默认是个 ObjectId 对象。在一个集合里面,每个文档都有唯一的 _id 值,用以确保集合里面的每个文档都能被唯一标识。MongoDB 采用 ObjectId 而不是其他比较常规的做法(如:自增主键)主要是因为在多个服务器上同步自动增加主键值既费力还费时。
创建新的 ObjectId 的方式
ObjectId()
也可以使用已经生成的 id 来取代 MongoDB 自动生成的 ObjectId,如:
ObjectId("5ee783e424a0205812217f25")
创建文档的时间戳由于 ObjectId 中存储了 4 个字节的时间戳,因此不需要单独为文档保存时间戳字段,需要时可以通过 getTimestamp() 函数来获取文档的创建时间:
ObjectId("5ee783e424a0205812217f25").getTimestamp()
ObjectId 转换为字符串在某些情况下,可能需要将 ObjectId 转换为字符串格式,方式为:
ObjectId().str 或 new ObjectId().str
MongoDB Map-Reduce
Map-Reduce 是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。MongoDB 中提供的 Map-Reduce 非常灵活,对大规模数据分析相当实用。
Map-Reduce 命令MongoDB 中 Map-Reduce 的基本语法:
db.collectionName.mapReduce(function() {// map 函数 emit(key, value);},function(key, values) {// reduce 函数 return reduceFunction},{ out: collection, query: document, sort: document, limit: number})
使用 Map-Reduce 就必须要实现两个函数:Map 函数和 Reduce 函数。Map 函数调用 emit(key, value) 遍历集合中所有记录, 将 key 与 value 传递给 Reduce 函数进行处理。Map 函数必须调用 emit(key, value) 返回键值对。参数说明:
map:映射函数(生成键值对序列,作为 reduce 函数参数)。
reduce:统计函数,reduce 函数的任务就是将 key-values 变成 key-value,也就是把 values 数组变成单个 value。
out:统计结果存放集合(不指定则使用临时集合,在客户端断开后自动删除)。
query:一个筛选条件,只有满足条件的文档才会调用 map 函数(注:query、limit、sort 可以随意组合)。
sort:和 limit 结合的 sort 排序参数(在发往 map 函数前就会给文档排序),可以优化分组机制。
limit:发往 map 函数的文档数量的上限(若没有 limit,单独使用 sort 的用处不大)。
以下实例在集合 orders 中查找 status:"A" 的数据,然后根据 cust_id 来分组,最后计算 amount 的总和:
db.orders.insertMany([ {"cust_id" : "A123", "amount" : 500, "status" : "A"}, {"cust_id" : "A123", "amount" : 250, "status" : "A"}, {"cust_id" : "A212", "amount" : 200, "status" : "A"}, {"cust_id" : "A123", "amount" : 300, "status" : "D"}])
db.orders.mapReduce(function() { emit(this.cust_id, this.amount);},function(key, values) { return Array.sum(values);},{ query : {"status" : "A"}, out : "total_amount"}).find()
使用 Map-Reduce添加数据:
db.posts.insertMany([{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "mark", "status": "active"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "mark", "status": "active"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "mark", "status": "active"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "mark", "status": "active"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "mark", "status": "disabled"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "runoob", "status": "disabled"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "runoob", "status": "disabled"},{ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "runoob", "status": "active"}])
接下来在 posts 集合中使用 mapReduce 函数来选取已发布的文章(status:"active"),并通过 user_name 分组,计算每个用户的文章数:
db.posts.mapReduce(function() { emit(this.user_name, 1);}, function(key, values) { return Array.sum(values);},{ query : {"status" : "active"}, out : "post_total"})
以上 mapReduce 的输出结果如下:{ "result" : "post_total", "timeMillis" : 32, "counts" : { "input" : 5, "emit" : 5, "reduce" : 1, "output" : 2 }, "ok" : 1}结果表明:共有5个符合查询条件(status:"active")的文档,在 map 函数中生成了5个键值对文档,最后使用 reduce 函数将相同的键值分为2组。具体参数说明:
result:储存结果的 collection 的名字。这是个临时集合,MapReduce 的连接关闭后就被自动删除了。
timeMillis:执行花费的时间,毫秒为单位。
input:满足条件被发送到 map 函数的文档个数。
emit:在 map 函数中调用 emit 的次数,也就是所有集合中的数据总量。
ouput:结果集合中的文档个数(count 对调试非常有帮助)。
ok:是否成功,成功为 1
err:如果失败,这里可以有失败原因,不过从经验上来看,原因比较模糊,作用不大。
注:临时集合参数还可以这样写 out: {"inline" : 1},如下:
db.posts.mapReduce(function() { emit(this.user_name, 1);}, function(key, values) { return Array.sum(values);},{ query : {"status" : "active"}, out : {"inline" : 1}}).find()
设置了 {"inline" : 1} 将不会创建临时集合,整个 Map-Reduce 的操作就在内存中进行,但这个选项只有在结果集中单个文档大小在 16MB 内时才有效。
使用 find 操作符来查看 mapReduce 的查询结果:
db.posts.mapReduce(function() { emit(this.user_name, 1);}, function(key, values) { return Array.sum(values);},{ query : {"status" : "active"}, out : "post_total"}).find()
以上查询显示如下结果:{"id" : "mark", "value" : 4}{"id" : "runoob", "value" : 1}用类似的方式,MapReduce 可以被用来构建大型复杂的聚合查询。Map 函数和 Reduce 函数可以使用 JavaScript 来实现,使得 MapReduce 的使用非常灵活和强大。
MongoDB 全文检索
全文检索对每一个词建立一个索引,指明该词在文档中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户。这个过程类似于通过字典中的检索字表查字的过程。MongoDB 从 2.4 版本开始支持全文检索,目前支持15种语言的全文索引:danish、dutch、english、finnish、french、german、hungarian、italian、norwegian、portuguese、romanian、russian、spanish、swedish、turkish
启用全文检索MongoDB 在 2.6 版本以后默认是开启全文检索的,如果使用之前的版本,则需要使用以下方式来启用全文检索:
db.adminCommand({setParameter : true, textSearchEnabled : true})
或者使用命令:mongod --setParameter textSearchEnabled=true
创建全文索引考虑以下 posts 集合的文档数据,包含了文章内容(post_text)及标签(tags):
db.posts.insert({"post_text": "enjoy the mongodb articles on Runoob","tags": [ "mongodb", "runoob"]})
可以对 post_text 字段建立全文索引:
db.posts.createIndex({"post_text" : "text"})
使用全文索引上面已经对 post_text 建立了全文索引,接下来就可以搜索文章中的关键词 runoob 了:
db.posts.find({$text : {$search : "runoob"}})
如果使用的是旧版本 MongoDB,则可以使用以下命令:
db.posts.runCommand("text", {search : "runoob"})
使用全文索引可以提高搜索效率。
删除全文索引使用 getIndexes() 列出所有的索引名称:
db.posts.getIndexes()
根据索引名称删除索引,命令如下:
db.posts.dropIndex("索引名称")
MongoDB 正则表达式
正则表达式是使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。许多程序设计语言都支持利用正则表达式进行字符串操作。MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式。MongoDB 使用 PCRE(Perl Compatible Regular Expression)作为正则表达式语言。不同于全文检索,我们使用正则表达式不需要做任何配置。考虑以下 posts 集合的文档数据,包含了文章内容(post_text)及标签(tags):
db.posts.insert({"post_text": "enjoy the mongodb articles on Runoob","tags": [ "mongodb", "runoob"]})
使用正则表达式MongoDB 中使用正则表达式得语法:
db.collectionName.find({"field" : {$regex : "keyWords"}}) 或 db.collectionName.find({"field" : /keyWords/})
例、使用正则表达式查找包含指定字符串的文章:
db.posts.find({"post_text" : {$regex : "runoob"}}).pretty()
以上查询也可以写为:
db.posts.find({"post_text" : /runoob/}).pretty()
不区分大小写的正则表达式如果检索时不需要区分大小写,则需要设置 $options 为 $i,命令如下:
db.collectionName.find({"field" : {$regex : "keyWords", $options : "$i"}})
以下命令将查找不区分大小写的字符串 runoob:
db.posts.find({"post_text" : {$regex : "runoob", $options : "$i"}}).pretty()
数组元素使用正则表达式还可以在数组字段中使用正则表达式来查找内容。 这在标签的实现上非常有用,如果需要查找包含以 run 开头的标签数据(ru 或 run 或 runoob),命令如下:
db.posts.find({"tags" : {$regex : "run"}}).pretty()
优化正则表达式查询如果文档中的字段设置了索引,那么使用索引比使用正则表达式匹配查找数据的查询速度更快。如果正则表达式是前缀表达式,所有匹配的数据将以指定的前缀字符串为开始(如: 正则表达式为 ^tut ,则查询语句将查找以 tut 开头的字符串)。
使用正则表达式时需要注意:正则表达式中使用变量,一定要使用 eval 将组合的字符串进行转换,不能直接将字符串拼接后传入给表达式。否则没有报错信息,只是结果为空!
格式如下:
var name = eval("/" + "keyWords" + "/i")db.collectionName.find({"field" : name})
等同于:
db.collectionName.find({"field" : {$regex : "keyWords", $Option : "$i"}})
以下是模糊查询包含 the 关键词, 且不区分大小写:
db.posts.find({"post_text" : eval("/the/i")}).pretty()
或
db.posts.find({"post_text" : eval("/" + "the" + "/i")}).pretty()
$regex 操作符介绍MongoDB 中使用 $regex 操作符来设置匹配字符串的正则表达式,使用 PCRE(Pert Compatible Regular Expression) 作为正则表达式语言。
MongoDB 中的 $regex 操作符:{<field> : {$regex : /pattern/,$options : '<options>'}}{<field> : {$regex : 'pattern',$options : '<options>'}}{<field> : {$regex : /pattern/<options>}}
MongoDB 中的正则表达式对象:{<field> : /pattern/<options>}
$regex 与正则表达式对象的区别:
在 $in 操作符中只能使用正则表达式对象,如:{"name" : {$in : [/^joe/i, /^jack/}}
在使用隐式的 $and 操作符中,只能使用 $regex,如:{"name" : {$regex : /^jo/i, $nin : ['john']}}
当 option 选项中包含 X 或 S 选项时,只能使用 $regex,如:{"name" : {$regex : /m.*line/, $options:"si"}}
$regex 操作符的使用$regex 操作符中的 option 选项可以改变正则匹配的默认行为,它包括 i、m、x、s 四个选项,其含义分别如下:
i:忽略大小写,如 {<field> : {$regex/pattern/i}} 设置 i 选项后,模式中的字母会进行大小写不敏感匹配。
m:多行匹配模式,如 {<field> : {$regex/pattern/, $options : 'm'} 设置 m 选项后,会更改 ^ 和 $ 字符的默认行为,分别使用于行的开头和结尾匹配,而不是与输入字符串的开头和结尾匹配。
x:忽略非转义的空白字符,如 {<field> : {$regex : /pattern/, $options : 'x'} 设置 x 选项后,正则表达式中的非转义的空白字符将被忽略,同时 # 会被解释为注释的开头注,只能显式位于 option 选项中。
s:单行匹配模式,如 {<field> : {$regex : /pattern/, $options : 's'} 设置 s 选项后,会更改 . 字符的默认行为,它会匹配所有字符,包括换行符(\n),只能显式位于 option 选项中。
使用 $regex 操作符时,需要注意几个问题:
i、m、x、s 可以组合使用。如:db.user.find({"name" : {$regex : /j*k/, $options : "si"}}).pretty()
在设置索引的字段上进行正则匹配可以提高查询速度,而且当正则表达式使用的是前缀表达式时,查询速度会进一步提高。如:db.user.find({"name" : {$regex : /^joe/}}).pretty()
MongoDB GridFS
GridFS 用于存储和恢复那些超过 16M(BSON 文件限制)的文件(如图片、音频、视频等)。
GridFS 也是文件存储的一种方式,但它存储在 MongoDB 的集合中。
GridFS 可以更好的存储大于 16M 的文件。
GridFS 会将大文件对象分割成多个小的 chunk(文件片段,一般为每个 256k 大小),每个 chunk 将作为 MongoDB 的一个文档(document)被存储在 chunks 集合中。
GridFS 用两个集合来存储一个文件,分别是:fs.files 与 fs.chunks。每个文件的实际内容被存在 chunks(二进制数据)中,和文件有关的 meta 数据(如filename、content_type、用户自定义属性等)将会被存在 files 集合中。
以下是简单的 fs.files 集合文档:{ "filename": "test.txt", "chunkSize": NumberInt(261120), "uploadDate": ISODate("2014-04-13T11:32:33.557Z"), "md5": "7b762939321e146569b07f72c62cca4f", "length": NumberInt(646)}
以下是简单的 fs.chunks 集合文档:{ "files_id": ObjectId("534a75d19f54bfec8a2fe44b"), "n": NumberInt(0), "data": "Mongo Binary Data"}
GridFS 添加文件假如使用 GridFS 的 put 命令来存储 mp3 文件,则需调用 ${MONGODB_HOME}/bin 下的 mongofiles.exe 工具。命令如下:
mongofiles.exe -d gridfs put song.mp3
参数 -d gridfs:指定存储文件的数据库名称,如果不存则会自动创建。Song.mp3 是音频文件名。使用以下命令来查看数据库中文件的文档:
db.fs.files.find()
以上命令执行后返回以下文档数据:{ _id: ObjectId('534a811bf8b4aa4d33fdf94d'), filename: "song.mp3", chunkSize: 261120, uploadDate: new Date(1397391643474), md5: "e4f53379c909f7bed2e9d631e15c1c41", length: 10401959 }
可以看到 fs.chunks 集合中所有的区块,还可以根据文件的 _id 获取区块(chunk)的数据,命令如下:
db.fs.chunks.find({files_id : ObjectId('534a811bf8b4aa4d33fdf94d')})
MongoDB 固定集合(Capped Collections)
MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,可以将其想象成一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素!
创建固定集合通过 createCollection 来创建一个固定集合,且 capped 选项设置为 true,如下:
db.createCollection("collectionName", {capped : true, size : 10000})
还可以指定文档个数,加上 max:1000 属性即可:
db.createCollection("collectionName", {capped : true, size : 10000, max : 1000})
其中,size 是整个集合空间大小,单位为 KB;max 是集合文档个数上线,单位是个。如果空间大小达到上限,则插入下一个文档时会覆盖第一个文档;如果文档个数达到上限,插入下一个文档时也会覆盖第一个文档。
判断集合是否为固定集合:
db.collectionName.isCapped()
还可以将已存在的集合转换为固定集合,命令如下:
db.runCommand({"convertToCapped" : "collectionName", size : 10000})
固定集合查询固定集合文档按照插入顺序储存的,默认情况下查询就是按照插入顺序返回的,也可以使用 $natural 调整返回顺序:
db.collectionName.find().sort({$natural : -1}).pretty()
固定集合的功能特点MongoDB 固定集合可以插入及更新,但更新不能超出 collection 的大小,否则更新失败。不允许删除,但可以调用 drop() 删除集合中的所有行。注意 drop 后需要显式地重建集合。在32位机器上一个 cappped collection 的最大值约为 482.5M,64位上只受系统文件大小的限制。
固定集合属性及用法
属性1、对固定集合进行插入速度极快2、按照插入顺序的查询输出速度极快3、能够在插入最新数据时,淘汰最早的数据
用法1、储存日志信息2、缓存一些少量的文档
MongoDB 自动增长
MongoDB 没有像 MYSQL 一样有自增的功能,MongoDB 中的 _id 是系统自动生成的12字节唯一标识。但在某些情况下,我们可能需要实现 ObjectId 自动增长功能。由于 MongoDB 没有实现这个功能,因此可以通过编程的方式来实现,以下将在 counters 集合中实现 _id 字段自增。首先创建 counters 集合,序列字段值可以实现自增:
db.createCollection("counters")
然后向 counters 集合中插入以下文档,使用 productid 作为 key:
db.counters.insert({"_id": "productid", "sequence_value": 0})
其中 sequence_value 字段是序列通过自动增长后的一个值。
创建 Javascript 函数接下来创建函数 getNextSequenceValue() 作为序列名的输入,指定的序列会自动增长 1 并返回最新序列值。本文实例中序列名为 productid。
function getNextSequenceValue(sequenceName) {var sequenceDocument = db.counters.findAndModify({ query : {_id : sequenceName}, update : {$inc : {sequence_value : 1}}, "new" : true});return sequenceDocument.sequence_value;}
使用 Javascript 函数最后使用 getNextSequenceValue() 函数创建一个新的文档, 并设置文档 _id 为返回的序列值:
db.products.insert({"id" : getNextSequenceValue("productid"),"product_name" : "Apple iPhone","category" : "mobiles"})db.products.insert({"id" : getNextSequenceValue("productid"),"product_name" : "Samsung S3","category" : "mobiles"})
验证函数是否有效:
db.products.find().pretty()
本文参考:
1、https://www.runoob.com/mongodb/mongodb-tutorial.html
2、https://docs.mongodb.com/manual/