注:本系列教程是自己学习的记录,内容来至 菜鸟教程
MongoDB入门教程01
MongoDB入门教程02
MongoDB入门教程03
MongoDB入门教程04
MongoDB入门教程05
1. MongoDB关系
MongoDB是如何管理类似关系型数据库中的级联关系的呢?
嵌入式关系
使用嵌入式方法,我们可以把用户地址嵌入到用户的文档中:
{
"_id" : ObjectId("5af10a5dc1ff9494de25bf1e"),
"contact" : "987654321",
"dob" : "01-01-1991",
"name" : "Tom Benzamin",
"address" : [
{
"building" : "22 A, Indiana Apt",
"pincode" : 123456.0,
"city" : "Los Angeles",
"state" : "California"
},
{
"building" : "170 A, Acropolis Apt",
"pincode" : 456789.0,
"city" : "Chicago",
"state" : "Illinois"
}
]
}
这种方式的缺点是数据冗余比较大
引用式关系
把用户数据文档和用户地址数据文档分开,通过引用文档的 _id
字段来建立关系,这种方式类似于关系型数据库。
user:
{
"contact": "987654321",
"dob": "01-01-1991",
"name": "Tom Benzamin",
"address_ids": [
ObjectId("5af10c5bc1ff9494de25bf1f"),
ObjectId("5af10c5bc1ff9494de25bf20")
]
}
> var result = db.user.findOne({name:"Jackey"}, {address_ids:1, _id:0})
> db.address.find({_id:{$in:result["address_ids"]}}).pretty()
{
"_id" : ObjectId("5af10c5bc1ff9494de25bf1f"),
"building" : "22 A, Indiana Apt",
"pincode" : 123456,
"city" : "Los Angeles",
"state" : "California"
}
{
"_id" : ObjectId("5af10c5bc1ff9494de25bf20"),
"building" : "170 A, Acropolis Apt",
"pincode" : 456789,
"city" : "Chicago",
"state" : "Illinois"
}
2. MongoDB查询分析
使用 explain()
> db.col.find({likes: {$gt:120}}, {title:1, _id:0}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.col",
"indexFilterSet" : false,
"parsedQuery" : {
"likes" : { "$gt" : 120 }
},
"winningPlan" : {
"stage" : "PROJECTION",
"transformBy" : {
"title" : 1,
"_id" : 0
},
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"likes" : 1
},
"indexName" : "likes_1", # 使用的索引
"isMultiKey" : false,
"multiKeyPaths" : {
"likes" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"likes" : [ "(120.0, inf.0]" ]
}
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "ChoodeMacBook-Pro.local",
"port" : 27017,
"version" : "3.6.4",
"gitVersion" : "d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856"
},
"ok" : 1
}
使用 hint()
显式指定一个索引
> db.col.find({likes: {$gt:120}, title:"Java 教程"}, {title:1,_id:0})
.hint({likes:1}).explain()
3. MongoDB原子操作
mongdb不支持事务,所以,在你的项目中应用时,要注意这点。无论什么设计,都不要要求mongodb保证数据的完整性。
但是mongodb提供了许多原子操作,比如文档的保存,修改,删除等,都是原子操作。
所谓原子操作就是要么这个文档保存到Mongodb,要么没有保存到Mongodb,不会出现查询到的文档没有保存完整的情况。
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.collection.findAndModify()
方法来判断书籍是否可结算并更新新的结算信息。
在同一个文档中嵌入的 available 和 checkout 字段来确保这些字段是同步更新的:
> db.books.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
可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作。
> { $inc : { field : value } }
$push
用法:
> { $push : { field : value } }
把value
追加到field
里面去,field
一定要是数组类型才行,如果field
不存在,会新增一个数组类型加进去。
$pushAll
同$push
,只是一次可以追加多个值到一个数组字段内。
> { $pushAll : { field : value_array } }
$pull
从数组field
内删除一个等于value
值。
> { $pull : { field : _value } }
$addToSet
增加一个值到数组内,而且只有当这个值不在数组内才增加。
$pop
删除数组的第一个或最后一个元素
> { $pop : { field : 1 } }
$rename
修改字段名称
> { $rename : { old_field_name : new_field_name } }
$bit
位操作,integer类型
> {$bit : { field : {and : 5}}}
偏移操作符
> t.find()
{ "_id" : ObjectId("4b97e62bf1d8c7152c9ccb74"), "title" : "ABC",
comments" : [ { "by" : "joe", "votes" : 3 }, { "by" : "jane", "votes" : 7 } ] }
> t.update( {'comments.by':'joe'}, {$inc:{'comments.$.votes':1}}, false, true)
> t.find()
{ "_id" : ObjectId("4b97e62bf1d8c7152c9ccb74"), "title" : "ABC", "comments" : [ { "by" : "joe", "votes" : 4 }, { "by" : "jane", "votes" : 7 } ] }
4. MongoDB 高级索引
考虑以下文档集合(users ):
> {
"address": {
"city": "Los Angeles",
"state": "California",
"pincode": "123"
},
"tags": [
"music",
"cricket",
"blogs"
],
"name": "Tom Benzamin"
}
以上文档包含了 address
子文档和 tags
数组。
索引数组字段
假设我们基于标签来检索用户,为此我们需要对集合中的数组 tags
建立索引。
在数组中创建索引,需要对数组中的每个字段依次建立索引。所以在我们为数组 tags
创建索引时,会为 music
、cricket
、blogs
三个值建立单独的索引。
使用以下命令创建数组索引:
> db.users.ensureIndex({"tags": 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
创建索引后,我们可以这样检索集合的 tags 字段:
> db.users.find({tags:"cricket"})
{ "_id" : ObjectId("5bd96058c62eb66d43e14e97"),
"address" : { "city" : "Los Angeles", "state" : "California", "pincode" : "123" },
"tags" : [ "music", "cricket", "blogs" ],
"name" : "Tom Benzamin" }
为了验证我们使用使用了索引,可以使用 explain 命令:
> db.users.find({tags: "cricket"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.users",
"indexFilterSet" : false,
"parsedQuery" : {
"tags" : {
"$eq" : "cricket"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"tags" : 1
},
"indexName" : "tags_1", // 瞧,这就是我们的索引
"isMultiKey" : true,
"multiKeyPaths" : {
"tags" : [
"tags"
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"tags" : [
"[\"cricket\", \"cricket\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "ChoodeMacBook-Pro.local",
"port" : 27017,
"version" : "3.6.4",
"gitVersion" : "d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856"
},
"ok" : 1
}
以上命令执行结果中会显示已经使用的索引。
索引子文档字段
假设我们需要通过city
、state
、pincode
字段来检索文档,由于这些字段是子文档的字段,所以我们需要对子文档建立索引。
为子文档的三个字段创建索引,命令如下:
> db.users.ensureIndex({"address.city":1,"address.state":1,"address.pincode":1})
一旦创建索引,我们可以使用子文档的字段来检索数据:
> db.users.find({"address.city":"Los Angeles"})
查询表达不一定遵循指定的索引的顺序,mongodb 会自动优化。所以上面创建的索引将支持以下查询:
> db.users.find({"address.state":"California","address.city":"Los Angeles"})
同样支持以下查询:
> db.users.find({"address.city":"LosAngeles","address.state":"California",
"address.pincode":"123"})
5. MongoDB 索引限制
额外开销
每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。所以,如果你很少对集合进行读取操作,建议不使用索引。
内存(RAM)使用
由于索引是存储在内存(RAM)中,你应该确保该索引的大小不超过内存的限制。
如果索引的大小大于内存的限制,MongoDB会删除一些索引,这将导致性能下降。
查询限制
索引不能被以下的查询使用:
- 正则表达式及非操作符,如
not
, 等。 - 算术运算符,如
$mod
, 等。 -
$where
子句
所以,检测你的语句是否使用索引是一个好的习惯,可以用explain
来查看。
索引键限制
从2.6版本开始,如果现有的索引字段的值超过索引键的限制,MongoDB中不会创建索引。
插入文档超过索引键限制
如果文档的索引字段值超过了索引键的限制,MongoDB不会将任何文档转换成索引的集合。与mongorestore和mongoimport工具类似。
最大范围
- 集合中索引不能超过64个
- 索引名的长度不能超过128个字符
- 一个复合索引最多可以有31个字段
6. MongoDB Map Reduce
Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。
MongoDB提供的Map-Reduce非常灵活,对于大规模数据分析也相当实用。
MapReduce 命令
以下是MapReduce的基本语法:
> db.collection.mapReduce(
function() {emit(key, value);}, //map 函数
function(key, values) {return reduceFunction}, //reduce 函数
{
out: collection,
query: document,
sort: document,
limit: number
}
)
使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数,Map 函数调用emit(key, value)
, 遍历 collection
中所有的记录, 将 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
的总和。
[图片上传失败...(image-842060-1540976273964)]
使用 MapReduce
考虑以下文档结构存储用户的文章,文档存储了用户的 user_name
和文章的 status
字段:
// 随机插入一些document
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。","user_name": "runoob", "status":"disabled" })
WriteResult({ "nInserted" : 1 })
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "runoob", "status":"active" })
WriteResult({ "nInserted" : 1 })
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "runoob", "status":"active" })
WriteResult({ "nInserted" : 1 })
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "runoob", "status":"active" })
WriteResult({ "nInserted" : 1 })
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "stefanchoo", "status":"active" })
WriteResult({ "nInserted" : 1 })
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "africa", "status":"active" })
WriteResult({ "nInserted" : 1 })
> db.posts.insert({ "post_text": "菜鸟教程,最全的技术文档。", "user_name": "astefanchoo", "status":"active" })
WriteResult({ "nInserted" : 1 })
....
现在,我们将在 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" : 165,
"counts" : {
"input" : 11,
"emit" : 11,
"reduce" : 2,
"output" : 5
},
"ok" : 1
}
结果表明,共有 11 个符合查询条件(status:"active")
的文档, 在map函数中生成了 5 个键值对文档,最后使用reduce函数将相同的键值分为 2 组。
具体参数说明:
- result 储存结果的collection的名字,这是个临时集合,MapReduce的连接关闭后自动就被删除了。
- timeMillis 执行花费的时间,毫秒为单位
- input 满足条件被发送到map函数的文档个数
- emit 在map函数中emit被调用的次数,也就是所有集合中的数据总量
- reduce reduce 操作次数
- ouput 结果集合中的文档个数(count对调试非常有帮助)
- ok 是否成功,成功为1
- err 如果失败,这里可以有失败原因,不过从经验上来看,原因比较模糊,作用不大
使用 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" : "africa", "value" : 1 }
{ "_id" : "astefanchoo", "value" : 1 }
{ "_id" : "mark", "value" : 4 }
{ "_id" : "runoob", "value" : 4 }
{ "_id" : "stefanchoo", "value" : 1 }
用类似的方式,MapReduce可以被用来构建大型复杂的聚合查询。
Map函数和Reduce函数可以使用 JavaScript
来实现,使得MapReduce的使用非常灵活和强大。
7. MongoDB 正则表达式
正则表达式是使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。
许多程序设计语言都支持利用正则表达式进行字符串操作。
MongoDB 使用 $regex
操作符来设置匹配字符串的正则表达式。
MongoDB使用PCRE (Perl Compatible Regular Expression)
作为正则表达式语言。
不同于全文检索,我们使用正则表达式不需要做任何配置。
考虑以下 posts
集合的文档结构,该文档包含了文章内容和标签:
> {
"post_text": "enjoy the mongodb articles on runoob",
"tags": [
"mongodb",
"runoob"
]
}
使用正则表达式
以下命令使用正则表达式查找包含runoob
字符串的文章:
> db.posts.find({post_text:{$regex:"runoob"}})
以上查询也可以写为:
> db.posts.find({post_text:/runoob/})
不区分大小写的正则表达式
以下命令将查找不区分大小写的字符串 runoob:
> db.posts.find({post_text:{$regex:"runoob",$options:"$i"}})
集合中会返回所有包含字符串 runoob 的数据,且不区分大小写:
> {
"_id" : ObjectId("53493d37d852429c10000004"),
"post_text" : "hey! this is my post on runoob",
"tags" : [ "runoob" ]
}
数组元素使用正则表达式
我们还可以在数组字段中使用正则表达式来查找内容。 这在标签的实现上非常有用,如果你需要查找包含以 run 开头的标签数据(ru 或 run 或 runoob), 你可以使用以下代码:
> db.posts.find({tags:{$regex:"run"}})
优化正则表达式查询
如果你的文档中字段设置了索引,那么使用索引相比于正则表达式匹配查找所有的数据查询速度更快。
如果正则表达式是前缀表达式,所有匹配的数据将以指定的前缀字符串为开始。例如: 如果正则表达式为 ^tut ,查询语句将查找以 tut 为开头的字符串。
这里面使用正则表达式有两点需要注意:
正则表达式中使用变量。一定要使用eval将组合的字符串进行转换,不能直接将字符串拼接后传入给表达式。否则没有报错信息,只是结果为空!实例如下:
> var name=eval("/" + 变量值key +"/i");
以下是模糊查询包含title关键词, 且不区分大小写:
title:eval("/"+title+"/i") // 等同于 title:{$regex:title,$Option:"$i"}
8. MongoDB自动增长
MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节唯一标识。
可以使用编程的方式实现id自增长
使用 counters 集合
创建 counters 集合,序列字段值可以实现自动长:
> db.createCollection("counters")
现在我们向 counters 集合中插入以下文档,使用 productid 作为 key:
{
"_id":"productid",
"sequence_value": 0
}
sequence_value
字段是序列通过自动增长后的一个值。
使用以下命令插入 counters
集合的序列文档中:
> db.counters.insert({_id:"productid",sequence_value:0})
创建 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"})
就如你所看到的,我们使用 getNextSequenceValue
函数来设置 _id
字段。
为了验证函数是否有效,我们可以使用以下命令读取文档,发现 _id 字段是自增长的:
> db.products.find()
{"_id" : 1, "product_name" : "Apple iPhone", "category" : "mobiles"}
{ "_id" : 2, "product_name" : "Samsung S3", "category" : "mobiles"}