MongoDB-关系、索引、原子操作

MongoDB的关系

MongoDB 的关系表示多个文档之间在逻辑上的相互联系。
文档间可以通过嵌入和引用来建立联系。
MongoDB 中的关系可以是:

  • 1:1 (1对1)
  • 1: N (1对多)
  • N: 1 (多对1)
  • N: N (多对多)

嵌入式关系

{
   "_id":ObjectId("52ffc33cd85242f436000001"),
   "contact": "987654321",
   "dob": "01-01-1991",
   "name": "Tom Benzamin",
   "address": [
      {
         "building": "22 A, Indiana Apt",
         "pincode": 123456,
         "city": "Los Angeles",
         "state": "California"
      },
      {
         "building": "170 A, Acropolis Apt",
         "pincode": 456789,
         "city": "Chicago",
         "state": "Illinois"
      }]
} 

db.jack.findOne({"name":"Tom Benzamin"},{"address":1})//address:1表示只显示地址字段(默认有_id)
find返回的数据类型是数组,findOne返回的数据类型是对下个。
优点:可以比较容易的获取和维护数据。
缺点:用户信息不断增加,数据量不断变大,会影响读写性能

引用式关系

引用式关系是设计数据库时经常用到的方法,这种方法把用户数据文档和用户地址数据文档分开,通过引用文档的 id 字段来建立关系。

{
   "_id":ObjectId("52ffc33cd85242f436000001"),
   "contact": "987654321",
   "dob": "01-01-1991",
   "name": "Tom Benzamin",
   "address_ids": [
      ObjectId("52ffc4a5d85242602e000000"),
      ObjectId("52ffc4a5d85242602e000001")
   ]
}

result = db.users.findOne({"name":"Tom Benzamin"},{"address_ids":1})
db.address.find({"_id":{"$in":result["address_ids"]}})

MongoDB数据库引用

MongoDB 引用有两种:

  • 手动引用(Manual References)
  • DBRefs

DBRefs

  • 场景:我们在不同的集合中 (address_home, address_office, address_mailing, 等)存储不同的地址(住址,办公室地址,邮件地址等)。
    这样,我们在调用不同地址时,也需要指定集合,一个文档从多个集合引用文档,我们应该使用 DBRefs。
{$ref :, $id : ,$db :}

$ref 集合名称;$id: 引用的id; $db 数据库名称,可选参数。

{
   "_id":ObjectId("53402597d852426020000002"),
   "address": {
       "$ref": "address_home",
       "$id": ObjectId("534009e4d852427820000002"),
       "$db": "runoob"
   },
   "contact": "987654321",
   "dob": "01-01-1991",
   "name": "Tom Benzamin"
}

address DBRef 字段指定了引用的地址文档是在 runoob 数据库下的 address_home 集合,id 为 534009e4d852427820000002,通过指定 $ref 参数(address_home 集合)来查找集合中指定id的用户地址信息:

>var jack = db.jack.findOne({"name":"Tom Benzamin"},{address:1})
>var dbRef = jack.address
>db[dbRef.$ref].findOne({"_id":ObjectId(dbRef.$id)})
结果 
{
   "_id" : ObjectId("534009e4d852427820000002"),
   "building" : "22 A, Indiana Apt",
   "pincode" : 123456,
   "city" : "Los Angeles",
   "state" : "California"
}

覆盖索引查询

特征:所有的查询字段是索引的一部分;所有的查询返回字段在同一个索引中;由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。
因为索引存在于RAM中,从索引中获取数据比通过扫描文档读取数据要快得多

db.lee.insert({
   "_id": ObjectId("53402597d852426020000002"),
   "contact": "987654321",
   "dob": "01-01-1991",
   "gender": "M",
   "name": "Tom Benzamin",
   "user_name": "tombenzamin"
})

db.lee.find();

创建联合索引
db.lee.createIndex({gender:1,user_name:1})

覆盖索引查询
db.lee.find({gender:"M"},{user_name:1,_id:0}) //只返回user_name,_id 不查询
上面的sql,不会去数据库文件中查找,直接从索引中提取数据。

