Skip to content

Instantly share code, notes, and snippets.

@safecat
Created October 16, 2015 09:41
Show Gist options
  • Save safecat/c455617955e166e4ec8c to your computer and use it in GitHub Desktop.
Save safecat/c455617955e166e4ec8c to your computer and use it in GitHub Desktop.
MongoDB权威指南学习笔记

MongoDB的哲学:能交给客户端驱动程序来做的事情就不要交给服务器来做。这种理念背后的原因是,即便是像MongoDB这样扩展性非常好的数据库,扩展应用层也要比扩展数据库层容易得多。将工作交由客户端来处理,就减轻了数据库扩展的负担。

##文档定义

  • 文档的key不能包含.$,且key区分大小写。
  • 每个文档自带一个_id
  • 文档的键值对是有序的。
  • 最大16MB。

##集合定义

  • 集合名不能含有system.
  • 使用.来建立子集合,例如blog.authors blog.posts
  • 固定集合只保存一定数量的文档,老的文档会被丢弃。写入速度快,固定集合不能分片。db.createCollection("fixedCollection", {"capped": true, "size": 100000, "max": 100}) 两个限制条件是“且”关系。
    • size 最大字节
    • max 文档数量
  • 非固定集合可以转换成固定集合,反之不能。
  • 从2.6开始,集合创建默认启用usePowerOf2Sizes,每次申请和释放空间都是2的幂次大小,32byte起步。开启后,mongodb能够更有效地重复利用空间。mongod申请空间从32byte到4M字节封顶,如果文档还需要更大的空间,mongod会把隔壁的整个4M空间拿来用。

##数据库

  • 不同的数据库可以放在不同的磁盘上。
  • 数据库名中,只能含有字母和数字,-_也是允许的,数据库名即是磁盘上的文件名。
  • 库名区分大小写,建议全部小写,最多64字节。
  • 库名不能是 admin(管理员库), local(本地库), config(分片配置库)
  • 命名空间的定义是 库名.集合名.子集合名,例如 cms.blog.posts。命名空间不应长于100个字节。
  • db变量表示当前选择的数据库。
  • GridFS便于管理和扩展,适合大规模存储大文件时使用。获取文件两次查询,一次metadata,一次content。

##服务器

  • 数据库默认端口27017,web管理默认端口28017。

##操作 ###基本操作

// 插入数据
var blog = {
	"title": "my blog",
	"date": ISODate("2012-08-24T21:12:09.982Z"),
	"count": 1
};
db.blog.insert(blog);
// 批量插入
db.blog.batchInsert([blog1, blog2, blog3]); //非事务安全
// 查询文档
db.blog.find();
db.blog.findOne();
// 替换文档
//   一次只能替换一个,和MySQL的UPDATE不同
blog.author = "XXY"
db.blog.update({"title": "my blog"}, blog);
db.blog.update({"title": "my blog"}, blog, false, true); //第三个参数为UPSERT,第四个参数为批量更新
// 保存文档(UPSERT)
var blog = db.blog.findOne();
blog.author = "XXYY";
blog.save();
// 修改文档
db.blog.update({"author" : "XXY"}, {"$inc" : {"count" : 1}})
// UPSERT(UPDATE OR INSERT)
db.blog.update({"title": "my blog"}, blog, true); //true for upsert
// 删除文档
db.blog.remove({"title": "my blog"});
// 删除集合
db.blog.drop(); //非常快

####findAndModify(相当于UPDATE/DELETE...WHERE...)

  • 参数
    • query 查询条件
    • sort 排序结果
    • update 修改器
    • remove 是否删除
    • new 是否返回更新后的文档(默认返回更新前)
    • fields 文档中需要返回的字段(可选参数)
    • upsert 是否upsert

####查询条件 使用样例:db.blogs.find({"count": {"$gt": 8}})

  • 大于小于:$lt / $lte / $gt / $gte
  • 不等于:{"$ne": 'draft'}
  • IN:{"$in": [8, 9, 10]}"$nin"
  • OR:{"$or": [{"is_hot": true}, {"hot_count": {"$gt": 10}}]}
  • NOT:"$not"
  • null:会匹配值为null的文档,或者没有这个键的文档。
  • 正则:{"mobile": /189\d{8}/}
  • 数组包含:{"$all": ["health", "shopping"]}
  • 数字长度:$size

####$slice操作符

  • -10 返回后10条
  • [23, 10] 跳过23条,返回10条

###修改器

