MongoDB 参考手册

学习《MongoDB 权威指南·第2版》的笔记,结合 MongoDB 官方最新文档(v3.6),简单记录一些概念、注意点、语法糖,方便使用的时候快速查阅、参考。

1. 文档

文档是 MongoDB 中数据的基本单元,非常类似于关系型数据库中的行。
文档的键是字符串,除了少数例外情况,键可以使用任意 UTF-8 字符:

  • 不能含有 \0 (空字符),这个字符用于表示键的结尾
  • . 和 $ 具有特殊意义,是被保留的,只能在特定环境下使用
  • MongoDB 不但区分类型,而且区分大小写。例如,下面的两个文档是不同的:
    {"foo" : 3}
    {"foo" : "3"}
    
  • 文档不能有重复的键

2. 集合

集合就是一组文档。如果将 MongoDB 中的一个文档比喻为关系型数据库中的一行,那么一个集合就相当于一张表。

  • 集合名不能以 “system.” 开头,这是为系统集合保留的前缀
  • 可以使用 db.collectionName 获取一个集合的全路径名,但是,如果集合名称中包含保留字或者无效的 JavaScript 属性名称,db.collectionName 就不能正常工作了。这个时候需要通过函数 db.getCollection( "collectionName" ) 访问相应的集合,或者使用数组访问语法。 比如,某个怪异的集合名称为 db.@#&!,要访问这个集合的话,可以数组访问方式:
    > var name = "@#&!"
    > db[name].find()
    
  • 普通集合是动态创建的,而且可以自动增长以容纳更多的数据。比如下面插入语句,如果集合 foo 不存在,则会自动创建该集合
    > db.foo.insert({"x" : 1})
    
  • MongoDB 中还有另一种不同类型的集合:固定集合。固定集合需要事先创建,同时指定它的大小,一旦创建成功后大小不能再改变。固定集合类似于循环队列,当空间被占满时,如果再插入新文档,固定集合会自动将最老的文档删除以释放空间,新文档会占据这块释放出来的空间。
    > // 创建一个名为 my_collection 大小为 100000 字节的固定集合
    > db.createCollection( "my_collection", { "capped" : true, "size" : 100000 } )
    > // 创建一个名为 my_collection2 大小为 100000 字节的固定集合,集合最多容纳 100 个文档
    > db.createCollection( "my_collection2", { "capped" : true, "size" : 100000, "max" : 100 } )
    

3. 数据库

多个文档组成集合,多个集合组成数据库。

  • 数据库名区分大小写,即便是在不区分大小写的文件系统中也是如此。简单起见,数据库名全部小写
  • 数据库最终会变成文件系统里的文件,而数据库名就是相应的文件名,这就是数据库名有限制的原因
  • 系统保留数据库
    • admin
      从身份验证的角度来讲,这是 “root” 数据库。如果将一个用户添加到 admin 数据库,那么这个用户将自动获得所有数据库的权限。再者,一些特定的服务器端命令也只能从 admin 数据库运行
    • local
      这个数据库永远不可以复制,且一台服务器上的所有本地集合都可以存储在这个数据库
    • config
      用于分片设置时,分片信息会存储在此数据库中

4. MongoDB shell

MongoDB 自带 JavaScript shell,可在 shell 中使用命令行与 MongoDB 实例交互

  • shell 连接到 MongoDB 实例

    $ mongo some-host:27017/mydb
    MongoDB shell version v3.6.1
    connecting to: mongodb://some-host:27017/mydb
    MongoDB server version: 3.6.1
    mongos>  
    
  • 如果想 shell 启动时不连接到任何 mongod 实例,可通过参数 --nodb 实现:

    $ mongo --nodb
    MongoDB shell version v3.6.1
    Welcome to the MongoDB shell.
    For interactive help, type "help".
    For more comprehensive documentation, see
            http://docs.mongodb.org/
    Questions? Try the support group
            http://groups.google.com/group/mongodb-user
    >  
    

    启动之后,在需要时运行 new Mongo(hostname) 命令就可以连接到想要的 mongod 了:

    > conn = new Mongo("some-host:27017")
    connection to some-host:27017
    > db = conn.getDB("mydb")
    mydb
    
  • 使用 shell 执行脚本。除了以交互式使用 shell 在,还可以将 JavaScript 脚本直接传给 shell

    $ mongo script1.js script2.js
    MongoDB shell version v3.6.1
    connecting to: mongodb://127.0.0.1:27017
    MongoDB server version: 3.6.1
    I am script1
    I am script2
    $
    
  • 如果希望在指定的主机/端口上的 mongod 运行脚本,需要指定地址,然后再跟上脚本文件名

    $ mongo --quiet some-host:3000/mydb script1.js script2.js
    

    --quiet可以让 shell 不打印 “MongoDB shell version...” 提示

  • 在交互模式下,如果想执行某个脚本文件时,可以使用 load() 函数:

    mongos> load("script1.js")
    I am script1
    mongos> 
    
  • 在脚本中可以访问 db 变量,以及其他全局变量。然而,shell 辅助函数(比如 "use db" 和 "show collections")不可以在文件中使用。这些辅助函数都有对应的 JavaScript 函数,如下表:

    辅助函数 等价函数
    use foo db.getSisterDB("foo")
    show dbs db.getMongo().getDBs()
    show collections db.getCollectionNames()
  • 如果在 shell 中想执行命令行程序,那么可以通过 run() 来实现。比如想查看某个文件夹下的文件列表:

    mongos> run("ls", "-l", "/home/myUser/my-scripts/")
    
  • .mongorc.js文件
    如果某些脚本会被频繁加载,可以将它们添加到 .mongorc.js 文件中,这个文件会在启动 shell 时自动允许。
    如果想禁止加载此文件,可以在 shell 启动时指定参数 --norc 参数。

  • 定制 shell 提示
    将 prompt 变量设为一个字符串或者函数,就可以重写默认的 shell 提示。比如提示当前时间:

    prompt = function() {
        return (new Date()) + "> ";
    };
    

    shell 启动后:

    Fri Jan 05 2018 00:20:55 GMT-500 (EST)> 
    

