文章目录
- 1.1. insertOne方法
- 1.2. insertMany方法
- 1.3. insert方法
- 1.4. 写入安全控制
- 1.4.1. MongoDB的部署模式
- 1.4.2. MongoDB的数据写入过程
- 1.4.3. 日志文件(恢复日志Journal)的特点
- 1.4.4. 解读配置项
- 2.1. 条件查询
- 2.2. 范围查询
- 2.4. limit和skip方法
- 2.5. `$type` 操作符
- 2.6. 子文档查询
- 2.7. 聚合查询
- 2.7.1. 管道和步骤
- 2.7.2 常用的例子
- 2.7.4. 使用Compass工具
一、插入文档
MongoDB的文档结构和Json基本上是一样的,但是MongoDB存储的是类Json的Bson二进制存储格式。
MongoDB的插入文档的操作有好几个,我们逐一的解释一下:
方法名称
| 方法描述
|
insert
| 如果插入的主键存在,抛出异常,提示主键异常,不保存当前数据(支持批量操作)
|
save
| 如果_id主键存在则更新数据,不存在就插入数据(已废弃insertOne或者replaceOne替代)
|
insertOne
| 向集合插入一个新的文档
|
insertMany
| 向集合插入一个或者多个文档
|
注意:这几个方法在我们集合不存在的时候都会自动创建集合并且写入数据。
1.1. insertOne方法
这个insertOne方法有两个参数:需要写入的文档信息(document)和写入的安全控制参数(writeConcern),关于写入安全控制后面会详细介绍!
> db.students.insertOne({"name": "张三", "age": "男", })
{
"acknowledged" : true,
"insertedId" : ObjectId("60557528299f04d494fd1da8")
}
> db.students.find()
{ "_id" : ObjectId("60557528299f04d494fd1da8"), "name" : "张三", "age" : "男" }
>
1.2. insertMany方法
这个方式是写入多条数据,基本上和insert一样。
> db.students.insertMany([{name: "李四", age: "女", sex: "565"},{name: "wabng", sex: "dfdfg"}])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("60557ea1299f04d494fd1da9"),
ObjectId("60557ea1299f04d494fd1daa")
]
}
>
这里需要注意一个参数的问题,insertMany多了一个ordered的参数(默认就是有序写入:true),在分片集合上按顺序写入的操作效率比较慢,每个操作必须等待上一个操作结束。
例外需要注意当ordered为true的时候,当在写入过程中发生错误的时候,会停止写入。
但是需要注意多条写入是发生错误的情况,下面演示一下这个情况:_id:12的那个我们在集合中已经存在,那个到我们写入的是否回报BulkWriteError
的错误。
> db.students.insertMany([
{"_id": 11, "name": "张三", "age": "男"},
{"_id": 23, "name": "32342"},
{"_id": 12, "name": "56756"},
{"_id": 123, "name": "56789"}
])
uncaught exception: BulkWriteError({
"writeErrors" : [
{
"index" : 2,
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: long_test.students index: _id_ dup key: { _id: 12.0 }",
"op" : {
"_id" : 12,
"name" : "56756"
}
}
],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
}) :
接着看一下写入的数据是那些:
> db.students.find()
{ "_id" : ObjectId("60557528299f04d494fd1da8"), "name" : "张三", "age" : "男" }
{ "_id" : ObjectId("60557ea1299f04d494fd1da9"), "name" : "李四", "age" : "女", "sex" : "565" }
{ "_id" : ObjectId("60557ea1299f04d494fd1daa"), "name" : "wabng", "sex" : "dfdfg" }
{ "_id" : ObjectId("60558a97299f04d494fd1dab"), "name" : "张三", "age" : "男" }
{ "_id" : 12, "name" : "张三", "age" : "男" }
{ "_id" : 11, "name" : "张三", "age" : "男" }
{ "_id" : 23, "name" : "32342" }
可以看到最后两条是没有写入的;接下来我们看一下无序写入:
> db.students.insertMany([
{"_id": 67, "name": "67", "age": "男"},
{"_id": 46, "name": "46"},
{"_id": 55, "name": "55"},
{"_id": 23, "name": "23"},
{"_id": 77, "name": }
],
{
ordered:false
}
)
uncaught exception: SyntaxError: expected expression, got '}' :
@(shell):1:164
>
> db.students.insertMany([{"_id": 67, "name": "67", "age": "男"}, {"_id": 46, "name": "46"}, {"_id": 55, "name": "55"}, {"_id": 23, "name": "23"}, {"_id": 77, "name": "77"}], {ordered:false})
BulkWriteError({
"writeErrors" : [
{
"index" : 3,
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: long_test.students index: _id_ dup key: { _id: 23.0 }",
"op" : {
"_id" : 23,
"name" : "23"
}
}
],
"writeConcernErrors" : [ ],
"nInserted" : 4,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
}) :
同样也会报错,但是数据除了Id重复那一条数据之外,其他数据都写进去了。
> db.students.find()
{ "_id" : 12, "name" : "张三", "age" : "男" }
{ "_id" : 11, "name" : "张三", "age" : "男" }
{ "_id" : 23, "name" : "32342" }
{ "_id" : 67, "name" : "67", "age" : "男" }
{ "_id" : 46, "name" : "46" }
{ "_id" : 55, "name" : "55" }
{ "_id" : 77, "name" : "77" }
1.3. insert方法
这个insert方式相当于将insertOne和insertMany功能的合并,该方法可以写入单条数据也可以写入多条数据。就不多做介绍了。
1.4. 写入安全控制
现在重点说一下写入安全控制,也就是这个方法都存在的一个属性:writeConcern
:这个属性用于控制写入安全级别的机制,通过使用写入安全机制可以提高数据的可靠性。这个属性里面有下面几个值可以配置:
配置项
| 备注
| 值
|
w
| 表示当数据写入n个节点时会返回给客户端成功的结构
| 0、1(默认)、n、majority
|
j
| 数据写入之后,并且Journal日志持久化成功,才返回写入成功
| 1(同步刷盘)
|
wtimeout
| 写入超时时间单位为ms
| |
这些值与我们的MongoDB的读写有很大关系,说到读写我们就需要考虑到MongoDB的集中部署方式:单机部署、高可用部署和分片集群部署。
1.4.1. MongoDB的部署模式
1.4.2. MongoDB的数据写入过程
对于MongoDB的来说,一个MongoDB有两个内存区域,一个是Data Buffer(数据缓冲)和Journal Buffer(日志缓冲)。
当数据进来的时候,数据操作分别写入日志缓冲和数据缓冲中,接着异步返回操作结果给客户端,与此同时后台线程会将日志缓冲和数据缓冲中的数据刷盘(此操作非常频繁,日志缓冲默认是100ms,数据缓冲默认是60s)。
对于多节点的复制集的写操作,大体和上面单节点的一致,但是开启复制集内存会多一个OPLOG区域,是节点之间进行同步的一个手段。MongoDB会把操作日志放到OPLOG中,然后OPLOG会复制到从节点上。从节点接收并执行OPLOG中的操作来达到数据的同步操作。
具体流程是这样的:到客户端数据来的时候,数据操作写入日志缓冲和数据缓冲,然后将日志缓冲中的操作日志放到OPLOG中;接着返回操作结果(异步);此时MongoDB的后台线程会将OPLOG复制到从节点,这个操作频率非常高,比日志刷盘还要高,从节点一直监听主节点,OPLOG一有变换就会进行复制操作;另外后台线程会将日志缓冲和数据缓冲中的数据进行刷盘。
1.4.3. 日志文件(恢复日志Journal)的特点
这个文件我们不设置的话在MongoDB的数据文件夹下有一个journal
文件夹,这文件很重要:
- 当系统宕机是恢复内存数据
- 默认是异步刷盘
- 刷盘间隔,MMAP引擎为30-100ms,Wiredtiger 为100M,超出之后创建一个新的日志文件
- 可使用
writeConcern
中的j
属性来设置是否同步刷盘(j :1
)
1.4.4. 解读配置项
我们设置writeConcern
就是为对写操作指定其返回的行为
w参数设置0 : 无任何返回,这样会出现数据丢失等情况
> db.students.insertOne({"_id": 120, "name": "张三", "age": "男"}, {writeConcern:{w: 0}})
{ "acknowledged" : false }
> db.students.insertOne({"_id": 121, "name": "张三", "age": "男"})
{ "acknowledged" : true, "insertedId" : 121 }
> db.students.insertOne({"_id": 120, "name": "张三", "age": "男"}, {writeConcern:{w: 0}})
{ "acknowledged" : false } // 没有任何错误提示
w参数设置为1(默认设置):此时会将确保日志缓冲写完才返回确定信息,但是并没有将日志刷盘,所以宕机(可以通过写数据时候通过kill掉mongod)的时候数据还是会丢失。
w参数设置为大于1或这majority
:此时在复制集的时候才有用,这个时候需要等待数据复制到n个节点在发送结果信息
j参数设置为1:这会将原来异步刷盘变成需要等待journal
刷盘才返回结果信息。
总结:w:0的时候性能是最好的但是最不安全,w:majority的时候最安全但是性能会打折扣。
二、查询文档
2.1. 条件查询
Mongodb的查询我们可以参照SQL的查询,首先先看一下条件查询怎么写:
单条件查询:select * from students where name = "张三"
db.students.find({"name": "张三"})
多条件查询:select * from students where name = "张三" and age = 12
db.students.find({"name": "张三", "age": 12})
db.students.find({$and: [{"name": "张三", "age": 12}]})
db.students.find({$or: [{"name": "张三", "age": 12}]})
AND和OR的一起使用:select * from students where age > 12 and (name = "张三" or sex = "女")
db.students.find({"age": {$gt:12}, $or: [{"name": "张三"},{"sex": "女"}]}).pretty()
可以正则去查询:相当于增强版的like查询
db.students.find({"name": "/^B/"})
2.2. 范围查询
示例:
db.students.find({"age": 12) // age = 12
db.students.find({"age": {$ne: 12}) // age <> 12
db.students.find({"age": {$gt: 12}) // age > 12
db.students.find({"age": {$gte: 12}) // age >= 12
db.students.find({"age": {$lt: 12}) // age < 12
db.students.find({"age": {$lte: 12}) // age =< 12
db.students.find({"age": {$exists: false}}) // age is null
db.students.find({"age": {$in: [12, 45, 30]}}) // age in (12, 45, 30)
db.students.find({"age": {$nin: [12, 45, 30]}}) // age not in (12, 45, 30)
2.4. limit和skip方法
获取指定数量的文档记录:limit
db.students.find().limit(5) // 可以设置获取数据的条数
跳过指定数量的数据:skip
db.students.find().skip(5) // 跳过前五条数据
db.students.find().limit(2).skip(5) // 跳过前五条数据,获取两条数据
排序操作:sort
方法,1 为升序排列; -1为降序排列
db.students.find().sort({age: 1})
2.5. $type
操作符
类型
| 数字
| 类型
| 数字
|
Double
| 1
| String
| 2
|
Object
| 3
| Array
| 4
|
Binary data
| 5
| Object Id
| 7
|
Date
| 9
| Null
| 10
|
Regular Expression
| 11
| JavaScript
| 13
|
Symbol
| 14
| 32位Integer
| 16
|
Timestamp
| 17
| 64位Integer
| 18
|
Boolean
| 8
| JavaScript (With scope)
| 15
|
通过类型查询:
db.students.find({name: {$type: 2}}) // 2 对应的数据类型的数字
db.students.find({name: {$type: "string"}})
2.6. 子文档查询
查询子文档:
> db.content.find().pretty()
{
"_id" : ObjectId("6055f282942a0f2467dbeba7"),
"title" : "测试文档",
"author" : "张安",
"content_len" : 123456,
"from" : {
"group" : "第一组",
"nums" : 12
}
}
> db.content.find({"from.nums": 12}).pretty() // 查询子文档,需要写子文档的路径
{
"_id" : ObjectId("6055f282942a0f2467dbeba7"),
"title" : "测试文档",
"author" : "张安",
"content_len" : 123456,
"from" : {
"group" : "第一组",
"nums" : 12
}
}
> db.content.find({"from": {nums: 12}}).pretty() // 这样子写不对
查询数组内容:
> db.colors.find().pretty()
{"_id" : ObjectId("6055f4ca942a0f2467dbebab"),"name" : "颜色一","col" : ["red","yellow"]}
{"_id" : ObjectId("6055f4ca942a0f2467dbebac"),"name" : "颜色分组二","col" : ["red"]}
{"_id" : ObjectId("6055f4ca942a0f2467dbebad"),"name" : "颜色分组三","col" : ["green"]}
> db.colors.find({col: "red"}).pretty() // col里面包含red
{"_id" : ObjectId("6055f4ca942a0f2467dbebab"),"name" : "颜色一","col" : ["red","yellow"]}
{"_id" : ObjectId("6055f4ca942a0f2467dbebac"),"name" : "颜色分组二","col" : ["red"]}
> db.colors.find({$or:[{col: "red"},{col: "green"}]}).pretty() // col里面包含red或者green
{"_id" : ObjectId("6055f4ca942a0f2467dbebab"),"name" : "颜色一","col" : ["red","yellow"]}
{"_id" : ObjectId("6055f4ca942a0f2467dbebac"),"name" : "颜色分组二","col" : ["red"]}
{"_id" : ObjectId("6055f4ca942a0f2467dbebad"),"name" : "颜色分组三","col" : ["green"]}
查询对象数组的内容:
db.movies.insertMany([
{
"title": "电影一",
"filming_locations": [
{"city": "城市一", state: "CA", "country": "USA"},
{"city": "城市二", state: "Lasio", "country": "Italy"},
{"city": "城市三", state: "SC", "country": "USA"}
]
},
{
"title": "电影二",
"filming_locations": [
{"city": "城市一", state: "CAT", "country": "USA"},
{"city": "城市五", state: "Lasio", "country": "Italy"},
{"city": "城市三", state: "BSC", "country": "USA"}
]
}
])
> db.movies.find({"filming_locations.state": "CA"}).pretty() // 查询子文档中state=CA
{
"_id" : ObjectId("6055f75c942a0f2467dbebae"),
"title" : "电影一",
"filming_locations" : [
{
"city" : "城市一",
"state" : "CA",
"country" : "USA"
},
{
"city" : "城市二",
"state" : "Lasio",
"country" : "Italy"
},
{
"city" : "城市三",
"state" : "SC",
"country" : "USA"
}
]
}
> db.movies.find({
"filming_locations": {
$elemMatch: {
"state": "SC",
"country": "USA"
}
}
}).pretty() // 查询子文档中必须包含 state=SC 和 country=USA
{
"_id" : ObjectId("6055f75c942a0f2467dbebae"),
"title" : "电影一",
"filming_locations" : [
{
"city" : "城市一",
"state" : "CA",
"country" : "USA"
},
{
"city" : "城市二",
"state" : "Lasio",
"country" : "Italy"
},
{
"city" : "城市三",
"state" : "SC",
"country" : "USA"
}
]
}
返回部分字段:
> db.content.find().pretty()
{
"_id" : ObjectId("6055f282942a0f2467dbeba7"),
"title" : "测试文档",
"author" : "张安",
"content_len" : 123456,
"from" : {
"group" : "第一组",
"nums" : 12
}
}
> db.content.find({},{"_id": 0, "title": 1, "author": 1}) // 0是不显示,1是显示
{ "title" : "测试文档", "author" : "张安" }
2.7. 聚合查询
聚合查询,我们需要看先了解一下MongoDB的聚合框架,这个框架可以作用在:
- 一个或者几个集合上
- 对集合中的数据进行一系列运算
- 将这些数据转换为期望的形式
MongoDB的聚合框架相当于SQL查询中的:Group By / Left Join / as 等。
2.7.1. 管道和步骤
整个聚合运算可以称为管道(pipline),它由多个步骤(stage)组成。具体过程是这样的:
- 接受一系列文档(原始数据)
- 每一个步骤对这些文档就行一系列运算
- 结果文档输出给下一个步骤
下面列举一写常用步骤:
步骤
| 作用
| SQL等效
|
$match
| 过滤
| where
|
$project
| 投影
| as
|
$sort
| 排序
| order by
|
$group
| 分组
| group by
|
limit | 结果限制
| skip/limit
|
$lookup
| 左外连接
| left outer join
|
$unwind
| 展开数组
| N/A
|
$graphLookup
| 图搜索
| N/A
|
bucket | 分面搜索
| N/A
|
2.7.2 常用的例子
我们对比SQL来看下MongoDB是如何执行聚合查询的
示例1
SQL写法:
select
first_name as '名',
last_name as '姓'
from students
where
sex = '男'
skip 100
limit 20
MongoDB聚合写法:
db.students.aggregate([
{$match: {sex: "男"}}, // 匹配sex:男的数据
{$skip: 100}, // 跳过前100条
{$limit: 20}, // 获取20条数据
{$project: { // 将firt_name作为‘名’, last_name作为'姓'
"名": "$fisrt_name",
"姓": "$last_name"
}}
])
示例2
SQL写法:
select
department,
count(null) as emp_qty
from users
where sex = '女'
group by department
having count(*) < 10
MongoDB聚合写法:
db.users.aggregate([
{$match: {sex: "女"}}, // 匹配性别为女的数据
{$group: { // 分组
_id: "$department",
emp_qty: {$sum: 1} // 增加一条属性emp_qty,每次遇到一条department数据加一
}},
{$match: { // 刷选emp_qty数量小于10
emp_qty: {$lt: 10}
}}
])
示例3
可以使用$unwind
展开数据,原始数据:
db.students.findOne()
{
name: "张三",
score: [
{subject: "语文", score: 84},
{subject: "数学", score: 64},
]
}
使用聚合展开:
db.students.aggregate([{$unwind: "$score"}])
{name: "张三", score: {subject: "语文", score: 84}}
{name: "张三", score: {subject: "数学", score: 64}}
示例4
分面查询:$bucket
和$facet
|————> [0, 10) ————> 120条
|————> [10, 20) ————> 20条
price —— |————> [20, 30) ————> 30条
|————> [30, 40) ————> 500条
|————> [40, +) ————> 10条
db.products.aggregate([
{
$bucket: {
groupBy: "$price", // 以price分组
boundaies: [0, 10, 20, 30, 40], // 分组的桶
default: "Other",
output: {"count": {$sum: 1}} // 增加一个属性,使用$sum统计数量
}
}
])
$facet
可以将多个维度组合到一起:
|————> [0, 10) ————> 120条
|————> [10, 20) ————> 20条
|—— price —— |————> [20, 30) ————> 30条
| |————> [30, 40) ————> 500条
文档 ——|
| |————> [1990, 2000) ————> 120条
|—— year —— |————> [2000, 2010) ————> 30条
|————> [2010, 2020) ————> 500条
|————> [2020, +) ————> 10条
db.products.aggregate([
{
$facet: {
price: {
$bucket: {...}
},
year: {
$bucket: {...}
}
}
}
])
2.7.4. 使用Compass工具
提供一份测试数据:https://molongyin.oss-cn-beijing.aliyuncs.com/mongodb/test.json