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个全文索引,索引可以包含多个键。
- 空间索引有
2d
和2dsphere
,后者用于球面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: expr
将expr
压入数组- 如果有分片行为,则先在每个分片上执行,再在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.数据位移引发的获取不一致问题