5. 查询

  • 查询集合中的所有文档,返回所有键/值对

    > // 若不指定查询文档参数,结果就返回集合 coll 中的所有文档
    > db.coll.find()
    
  • 简单条件查询
    find() 的第一个参数决定了要返回哪些文档,这个参数是一个文档,用于指定查询条件。可以向查询文档加入多个键/值对,将多个查询条件组合在一起,这样的查询条件会被解释成 “条件1 AND 条件2 AND ... 条件N”。

    > // 查询名字为 dereck 且年龄为 27 的用户
    > db.users.find({"name" : "dereck", "age" : 27})
    
  • 指定需要返回的键
    find() 的第二个参数用来指定想要的键。只返回需要的键的好处是,既会节省传输的数据,又能节省客户端解码文档的时间和内存消耗。

    > db.users.find({}, {"name" : 1, "age" : 1})
    

    1 表示需要返回该键,0 表示不返回

  • 比较操作符
    "$lt"、"$lte"、"$gt"、"$gte"、"$ne" 分别对应 <、<=、>、>=、!=

    > // 查询 18~30 岁(含)的用户
    > db.users.find({ "age" : { "$gte" : 18, "$lte" : 30 } })
    
  • OR 查询
    有两种方式进行 OR 查询:"in" 用来对**单个**键进行 OR 查询;"or" 除了可以单个键,还可以用来对多个键进行 OR 查询。

    > // "$in" 非常灵活,可以指定不同类型的条件和值
    > db.users.find({ "user_id" : { "$in" : [ 12345, "dereck" ] })
    
    > // "$or" 接受一个包含所有可能条件的数组作为参数
    > db.users.find({ "$or" : [ {"country" : "China"}, {"city" : "Shanghai"} ] })
    

    当某个查询既可以使用 "in" 也是使用 "or" 完成时,优先使用 "in" 查询,因为 "in" 只执行单次查询, 而 "or" 需要执行多次查询(比如,{"in":[ 12345, "dereck" ]} 需要两次)后将结果合并再返回,效率较低。如果不得不使用 "or" ,MongoDB 需要检查每次查询的结果集并且从中移除重复的文档(有些文档可能会被多个 "or" 字句匹配到)。

  • not "not" 是元条件,可以用在任何其他条件之上

  • null 类型的查询
    null 不仅会匹配某个键的值是 null,而且还会匹配不包含这个键的文档。
    假设集合 c 中包含如下三条文档:

    > db.c.find({}, {"_id" : 0})
    { "y" : null }
    { "y" : 1 }
    { "y" : 2 }
    

    查询 "y" 键为 null 的文档,可以得到预期的一条文档:

    > db.c.find({"y" : null}, {"_id" : 0})
    { "y" : null }
    

    查询 "z" 键为 null 的文档,结果返回了整个集合,因为所有文档都没有 "z" 键:

    > db.c.find({"z" : null}, {"_id":0})
    { "y" : null }
    { "y" : 1 }
    { "y" : 2 }
    

    如果想要结果仅返回包含指定键且键值为 null 的文档的话,需要通过 "$exists" 条件判定键值已存在:

    > db.c.find({"z" : {"$in" : [null], "$exists" : true}},  {"_id":0})
    
  • 数组查询

    • 通过 "$all" 来实现对数组的多个元素进行匹配。假设集合 food 包含以下三条文档:
    > db.food.find({}, {"_id" : 0})
    { "fruit" : [ "apple", "banana", "peach" ] }
    { "fruit" : [ "apple", "kumquat", "orange" ] }
    { "fruit" : [ "cherry", "banana", "apple" ] }
    

    找到既有 "apple" 又有 "banana" 的文档:

    > // "apple"、 "banana" 的先后顺序无关紧要
    > db.food.find({"fruit" : {"$all" : [ "apple", "banana" ]}}, {"_id" : 0})
    { "fruit" : [ "apple", "banana", "peach" ] }
    { "fruit" : [ "cherry", "banana", "apple" ] }
    
    • 可以使用 key.index 语法指定下标,来查询数组特定位置的元素,下标从 0 开始:
    > db.food.find({"fruit.2" : "peach"}, {"_id" : 0})
    { "fruit" : [ "apple", "banana", "peach" ] }
    
    • 使用 "$size" 查询特定长度的数组
    > db.food.find({"fruit" : {"$size : 3"}}, {"_id" : 0})
    { "fruit" : [ "apple", "banana", "peach" ] }
    { "fruit" : [ "apple", "kumquat", "orange" ] }
    { "fruit" : [ "cherry", "banana", "apple" ] }
    
    • 在 find() 的第二个参数中,通过使用 "$slice" 操作符返回某个键匹配的数组元素的一个子集:

    通过 { "key" : n} 返回数组 "key" 的前 n 个元素:

    > // 每个文档返回前 2 个水果
    > db.food.find({}, {"_id" : 0, "fruit" : {"$slice" : 2}})
    { "fruit" : [ "apple", "banana" ] }
    { "fruit" : [ "apple", "kumquat" ] }
    { "fruit" : [ "cherry", "banana" ] }
    

    通过 { "key" : -n} 返回数组 "key" 的后 n 个元素:

    > // 每个文档返回后 2 个水果
    > db.food.find({}, {"_id" : 0, "fruit" : {"$slice" : -2}})
    { "fruit" : [ "banana", "peach" ] }
    { "fruit" : [ "kumquat", "orange" ] }
    { "fruit" : [ "banana", "apple" ] }
    

    通过 { "key" : [start, end] } 返回数组 "key" 的中间部分元素,如果数组元素不足,则返回 start 后的所有元素:

    > db.food.insert({"fruit" : [ "cherry", "banana", "apple", "orange" ]})
    WriteResult({ "nInserted" : 1 })
    > db.food.find({}, {"_id" : 0, "fruit" : { "$slice" : [2, 3] }}
    { "fruit" : [ "peach" ] }
    { "fruit" : [ "orange" ] }
    { "fruit" : [ "apple", "orange" ] }
    
  • 内嵌文档查询
    通过点表示法查询内嵌文档。比如查询如下文档:

    {
        "name" : {
            "first" : "Dereck",
            "last" : "Yu"
        },
        "age" : 29
    }
    

    通过点表示法针对特定的键进行查询:

    > db.people.find({ "name.first" :"Dereck", "name.last" : "Yu" }, {"_id" : 0})
    { "name" : { "first" : "Dereck", "last" : "Yu"}, "age" : 29 }
    
  • 内嵌文档的数组元素的查询
    如果内嵌文档是一个数组,那么上述“点表示法”查询可能就得不到正确的结果了。比如以下 blog 集合中查询由 "dereck" 发表的 5 分以上的评论:

    {
        "content" : "hello world",
        "comments" : [
            {
                "author" : "dereck",
                "score" : 3,
                "comment" : "terrible post"
            },
            {
                "author" : "sherry",
                "score" : 6,
                "comment" : "nice post"
            }
        ]
    }
    

    尝试使用“点表示法”查询:

    > // 下面这条查询语句返回了所有结果,因为 author 条件在第一条评论中符合了,而 score 条件在第二条评论中符合了,所以两条评论都被返回
    > db.blog.find({ "comments.author" : "dereck", "comments.score" : {"$gte" : 5} }, {"_id" : 0}).pretty()
    {
        "content" : "hello world",
        "comments" : [
            {
                "author" : "dereck",
                "score" : 3,
                "comment" : "terrible post"
            },
            {
                "author" : "sherry",
                "score" : 6,
                "comment" : "nice post"
            }
        ]
    }
    

    要匹配数组中的单个内嵌文档,需要使用 "$elemMatch" 将限定条件进行分组,对单个内嵌文档中的多个键进行匹配

    > db.blog.find({ "comments" : { "$elemMatch" : { "author" : "dereck", "score" : { "$gte" : 5 } } } }, {"_id" : 0})
    
  • where 查询 键/值对是一种表达能力非常好的查询方式,但有些需求不能通过这种方式表达,此时可以使用 "where" 语句。 "where" 可以在查询中执行任意的 JavaScript ,因此为了安全起见,应该严格限制或者消除 "where" 语句的使用,应该禁止终端用户使用任意的 "$where" 语句。
    假设有如下文档,需要返回两个键具有相同值的文档:

    > db.foo.insert({ "x" : 1, "y" : 2, "z" : 3 })
    > db.foo.insert({ "x" : 2, "y" : 2, "z" : 4 })
    >
    > // 第二个文档中,"x" 和 "y" 的值相同,所以应该返回比文档
    > db.foo.find({ "$where" : function() {
    ... for (var key1 in this) {
    ...     for (var key2 in this) {
    ...         if ( key1 != key2 && this[key1] == this[key2] )
    ...             return true;
    ...     }
    ... }
    ... return false;
    ... } }):
    { "_id" : ObjectId("5a54647675b6e8450f1d5fd2"), "x" : 2, "y" : 2, "z" : 4 }
    

    不是非常必要时,一定要避免使用 "where" 查询,它不能使用索引,每个文档都要从 BSON 转换成 JavaScript 对象,然后通过 "where" 表达式来运行,在速度上要比常规查询慢很多。

  • limit
    要限制结果数量,可在 find() 后使用 limit() 函数,limit 指定的是上限,而非下限。

    > // 只返回 3 个结果,如果匹配的结果不足 3 个,则返回所有
    > db.c.find().limit(3)
    
  • skip
    略过前面 N 个匹配的文档,返回余下的文档,如果集合里面能匹配的文档不足需要略掉的数量,则不会返回任何文档。

    > db.c.find().skip(3)
    

    避免使用 skip 略过大量结果,数量非常多的话会变得非常慢,因为它要先找到需要被略过的数据,然后再抛弃这些数据

  • sort
    接受一个对象作为参数,这个对象是一组键值对,键对应文档的键名,值代表排序的方向。排序方向可以是 1 (升序)或者 -1 (降序)。如果指定了多个键,则按照这些键被指定的顺序逐个排序。

    > db.c.find().sort({ "name" : 1, "age" : -1 })
    

6. 创建、更新、删除文档

  • 插入单个文档

    > db.foo.insert({"x" : 1})
    WriteResult({ "nInserted" : 1 })
    

    从 3.2 版本开始,可以使用 "insertOne()" 方法:

    > db.foo.insertOne({"y" : 1})
    {
        "acknowledged" : true,
        "insertedId" : ObjectId("5a57002cbfe1dc293187ec6d")
    }
    
  • 批量插入多个文档
    如果要向集合中插入多个文档,批量插入会快一些,一次发送数十、数百乃至数千个文档会明显提高插入的速度

    > db.foo.batchInsert([{"x" : 1}, {"y" : 2},  {"z" : 3}])
    

    如果在执行批量插入的过程中有一个文档插入失败,那么在这个文档之前的所有文档都会成功插入到集合中,而这个文档以及之后的所有文档全部插入失败。如果希望 batchInsert 忽略错误并且继续执行后续插入,可以使用 continueOnError 选项。 shell 并不支持这个选项,但所有的驱动程序都支持

    从 3.2 版本开始,可以使用 "insertMany()" 方法:

    > db.foo.insertMany([
    ...     { "a" : 10 },
    ...     { "a" : 11 },
    ...     { "a" : 12 }
    ... ])
    {
        "acknowledged" : true,
        "insertedId" : [
            ObjectId("5a570a6dbef1dc293187ec6e"),
            ObjectId("5a570a6dbef1dc293187ec6f"),
            ObjectId("5a570a6dbef1dc293187ec70")
        ]
    }
    
  • 删除文档
    删除数据是永久性的,不能撤销,也不能恢复
    删除集合中的所有文档:

    > db.foo.remove({})
    

    删除符合条件的文档:

    > db.employee.remove({"department" : "A"})
    

    remove() 函数会删除文档数据,但是不会删除集合本身,也不会删除集合的元信息。
    从3.2版本开始,可以使用"deleteOne()""deleteMany()" 方法:

    > db.foo.deleteOne({"y":1})
    { "acknowledged" : true, "deletedCount" : 1 }
    > 
    > db.foo.deleteMany( { "a" : { "$gte" : 11 } } )
    { "acknowledged" : true, "deletedCount" : 2 }
    
  • 清空集合
    删除文档通常很快,但如果要清空整个集合,使用 drop() 更快,该函数不能指定任何限定条件,整个集合都会被删除,所有元数据也都不见了

    > db.foo.drop()
    
  • 更新文档
    更新操作是不可分割的:若是两个更新同时发生,先到达服务器的先执行,接着执行另外一个。所以,两个需要同时进行的更新会迅速接连完成,比过程不会破坏文档:最新的更新会取得“胜利”。

    > var dereck = db.people.findOne()
    > dereck
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 29
    }
    > dereck.age++
    29
    > db.people.update({"_id" : dereck_id}, dereck)
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30
    }
    

    从3.2版本开始,可以使用"updateOne()""updateMany()"
    "replaceOne()" 方法。

  • 使用修改器
    通常文档只会有一部分要更新,可以使用原子性的更新修改器,指定对文档中的某些字段进行更新。更新修改器是种特殊的键用来指定复杂的更新操作,比如修改、增加或者删除键,还可能是操作数组或者内嵌文档。

    • $inc 修改器用来增加(或减少)整型、长整型或双精度浮点型的值,如果指定的键不存在,则创建此键
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30
    }
    > // 执行完以下语句会新建 "score" 键
    > db.people.update({"name" : "dereck"}, {"$inc" : {"score" : 100}})
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 100
    }
    > 
    > // 更改 "score" 键的值
    > db.people.update({"name" : "dereck"}, {"$inc" : {"score" : -10}})
    WriteResult({"nMatched":1,"nUpserted":0,"nModified":1})
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 90
    }
    
    • $set 修改器用来指定一个字段的值,如果这个字段不存在,则创建它。用法与 $inc 类似,不过可以修改任意类型
    > db.people.update({"name" : "dereck"}, { "$set" : { "email" : "dereck@example.com"  }})
    WriteResult({"nMatched":1,"nUpserted":0,"nModified":1})
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 90,
        "email" : "dereck@example.com"
    }
    
    • 如果需要完全删除某个键,可以使用 $unset 修改器
    • $push 修改器用于向数组末尾添加一个元素,如果数组不存在,就创建一个新的数组
    > // 以下语句试图向 "addresses" 键添加一个地址,此时这个键不存在,执行完后创建该键
    > db.people.update({"name" : "dereck"}, { "$push" : { "addresses" : "Road A"  }})
    WriteResult({"nMatched":1,"nUpserted":0,"nModified":1})
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 90,
        "email" : "dereck@example.com",
        "addresses" : [
            "Road A"
        ]
    }
    
    • 使用$each 子操作符,可以通过一次 $push 操作添加多个值
    > db.people.update({"name" : "dereck"}, {"$push" : { "addresses" : { "$each" : ["Road B", "Road C" ]}}})
    
    • $slice$push 组合在一起使用,可以固定数组的最大长度,$slice 的值必须是负数。比如,下面的代码确保 addresses 的值最多有5个,如果数组的元素没有超过5个,那么所有元素都保留;如果多于5个的话,那么只有最后5个元素得以保留。
    > db.people.update({"name" : "dereck"}, {"$push" : { "addresses" : { "$each" : ["Road D", "Road E",  "Road F"], "$slice" : -5}}})
    WriteResult({"nMatched":1,"nUpserted":0,"nModified":1})
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 90,
        "email" : "dereck@example.com",
        "addresses" : [
            "Road B",
            "Road C",
            "Road D",
            "Road E",
            "Road F"
        ]
    }
    
    • 使用 $addToSet 可以避免向数组中插入重复值
    > db.people.update({"name" : "dereck"},{"$addToSet" : {"addresses" : { "$each" :["Road F", "Road G"]}}})
    
    • 删除数组元素
      若是把数组看成队列或者栈,可以用 $pop 修改器,它可以从数组的任何一端删除元素。 { "$pop" : { "key" : 1 }} 从数组末尾删除一个元素;{ "$pop" : { "key" : -1 }} 则从头部删除。
      当需要通过条件匹配删除元素时,可以使用 $pull 修改器
    > // 删除 "Road A"
    > db.people.update({"name" : "dereck"},{"$pull" : {"addresses" : "Road A" }})
    
  • upsert
    upsert (upsert = update + insert)是一种特殊的更新,不是一个操作符。如果没有找到符合更新条件的文档,就会以这个条件和更新文档为基础创建一个新的文档;如果找到了匹配的文档,则正常更新。
    update() 函数的第 3 个 bool 类型的参数就表示比更新是否为 upsert。

    > db.people.update({"name" : "sherry"}, {"$set" : {"age" : 20}}, true)
    

    有时需要在创建文档的同时创建字段并为它赋值,但是在之后所有的更新操作中,这个字段都不再改变,此时可以使用 $setOnInsert。比如集合 people 当前有两条文档信息,现在插入一条新的文档:

    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 90,
        "email" : "dereck@example.com",
        "addresses" : [
            "Road B",
            "Road C",
            "Road D",
            "Road E",
            "Road F",
            "Road G"
        ]
    }
    {
        "_id" : ObjectId("5a4f4af4f60a0bbebb3e3eb2"),
        "name" : "sherry",
        "age" : 28
    }
    > // 以下语句将插入一条文档,并且设置字段 birthday 的值
    > db.people.update({ "name" : "duoduo"}, { "$setOnInsert" : { "birthday" : "2015-05-03"}}, true)
    > 
    > db.people.find().pretty()
    {
        "_id" : ObjectId("5a4f20eff85ff534e67a4126"),
        "name" : "dereck",
        "age" : 30,
        "score" : 90,
        "email" : "dereck@example.com",
        "addresses" : [
            "RoadB",
            "Road C",
            "Road D",
            "Road E",
            "Road F",
            "RoadG"
        ]
    }
    {
        "_id" : ObjectId("5a4f4af4f60a0bbebb3e3eb2"),
        "name" : "sherry",
        "age" : 28
    }
    {
        "_id" : ObjectId("5a52d117f60a0bbebb826c34"),
        "name" : "duoduo",
        "birthday" : "2015-05-03"
    }
    

    如果 update 方法执行的是 update 操作而不是 insert 操作,那么 $setOnInsert 操作符将无效,即在执行上述语句之前,如果集合中已经存在一条 name 为 duoduo 的文档的话,上述 update 语句将不会有任何更新操作

  • 更新多个文档
    默认情况下,更新只能对符合匹配条件的第一个文档执行操作,即使有多个文档符合条件,只有第一个文档会被更新,其他文档不会发生变化。要更新所有匹配的文档,需要将 update 的第 4 个参数设置为 true 。

    db.collectionName.update(query, obj, upsert, multi)
    

    update 的 4 个参数分别表示:

    • query - 需要更新的匹配条件
    • obj - 更新后的新的对象
    • upsert - 布尔类型,如果找不到匹配对象,是否新插入一条文档,默认是 false
    • multi - 布尔类型,是否更新所有符合匹配条件的对象,默认是 false