// 设置属性
{"$set": {"author": "YYX"}}
{"$set": {"author.age": 10}}
// 移除属性
{"$unset": {"author": null}}
// 增加,如果没有这个属性会创建一个
{"$inc": {"count": 30}}
// 添加数组元素,如果没有这个属性会创建一个
{"$push": {"title": "my second blog"}}
{"$push": {"title": {"$each": ["blog3", "blog4"]}}} //加入两个元素
{"$push": {"top10db": {
	"$each": [{"name": "mysql", "hot_count": 10}],
	"$slice": -10, //保留最后10个元素
	"$sort": {"hot_count": -1} //slice之前根据hot_count倒序排列
}}}
// 向数组中插入不重复的值
{"$addToSet": {"top10db": "db2"}}
// 删除数组的元素
{"$pop": {"top10db": 1}} //删除末尾的1个元素
{"$push": {"top10db": "db2"}} //删除"db2"元素
// 修改第一个元素的名字
{"$set": {"top10db.0.name": "MySQL"}}
// 添加时设置
db.blog.update({"title": "my blog"}, {
	"title": "my blog"
	"$setOnInsert" : {"createdAt" : new Date()}
}, true); // 如果update则不设置时间戳,insert则设置

###操作原理

  • 如果修改文档时增加的长度超过了填充因子,则文档会被移到集合最后,增长因子的原理见附图1。(不论是find()的结果还是磁盘存储,都是如此。)
    这同时会产生游标查询结果不一致的问题,可以用 db.blogs.find().snapshot() 来解决问题。参见附图2。
  • 两种最基本的写入安全机制是应答式写入(acknowledged wirte)和非应答式写入(unacknowledged write)。应答式写入是默认的方式:数据库会给出响应,告诉你写入操作是否成功执行。非应答式写入不返回任何响应,所以无法知道写入是否成功。
  • 使用普通的AND型查询时,总是希望尽可能用最少的条件来限定结果的范围。OR型查询正相反:第一个条件应该尽可能匹配更多的文档,这样才是最为高效的。
  • skip的本质是先拿到所有的数据,然后丢掉skip的部分。所以在mongo中分页,可以采用“拿到上一页最后一条时间,然后把上一条时间作为下一页的比较条件”的方式。
  • 如果需要随机获取一个文档,可以给每个文档一个随机键,内容是0-1的随机数,取出时生成一个随机数,取第一个大于此的即可(找不到再取个小于此的)。

##数据类型

  • null
  • 布尔值:true false
  • 数值:3 3.14 NumberInt("3")(4字节带符号) NumberLog("3")(8字节带符号)
  • 字符串:"string"
  • 日期:new Date()
  • 正则:/foobar/i
  • 数组:[1, 2, 3]
  • 多维数组
  • 对象ID:ObjectId()(12字节,由24个十六进制数字组成,组成方式是 时间戳4B|主机名散列3B|PID2B|计数器3B)
  • 二进制:无法在js shell中表示
  • 代码:function(){...}

##索引

  • 通过ensureIndex({"field_name": 1})来创建索引,每个集合上的索引不得超过64个。
  • sort的顺序与索引相关,如果sort两个字段,其中的第一个没有索引,则索引发挥不了多少作用。
  • 建立索引的过程中,可以通过db.currentOp()查看索引的建立进度。
  • mongodb在内存中排序的结果集不可以超过32M,否则mongodb会报错,拒绝排序。
  • 一般的{"sortKey": 1, "queryCriteria": 1}的所有比较有用。
  • 使用hint强制使用索引
  • {'key': {'$exists': true}}完全不适用索引
  • $ne需要遍历索引中所有的其它条件。例如 ne:3即为<3 AND >3
  • $not有时不会使用索引,它能把 lt 转换成 gte,但大部分查询最后都变成扫表。
  • $nin总是扫表。
  • OR查询会将每个条件分别用索引查询然后合并,相比较而言,IN的效率更高。
  • 可以索引数组的元素,可以索引数组元素的键。
  • 每个索引中只能有一个数组,否则在多键索引中会有笛卡尔积出现。
  • .hint({"$natural": 1})强制全表扫描
  • 索引字段必须少于1024字节。
    • 从MongoDB2.6开始,如果集合中有文档的被索引内容超过了1KB,则mongo不会创建这个索引。在之前的版本中,mongo会创建索引,但不会索引这个文档。
    • 对于超过这个限制的文档,插入会报错。但之前的版本中,允许插入,但不索引。
    • mongorestore和mongoimport不会迁移超过限制的文档。
    • 参考:http://docs.mongodb.org/manual/reference/limits/#Index-Key-Limit
  • 建立唯一索引时,如果有重复值,可以用dropDups选项进行去除。
  • 稀疏索引不包含没有该键的文档。
  • db.collectionName.getIndexes()查看集合上所有索引。
  • dropIndex删除索引
  • 默认的,创建索引时会阻塞所有读写请求,可以使用background选项在后台创建。
  • 全文索引会进行分词,不区分大小写,每个集合上只能有一个,但可以指定多个字段,且分权重。
  • 全文索引不支持中文分词。当遇到包含中文的查询时,能够查询被字符分开的中文,但无法将中文切为单字查询,例如查询“中国”,只能查询到"xx中国"或者"中国-啊",而不能查询到"中国啊"的字符。
  • 每个集合最多含有1个全文索引,索引可以包含多个键。
  • 空间索引有2d2dsphere,后者用于球面2D。可以与其他字段一起建立复合索引
  • 空间索引可以查询 交集(有哪些街道穿过等),包含(完全包含的单位),附近。

