2 Mongodb CRUD 操作
Mongodb Manual阅读笔记:CH2 Mongodb CRUD 操作Mongodb Manual阅读笔记:CH3 数据模型(Data Models)Mongodb Manual阅读笔记:CH4 管理Mongodb Manual阅读笔记:CH5 安全性Mongodb Manual阅读笔记:CH6 聚合Mongodb Manual阅读笔记:CH7 索引Mongodb Manual阅读笔记:CH8 复制集Mongodb Manual阅读笔记:CH9 Sharding
mongodb提供了创建,读取,修改,删除简称CRUD
2.1 Mongodb CRUD简介
mongodb存储的文档是和json格式类似的,但是mongodb存储的是BSON也就是,以2进制方式保存JSON格式数据。在mongodb中文档相当于记录,而存文档的Collection相当于表。
2.1.1数据库操作
2.1.1.1查询
Mongodb可以对一个collection通过条件来过滤文档,并且可以用projection选择输出
2.1.1.2数据修改
创建,更新,删除数据,只能修改单个collection(不包含嵌入方式)
2.1.2 相关特性
2.1.2.1索引
Mongodb可以顺序的表示,也可以强制唯一性保存,mongodb中索引也是用b树保存,
2.1.2.2读偏好
读偏好主要是用在复制集群中,从哪个实例中读取。
2.1.2.3写注意(write concern)
不同的写注意保证了不同的数据写入要求,级别越低写入越快,但是写入安全性越低
2.2 Mongodb CRUD概述
读操作:游标,查询优化,分布式查询
写操作:写注意,分布式写操作
2.2.1读操作
查询接口
Mongodb通过提供的接口db.collection.find()方法来查询数据,会返回一个游标
查询行为
1.所有的查询只能针对一个collection
2.可以通过limits,skips,sort来影响查询结果
3.用sort()来范围查询的顺序
4.update修改数据和select的条件语法一样
5.在aggregation(聚合)管道中,$match管道提供了对mongodb的访问
查询语句
Projection
Projection用来限制输出那些字段,_id是默认输出的。
Projection行为
1._id会默认出现,除非显示设置
2.对于包含array的字段,提供了$elemMatch,$slice,$等操作符
3.对于聚合,使用$Project来处理
2.2.1.1游标
mongodb查询db.collection.find()返回一个游标,若没有分配给一个变量直接运行20次
游标行为对于游标默认不活动时间超过10分钟就会被销毁,当然可以指定noTimeout标记,来先限制等待。
游标隔离级别游标在有效期内是不隔离的,若有写入操作就会影响,可以使用快照模式来处理
游标Batch数据从mongodb到客户端是以batch的方式,batch的大小最大不超过最大BSON文档的大小,对于很多查询而言,第一个batch返回101个文档,或者1MB的大小,之后是4MB。
对于包含sort的查询但是没有索引,mongodb会导入到内存然后排序,会在第一个batch的时候返回所有的结果。用cursor.hasnext()和cursor.next()的组合来看有没有下一个batct,获取下一个batch
游标信息通过命令cursorInfo来获取游标的信息:1.打开的游标总数,2.当前使用的客户端游标大小,3.从服务重启来,超时的游标数。
2.2.1.2优化查询
索引和其他数据库中的一样,减少了没必要数据的查询。
创建索引来支持查询使用db.collection.ensureIndex()来创建索引
索引的选择度某些查询操作时选择度很差,比如$nin,$ne,高选择度的才可以更好的使用索引,$regex 也是没有办法使用索引的。
覆盖查询覆盖查询的条件:1.所有的字段都是索引的一部分,2.所有返回的字段也在索引中
2.2.1.3查询计划
Mongodb优化器处理查询,并根据索引选择一个最有效的索引,当collection的数据量被改变优化器会偶尔重建计划,可以使用explain()来查看执行计划。
查询优化
优化器创建计划的过程:1.通过不同的索引,并发的执行查询,2.记录负荷的结果到buffer中,3.当非排序的查询计划全部返回,或者顺序的执行计划全部返回,或者顺序的执行计划返回了超过了一个指标,那么会停止其他执行的计划并选择这个计划
计划重建
当发生以下事件,计划会被重建:1.超过1000个写入,2.重建索引,3.添加或者删除索引,4.服务进程重启
2.2.1.4分布式查询
从Sharded集群中读,Sharded允许数据分区,并对应用程序透明,查询会通过路由到达相关的实例中,通过shard key若没有shard key,查询会分发到索引的shard实例中取执行。
从复制集群中读,在复制集群中使用读偏好,来决定从哪个实例中读取数据,读偏好设置有一下几个好处:1.减少延迟,2.提供读的带宽,3.备份作用,4.容灾切换。同时也要注意,主库和分库之间的延迟,分库不能代表当前主库的数据状态。
2.2.2写操作
和读一样写操作只能用在一个collection上。
创建
通过接口,db.collection.insert()在collection上创建一个文档,若update接口在有upset的标记下,也可以实现创建文档的功能。
插入的行为
如果你插入时,没有指定_id那么mongodb会分配一个,若指定了_id需要保持在collection中是唯一的。
更新
通过接口,db.collection.update()来实现更新操作,也可以用save
更新行为
update默认只对一行进行更新,若要对多行进行更新要使用multi选项,通没有指定类似set的参数,就会把所有影响的文档替换,当更新是,文档的大小超过了原先分配的大小,那么会重新分配空间,然后把文档放入,这样会导致,文档内的字段重排。
删除
删除通过接口,db.collection.delete()实现。
删除的行为
默认使用delete不太条件,会把所以的文档全部删除
写操作的隔离性
修改一个文档,可以支持原子性,但是对多个文档的修改就不支持原子性,尽管如此单还是可以使用隔离操作(isolaation_operator)。也可以通过人工方式实现2阶段提交,来保证隔离性。
2.2.2.1写注意(write concern)
弱写注意,可能会导致一些错误,因为不需要等服务端返回写入成功,强写注意,client可以等待写入完成。db.runCommand( { getLastError: 1, w: 2 } )来配置。
写注意级别:忽略错误(Error Ingored),最低级的级别,mongodb不会通知写入操作完成,包含 发生错误,性能最佳,但是会影响写入数据的一致性和持久性,w:-1
不通知:mongodb不会通知写入成功或者失败,但是当发生错误是,还是会尽力接受和处理错误,可以发现由系统网络配置导致的网络错误。w:0
通知:可以取回一个写入操作信息,这个级别,允许客户端抓取网络,重复key等错误,w:1。mongodb默认使用这个级别,写注意默认使用无参数的getLastError,对于复制集群可以在getLastErrorDefaults中定义默认的写注意,若没有定义默认使用getLastError取通知.
日志:使用这个级别,只有在提交,写入日志后,服务才会通知写入操作,这样可以保证服务崩溃后数据库的恢复。使用这个操作要指定w:1,j:true
复制通知:使用这个级别可以保证,写操作写入到了复制集群,w:>1,在复制集群下,只要再设置j:true,主库就可以用日志写注意级别。
2.2.2.2分布式写
在sharded集群中写:可以根据shard key来对分区插入数据,可以提高写入性能,若没有key,会被广播到所有的shard,若数据库只插入到一个key,那么可能照成单点性能问题。
在复制集中写:在复制集中写入,会被写入到primary上,然后记录在oplog或者log中,然后根据oplog分发到分库中,对于密集的写入操作会导致分库和主库之间严重的延迟,虽然可以通过写注意保证一致性,但是会造成写入性能问题
2.2.2.3写操作性能
索引:和其他数据库一样,写操作会修改索引,会带来一些性能消耗。
文档增长:update 操作文档的空间时,会分配一个新空间,然后把文档复制进去,这样会加大update的时间,in-place比文档增长来的有效。
2.2.2.4存储性能
硬件:很多元素和存储系统有关,影响着服务的性能,如,随机读写性能,磁盘cache,预读,RAID级别。
日志:mongodb使用顺序写方式来记录日志,日志提供了一致性和crash弹性,考虑一下方面来增加日志性能:1.使用独立的设备,2.若写注意级别为日志,mongodb通过减少提交间隔来减少写负荷,3.通过手动配置journalCommitInterval来减少提交间隔。
2.2.2.5大数据量写入
使用insert方法:insert方法通过传入一个数组来执行大数据量写入。通过写注意的设置大数据量写入,可以显著的提高性能。并且通过ContinueOnError选项可以让批量插入有错误下还能完成。
在Shard集群中:ContinueOnError只在非分片中有效,这类大批量写入,对sharded集群来说会降低性能若要写入考虑一下方案:
预分配splits:大批量导入中,若collection是空的,那么只有一个spilt,当插入时 重新分配split导致性能问题所以要预先split
避免单调增长:可能大批量插入只针对一个sharded key,这样sharded的性能就没 有办法体现,如无法避免,可以考虑:1.二进制方式翻转shard key,2.key的前16位和 后16位互换。
2.2.2.6行留白(Record Padding)
因为超出文档就会重新分配,为了避免这个问题,可以事先留白,减少重分配的可能性。
留白因子:可以使用db.collection.stats()查看当前的paddingFactor,留白的大小可以通过一个公式计算:padding size = (paddingfactor-1)*<document size>。留白并不能对每个文档做准确的设置,只能根据文档大小的平均数。留白的大小只能通过执行compact或者repairDatabase选项回收。
删除留白:可以通过compact,repairDatabase和初始化复制同步选项来移除,导出导入也可以。
行分配策略:为了更加有效的使用因为删除而空闲的空间,或者文档重新分配的空间,可以指定mongodb分配的行大小,大小为2的n次。
2.3 Mongodb CRUD教程
2.3.1 插入文档
在mongodb中,可以使用insert,update,save来插入数据
2.3.1.1使用insert插入文档
向inventory插入数据:db.inventory.insert( { _id, type: "misc", item: "card", qty: 15 } )
2.3.1.2使用update插入文档
使用update插入文档的时候要指明upsert:true,表示如果有数据则修改,没数据则插入
db.inventory.update(
{ type: "book", item : "journal" },
{ $set : { qty: 10 } },
{ upsert : true }
)
, "type" : "book" }
插入的文档如果没有指定_id mongodb会自动分配一个_id。
2.3.1.3使用save插入文档
往inventory插入数据db.inventory.save( { type: "book", item: "notebook", qty: 40 } )
{ "_id" : ObjectId("51e866e48737f72b32ae4fbc"), "type" : "book", "item" : "notebook", "qty" : 40 }
2.3.2查询文档
在mongodb中,find()返回一个游标,通过游标来获取返回的文档。
2.3.2.1 查询全部的文档
可以使用如下来查询所有的文档:
db.inventory.find( {} ),db.inventory.find()
2.3.2.2指定条件查询文档
如果要在inventory中查询type为food或者snacks的:
db.inventory.find( { type: { $in: [ 'food', 'snacks' ] } } )
2.3.2.3使用and条件查询
在mongodb中,只要都好分开就表示and条件
db.inventory.find( { type: 'food', price: { $lt: 9.95 } } )
2.3.2.4 使用or条件查询
db.inventory.find(
{ $or: [
{ qty: { $gt: 100 } },
{ price: { $lt: 9.95 } }
]
}
)
2.3.2.5使用and和or 查询
db.inventory.find( { type: 'food', $or: [ { qty: { $gt: 100 } },
{ price: { $lt: 9.95 } } ]
} )
2.3.2.6子文档
当要查询子文档的时候,可以指定子文档的所有值,也可以使用.(点号) 获取子文档的字段值。
精确的匹配文档
也就是匹配整个子文档
db.inventory.find(
{
producer: {
company: 'ABC123',
address: '123 Street'
}
}
)
使用子文档的字段匹配
用点号获取子文档的值
db.inventory.find( { 'producer.company': 'ABC123' } )
2.3.2.7数组
匹配方式和子文档类似
匹配整个数组
db.inventory.find( { tags: [ 'fruit', 'food', 'citrus' ] } )
匹配某个元素
只要某个元素在数组中都会被匹配
db.inventory.find( { tags: 'fruit' } )
匹配指定元素
db.inventory.find( { 'tags.0' : 'fruit' } )
子文档数组
使用数组所有匹配子文档中的字段:db.inventory.find( { 'memos.0.by': 'shipping' } )
多字段匹配
db.inventory.find(
{
'memos.memo': 'on time',
'memos.by': 'shipping'
}
)
2.3.3限制查询返回的字段
Projection可以限制字段的输出
2.3.3.1 返回所有字段
Find,函数第二个参数就是projection如果不填就是全部返回
2.3.3.2返货指定字段和_id
db.inventory.find( { type: 'food' }, { item: 1, qty: 1 } )
其中_id字段是默认返回的所有不用指定
2.3.3.3只返回指定字段
db.inventory.find( { type, qty, _id:0 } )
因为_id是默认返回的,所以要设置_id不返回。
2.3.3.4 返回除某字段外的所有字段
db.inventory.find( { type: 'food' }, { type:0 } )
2.3.3.5数组字段的Projection
对于数组来说唯一可用的projection操作就是$elemMath和$slice
2.3.4在mongo shell中迭代一个游标
如find函数没有赋值给变量,在shell中会直接迭代20次。
2.3.4.1手动迭代游标
通过以下方式可以直接迭代20次
var myCursor = db.inventory.find( { type: 'food' } );
myCursor
也可以手动迭代
var myCursor = db.inventory.find( { type: 'food' } );
var myDocument = myCursor.hasNext() ? myCursor.next() : null;
if (myDocument) {
var myItem = myDocument.item;
print(tojson(myItem));
}
还可以用forEach方法
var myCursor = db.inventory.find( { type: 'food' } );
myCursor.forEach(printjson);
2.3.4.2迭代索引
在mongo shell中可以可以直接把游标toArray(),然后直接访问数组即可。
var myCursor = db.inventory.find( { type: 'food' } );
var documentArray = myCursor.toArray();
var myDocument = documentArray[3];
2.3.5分析查询性能
Explain()游标方法,这个方法用来分析查询的效率,确定如何使用索引。
2.3.5.1 评估查询性能
db.inventory.find( { type: 'food' } ).explain()
结果
{
"cursor" : "BtreeCursor type_1",
"isMultiKey" : false,
"n" : 5,
"nscannedObjects" : 5,
"nscanned" : 5,
"nscannedObjectsAllPlans" : 5,
"nscannedAllPlans" : 5,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : { "type" : [
[ "food",
"food" ]
] },
"server" : "mongodbo0.example.net:27017"
}
Cursor的值为btreecursor表示是用了索引
n表示返回的文档数
2.3.5.2比较索引性能
如果要比较索引之间的性能可以使用hint来强制
db.inventory.find( { type: 'food' } ).hint( { type: 1 } ).explain()
db.inventory.find( { type: 'food' } ).hint( { type: 1, name: 1 } ).explain()
2.3.6修改文档
修改文档的操作有,update 和save,update是修改文档,save如果文档存在则替换文档。
2.3.6.1使用update修改多个文档
要修改多个文档时,要把multi-true设置上。
db.inventory.update(
{ type : "book" },
{ $inc : { qty : -1 } },
{ multi: true }
)
2.3.6.2使用save修改文档
Save可以替换已经存在的文档
db.inventory.save(
{
_id: 10,
type: "misc",
item: "placard"
}
)
2.3.7删除文档
删除文档一般使用remove方法
2.3.7.1删除所有文档
直接写不带参数的remove就会把collection中的文档全部删除。
db.inventory.remove()
2.3.7.2 带条件删除文档
db.inventory.remove( { type : "food" } )
2.3.7.3 带条件删除一个文档
db.inventory.remove( { type : "food" }, 1 )
2.3.8 执行二阶段提交
当处理多文档的更新和事务,可以使用二阶段提交的方式来写多个文档
2.3.8.1背景
单文档操作是原子的,多文档没有,所以多文档的操作不能有原子性保障,当执行事务时就可能发生以下问题:
1.原子性:如果一个操作失败,事务必须回滚到之前的状态
2.隔离性:同步发生的事务,在事务过程中必须看到一致性的数据
3.一致性:如果发生数据库错误,中断了事务,数据库必须能够恢复到一致的状态
多文档的写可以使用二阶段提交
2.3.8.2模式
概述
假设要做转账操作,我们有一个accounts collection和一个transactions collection用来保存用户数据和事务数据。
db.accounts.save({name: "A", balance: 1000, pendingTransactions: []})
db.accounts.save({name: "B", balance: 1000, pendingTransactions: []})
事务描述
设置事务初始化状态:在创建的 transactions中插入一条数据并且状态为initial。
db.transactions.save({source: "A", destination: "B", value: 100, state: "initial"})
然后把事务状态设置为pending:在要处理事务之前把事务状态设置为pending表示处理这个事务。db.transactions.update({_id: t._id}, {$set: {state: "pending"}})
应用事务到2个账号上:
db.accounts.update({name: t.source, pendingTransactions: {$ne: t._id}}, {$inc: {balance: -t.value}, $push: {pendingTransactions: t._id}})
db.accounts.update({name: t.destination, pendingTransactions: {$ne: t._id}}, {$inc: {balance: t.value}, $push: {pendingTransactions: t._id}})
更新account的pendingTransaction中没有t._id的事务,就是t._id不是已经在处理事务,然后设置值,并把设置上pendingTransaction。
设置事务提交状态:db.transactions.update({_id: t._id}, {$set: {state: "committed"}})
删除pending事务:
db.accounts.update({name: t.source}, {$pull: {pendingTransactions: t._id}})
db.accounts.update({name: t.destination}, {$pull: {pendingTransactions: t._id}})
设置事务完成:
db.transactions.update({_id: t._id}, {$set: {state: "done"}})
从错误场景中恢复
可能会出现错误的场景:
1.在初始化事务,应用事务到2个账号上之间出错,为了恢复应用程序应该获取pending状态的事务,然后继续处理。
2.发生在应用事务,事务完成之间的错误,为了恢复应用程序应该获取done状态的事务,然后继续处理。
回滚:某些场景就需要回滚操作
1.当应用事务之后,已经提交了事务,就不应该去回滚事务,而是重新创建一个新的事务,来交换数据
2.在创建事务后,但是没有应用事务,可以使用一下步骤:
设置事务状态为取消:db.transactions.update({_id: t._id}, {$set: {state: "canceling"}})
Undo事务:
db.accounts.update({name: t.source, pendingTransactions: t._id}, {$inc: {balance: t.value}, $pull: {pendingTransactions: t._id}})
db.accounts.update({name: t.destination, pendingTransactions: t._id}, {$inc: {balance: -t.value}, $pull: {pendingTransactions: t._id}})
把事务状态修改为已取消:db.transactions.update({_id: t._id}, {$set: {state: "canceled"}})
多应用程序:多应用程序的时候很容易造成一致性问题和冲突。在这种情况下,在transaction中加入application字段,来区别应用程序。并且使用findAndModify()
t = db.transactions.findAndModify({query: {state: "initial", application: {$exists: 0}},
update: {$set: {state: "pending", application: "A1"}},
new: true})
2.3.8.3在生产环境使用二阶段提交
在例子中,我们假设了一些东西比如:
1.总是可以回滚一个用户
2.账号的剩余可以为负数
但是在生产环境下需要考虑:
1.在把事务设置为pending的时候,需要查看账号是否有足够的资金。
2.当事务设置提交时,需要修改借贷方
当然这些操作时在一个文档里面的,是原子的
也需要考虑:
1.数据库接口的写注意(wirte concern)
2.mongod的journaling是否开启确保数据库非干净关闭后数据库状态的恢复。
2.3.9 创建tailable cursor
2.3.9.1概述
默认游标使用完之后就会被关闭,但是Tailable Cursor不会,当文档有插入,Tailable Cursor也可以取到。对于高写入的capped collection上使用tailable cursor太昂贵。
Tailable cursor有一下特点:
1.游标不会使用索引,以自然顺序返回
2.tailable cursor在初始化阶段比较昂贵,新增的文档代价并不是很大。
3.某些情况下tailable curos会不可用,没有返回的文档,返回了collection的末尾,然后被应用程序删掉了。
2.3.9.2 C++例子
看手册p75
2.3.10隔离操作顺序
看手册p76
2.3.11创建自增字段
本节介绍2个创建自增的方法,计数collection,乐观循环(optimistic loop)
2.3.11.1计数Collection
使用一个独立的计数Collection来跟踪最新的顺序,_id表示顺序的名,seq表示最新的顺序。
1.插入一个userid的初始化值到计数collection中
db.counters.insert(
{
_id: "userid",
seq: 0
}
)
2.创建一个getNextSequence函数来获取一个名字的下一个顺序值。
function getNextSequence(name) {
var ret = db.counters.findAndModify(
{
query: { _id: name },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
3.在插入时使用getNextSequence函数
db.users.insert(
{
_id: getNextSequence("userid"),
name: "Sarah C."
}
)
2.3.11.2 乐观循环
读者觉得很奇葩的方法,手册 p80。
2.3.12 更新数组中限制更新行数
2.3.12.1 方式
有students collection有一个文档
{
_id: 1,
scores: [
{ attempt: 1, score: 10 },
{ attempt: 2 , score:8 }
]
}
然后再$push里面使用$each和$sort来实现
db.students.update(
{ _id: 1 },
{ $push: { scores: { $each : [
{ attempt: 3, score: 7 },
{ attempt: 4, score: 4 }
],
$sort: { score: 1 },
$slice: -3
}
}
}
)
输出结果
{
"_id" : 1,
"scores" : [
{ "attempt" : 3, "score" : 7 },
{ "attempt" : 2, "score" : 8 },
{ "attempt" : 1, "score" : 10 }
]
}
2.4 MongoDB CRUD 指南
2.4.1 查询游标方法
Cursor.count:返回文档的个数
Cursor.explain:显示查询的执行计划
Cursor.hint:强制mongo使用指定的索引
Cursor.limit:显示游标返回文档个数
Cursor.next:获取游标中下一个文档
Cursor.skip:跳过给定数量的文档
Cursor.sort:对指定字段排序
Cursor.toArray:游标返回成数组
2.4.2 查询和数据操作
Db.collection.count:返回collection文档个数
Db.collection.distinct:以数组方式返回指定字段的不同值
Db.collection.find:在collection执行find返回游标
Db.collection.findOne:返回一个文档
Db.collection.insert:在collection上创建文档
Db.collection.remove:删除collection上的文档
Db.collection.save:可以insert,也可以替换已经存在的文档
Db.collection.update:修改已经存在的文档
2.4.3 MongoDB CRUD指南文档
主要介绍,写注意(write concern),sql 到 mongodb的对照表,mongodb的驱动和客户端lib。
2.4.3.1 写注意(write concern)
概述
Write concern,mongodb为写入提供的保障,但是是通过消耗性能来保证write的安全性。可以对数据的重要性不同分级。
可以用的write concern
提供write concern,写入操作之后,驱动使用getLastError来获取最后才做的信息。返回一个err字段,:
1.null表示写入成功
2.非null就表示具体的错误
GetLastErroe的参数:
J或者journal选项:指定这个选项后,mongodb会写入日志到磁盘的journal上确保mongodb突然关闭之后数据不会丢失。
如下:db.runCommand( { getLastError: 1, j: "true" } )
如果在mongod中并没有启用journal,然后getLastError指定了 j:true会返回一个jnote字段包含了这个文档。
W选项:这个选项可以停用write concern,也可以指定复制集的write concern。可选值:
-1:关闭write operation通知
0:关闭基本的写入操作通知,但是返回socket异常和网络异常
1:提供通知,说明已经写入到了单个实例,或者复制集的primary
>1:说明要写入到>1个实例中。一般用户复制集,如果这个值大于复制集成员,mongodb会一致等下去。
Majority:确认写入操作已经传播到大多数的已配置复制集
Tag设置:使用tag设置可以很好的控制复制集成员很好的控制的不同write concern。
getLastError还提供了一个超时,如果给0会一直等待。
2.4.3.2 sql 到 mongodb的对照表
手册p85
2.4.3.3 mongodb驱动和客户端lib
手册p95