当所有的索引字段是一个数组或者是一个子文档 不能使用覆盖索查询;可以用explain()方法查看下执行结果;

db.lee.find({gender:"M"},{user_name:1,_id:0}).explain() 执行结果
{
    namespace: "test.lee", //集合
    indexFilterSet: false, //是否使用索引
    parsedQuery: { //查询条件
        gender: {
            $eq: "M"
        }
    },
    queryHash: "8378F74D",
    planCacheKey: "07CB9482",
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {  //最佳执行计划
        stage: "PROJECTION_COVERED",
        transformBy: {
            "user_name": 1,
            _id: 0
        },
        inputStage: {
            stage: "IXSCAN",
            keyPattern: {
                gender: 1,
                "user_name": 1
            },
            indexName: "gender_1_user_name_1",
            isMultiKey: false, //如果索引建立在array上,此处将是true。
            multiKeyPaths: {
                gender: [ ],
                "user_name": [ ]
            },
            isUnique: false,
            isSparse: false,
            isPartial: false,
            indexVersion: NumberInt("2"),
            direction: "forward",
            indexBounds: {
                gender: [
                    "[\"M\", \"M\"]"
                ],
                "user_name": [
                    "[MinKey, MaxKey]"
                ]
            }
        }
    },
    rejectedPlans: [ ]
}
  • indexBounds:当前查询具体使用的索引
  • stage:类型有COLLSCAN 全表扫描, IXSCAN/索引扫描、FETCH/根据索引去检索文档、SHARD_MERGE/合并分片结果、SORT内存中惊醒了排序、LIMIT使用limit限制返回数、SKIP使用skip进行跳过、IDHACK/针对_id进行查询、PROJECTION:限定返回字段时候stage的返回
  • direction:搜索方向

使用 db.lee.find({gender:"M"},{user_name:1,_id:0}).explain('executionStats')

单独查看executionStats列
{
    executionSuccess: true, //true 成功
    nReturned: NumberInt("1"), //返回的结果数
    executionTimeMillis: NumberInt("0"),//执行耗时,越少越好
    totalKeysExamined: NumberInt("1"),//索引扫描次数
    totalDocsExamined: NumberInt("0"),//文档扫描次数
    executionStages: {
        stage: "PROJECTION_COVERED",
        nReturned: NumberInt("1"),//查询结果数量
        executionTimeMillisEstimate: NumberInt("0"),//预估耗时
        works: NumberInt("2"),//工作单元数,一个查询会分解成小的工作单元
        advanced: NumberInt("1"),//优先返回的结果数
        needTime: NumberInt("0"),
        needYield: NumberInt("0"),
        saveState: NumberInt("0"),
        restoreState: NumberInt("0"),
        isEOF: NumberInt("1"),
        transformBy: {
            "user_name": 1,
            _id: 0
        },
        inputStage: {
            stage: "IXSCAN",
            nReturned: NumberInt("1"),
            executionTimeMillisEstimate: NumberInt("0"),
            works: NumberInt("2"),
            advanced: NumberInt("1"),
            needTime: NumberInt("0"),
            needYield: NumberInt("0"),
            saveState: NumberInt("0"),
            restoreState: NumberInt("0"),
            isEOF: NumberInt("1"),
            keyPattern: {
                gender: 1,
                "user_name": 1
            },
            indexName: "gender_1_user_name_1",
            isMultiKey: false,
            multiKeyPaths: {
                gender: [ ],
                "user_name": [ ]
            },
            isUnique: false,
            isSparse: false,
            isPartial: false,
            indexVersion: NumberInt("2"),
            direction: "forward",
            indexBounds: {
                gender: [
                    "[\"M\", \"M\"]"
                ],
                "user_name": [
                    "[MinKey, MaxKey]"
                ]
            },
            keysExamined: NumberInt("1"),
            seeks: NumberInt("1"),
            dupsTested: NumberInt("0"),
            dupsDropped: NumberInt("0")
        }
    }
}

MongoDB 原子操作

MongoDB不支持事务;但是提供了很多原子操作比如文档的保存,修改,删除等,都是原子操作
所谓原子操作就是要么这个文档保存到Mongodb,要么没有保存到Mongodb,不会出现查询到的文档没有保存完整的情况。