##聚合与管道

db.feeds.aggregate(
	{$project: {source: 1}}, //将source字段投射
	{$group: {"_id" : "$source", count: {$sum: 1}}}, //将source字段视为ID聚合
	{$sort: {count: -1}}, //根据count倒序排列
	{$limit: 5} //限制
)
{ "_id" : "instagram", "count" : 141345 }
{ "_id" : "wode", "count" : 58049 }
{ "_id" : "updateFavorites", "count" : 14886 }
{ "_id" : "blogbus", "count" : 3047 }
{ "_id" : "favorites", "count" : 370 }
  • db.coll.count() 记数
  • db.coll.distinct(field[, query]) 找出不同值
  • db.coll.group({ key, reduce, initial [, keyf] [, cond] [, finalize] })
    • key 分组依据
    • initial reduce 函数的计算基础
    • $reduce: function(doc, prev) doc当前文档,prev累加器文档
    • 剩下的🐶一样的参数请自己去看文档吧!

###管道

  • $match{$match: {sex: 'female'}} 可以使用 gt 等操作符。可以使用索引。
  • $project{$project: {author: 1, _id: 0, count: 'like'}} 1要,0不要,字符串重命名,重命名后不使用索引。
    • 数学表达式:{pay: {$add: ['$salary', '$bonus']}}
      $subtract $multiply $devide
    • 日期表达式:{month: {$month: '$createdAt'}}
      $year $month $week $dayOfMonth
    • 字符串表达式:{firstLetter: {$substr: ['$name', 0, 1]}}
      $concat:['$firstName', '$lastName'] $toLower $toUpper
    • 逻辑表达式:$cmp: [age, friendAge] 相等返回0,小于负数,大于正数。
      $strcasecmp $eq/ne/gt/gte/lt/lte $and/or/not
      $cond:[boolExpr, trueExpr, falseExpr] $ifNull:[notNullExpr, nullExpr]
  • $group:单字段分组 {$group: {_id: '$type'}} 多字段分组 {$group: {_id: {province: '$province', city: '$city'}}}
    • $sum/avg/max/min/first/last
    • $addToSet: expr 如果当前数组没有expr,则将其添加到数组中
      $push: exprexpr压入数组
    • 如果有分片行为,则先在每个分片上执行,再在mongos上执行管道工作
  • $unwind:拆分数组
  • $sort/limit/skip
  • 如果一个聚合操作占用了超过20%的内存,这个操作会报错。

##MapReduce 例如,我们有了很多用户的电话本,需要找出这些记录中,哪些人的出现频率最高

// map会在每个文档上执行,this指代当前文档,emit返回两样东西:1.统计所用的key,2参与reduce计算的文档
map = function(){
	emit(this.contact, {count: 1});
}
// reduce会被反复多次调用,reduce的结果也会被reduce调用,所以reduce的返回一定要和map中emit的第二个参数格式相同
// reduce的第一个参数可能不参与计算,如果我们只是记数的话。
reduce = function(key, emits){
	total = 0;
	for(var i in emits){
		total += emits[i].count;
	}
	return {count: total};
}
// 最终执行时
mr = db.runCommand({mapreduce: "friendContacts", map: map, reduce: reduce, out: 'topContact'})
// 执行过程中查看
db.currentOp()
// 查看结果
db[mr.result].find().sort({"value.count": -1})

##程序设计

  • 两个有关联的资源,是应当单独拉一个集合出来,还是应该作为子文档?
  • 使用TTL集合删除旧数据。
  • mongo为每个连接维护一个请求队列,队列中的请求会被依次执行,每个连接的数据库是一致的。mongodb提供多个数据库的一致性级别
  • mongo不支持事务
  • mongo无法连表

##附图 ###附图1.集合增长因子的实现原理

###附图2.数据位移引发的获取不一致问题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment