MongoDB权威指南学习笔记(2)--设计应用

Mongo

设计应用

索引

使用ensureIndex()创建索引

db.users.ensureIndex({
    "username:1
})

简介

通常。在一个特定的集合,不应该拥有两个以上的索引

复合索引

索引的值是按照一定顺序排列的,因此,使用索引键对文档进行排序非常快。然而,只有在首先使用索引键进行排序时,索引才有用。

复合索引就是建立在多个字段上的索引

db.users.ensureIndex({
    "age": 1,
    "username:1
})

通常来说,如果mongodb使用索引进行查询,那么查询结果文档通常就是按照索引顺序排序的

如果对查询结果的范围做了限制,那么mongo在几次匹配之后就可以不在扫描索引,在这种情况下,将排序键放在第一位时一个和好的策略。

可以通过hint来强制使用某个特定的索引

使用复合索引

在多个键上建立的索引就是复合索引

选择键的方向

索引使用的方向,与排序方向相同即可,注意,相互反转(在每个方向上*-1)的索引时等价的{"age":1,"username":-1}适用的查询和{"age"-1,"username"1}是完全一样的

只有基于多个查询条件进行排序时,索引方向才是你叫重要的,如果只是基于单一索引键进行排序

使用覆盖索引

如果你的查询只需要查找索引中包含的字段,那就根据没必要获取实际的文档。当一个索引包含用户请求的所有字段,可以认为这个索引覆盖了本次查询。在实际中,应该使用覆盖索引,而不是获取文档

为了确认查询只使用索引就可以完成,应该使用投射来指定不要返回_id字段

如果在覆盖索引上执行explain()indexOnly字段的值要设为true

隐式索引

复合索引具有双重功能,而且对不同的查询可以表现出不同的索引。

如果有一个拥有n个键的索引,难免你同时得到了所有这n个键的前缀组成的索引。

$操作符如何使用索引

低效率的操作符

$where查询和检查一个键是否存在的查询完全无法使用索引

$ne查询可以使用索引,但并不是很有效,因为必须要查看所有索引的条目

$nin就总是要进行全表扫描

范围

设计多个字段的索引时,应该将会用于精确匹配的字段防到索引的前面,将用于范围匹配的字段放到最后

索引对象和数组

mongo允许对嵌套字段和数组建立索引,嵌套对象和数组字段可以与符合索引中顶级字段一起使用。

索引嵌套文档

可以在嵌套文档的键上建立索引,方式和正常的键一样。

例如:

{
    "username:"sid",
    "loc":{
        "ip":"1.2.3.4"
        "city":"xxx"
        "state":"xxx"
    }
}

需要在loc的某一个字段建立索引,以便提高这个字段的查询速度

db.users.ensureIndex({
    "loc.city":1
})
  • 对嵌套文档本身建立索引和对嵌套文档的某个字段建立索引是不同的
  • 对整个文档建立索引,只会提高整个字段子文档的查询速度。只有在进行与子文档字段顺序完全匹配的子文档查询(db.users.find({"loc":{"ip":"1.2.3.4","city":"xxx","state":"ny"}})),查询优化器才会使用索引,无法对形如db.users.find({"loc.city":"xxx"})的查询使用索引
索引数组

对数组建立索引,可以高效的搜索数组中的特定元素

多键索引

对于索引的键,如果这个键在文档中是一个数组,那么这个索引就会呗还标记为多键索引,多键索引可能会比非多键索引慢一些,可能会友多个索引条目指向同一个文档,因此在返回结果时必须要先去除重复的内容

索引基数

基数就是集合中某个字段拥有不同值的数量,一般来说,应该在基数比较高的键上建立索引,或者至少应该吧基数高的键放在复合索引的前面

使用explain()和hint()

explain()能够提供大量的查询相关的信息。对于任意查询,都可以在最后添加一个explain()调用

字段说明:

  • "cursor": "BtreeCursor age_1_username_1"
    BtreeCursor表示使用了索引,使用了{"age":1,"username":1}的索引
  • "isMultiKey":false
    用于说明本次查询是否使用了多键索引
  • "n":8332
    本次查询返回的文档数量
  • nscannedObjects":8332
    按到索引去磁盘上查找实际文档的次数
  • "nscanned":8332
    如果有使用索引,那么这个数字就是查找过的索引条目数量,如果本次查询是一次全表查询,那么这个数字就表示检查过的文档数量。
  • "scanAndOrder":false
    是否在内存中对结果集进行了排序
  • "indexOnly":false
    是否只使用索引就能完成此次查询
  • "nYields":0
    为了让写入请求能够顺序执行,本次插叙暂停的次数
  • "millis":91
    数据库执行本次查询所耗费的毫秒数
  • "indexBounds":{...}
    描述了索引的使用情况,给出了索引的遍历范围