7. 索引

不使用索引的查询称为全表扫描,对于大集合来说,全表扫描的效率非常低。使用了索引的查询几乎可以瞬间完成。然而,使用索引是有代价的:对于添加的每一个索引,每次写操作(插入、更新、删除)都将耗费更多的时间。

  • 创建索引
    假设集合 users 有 1000000 条文档,文档结构如下:

    > db.users.findOne()
    {
        "_id" : ObjectId("5a5479c075b6e8450f1ee673"),
        "i" : 1,
        "name" : "user1",
        "age" : 22,
        "created" : ISODate("2018-01-09T08:13:52.193Z")
    }
    

    在字段 name 上创建一个索引:

    > db.users.ensureIndex( { "name" : 1 } )
    

    索引的值是按一定顺序排列的,因此,在使用索引键对文档进行排序时非常快。然而,只有在首先使用索引键进行排序时,索引才有用。例如,在下面的排序里, "name" 上的索引没什么作用:

    > db.users.find().sort({ "age" : 1, "name" : 1})
    

    这里先根据 "age" 排序再根据 "name" 排序,所以"name"上的索引没起作用。

  • 复合索引
    复合索引是一个建立在多个字段上的索引。如果查询中有多个排序方向或者查询条件中有多个键,这个索引会非常有用。比如,根据上述排序创建复合索引:

    > db.users.ensureIndex( { "age" : 1, "name" : 1 } )
    
  • 使用覆盖索引
    当一个索引包含了用户请求的所有字段,就称这个索引覆盖了本次查询。应该优先使用覆盖索引,为了确保查询只使用索引就可以完成,应该使用投射({"_id" : 0})来指定不要返回 "_id" 字段(除非它了索引的一部分)。

  • 隐式索引
    如果有一个有 N 个键的索引,那么同时可以“免费”得到所有这 N 个键的前缀组成的索引。比如,有一个如下索引:

    { "a" : 1, "b" : 1, "c" : 1,  ..., "z" : 1 }
    

    那么可以使用如下一系列索引:

    { "a" : 1 }
    { "a" : 1, "b" : 1 }
    { "a" : 1, "b" : 1, "c" : 1}
    ...
    

    这些键的任意子集所组成的索引并不一定可用。比如,使用 {"b" : 1} 或者 {"a" : 1, "c" : 1} 作为索引的查询是不会被优化的。

  • 索引嵌套文档
    比如对如下文档中的 "city" 字段创建索引:

    {
        "name" : "dereck",
        "address" : [
            "zone" : "pudong",
            "city" : "shanghai",
            "country" : "China"
        ]
    }
    
    > db.users.ensureIndex( { "address.city" : 1 } )
    
  • 唯一索引
    唯一索引可以确保集合里的每一个文档的指定键都有唯一值。在创建索引的同时,指定 {"unique" : true}。比如,下面语句确保了集合 users 中的 "username" 是唯一的:

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

    如果一个文档没有对应的键,索引会将其作为 null 存储。所以,如果对某个键建立了唯一索引,插入多个缺少改索引键的文档时会失败。
    索引的大小是有限制的(8 KB),超出限制的条目不会被包含在索引里,也不会受到索引的约束。也就是说,使用索引查询的时候,返回的结果可能会漏掉一些文档;同样的,可以插入多个对应键超过 8 KB 长的文档,因为此时索引限制失效。
    在已有的集合上创建唯一索引时可能会失败,因为集合中已经存在重复值了。此时可以在创建索引时同时指定 "dropDups" 选项来去除重复。这个一个粗暴的方式,当遇到重复的值时,它会保留第一个,之后的重复文档都会被删除,无法认为控制。所以,慎用

    > db.users.ensureIndex( { "username" : 1, "unique" : true,  "dropDups" : true } )
    
  • 稀疏索引
    唯一索引会把 null 看做值,所以无法将多个缺少唯一索引中的键的文档插入到集合中,如果希望唯一索引只对包含相应键的文档生效,这时可以将 unique 和 sparse 选项组合在一起来创建稀疏索引。

    > db.users.ensureIndex( {"username":1,"unique":true, "sparse" : true })
    
  • TTL 索引
    “time-to-live index”,具有生命周期的索引。这种索引为每一个文档设置一个超时时间,当文档到达预设值的老化程度之后就会被删除。
    在 ensureIndex 中指定 expireAfterSeconds 选项就可以创建一个 TTL 索引:

    > // 超时时间为 1 小时
    > db.users.ensureIndex({"lastUpdated" : 1}, {"expireAfterSeconds" : 60*60})
    

    MongoDB 每分钟对 TTL 索引进行一次清理。可以使用 "collMod" 命令修改 "expireAfterSeconds" 的值:

    > db.runCommand({"collMod" : "users",  "index" : {"name" : "lastUpdated_1", "expireAfterSeconds" : 60*60*2}})
    

    一个给定的集合上可以有多个 TTL 索引,但 TTL 索引不能是复合索引。

  • 索引管理
    查看给定集合上的所有索引信息:

    > db.users.getIndexes()
    

    标识索引,创建索引的同时指定索引名称:

    > db.users.ensureIndex({ "a" : 1, "b" : 1, "c" : 1}, {"name" : "somename"})
    

    删除索引:

    > db.users.dropIndex( "x_1_y_1" )
    
  • 何时不应该用索引

    提取较小的子数据集时,索引非常高效。结果集在原集合中所占的比例越大,索引的速度就越慢,因为使用索引需要进行两次查找:一次是查找索引条目,一次是根据索引指针去查找相应的文档。而全表扫描只需要进行一次查找:查找文档。

    索引通常适用的情况 全表扫描通常适用的情况
    集合较大 集合较小
    文档较大 文档较小
    选择性查询 非选择性查询

    可以使用 { "$natural" : 1 } 强制 MongoDB 做全表扫描,返回的结果是按照磁盘上的顺序排列的:

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