实例说明了在一个相同的文档中如何确保嵌入字段关联原子操作(update:更新)的字段是同步的。

初始数据
db.summer.insert({
          _id: 123456789,
          title: "MongoDB: The Definitive Guide",
          author: [ "Kristina Chodorow", "Mike Dirolf" ],
          published_date: ISODate("2010-09-24"),
          pages: 216,
          language: "English",
          publisher_id: "oreilly",
          available: 3,
          checkout: [ { by: "joe", date: ISODate("2012-10-15") } ]
        });

注意这里checkout是单元素的一个数组,available的值是3
来执行一下

db.summer.findAndModify ({
   query: {
            _id: 123456789,
            available: { $gt: 0 }
          },
   update: {
             $inc: { available: -1 },//$inc可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作。
             $push: { checkout: { by: "abc", date: new Date() } }
           }
})

执行db.summer.find()
 {
    _id: 123456789,
    title: "MongoDB: The Definitive Guide",
    author: [
        "Kristina Chodorow",
        "Mike Dirolf"
    ],
    "published_date": ISODate("2010-09-24T00:00:00.000Z"),
    pages: 216,
    language: "English",
    "publisher_id": "oreilly",
    available: 2,
    checkout: [
        {
            by: "joe",
            date: ISODate("2012-10-15T00:00:00.000Z")
        },
        {
            by: "abc",
            date: ISODate("2022-06-17T10:34:24.275Z")
        }
    ]
}

此时available=2,且checkout多了一个元素

常用原子操作命令

  • $set 用来指定一个键并更新键值,若键不存在并创建。{ $set : { field : value } }
  • $unset 用来删除一个键 { $unset : { field : 1} }
  • $inc
    $inc可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作。
    { $inc : { field : value } }
  • $push 用法:
    { $push : { field : value } }
    把value追加到field里面去,field一定要是数组类型才行,如果field不存在,会新增一个数组类型加进去。$pushAll 可以一次增加多个到数组字段内 { $pushAll : { field : value_array } }
  • $pull 从数组field内删除一个等于value值。{ $pull : { field : _value } }
  • $addToSet 增加一个值到数组内,而且只有当这个值不在数组内才增加。
  • $pop 删除数组的第一个或最后一个元素{ $pop : { field : 1 } }
  • $rename 修改字段名称 { $rename : { old_field_name : new_field_name } }
  • $bit 位操作,integer类型 {$bit : { field : {and : 5}}}
操作一把

更新title字段:
db.summer.update({ _id:123456789}, {$set: {'title': 'jack is very handsome man'}})