索引类型

唯一索引

唯一索引可以确保集合的每一个文档都有唯一值

如果向保证同文档的“username”键都拥有不同的值,那么可以创建一个唯一索引

db.users.ensureIndex(
    {
        "username":1
    },
    {
        "unique":true
    }
)
复合唯一索引

创建符合唯一索引时,单个键的值可以相同,但所有键的组合值必须时唯一的

去除重复

在已有的集合创建唯一索引时可能会失败,因为集合中肯能已经存在重复值了,通常需要先对已有的数据进行处理,在极少数情况下,可能希望直接删除重复的值,创建索引时使用dropDups选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会呗删除

db.users.ensureIndex(
    {
        "username":1
    },
    {
        "unique":true,
        "dropDups";true
    }
)

索引管理

所有的数据库索引信息都存储在system.indexes集合中,这个是一个保留集合,不能在其中插入或者删除文档,直蹦通过ensureIndex或者dropIndexes对其进行操作

创建一个索引之后,可以执行db.collectionName.getIndexes()查询给定集合上的所有索引信息

特殊的索引和集合

固定集合

mongo中普通的集合是动态的,可以自动增长,但是固定集合,固定集合需要事先创建好,而却他的大小时固定的。固定集合的行为类似于循环队列,如果已经满了,最老的文档会被删除,新插入的文档会占据这块空间

创建固定集合

不同于普通集合,固定集合必须在使用前显示创建,可以使用create命令创建固定集合,使用createCollection函数

创建一个名为my_collection大小为10000字节的固定集合

db.createCollection("my_collection",{
    "capped":true,
    "size":10000
})

限制固定集合中的文档的数量

db.createCollection("my_collection",{
    "capped":true,
    "size":10000,
    "max":100
})

创建固定集合还可以将已有的某个常规集合转换成固定集合,使用convertToCapped命令

db.runCommand("convertToCapped","test","size":10000)

自然排序

对于固定排序,自然排序就是文档从旧到新排序的,当然也可以按照从新到旧的顺序排序

db.my_collection.find().sort({
    "$natural":-1
})

TTL索引

允许为每一个文档设置一个超市时间,一个文档到达预设置的老化程度之后就会呗删除

在ensureIndex中指定expireAlterSecs选项就可以创建一个TTL索引

db.foo.ensureIndex(
    {
        "lastUpdate":1
    },
    {
        "expireAlterSecs":60*60*24
    }
)

在lastUpdate字段上建立了一个ttl索引,如果一个文档的lastUpdate字段存在并且它的值时日期类型,当服务器时间比文档的lastUpdate字段的时间晚expireAlterSecs秒时,文档就会呗删除

mongo每分钟对ttl索引进行一次清理,所以不应该依赖以秒为单位保证索引的存活状态

地理空间索引

mongo支持几种类型的地理空间索引,其中常用的时2dsphere索引和2d索引

地理空间查询的类型

可以使用多种不同类型的地理空间查询:交集、包含、以及接近。查询时,需要将希望查找的内容制定为形如{"$geometry":geoJsonDesc}的GeoJson对象

例如:可以使用$geoIntersects操作符找出与查询位置相交的文档

var eastVillage={
    "type":"xxx",
    "coordinates":{
        [-73.9917900,40.7264100],
        [-73.9917900,40.7264100],
        [-73.9917900,40.7264100],
    }
}

db.open.street.map.find({
    "loc":{
        "$geoIntersects":{
            "$geometry":eastVillage
        }
    }
})

使用"$within"查询完全包含在某个区域的文档

db.open.street.map.find({
    "loc":{
        "$within":{
            "$geometry":eastVillage
        }
    }
})

使用”$near“查询附近的位置

db.open.street.map.find({
    "loc":{
        "$near":{
            "$geometry":eastVillage
        }
    }
})

$near是唯一一个会对查询结果进行自动排序的地理空间操作符,返回结果时按照距离由近及远排序的

使用GridFS存储文件

shell下使用mongofiles 命令即可

聚合

聚合框架

对聚合框架可以对集合中的文档进行变化和组合,可以用多个构件创建一个管道,用于对一连串的文档进行处理,包括筛选投射分组排序限制跳过
将一系列操作分别传给aggregate()函数即可

