共计 15882 个字符,预计需要花费 40 分钟才能阅读完成。
引子
最近做项目利用 mongo 记录的日志做数据统计。着了非关系型数据库的迷,于是乎买了本《MongoDB 实战》学习了一番。记录一下学习笔记,共享之。
准备
我在自己的 Linux 服务器上装了最新版的 Mongo。记录一下安装链接还有一个遇到的问题。
Linux 安装 mongo https://blog.51cto.com/136418…
我想看数据库状态的时候遇到了一个权限问题
> db.serverStatus()
{
"ok" : 0,
"errmsg" : "not authorized on admin to execute command {serverStatus: 1.0, lsid: { id: UUID(\"bbda7ede-9e92-492b-ae2f-f0f641fba261\") }, $db: \"admin\"}",
"code" : 13,
"codeName" : "Unauthorized"
}
解决方法:https://www.cnblogs.com/alexz…
如果看了上面那个解决方法,我们再次进入 mongo shell 模式(以 admin)需要输入一下命令:
mongo -u ‘admin’ -p ‘123’ –authenticationDatabase ‘admin’
基础概念
mysql | mongo | 解释 |
---|---|---|
database | database | 数据库 |
table | collection | 集合 |
row | document | 文档 |
column | field | 域 |
Mongo 储存数据都是以 BSON 格式的,类似于 JSON,如下:
{"_id": ObjectId("5d399bb2b52d6dc8a4ff6b42"),
"name": "pjjlt"
}
如果不指定主键,会默认生成一个_id(长度一共 12 字节),生成规则:4 个字节的时间戳 + 3 个字节的机器 Id+ 2 个字节的进程 Id+ 3 个字节的随机数
Mongo 操作
基础操作
> show dbs #查看所有数据库和使用情况
admin 0.000GB
config 0.000GB
local 0.000GB
> use pjjlt #切换到 pjjlt 库,发现没有创建之
switched to db pjjlt
> db #查看当前在操作哪个数据库
pjjlt
> db.myCollection.insert({"name":"pjjlt"}) #在 pjjlt 库的 myCollection 集合 (没有此集合,创建之) 插入一条数据
WriteResult({"nInserted" : 1})
> show collections #查看本库 (pjjlt) 下所有集合
myCollection
> db.myCollection.find() #查询某集合数据
{"_id" : ObjectId("5d399bb2b52d6dc8a4ff6b42"), "name" : "pjjlt" }
#篇幅原因,不演示下面操作了,直接说解释
>db.myCollection.drop() #删除某集合
>db.dropDatabase() #删除某数据库
>db.serverStatus() #查看服务器状态信息(查看引擎就在这边看,可以看到 mongo4 的默认引擎已经是 wiredtiger 了)>db.stats() #当前数据库下简单信息 可以查看本库下有多少集合
>db.myCollection.stats() #查看某集合的基础信息
>db.runCommand() #可以执行某个 function()方法
>db.help() #查看数据库层面所有操作
>db.myCollection.help() #查看集合层面所有操作
>db.listCommands() #列举数据库所有命令
数据插入
数据插入,可分为 insert 和 save 方法。具体所有方法,可以先输入一段代码,再按两下 tab 键查看。
# 两下 TAB 键,看下有以下方法。insert 可以实现后面两个方法的功能,即插入一条或多条
> db.myCollection.insert
db.myCollection.insert( db.myCollection.insertMany( db.myCollection.insertOne(
#插入一条记录
> db.myCollection.insert({"name":"haha"})
WriteResult({"nInserted" : 1})
#插入多条记录,输出的信息会更加详细
> db.myCollection.insert([{"name":"hehe"},{"name":"heihei"}])
BulkWriteResult({"writeErrors" : [],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : []})
#看下 myCollection 集合下有多少数据
> db.myCollection.count()
4
#再看下内容,_id 是自动生成的主键。> db.myCollection.find()
{"_id" : ObjectId("5d399bb2b52d6dc8a4ff6b42"), "name" : "pjjlt" }
{"_id" : ObjectId("5d3a6bafd40e94efd747de7b"), "name" : "haha" }
{"_id" : ObjectId("5d3a6c3fd40e94efd747de7c"), "name" : "hehe" }
{"_id" : ObjectId("5d3a6c3fd40e94efd747de7d"), "name" : "heihei" }
#看一下 save 的插入功能,save 可以指定_id
#如果有_id 存在则更新,没有就是插入,功能类似 insert
> db.myCollection.save({"name":"save0"})
WriteResult({"nInserted" : 1})
> db.myCollection.save([{"name":"save1"},{"name":"save2"}])
BulkWriteResult({"writeErrors" : [],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : []})
> db.myCollection.find()
{"_id" : ObjectId("5d399bb2b52d6dc8a4ff6b42"), "name" : "pjjlt" }
{"_id" : ObjectId("5d3a6bafd40e94efd747de7b"), "name" : "haha" }
{"_id" : ObjectId("5d3a6c3fd40e94efd747de7c"), "name" : "hehe" }
{"_id" : ObjectId("5d3a6c3fd40e94efd747de7d"), "name" : "heihei" }
{"_id" : ObjectId("5d3a927fb4d620841817e434"), "name" : "save0" }
{"_id" : ObjectId("5d3a92a2b4d620841817e436"), "name" : "save1" }
{"_id" : ObjectId("5d3a92a2b4d620841817e437"), "name" : "save2" }
数据修改
数据修改有命令 save 和 update,其中 update 有具有局部更新和替换更新的功能
# 先看下 save 方法,指定_id,进行修改
> db.myCollection.save({"_id":ObjectId("5d3a92a2b4d620841817e437"),"name":"save3"})
WriteResult({"nMatched" : 1, "nUpserted" : 0, "nModified" : 1})
> db.myCollection.find()
{"_id" : ObjectId("5d399bb2b52d6dc8a4ff6b42"), "name" : "pjjlt" }
{"_id" : ObjectId("5d3a6bafd40e94efd747de7b"), "name" : "haha" }
{"_id" : ObjectId("5d3a6c3fd40e94efd747de7c"), "name" : "hehe" }
{"_id" : ObjectId("5d3a6c3fd40e94efd747de7d"), "name" : "heihei" }
{"_id" : ObjectId("5d3a927fb4d620841817e434"), "name" : "save0" }
{"_id" : ObjectId("5d3a92a2b4d620841817e436"), "name" : "save1" }
{"_id" : ObjectId("5d3a92a2b4d620841817e437"), "name" : "save3" }
#然后看 update 方法,同 insert 一样,update 可以实现后面两个方法
> db.myCollection.update
db.myCollection.update( db.myCollection.updateMany( db.myCollection.updateOne(
看下 update 语法
db.collection.update(
<query>, #update 的查询条件,类似 sql update 语句 where 后面的部分
<update>, #update 的对象和一些更新的操作符等,也可以理解为 sql update 语句 set 后面的
{
upsert: <boolean>, #可选,这个参数的意思是,如果不存在 update 的记录,是否插入 objNew,true 为插入,默认是 false,不插入
multi: <boolean>, #可选,mongodb 默认是 false,只更新找到的第一条记录,如果这个参数为 true,就把按条件查出来多条记录全部更新
writeConcern: <document> #可选,抛出异常的级别
}
)
接着回到例子中,为了说明方便,我们新建一个新的集合 user
# 给新的集合建一个新的文档
> db.user.insert({"username":"pjjlt","age":25})
WriteResult({"nInserted" : 1})
> db.user.find().pretty()
{"_id" : ObjectId("5d3aa1fcb4d620841817e438"),
"username" : "pjjlt",
"age" : 25
}
# ok,把 pjjlt 的年龄改成 18,使用了关键字 $set, 待会会演示不使用这个关键字的效果
> db.user.update({"_id" : ObjectId("5d3aa1fcb4d620841817e438")},{$set:{"age":18}})
WriteResult({"nMatched" : 1, "nUpserted" : 0, "nModified" : 1})
> db.user.find().pretty()
{"_id" : ObjectId("5d3aa1fcb4d620841817e438"),
"username" : "pjjlt",
"age" : 18
}
#如果使用了 $set 是局部更新,如果不使用,就是替换更新,为了区分,我们已跟新新的域 city 为例
> db.user.update({"_id" : ObjectId("5d3aa1fcb4d620841817e438")},{"city":"SJZ"})
WriteResult({"nMatched" : 1, "nUpserted" : 0, "nModified" : 1})
> db.user.find().pretty()
{"_id" : ObjectId("5d3aa1fcb4d620841817e438"), "city" : "SJZ" }
#发现原始数据也没了,因为被全部替换了
数据删除
数据删除可以使用 deleteMany、deleteOne、remove
# 两下 Tab
> db.user.delete
db.user.deleteMany( db.user.deleteOne(
# 删除没数据了
> db.user.deleteOne({"_id" : ObjectId("5d3aa1fcb4d620841817e438")})
{"acknowledged" : true, "deletedCount" : 1}
> db.user.find().pretty()
>
# 再增加几条数据
> db.user.find()
{"_id" : ObjectId("5d3ab2f8b4d620841817e439"), "username" : "pjjlt", "age" : 25 }
{"_id" : ObjectId("5d3ab2fdb4d620841817e43a"), "username" : "pjjlt1", "age" : 25 }
{"_id" : ObjectId("5d3ab302b4d620841817e43b"), "username" : "pjjlt2", "age" : 25 }
{"_id" : ObjectId("5d3ab322b4d620841817e43c"), "username" : "pjjlt3", "age" : 18 }
remove 语法
db.collection.remove(
<query>, #可选,查询条件
{
justOne: <boolean>, #可选,设置为 true 或者 1,表示只删除一个文档,设置为 false,表示删除所有匹配的文档,默认为 false
writeConcern: <document> #可选,抛出异常的级别
}
)
再回到例子删除一下。需要手动释放磁盘空间
> db.user.remove({"age" : 25})
WriteResult({"nRemoved" : 3})
> db.user.find()
{"_id" : ObjectId("5d3ab322b4d620841817e43c"), "username" : "pjjlt3", "age" : 18 }
#手动释放磁盘空间
> db.repairDatabase()
{"ok" : 1}
数据查询
几乎大部分的业务都和查询有关。
范围查询运算符
运算符 | 描述 |
---|---|
$lt | 小于 |
$gt | 大于 |
$lte | 小于等于 |
$gte | 大于等于 |
准备一个大数据集合,往 numbers 集合里面添加 1000 个数字
# 前面那三个点是自动生成的,对代码没有影响,请忽略
> for(i=0;i<1000;i++){... db.numbers.save({num:i});
... }
WriteResult({"nInserted" : 1})
#测试一个 $lt,其他雷同
> db.numbers.find({"num":{$lt:5}})
{"_id" : ObjectId("5d3ac7b7b4d620841817e43d"), "num" : 0 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e43e"), "num" : 1 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e43f"), "num" : 2 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e440"), "num" : 3 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e441"), "num" : 4 }
集合操作符
运算符 | 描述 |
---|---|
$in | 如果任意参数引用集合里,则匹配 |
$all | 如果所有参数再引用集合里且被使用在包含数组的文档中,则匹配 |
$nin | 如果没有参数在引用集合里,则匹配 |
# 插入一条记录
> db.tools.insert({tools:["AAA","BBB","CCC","DDD","EEE"]})
WriteResult({"nInserted" : 1})
#查看一下
> db.tools.find()
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"] }
#通过 $in 搜出来了
> db.tools.find({tools:{... $in:["AAA","FFF","ZZZ"]
... }
... })
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"] }
#$all 搜索测试
> db.tools.find({tools:{ $all:["AAA","FFF","ZZZ"] } })
> db.tools.find({tools:{ $all:["AAA","BBB"] } })
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"] }
#$nin 搜索测试
> db.tools.find({tools:{ $nin:["AAA","BBB"] } })
> db.tools.find({tools:{ $nin:["ZZZ","YYY"] } })
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"] }
布尔运算符
运算符 | 描述 |
---|---|
$ne | 不匹配参数 |
$not | 不匹配结果 |
$or | 有一个条件匹配就成立 |
$nor | 所有条件都不匹配 |
$and | 所有条件都匹配 |
$exists | 判断元素是否存在 |
# 接着上面的例子,使用 $ne
> db.tools.find({tools:{$ne:"AAA"}})
> db.tools.find({tools:{$ne:"ZZZ"}})
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"] }
#$not 看就就像条件取反
> db.tools.find({tools:{$not:{$in:["AAA"]}}})
> db.tools.find({tools:{$not:{$in:["ZZZ"]}}})
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"] }
#为了演示下面的功能,添加新域给刚才那个文档
> db.tools.update({"_id" : ObjectId("5d3ad78eb4d620841817e825")},{$set:{"city":"SJZ"}})
WriteResult({"nMatched" : 1, "nUpserted" : 0, "nModified" : 1})
> db.tools.find().pretty()
{"_id" : ObjectId("5d3ad78eb4d620841817e825"),
"tools" : [
"AAA",
"BBB",
"CCC",
"DDD",
"EEE"
],
"city" : "SJZ"
}
#$or
> db.tools.find({$or:[ {tools:{$in:["AAA"]}}, {"city":"BJ"} ]} )
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"], "city" : "SJZ" }
#$nor 所有条件都不匹配
> db.tools.find({$nor:[ {tools:{$in:["ZZZ"]}}, {"city":"BJ"} ]} )
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"], "city" : "SJZ" }
#$and
> db.tools.find({$and:[ {tools:{$in:["AAA"]}}, {"city":"BJ"} ]} )
> db.tools.find({$and:[ {tools:{$in:["AAA"]}}, {"city":"SJZ"} ]} )
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"], "city" : "SJZ" }
#$exists
# tools 集合是否存在 tools 域
> db.tools.find({"tools":{$exists:true}})
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"], "city" : "SJZ" }
# tools 集合是否不存在 tools 域
> db.tools.find({"tools":{$exists:false}})
>
数组操作符
运算符 | 描述 |
---|---|
$elemMatch | 如果提供的所有词语在相同的子文档中,则匹配 |
$size | 如果子文档数组大小与提供的文本值相同,则匹配 |
# 在新的集合里面插入新的文档
> db.products.insert({addresses:[{name:"home",city:"sjz"},{name:"work",city:"bj"}]})
WriteResult({"nInserted" : 1})
> db.products.find().pretty()
{"_id" : ObjectId("5d3ae3763d2312bc3a0be84c"),
"addresses" : [
{
"name" : "home",
"city" : "sjz"
},
{
"name" : "work",
"city" : "bj"
}
]
}
#$elemMatch 需要子文档的哦
> db.products.find({addresses:{$elemMatch:{"name":"home","city":"sjz"}}})
{"_id" : ObjectId("5d3ae3763d2312bc3a0be84c"), "addresses" : [{ "name" : "home", "city" : "sjz"}, {"name" : "work", "city" : "bj"} ] }
#size
> db.products.find({"addresses":{$size:1}})
> db.products.find({"addresses":{$size:2}})
{"_id" : ObjectId("5d3ae3763d2312bc3a0be84c"), "addresses" : [{ "name" : "home", "city" : "sjz"}, {"name" : "work", "city" : "bj"} ] }
排序分页分隔符
运算符 | 描述 |
---|---|
$sort | 排序 1 正序;-1 倒叙 |
$limit | 显示多少条 |
$skip | 跳过多少数据 |
注意和 MySQL 一样,skip 操作大数的时候会很慢,需要提前缩小范围
# 使用 numbers 这个集合
> db.numbers.find().sort({"num":-1}).skip(33).limit(9)
{"_id" : ObjectId("5d3ac7b8b4d620841817e803"), "num" : 966 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e802"), "num" : 965 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e801"), "num" : 964 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e800"), "num" : 963 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e7ff"), "num" : 962 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e7fe"), "num" : 961 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e7fd"), "num" : 960 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e7fc"), "num" : 959 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e7fb"), "num" : 958 }
其他操作符
运算符 | 描述 |
---|---|
$where | 执行任意 JavaScript 来选择文档 |
$regex | 匹配正则表达式 |
$mod[(quatient),(result)] | 如果元素除以除数符合结果则匹配 |
$type | 如果元素的类型符合指定的 BSON 类型则匹配 |
$text | 允许在建立文本索引的字段上执行文本搜索 |
null | 不是关键词,不用加 $,只是看一下使用 |
# 只演示下 mod 和 regex
#接着用刚才 numbers 那个集合
> db.numbers.find({"num":{$mod:[200,3]}})
{"_id" : ObjectId("5d3ac7b8b4d620841817e440"), "num" : 3 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e508"), "num" : 203 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e5d0"), "num" : 403 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e698"), "num" : 603 }
{"_id" : ObjectId("5d3ac7b8b4d620841817e760"), "num" : 803 }
#使用刚才 user 集合,搜索所有 username 以 p 开头的文档
> db.user.find({"username":{$regex:/^p.*/}})
{"_id" : ObjectId("5d3ab322b4d620841817e43c"), "username" : "pjjlt3", "age" : 18 }
#null 的使用,搜索出没有这个域的文档,或者这个域为 null
> db.user.find({"username":null})
> db.user.find({"username111":null})
{"_id" : ObjectId("5d3ab322b4d620841817e43c"), "username" : "pjjlt3", "age" : 18 }
聚合操作 aggregate
可以把聚合框架理解成 SQL 中的 GROUP BY。为了调用聚合框架就要定义一个管道。聚合管道里的每一步输出作为下一步的输入。类似于流的概念。
运算符 | 描述 |
---|---|
$project | 指定输出文档里的字段(项目) |
$match | 选择要处理的字段,与 find()类似 |
$limit | 限制传递给下一步文档数量 |
$skip | 跳过一定数量的文档 |
$unwind | 拓展数组,为每一个数组入口生成一个输出文档 |
$group | 根据 key 来分组文档 |
$skip | 排序文档 |
$geoNear | 选择某个地理位置附近的文档 |
$out | 把管道的结果写入某集合 |
$redact | 控制特定数据的访问 |
以上很多命令符与上面查询命令符功能相同,$geoNear(这个貌似很有趣,类似于图论中迪杰斯特拉、弗洛伊德那些算法,可以计算坐标点之间的物理距离)和 $redact 先不过多的讨论。$limit、$skip、$skip 也过于简单,上面演示过了,这里不再重复。看一下这些命令和关系型数据库 SQL 的类比
SQL 命令 | 聚合命令操作符 |
---|---|
SELECT | $project $group functions : $sum,$min,$avg,etc. |
FROM | db.collectionName.aggregate(…) |
JOIN | $unwind |
WHERE | $match |
GROUP BY | $group |
HAVING | $match |
但是集合命令操作符不必按照以上顺序进行书写,比如可以先写 $match 进行条件筛选,然后进行其他操作(其实,也往往建议这么做,毕竟第一步就把大部分没用的数据过滤了)
# 为了方便说明,新建一个集合
> db.score.find()
{"_id" : ObjectId("5d3bac2d78b22e869eb2fd26"), "name" : "pjjlt", "age" : 25, "city" : "sjz" }
{"_id" : ObjectId("5d3bac5878b22e869eb2fd27"), "name" : "qex", "age" : 112, "city" : "london" }
{"_id" : ObjectId("5d3bad9078b22e869eb2fd28"), "name" : "haha", "age" : 24, "city" : "sjz" }
{"_id" : ObjectId("5d3bada078b22e869eb2fd29"), "name" : "heihei", "age" : 25, "city" : "bj" }
#验证 $match 和 $project
> db.score.aggregate([... {$match:{age:{$lt:100}}}, // 条件过滤
... {$project:{"_id":0}} // 不显示主键
... ])
{"name" : "pjjlt", "age" : 25, "city" : "sjz"}
{"name" : "haha", "age" : 24, "city" : "sjz"}
{"name" : "heihei", "age" : 25, "city" : "bj"}
#接下来分组显示每个城市的平均年龄
> db.score.aggregate([{$group:{_id:"$city",avgAge:{$avg:"$age"}}} ])
{"_id" : "bj", "avgAge" : 25}
{"_id" : "london", "avgAge" : 112}
{"_id" : "sjz", "avgAge" : 24.5}
对于 $group, 对于原始数据如 city、age,进行显示的时候前面也需要加上 $,如 $city、$age,否则显示为 null
另外展示一下 $group 中可以使用的命令
命令 | 描述 |
---|---|
$addToSet | 为组里唯一的值创建一个数组 |
$first | 组里第一个值,只有前缀 $sort 才有意义 |
$last | 组里最后一个值,只有前缀 $sort 才有意义 |
$max | 组里某个字段的最大值 |
$min | 组里某个字段的最小值 |
$avg | 某个字段的平均值 |
$push | 返回组内所有值的数组。不去重复值 |
$sum | 求组内所有值的和 |
# 回来接着展示 $out 字符,并且 $out 字符类似于 Java8 中的流收集器,必须放最后执行
> db.score.aggregate([{$group:{_id:"$city",avgAge:{$avg:"$age"}}} ,{$out:'aResult'}])
#删除一下变量中的数据
> db.aResult.find()
{"_id" : "bj", "avgAge" : 25}
{"_id" : "london", "avgAge" : 112}
{"_id" : "sjz", "avgAge" : 24.5}
#最后是 $unwind,接着用上次那个 tools 集合
> db.tools.find()
{"_id" : ObjectId("5d3ad78eb4d620841817e825"), "tools" : ["AAA", "BBB", "CCC", "DDD", "EEE"], "city" : "SJZ" }
> db.tools.aggregate([... {$project:{"_id":0}},
... {$unwind:"$tools"}
... ])
{"tools" : "AAA", "city" : "SJZ"}
{"tools" : "BBB", "city" : "SJZ"}
{"tools" : "CCC", "city" : "SJZ"}
{"tools" : "DDD", "city" : "SJZ"}
{"tools" : "EEE", "city" : "SJZ"}
同关系型数据库一样,在 project 的时候,可以对数据进行进一步加工,可选函数有 concat、toLower、toUpper 这种字符串函数;add、mod、multiply 这种算数运算函数;日期运算符;逻辑运算符;集合操作符等。以后有空在总结。
原子性
mongo 在事务处理方面不如 mysql。比如两个不同线程 update 的时候(+ 1 操作)可能会出现数据被覆盖的现象。特别介绍一个具有原子性的操作 findAndModify(), 总找到到修改提交是一个原子事件,期间其他线程查询该数据会被拒绝。
当然为了保护数据,在业务代码层面(比如 java)可以考虑加锁。
> db.score.find({"name":"qex"})
{"_id" : ObjectId("5d3bac5878b22e869eb2fd27"), "name" : "qex", "age" : 112, "city" : "london" }
#修改 qex 的年龄
> db.score.findAndModify({... query:{"name":"qex"},
... update:{$inc:{"age":+3}}
... })
{"_id" : ObjectId("5d3bac5878b22e869eb2fd27"),
"name" : "qex",
"age" : 112,
"city" : "london"
}
> db.score.find({"name":"qex"})
{"_id" : ObjectId("5d3bac5878b22e869eb2fd27"), "name" : "qex", "age" : 115, "city" : "london" }
总结一下更新操作符
操作符 | 描述 |
---|---|
$inc | 根据给定的值更改数字 |
$set | 设置字段为给定的值 |
$unset | 取消设置字段 |
$rename | 重命名字段给定的值 |
$setOnInsert | 在 upsert 中,插入时设置字段 |
$bit | 只执行按位更新字段 |
$ | 根据查询选择器定位要更新的子文档 |
$push | 添加值到数组中 |
$addToSet | 添加值到数组中,重复了也不处理 |
$pop | 从数组中删除第一个或者最后一个值 |
$pull | 从数组中删除匹配查询条件的值 |
$pullAll | 从数组中删除多个值 |
$each | 从 push 和 addToSet 一起使用来操作多个值 |
$slice | 从 push 和 each 一起使用来缩小更新后数组的大小 |
$sort | 从 push 和 each、slice 一起使用来排序数组中的子文档 |
$isolated | 隔离其他操作,不允许其他操作交叉更新多个文档 |
索引
索引分类
- 唯一索引
确保该数据在该集合只有一份,比如说主键,格式:
> db.user.createIndex({username:1},{unique:true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
- 稀疏索引
稀疏索引可以保证某些文档不包含这个属性,就不会被筛选出来。例如通过电话号码建索引,但是集合中有的文档电话号码是 null,在创建索引的时候就不会把这些文档考虑进来。
> db.user.createIndex({username:1},{sparse:true})
- 多键索引
索引字段是个数字,比如 tools 集合中的 tools 域,这意味着针对这些数组任意值在索引上的查询都会定位到文档上
- 哈希索引
哈希索引的好处是可以让索引上的入口是均匀分布的
db.user.createIndex({username:'hashed'})
- 地理位置索引
查询某个地点附近的文档,基于经纬度来储存每个文档。
当然从引用域的数量来说还可以分为单键索引和复合索引,和 mysql 的行为类似。
索引管理
- 建立索引
使用 createIndex()。由于建立索引的时候,数据量大会导致系统长时间等待,所以要可以指定后台索引(由于需要大量数据,我没实验),还有离线索引等等。
db.user.createIndex({username:'hashed'},{background:true})
索引反复操作可能会在内存中产生大量碎片,可以使用命令进行碎片整理(索引新建之)
> db.user.reIndex()
{
"nIndexesWas" : 2,
"nIndexes" : 2,
"indexes" : [
{
"v" : 2,
"key" : {"_id" : 1},
"name" : "_id_",
"ns" : "pjjlt.user"
},
{
"v" : 2,
"unique" : true,
"key" : {"username" : 1},
"name" : "username_1",
"ns" : "pjjlt.user"
}
],
"ok" : 1
}
- 查看索引
查看某个集合的索引使用 getIndexes()
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {"_id" : 1},
"name" : "_id_",
"ns" : "pjjlt.user"
},
{
"v" : 2,
"unique" : true,
"key" : {"username" : 1},
"name" : "username_1",
"ns" : "pjjlt.user"
}
]
- 删除索引
> db.user.dropIndex("username_1") #名字可以在查看索引的时候找到
{"nIndexesWas" : 2, "ok" : 1}
引擎
主要有两种:MMAPV1 和 WiredTiger. 在 2.6 版本之前只有 MMAPV1,在 3.0 之后有了 WiredTiger,但是默认还是 MMAPV1。本篇使用的是 4 版本,已经默认是 WiredTiger 引擎了。相对于说 MMAPV1,WiredTiger 使用磁盘空间小得多,只需要将近 7、8 分之一的空间。记得从旧版本的 mongo 升级,导数据,原来 20G 的数据最后只有个位数(具体几 G 忘了),吓一跳,还担心会丢数据,原来是引擎变了。同时锁的颗粒度从数据库到文档级别,并发性能提升不少。总之,新的引擎占资源小,性能还高。
复制和分片
复制:复制是跨多个 Mongo 服务器(节点)分布和维护数据的方法。MongoDB 可以把数据从一个节点复制到其他节点并在修改时进行同步。这种类型的复制是通过一个叫可复制集的机制提供。集群中的节点配置为自动同步数据,并且在服务器出错十自动容灾。MongDB 还有一种旧的复制方法:主从复制。一般复制数据的时候,数据同步到了大部分(大于 50%,比如说三台机器中的两台)节点就可以认为复制操作完成。可复制集应该保护至少 3 个成员,其中一个可以当裁判。
分片:分片是把大型数据集进行分区成更小的可管理的片的过程。简单来说就是一台 MongoDB 服务器盛不下或处理不了那么多数据(1,2,3,4,5,6)了,就将这些数据分到三台小点的机子上,分别维护(1,4)(2,5)(3,6)
参考
《MongoDB 实战》
菜鸟教程 https://www.runoob.com/mongod…