新增comments字段
db.summer.update({ _id:123456789}, {$set: {'comments': 'jack is very handsome man'})

删除comments字段
db.summer.update({ _id:123456789}, {$unset: {'comments': 1})

对 pages加1和减1
db.summer.update({ _id:123456789}, {$inc: {'pages': 1})
db.summer.update({ _id:123456789}, {$inc: {'pages': -1})

删除掉author中的 Kristina Chodorow
db.summer.update({ _id:123456789}, {$pull: {'author': 'Kristina Chodorow' }})

由于上面一步删掉了Kristina Chodorow',author中只剩下Mike Dirolf,所以Mike Dirolf是插入不进去的,只能‘Mike Dirolf1’ 才是不重复的
db.summer.update({ _id:123456789}, {$addToSet: { 'author': 'Mike Dirolf1'}})

删掉刚才加入的Mike Dirolf1
db.summer.update({ _id:123456789}, {$pop: { 'author': 1}})

字段名由author改为author_rename

db.summer.update({ _id:123456789}, {$rename: { 'author': 'author_rename'}})

让pages的值位移
db.summer.update({ _id:123456789}, {$bit: { 'pages': {and: 2}}})

MongDB 高级索引

插入语句

db.gj.insertOne({
   "address": {
      "city": "Los Angeles",
      "state": "California",
      "pincode": "123"
   },
   "tags": [
      "music",
      "cricket",
      "blogs"
   ],
   "name": "Tom Benzamin"
})
以上文档包含了 address 子文档和 tags 数组。

索引数组字段

假设我们基于标签来检索用户,为此我们需要对集合中的数组 tags 建立索引。

在数组中创建索引,需要对数组中的每个字段依次建立索引。所以在我们为数组 tags 创建索引时,会为 music、cricket、blogs三个值建立单独的索引。

使用以下命令创建数组索引:

db.gj.createIndex({"tags":1})

索引子文档字段

假设我们需要通过city、state、pincode字段来检索文档,由于这些字段是子文档的字段,所以我们需要对子文档建立索引。

为子文档的三个字段创建索引,命令如下
db.gj.createIndex({"address.city":1,"address.state":1,"address.pincode":1})
查询表达不一定遵循指定的索引的顺序,mongodb 会自动优化。

MongoDB 索引限制

  • 额外开销:每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。所以,如果你很少对集合进行读取操作,建议不使用索引。
  • 内存(RAM)使用:
    由于索引是存储在内存(RAM)中,你应该确保该索引的大小不超过内存的限制。
    如果索引的大小大于内存的限制,MongoDB会删除一些索引,这将导致性能下降。
  • 查询限制:索引不能被以下的查询使用:
    正则表达式及非操作符,如 $nin, $not, 等。
    算术运算符,如 $mod, 等。
    $where 子句 所以,检测你的语句是否使用索引是一个好的习惯,可以用explain来查看。
  • 最大范围:集合中索引不能超过64个,索引名长度不能超过128个字符,一个符合索引最多可以有31个字段
  • 强制索引:db.orders.find({onumber:1}).hint("onumber_1") 能够使用hint强制用某个索引
  • 索引不能修改,只能删除重新建立
  • 索引类型:
    • 唯一索引
    • 复合索引
    • 多列索引
    • 稀松索引
    • 单键所以
    • 多键值索引
    • 地理索引
    • 全文索引:提供的文本索引支持对字符串内容的文本搜索查询,但是这种索引因为需要检索的文件比较多,因此在使用的时候检索时间较长。
    • 散列索引:指按照某个字段的散列值来建立索引,目前主要用于 MongoDB Sharded Cluster 的散列分片,散列索引只能用于字段完全匹配的查询,不能用于范围查询等。
    • MapReduce 命令

      db.collection.mapReduce(
      function() {emit(key,value);},  //map 函数
      function(key,values) {return reduceFunction},   //reduce 函数
      {
      out: collection,
      query: document,
      sort: document,
      limit: number
      }
      )

使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数,Map 函数调用 emit(key, value), 遍历 collection 中所有的记录, 将 key 与 value 传递给 Reduce 函数进行处理。

Map 函数必须调用 emit(key, value) 返回键值对。

参数说明:

map :映射函数 (生成键值对序列,作为 reduce 函数参数)。
reduce 统计函数,reduce函数的任务就是将key-values变成key-value,也就是把values数组变成一个单一的值value。。
out 统计结果存放集合 (不指定则使用临时集合,在客户端断开后自动删除)。
query 一个筛选条件,只有满足条件的文档才会调用map函数。(query。limit,sort可以随意组合)
sort 和limit结合的sort排序参数(也是在发往map函数前给文档排序),可以优化分组机制
limit 发往map函数的文档数量的上限(要是没有limit,单独使用sort的用处不大)
以下实例在集合 orders 中查找 status:"A" 的数据,并根据 cust_id 来分组,并计算 amount 的总和。

db.posts.mapReduce(
    function() {
        emit(this.user_name, 2);
    },
    function(key, values) {
        return Array.sum(values)
    },
    {
        query: {
            status: "active"
        },
        out: {inline:1}
    }
)

正则表达式

正则表达式是使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。
MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式。
MongoDB使用PCRE (Perl Compatible Regular Expression) 作为正则表达式语言。
令使用正则表达式查找包含 runoob 字符串的文章:
db.chick.find({post_text:{$regex:"runoob"}})
db.chick.find({post_text:/runoob/}) //也可以这么写

  • 不区分大小写:db.posts.find({post_text:{$regex:"runoob",$options:"$i"}})
  • 数组元素使用正则:需要查找包含以 run 开头的标签数据(ru 或 run 或 runoob), 你可以使用以下代码:

db.chick.find({tags:{$regex:"run"}})

  • $regex操作符的使用

$regex操作符中的option选项可以改变正则匹配的默认行为,它包括i, m, x以及S四个选项,其含义如下

  • i 忽略大小写,{{$regex/pattern/i}},设置i选项后,模式中的字母会进行大小写不敏感匹配。

  • m 多行匹配模式,{{$regex/pattern/,$options:'m'},m选项会更改^和$元字符的默认行为,分别使用与行的开头和结尾匹配,而不是与输入字符串的开头和结尾匹配。

  • x 忽略非转义的空白字符,{:{$regex:/pattern/,$options:'m'},设置x选项后,正则表达式中的非转义的空白字符将被忽略,同时井号(#)被解释为注释的开头注,只能显式位于option选项中。

  • s 单行匹配模式{:{$regex:/pattern/,$options:'s'},设置s选项后,会改变模式中的点号(.)元字符的默认行为,它会匹配所有字符,包括换行符(\n),只能显式位于option选项中。
    使用$regex操作符时,需要注意下面几个问题:

  • i,m,x,s可以组合使用,例如:{name:{$regex:/j*k/,$options:"si"}}
    在设置索弓}的字段上进行正则匹配可以提高查询速度,而且当正则表达式使用的是前缀表达式时,查询速度会进一步提高,例如:{name:{$regex: /^joe/}

  • MongoDB 管理工具: Rockmongo(php5写的)、NoSQLBooster for MongoDB、Navicat Premium

MongoDB 固定集合(Capped Collections)

MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素!

  • 创建固定集合:db.createCollection("cappedLogCollection",{capped:true,size:10000})
    还可以指定文档个数,加上max:1000属性:
    db.createCollection("cappedLogCollection",{capped:true,size:10000,max:1000})
    size 是整个集合空间大小,单位为【字节】
    max 是集合文档个数上线,单位是【个】
    如果空间大小到达上限,则插入下一个文档时,会覆盖第一个文档;如果文档个数到达上限,同样插入下一个文档时,会覆盖第一个文档。两个参数上限判断取的是【与】的逻辑
    判断集合是否为固定集合:
    db.cappedLogCollection.isCapped() //true 为是

把普通集合jack转为固定集合
db.runCommand({"convertToCapped":"jack",size:10000})

固定集合查询
固定集合文档按照插入顺序储存的,默认情况下查询就是按照插入顺序返回的,也可以使用$natural调整返回顺序。
db.cappedLogCollection.find().sort({$natural:-1})

固定集合属性及用法
属性
属性1:对固定集合进行插入速度极快
属性2:按照插入顺序的查询输出速度极快
属性3:能够在插入最新数据时,淘汰最早的数据
用法
用法1:储存日志信息
用法2:缓存一些少量的文档

MongoDB 自动增长

MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节唯一标识。
由于 MongoDB 没有实现这个功能,我们可以通过编程的方式来实现,以下我们将在 counters 集合中实现_id字段自动增长。

考虑以下 products 文档。我们希望 _id 字段实现 从 1,2,3,4 到 n 的自动增长功能。

{
"_id":1,
"product_name": "Apple iPhone",
"category": "mobiles"
}
为此,创建 counters 集合,序列字段值可以实现自动长:

db.createCollection("counters")
db.counters.insert(
{
"_id":"productid",
"sequence_value": 0
}
)

创建一个Javascript函数
现在,我们创建函数 getNextSequenceValue 来作为序列名的输入, 指定的序列会自动增长 1 并返回最新序列值。在本文的实例中序列名为 productid 。

function getNextSequenceValue(sequenceName){
var sequenceDocument = db.counters.findAndModify(
{
query:{_id: sequenceName },
update: {$inc:{sequence_value:1}},
"new":true
});
return sequenceDocument.sequence_value;
}

插入数据的时候
db.products.insert({
"_id":getNextSequenceValue("productid"),
"product_name":"Samsung S3",
"category":"mobiles"})
这样查出来的时候就是有序的id了

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注