db.articles.aggregate(
    {
        "$project":{
            "author:1
        }
    },
    {
        "$group":{
            "_id":"$auhtor",
            "count":{
                "$sum":1
            }
        }
    },
    {
        "$sort":{
            "count":-1
        }
    },
    {
        "$limit":5
    }
)
  • $project:通过指定"filename",1选择需要投射的字段,0排序不需要的字段,执行完$project操作,结果集会以{"_id":id,"filename":xxx}形式表示
  • $group:指定需要进行分组的字段,是由“_id”:"$author"指定的,第二个字段为分组的每个文档的“count”字段+1,(新加入的文档中并不会有"count"字段,这是"$group"创建的一个新字段),执行后文档结构为{"_id":"auhthorName","count":articleCount}
  • $sort:对文档中的"count"字段进行降序排序
  • $limit:限制最终返回结果为当前结果中的5个文档

管道操作符

$match

用于对文档集合进行筛选,之后就可以在筛选得到的文档子集做聚合

  • 不能在$match中使用地理空间操作符
  • 尽可能将$match放在管道的前面位置

$project

可以从文档中提取字段,可以重命名字段

只包含一个author字段

db.articles.aggregate({
    "$project":{
        "author":1,
        "_id":0
    }
})

将投射过的字段进行重命名,将"_id"在返回结果中重命名为"userId"

db.users.aggregate(
    {
        "$project":{
            "userId":"$_id",
            "_id":0
        }
    }
)
  • "$fidldname"会引用fieldname字段的值
  • "$tag.3"会被替换为tags数组中的第4个元素
  • 必须显式将“_id”排除,否在这个字段的值将会返回两次
数学表达式

算术表达式可用于操作数值,指定一组数值,就可以使用这个表达式进行操作了

将”salary“和”bonus“字段的值相加

db.employees.aggregate(
    {
        "$project":{
            "todayPay:{
                "$add":["$salary","$bonus"]
            }
        }
    }
)

操作符的语法:

  • "$add":[expr1[,expr2,...,exprN]]
    接受一个或多个表达式作为参数,将这些表达式相加
  • "$subtract":[expr1,expr2]
    接受两个表达式作为参数,用第一个表达式减去第二个表达式作为结果
  • "$multiply":[expr1[,expr2,...,exprN]]
    接受一个或者多个表达式,并且将它们相乘
  • ”$divide“:[expr1,expr2]
    接受两个表达式,用第一个表达式除以第二个表达式的商作为结果
  • "$mod":[expr1,expr2]
    接受两个表达式,将第一个表达式除以第二个表达式得到的余数作为结果
日期表达式
  • $year
  • $month
  • $week
  • $dayOfMonth
  • $dayOfWeek
  • $dayOfYear
  • $hour
  • $minute
  • $second
字符串表达式
  • "$substr":[expr,startOffset,numToReturn]
    第一个参数expr必须是个字符串,截取这个字符串的子串(从startOffset字节开始的numToReturn字节)
  • "$concat":[expr1,expr2,...,exprN]
    将给定的表达式(或者字符串)连接在一起作为返回结果
  • "$toLower":expr
    参数expr必须是个字符串值,返回expr的小写形式
  • ”$toUpper:expr
    参数expr必须是个字符串值,返回expr的大写形式
逻辑表达式
  • "$cmp":[expr1,expr2]
    比较expr1和expr2的大小,如果expr1小于expr2,返回负数,反之返回正数
  • "$strcasecmp":[string1,string2]
    比较string1和string2,区分大小写,只对罗马字符组成的字符串有效
  • "$eq"/”$ne“/"$gt"/"gte"/"$lt"/"$lte":[expr1,expr2]
    你叫expr1和expr2的大小,返回true或者false

布尔表达式

  • "$and":[expr,[,expr2,...,exprN]]
    所有表达式的值都是true,那就返回true,否则返回false
  • "$or":[expr,[,expr2,...,exprN]]
    只要有任意表达式的值为true,返回true,否贼返回false
  • "$not":expr
    对expr取反

控制语句

  • "$cond":[booleanExpr,trueExpr,falseExpr]
    如果booleanExpr的值为true,那就返回trueExpr,否则返回falseExpr
  • "$isNull":[expr,replacementExpr]
    如果expr是null,返回replacementExpr,否则返回expr

$group

将文档依据特定字段的不同值进行分组

算术操作符
  • “$sum”: value
    对于分组中的每一个文档,将value与结果相加

  • “$avg”: value
    返回每个分组的平均值

极值操作符
  • “$max”: expr
    返回分组内的最大值

  • “$min”: expr
    返回分组内的最小值

  • “$first": expr
    返回分组的第一个值

  • “$last": expr
    返回分组的最后一个值

数组操作符
  • “$addToSet”: expr
    如果当前数组中不包含expr,那就将它添加到数组中,在反结果集中,每个元素最多只出现一次,而且元素的顺序时不确定的

  • “$push”: expr
    不管expr时什么值,都将它添加到数组只能怪,返回包含所有值的数组

