学习《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
用于分片设置时,分片信息会存储在此数据库中
- admin
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 查询:”or” 除了可以单个键,还可以用来对多个键进行 OR 查询。> // "$in" 非常灵活,可以指定不同类型的条件和值 > db.users.find({ "user_id" : { "$in" : [ 12345, "dereck" ] })
> // "$or" 接受一个包含所有可能条件的数组作为参数 > db.users.find({ "$or" : [ {"country" : "China"}, {"city" : "Shanghai"} ] })
当某个查询既可以使用 “or” 完成时,优先使用 “in” 只执行单次查询, 而 “in”:[ 12345, “dereck” ]} 需要两次)后将结果合并再返回,效率较低。如果不得不使用 “or” 字句匹配到)。
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” 语句。
假设有如下文档,需要返回两个键具有相同值的文档:> 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” 表达式来运行,在速度上要比常规查询慢很多。
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 })