$unwind

拆分可以将数组中的每一个值拆分为单独的文档

如果希望在查询中得到特定的子文档,先使用“$unwind”得到所有子文档,再使用“$match”得到想要的文档

$sort

根据任何字段或多个字段进行排序

$limit

接受一个数字n,返回结果集中的前n个文档

$skip

接受一个数字m,丢弃结果集中的钱n个文档

MapReduce

找出集合中的所有键

map函数使用特定的emit函数返回要处理的值,emit会给mapreduce一个键和一个值

map=function(){
    for (var key in this){
        emit(key,{
            count:1
        })
    }
}

reduce=function(key,emits){
    total=0;
    for (var i in emits){
        total+=emit[i].count;
    }
    return {
        "count":total
    };
}


mr=db.runCommand(
    {
        "mapreduce":"foo",
        "map":map,
        "reduce":reduce
    }
)

操作相关元信息

  • "reuslt":"tmp.mr.mapreduce_1266787811_1"
    存放mapreduce结果的集合名,临时集合
  • "timeMollis":12
    操作花费的时间,单位时毫秒
  • “counts”:{...}
    用于调试,包含三个键
  • "input":6
    发送到map函数的文档个数
  • "emit":14
    在map函数中emit调用的次数
  • "output":5
    结果集合中的文档数量

聚合命令

count

返回集合中文档的数量

db.foo.count({"x";2})

distinct

用来找出给定键的所有不同值,使用时必须指定集合和键

db.runCommand(
    {
        "distinct":"people",
        "key":"age"
    }
)

group

选定分组所依据的键进行分组,然后对分组内的文档进行聚合得到结果文档

db.runCommand(
    {
        "ns":"stocks",
        "key":"day",
        "inital":{
            "time":0
        },
        "$reduce":funcion(doc,prev){
            if (doc.time>prev.time){
                prev.price=doc.price;
                price.time=doc.time;
            }
        }
    }
)
  • "ns":"stock":指定要进行分组的集合
  • "key":"day":指定文档分组依据的键
  • "initial":{"time":0}:每一组reduce函数调用中的初始time值,会作为初始文档传递给后续过程。每一组的所有成员都会使用这个累加器,所以它的任何变化都可以保存下来
  • "reduce":function(doc,prev){}:在集合内的每个文档上执行,系统会传递两个参数,当前文档和累加器文档。
使用完成器

完成器用于精简从数据库传到用户的数据

将函数作为键使用

分组所依据 的条件非常复杂,需要定义一个函数来决定文档分组所依据的键

定义分组函数就要用到$keyf键,使用$keyf的group命令

db.posts.group(
    {
        "ns":"posts",
        "$keyf":function(x){
            return x.category.toLowerCase();
        },
        "initializer":...
    }
)

应用程序设计

范式化与反范式化

决定何时采用范式化何时采用反范式化需要根据自己的应用程序的实际情况仔细权衡

一般来说,数据生成越频繁,就越不应该将这些数据内嵌到其他文档中

如果内嵌字段或者内嵌字段数量时无限增长的,那么应该将这些内容保存在单独的集合中,使用引用的方式进行访问

如果某些字段时文档数据的一部分,那么需要将这些字段内嵌到文档中

如果在查询文档时经常需要将需要将某个字段排除出去,那么这个字段应该放在另外的集合中

内嵌数据与引用数据的比较:

| 更适合内嵌 | 更适合引用 |
| -------- | ----- | ---- |
| 子文档较小| 子文档较大|
| 数据不会定期改变 | 数据经常改变 |
| 最终数据一致即可 | 中间阶段的数据必须一致 |
| 文档数据小幅增加 | 文档数据大幅增加 |
| 数据通常需要执行二次查询才能获得 | 数据通常不包含在结果中 |
| 快速读取 | 快速写入 |

优化数据操作

需要在写入效率更高的模式与读取更高的模式之间权衡

不适合MongoDB的场景

  • 不支持事务
  • 在多个不同维度上对不同类型的数据进行连接

注:

  • 上述测试在MongoDB 3.4.3-8-g05b19c6中成功
  • 上述文字皆为个人看法,如有错误或建议请及时联系我
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,310评论 0 5
  • 毕业那天,我以为我离开了地狱,直到现在才知道我离开了天堂。 拍照只需要三秒,切定格了六年。 ...
    突然心痛阅读 177评论 1 1
  • 欢迎关注幼儿说,用简书的妈咪,都是有品味的母亲 今天说的这孩子,她只是遭遇了跟大多数孩子可能都会遇到的事情,但就是...
    幼儿说阅读 21,637评论 1 1