《MongoDB 权威指南(第2版)》阅读笔记


第 1 章 MongoDB 简介

MongoDB 是一款强大、灵活,且易于扩展的通用型数据库。它能扩展出非常多的功能,如二级索引(Secondary index)、范围查询(range query)、排序、聚合(aggregation),以及地理空间索引(geospatial inde)。

1.1 易于使用

MongoDB 是一个面向文档(document-oriented)的数据库,而不是关系型数据库。

与关系型数据库相比,面向文档的数据库不再有“行”(row)的概念,取而代之的是更为灵活的“文档”(document)模型。通过在文档中嵌入文档和数组,面向文档的方法能够仅使用一条记录来表现复杂的层次关系。

另外,不再有预定义模式(predefined schema):文档的键(key)和值(value)不再是固定的类型和大小。根据需要添加或删除字段变得更容易了。通常,由于开发者能够进行快速迭代,所以开发进程得以加快。而且,试验更容易进行。开发者能尝试大量的数据模型,从中选择一个最好的。

1.2 易于扩展

MongoDB 的设计采用横向扩展。面向文档的数据模型使它能很容易的在多台服务器之间进行数据分割。MongoDB 能自动处理跨集群的数据和负载,自动重新分配文档,以及将用户请求路由到正确的机器上。开发者只需要集中精力编写应用程序,而不需要考虑如何扩展的问题。如果一个集群需要更大的容量,只需要向集群添加新服务器,MongoDB 就会自动将现有数据向新服务传送。

1.3 丰富的功能

  • 索引
    MongoDB 支持通用二级索引,允许多种快速查询,且提供唯一索引、复合索引、地理空间索引,以及全文索引。
  • 聚合
    MongoDB 支持“聚合管道”(aggregation pipeline)。用户能通过简单地片段创建复杂的聚合,并通过数据库自动优化。
  • 特殊的集合类型
    MongoDB 支持存在时间有限的集合,适用于那些将在某个时刻过期的数据,如会话(session)。类似地,MongoDB 也支持固定大小的集合,用于保存近期数据,如日志。
  • 文件存储(file storage)
    MongoDB 支持一种非常易用的协议,用于存储大文件和文件元数据。

第 2 章 MongoDB 基础知识

  • 文档是 MongoDB 中数据库基本单元,非常类似于关系数据库管理系统中的行。
  • 集合(collection)可以看作是一个拥有动态模式(dynamic schema)的表。
  • MongoDB 的一个实例可以拥有多个相互独立的数据库(database),每一个数据库都有自己的集合。
  • 每一个文档都有一个特殊的键“_id“,这个键在文档所属的集合中是惟一的。
  • MongoDB 自带了一个简单但功能强大的 JavaScript shell,可用于管理 MongoDB 的实例或数据操作。

2.1 文档

文档是 MongoDB 的核心概念。文档就是键值对的一个有序集。文档中的值可以是多种不同的数据类型(甚至可以是一个完整的内嵌文档)。

文档的键是字符串,除了少数例外情况,键可以使用任意 UTF-8 字符。

  • 键不能含有 \0 (空字符)。这个字符用于表示键的结尾。
  • . 和 $ 具有特殊意义,只能在特定环境下使用。通常这两个字符是被保留的。

MongoDB 不但区分类型,还区分大小写。

MongoDB 的文档不能有重复的键。

2.2 集合

集合就是一组文档。

2.2.1 动态模式

集合是动态模式的。在一个集合里面的文档可以是各式各样的。例如,下面两个文档可以存储在同一个集合里面:

1
2
{"greeting":"hello,world!"}
{"foo":5}

既然没有必要区分不同类型文档的模式,为什么还要使用多个集合呢?这里有几个重要的原因:

  • 如果把各种各样的文档不加区分地放在同一个集合里,无论对开发者还是管理员来说都是噩梦。开发者要么确保每次查询只返回固定类型的文档,要么让执行查询的应用程序来处理所有不同类型的文档。
  • 在一个集合里查询特定类型的文档在速度上也不划算,分开查询多个集合要快得多。
  • 把同种类型的文档放在一个集合里,数据会更加集中。
  • 创建索引时,需要使用文档的附加结构(特别是创建唯一索引时)。索引时按照集合来定义的。在一个集合中只放入一种类型的文档,可以更有效地对集合进行索引。

2.2.2 命名

集合名可以是满足下列条件的任意 UTF-8 字符串。

  • 集合名不能是空字符串(“”)。
  • 集合名不能包含 \0 字符(空字符),这个字符表示集合名的结束。
  • 集合名不能以 ”system.“ 开头,这是为系统集合保留的前缀。如,system.user 这个集合保存着数据库的用户信息。
  • 用户创建的集合不能在集合名中包含保留字符 ‘$’,。因为某些系统生成的集合中包含 $ ,很多驱动程序确实支持在集合名里包含该字符。除非你要访问这种系统创建的集合,否则不应该在集合名中包含 $ 。

** 子集合 **
组织集合的一种惯例是使用 “.” 分隔不同命名空间的子集合。例如,一个具有博客功能的应用可能包含两个集合,分别是 blog.posts 和 blog.authors。这是为了使组织结构更清晰。

2.3 数据库

在 MongoDB 中,多个文档组成集合,而多个集合组成可以组成数据库。一个 MongoDB 实例可以承载多个数据库,每个数据库拥有 0 个或者多个集合。每个数据库都有独立的权限,在磁盘上,不同的数据库也放置在不同的文件中。一般将一个应用程序的所有数据都存储在同一个数据库中。要存储多个应用程序,就需要使用不同的数据库。

数据库可以是满足以下条件的任意 UTF-8 字符串。

  • 不能是空字符串 (“”).
  • 不能含有 /、\、.、”、*、<、>、:、|、?、$、\0. 基本上,只能使用 ASCII 中的字母和数字。
  • 数据库名区分大小写。简单起见,数据库名应全部小写。
  • 数据库名最多为 64 字节。

保留的数据库名:

  • admin
  • local
  • config

把数据库名添加到集合名前,得到集合的完全限定名,即命名空间(namdspace)。命名空间的长度不得超过 121 字节,且在实际使用中应小于 100 字节。

2.4 启动 MongoDB

MongoDB 在没有参数的情况下会使用默认目录 /data/db . 如果参数目录不存在或不可写,服务器会启动失败。因此,在启动前创建数据目录非常重要。

MongoDB 默认监听 27017 端口。如果端口被占用,则启动失败。

2.5 MongoDB shell 简介

2.5.1 运行 shell

MongoDB shell是一个功能完备的 JavaScript 解释器,可运行任意 JavaScript 程序。如进行简单的数学运算、利用 JavaScript 的标准库还可以定义和调用 JavaScript 函数。

2.5.2 MongoDB 客户端

查看 db 当前指向哪个数据库

1
db

选择数据库

1
use test

2.5.3 shell 中的基本操作

创建、读取、更新和删除

1.创建

1
2
3
post = {"title":"My Blog Post",
"content":"Here's my blog post.",
"date":new Date()}

使用 insert 方法将其保存到 blog 集合中:

1
db.blog.insert(post)

查看

1
db.blog.find()

2.读取
find 和 findOne 方法可以用于查询集合里的文档。若只想查看一个文档,可用 findOne:

1
db.blog.findOne()

3.更新
update 接受(至少)两个参数:第一个是限定条件(用于匹配待更新的文档),第二个是新的文档。假设我们要为先前写的文章增加评论功能,就需要增加一个新的键,用于保存评论数组。

首先,修改变量 post,增加 “comments” 键:

1
post.comments = []

然后执行 update 操作:

1
db.blog.update({title:"My Blog Post"},post)

4.删除
使用 remove 方法可将文档从数据库中永久删除。它可以接受一个作为限定条件的文档作为参数。

1
db.blog.remove({title:"My Blog Post"})

2.6 数据类型

  • null
    null 用于表示空值或者不存在的字段:

    1
    {"x":null}
  • 布尔型
    有两个值 true 或 false:

    1
    {"x":"false"}
  • 数值
    shell 默认使用 64 位浮点型数值。

    1
    {"x":3.14}

    或:

    1
    {"x":3}

对于整型值,可使用 NuberInt 类(4字节带符号整数)或 NumberLong 类(8字符带符号整数):

1
2
{"x":NumberInt("3")}
{"x":NumberLong("3")}
  • 字符串
    UTF-8 字符串都可表示为字符串类型的数据:

    1
    {"x":"foobar"}
  • 日期
    日期被存储为自新纪元以来经过的毫秒数,不存储时区:

    1
    {"x":new Date()}
  • 正则表达式
    查询时,使用正则表达式作为限定条件,语法也与 JavaScript 的正则表达式语法相同:

    1
    {"x":/foobar/i}
  • 数组
    数据列表或数据集可以表示为数组;

    1
    {"x":["a","b","c"]}
  • 内嵌文档
    文档可嵌套其他文档,被嵌套的文档作为父文档的值:

    1
    {"x":{"foo":"bar"}}
  • 对象 id
    对象 id 是一个 12 字节的 ID,是文档的唯一标识

    1
    {"x":ObjectId()}
  • 二进制数据
    二进制数据时一个任意字节的字符串。它不能直接在 shell 中使用。如果要将非 UTF-8 字符保存到数据库中,二进制数据是唯一的方式。

  • 代码
    查询和文档中可以包括任意 JavaScript 代码:

    1
    {"x":function(){/*...*/}}

2.6.2 日期

在 JavaScript 中, Date 类可以用作 MongoDB 的日期类型。创建日期对象时,应使用 new Date(…).

2.6.5 _id 和 ObjectId

MongoDB 中存储的文档必须包含一个 “_id” 键。这个键可以是任何类型的,默认是 ObjectId 对象。在一个集合里面,每个文档都有唯一的 “_id”, 确保集合里面每个文档都能被唯一标识。

1.ObjectId
ObjectId 是 “_id” 的默认类型。它被设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。

ObjectId 使用 12 字节的存储空间,是一个由 24 个十六进制数字组成的字符串(每个字节可以存储两个十六进制数组)。

ObjectId 的前 4 个字节是从标准纪元开始的时间戳,单位为秒。接下来的 3 字节是所在主机的唯一标示符。通常是机器主机名的散列值(hash)。这样就可以确保不同主机生成不同的 ObjectId 而不产生冲突。接下来的两字节来自产生 ObjectId 的进程的进程标识符(PID),这样可以确保在同一台机器上并发的多个进程产生的 ObjectId 是惟一的。

前 9 字节保证了同一秒钟不同机器不同进程产生的 ObjectId 是惟一的。最后 3 字节是一个自动增加的计数器,确保相同进程同一秒产生的 ObjectId 也是不一样的。一秒钟最多允许每个进程拥有 2563(16 777 216) 个不同的 ObjectId。

2.7 使用 MongoDB Shell

连接 MongoDB 实例:

1
mongo some-host:27017/myDB

启动 mongo shell 时不连接到任何 mongod :

1
mongo --nodb

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

1
2
conn = new Mongo("localhost:27017")
db = conn.getDB("test")

2.7.1 shell 小贴士

由于 mongo 是一个简化的 JavaScript shell,可以通过查看 JavaScript 的在线文档得到大量帮助。可以使用 help 命令查看 MongoDB 特有的功能。

1
2
3
help
db.help() //查看数据库级别的帮助
db.foo.help() //查看集合级别的帮助

如果想知道一个函数是做什么用的,可以直接在 shell 输入函数名(函数名后不要加小括号),这样就可以看到相应函数的 JavaScript 实现代码。

1
db.foo.update

2.7.2 使用 shell 执行脚本

1
mongo script1.js script2.js script3.js

mongo shell 会依次执行传入的脚本,然后退出。

1
mongo --quiet server-1:27017/foo script1.js script2.js script3.js

这样可以将 db 指向 server-1:27017 上的 foo 数据库,然后执行这三个脚本。运行 shell 时指定的命令行选项要出现在地址之前。

可以使用 load() 函数,从交互式 shell 中运行脚本:

1
load("script1.js")

可以使用脚本将变量注入到 shell 中。例如,在操作系统中创建如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
var connectTo = function(port,dbname){
if(!port){
port = 27017;
}

if(!dbname){
dbname = "test"
}

db = connect("localhost:" + port + "/" + dbname);
return db
};

在 mongo shell 中加载这个脚本, connectTo 函数就可以使用了:

1
2
load('defineConnectTo.js')
typeof connectTo

默认情况下,shell 会在运行 shell 时所处的目录中查找脚本(可以使用 run(“pwd””) 命令查看)。也可以在 load 函数中指定一个相对路径或者绝对路径。也可以使用 run() 函数来执行命令行程序。

1
run("ls","-l","/")

2.7.3 创建 .mongorc.js 文件

如果某些脚本被频繁加载,可以将它们添加到 .mongorc.js 文件中,这个文件在启动 shell 时自动运行。将该文件放在当前用户的主目录下,如 /root 目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 定制欢迎内容
var compliment = ["attractive","intelligent","like Batman"];
var index = Math.floor(Math.random()*3);

print("Hello, you're looking particularly " + compliment[index] + " today!");

var no = function(){
print("Not on my watch.");
};
// 禁止删除数据库
db.dropDatabase = DB.prototype.dropDatabase = no;
// 禁止删除集合
DBCollection.prototype.drop = no;
// 禁止删除索引
DBCollection.prototype.dropIndex = no;
// 定制 shell 提示
prompt = function(){
if(typeof db == 'undefined'){
return '(nodb)>';
}

try{
db.runCommand({getLastError:1});
}
catch(e){
print(e);
}

return db+">";
};

2.7.5 编辑复合变量

对于大块代码或者对象,可以调用操作系统编辑器。在 shell 中设置 EDITOR 变量,也可以在 .mongorc.js 文件中添加一行内容: EDITOR=”编辑器路径”,以后就不用单独设置了。

1
2
3
EDITOR="/usr/bin/vim"
var wap = db.col.findOne({x:1})
edit wap

第 3 章 创建、更新和删除文档

3.1 插入并保存文档

插入一个文档使用 insertOne 方法:

1
2
3
db.inventory.insertOne(
{ item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } }
)

3.1.1 批量插入

批量插入速度会更快一些:

1
2
3
4
5
6
db.inventory.insertMany([
{ item: "journal", qty: 25, tags: ["blank", "red"], size: { h: 14, w: 21, uom: "cm" } },
{ item: "mat", qty: 85, tags: ["gray"], size: { h: 27.9, w: 35.5, uom: "cm" } },
{ item: "mousepad", qty: 25, tags: ["gel", "blue"], size: { h: 19, w: 22.85, uom: "cm" } }
])
db.inventory.find()

如果集合当前不存在,则插入操作将创建集合。

3.2 删除文档

删除集合中的所有元素,但不会删除集合本身。

1
db.foo.remove({})

remove 函数接受一个查询文档作为可选参数。只有符合条件的文档才会被删除。

1
db.mailing.list.remove({"opt-out":true})

删除数据是永久性的,不能撤销,也不能恢复。

删除集合中的所有元素,同时删除集合本身。

1
db.foo.drop()

3.3 更新文档

3.3.1 文档替换

1
2
3
4
5
6
{
"_id":ObjectId("5cb6e441040c0c1ce40033a7"),
"name":"joe",
"friends":32,
"enimes":2
}

假如要将以上文档中的 “friends” 和 “enemies” 两个字段移到 “relationships” 子文档中,可以这么做:

1
2
3
4
5
6
7
var joe=db.users.findOne({"name":"joe"})
joe.relationships={"friendd":joe.friends,"enemies":joe.enemies}
joe.username=joe.name
delete joe.friends
delete joe.enemies
delete joe.name
db.users.update({"name":"joe"},joe)

3.3.2 使用修改器

1.”$set” 修改器入门
“$set” 用来指定一个字段的值,如果该字段不存在,则创建它。

例如,用户资料存储在下面的文档里:

1
2
3
4
5
6
7
{
"_id": ObjectId("5cb6ee4a040c0c1ce40033a9"),
"name": "joe",
"age": 30,
"sex": "male",
"location": "Wisconsin"
}

要想添加喜欢的书籍进去,可以使用 “$set”:

1
db.users.update({"_id":ObjectId("5cb6ee4a040c0c1ce40033a9")},{"$set":{"favorite book":"War and Peace"}})

也可以将喜欢的书籍更改为其他值:

1
db.users.update({"_id":ObjectId("5cb6ee4a040c0c1ce40033a9")},{"$set":{"favorite book":"Green Eggs and Ham"}})

使用 “$set” 甚至可以修改键的类型:

1
db.users.update({"_id":ObjectId("5cb6ee4a040c0c1ce40033a9")},{"$set":{"favorite book":["Green Eggs and Ham","Cat's Cradle","Foundation Trilogy"]}})

可以使用 “$unset” 将这个键完全删除:

1
db.users.update({"_id":ObjectId("5cb6ee4a040c0c1ce40033a9")},{"$unset":{"favorite book":1}})

也可以使用 “$set” 修改内嵌文档。

2.增加或减少
“$inc” 修改器用来增加已有键的值,没有该键则直接创建一个。例如,更新集合 inventory 中 item 为 canvas 的文档,使其 qty 值增加,可以这么写:

1
2
db.inventory.findOne()
db.inventory.update({"item":"canvas"},{"$inc":{"qty":2}})

3.数组修改器

  • 添加元素
    如果数组已经存在,”$push” 会向已有的数组末尾加入一个元素,要是没有就创建一个新的数组。例如,假设要存储博客文章,要添加一个用于保存数组的 “comments” 键。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    > db.blog.posts.findOne()
    {
    "_id": ObjectId("5cb81579040c0c1ce40033aa"),
    "title": "A blog post",
    "content": "bla bla bla"
    }
    > db.blog.posts.update({"title":"A blog post"}, {"$push":{"comments":{"name":"joe","email":"joe@example.com","content":"nice post."}}})
    > db.blog.posts.findOne()
    {
    "_id": ObjectId("5cb81579040c0c1ce40033aa"),
    "title": "A blog post",
    "content": "bla bla bla",
    "comments": [
    {
    "name": "joe",
    "email": "joe@example.com",
    "content": "nice post."
    }
    ]
    }
    如果还想添加一条评论,继续使用 “$push”:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    > db.blog.posts.update({"title":"A blog post"}, {"$push":{"comments":{"name":"bob","email":"bob@example.com","content":"good post."}}})
    > db.blog.posts.findOne()
    {
    "_id": ObjectId("5cb81579040c0c1ce40033aa"),
    "title": "A blog post",
    "content": "bla bla bla",
    "comments": [
    {
    "name": "joe",
    "email": "joe@example.com",
    "content": "nice post."
    },
    {
    "name": "bob",
    "email": "bob@example.com",
    "content": "good post."
    }
    ]
    }
    使用 “$each” 子操作符,可以通过一次 “$push” 操作添加多个值:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    > db.blog.posts.update({"title":"A blog post"}, {"$push":{"tickets":{"$each":[10,20,15]}}})
    > db.blog.posts.findOne()
    {
    "_id": ObjectId("5cb81579040c0c1ce40033aa"),
    "title": "A blog post",
    "content": "bla bla bla",
    "comments": [
    {
    "name": "joe",
    "email": "joe@example.com",
    "content": "nice post."
    },
    {
    "name": "bob",
    "email": "bob@example.com",
    "content": "good post."
    }
    ],
    "tickets": [
    10,
    20,
    15
    ]
    }
    如果希望数组的最大长度是固定的,那么可以将 “$slice” 和 “$push” 组合使用:
    1
    > db.blog.posts.update({"title":"A blog post"}, {"$push":{"top10":{"$each":["Nightmare on Elm Street","Saw"],$slice:-10}}})
    $slice 的值必须是负整数。

如果数组的元素数量小于 10,那么所有元素都会保留。如果数组的元素数量大于 10,那么只要最后 10 个元素会保留。

类似的用法如 $ne 和 $addToSet 不一一举例。用到的时候再查。

  • 删除元素
    {$pop:{“key:”:1} 从数组末尾删除一个元素, {$pop:{“key:”:-1} 则从头部删除。
    删除待办列表中的 laundry:

    1
    db.lists.update({},{$pull:{"todo:":"laundry"}})
  • 基于位置的数组修改器
    有两种方法操作数组中的值:通过位置或者定位操作符 “$” 。
    数组下标都是从 0 开头的,可以将下标直接作为键来选择元素。
    定位操作符 “$” 标识数组中要更新的元素,而不显式指定元素在数组中的位置。

例如,将 _id 为 1 的学生的成绩 80 改为 82,则可以这么做,而无需知道数组的下标:

1
2
3
4
5
6
7
8
9
> db.students.insert([
{ "_id" : 1, "grades" : [ 85, 80, 80 ] },
{ "_id" : 2, "grades" : [ 88, 90, 92 ] },
{ "_id" : 3, "grades" : [ 85, 100, 90 ] }
])
> db.students.updateOne(
{ _id: 1, grades: 80 },
{ $set: { "grades.$" : 82 } }
)

定位符只更新第一个匹配的元素。

以上更新操作符的具体用法请参考 mongoDB 官方文档 Update Operators

3.3.3 update 函数参数

update 函数格式如下:

1
2
3
4
5
6
7
8
9
10
11
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>,
collation: <document>,
arrayFilters: [ <filterdocument1>, ... ]
}
)

当 upsert 设置为 true 时,则在没有文档与查询条件匹配时创建新文档。
当 multi 设置为true,则更新符合 query 条件的多个文档。

3.3.5 返回被更新的文档

db.collection.findAndModify() 方法可以修改并返回单个文档。其格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
db.collection.findAndModify({ 
query:<document>,
sort:<document>,
remove:<boolean>,
update:<document>,
new:<boolean>,
fields:<document>,
upsert:<boolean>,
bypassDocumentValidation :<boolean>,
writeConcern:<document>,
collat​​ion:<document>,
arrayFilters:[<filterdocument1>,...]
});

其中具体参数的使用请参照 mongoDB 官方文档。

第 4 章 查询

4.1 find 简介

find 方法格式如下:

1
db.collection.find(query, projection)

其中,参数说明如下:

Parameter Type Description
query document 可选的。指定使用选择过滤查询操作。要返回集合中的所有文档,请省略此参数或传递空文档({})。
projection document 可选的。指定要在与查询过滤器匹配的文档中返回的字段。要返回匹配文档中的所有字段,请省略此参数。

例如:想要查找 “age” 值为 27 的所有文档,可以这样写:

1
db.user.find({"age":27})

可以向查询文档加入多个键/值对,将多个查询条件组合在一起,例如:

1
db.user.find({"age":27,"username":"joe"})

4.1.1 指定需要返回的键

通过 find 或 findOne 的第二个参数指定想要返回的键。

例如,如果只对用户集合的 “username” 和 “email” 键感兴趣,可以使用如下查询返回这些键:

1
db.users.find({},{"username":1,"email":1})

默认情况下, “_id” 这个键总是被返回。入股不需要查询返回某个键/值对时,只需要将第二个参数的键的值设为 0 即可。

##4.2 查询条件

4.2.1 查询条件

比较操作符 “$lt”、”$lte”、”$gt” 、 “$gte” 和 “$ne” 分别对应 <、<=、> 、 >= 和 != 。例如,查询 18~30 岁(含)的用户,可以向下面这样:

1
db.users.find({"age":{"$gte":18,"$lte":30}})

这样的范围查询对日期尤为有用。例如,要查找在 2007 年 1 月 1 日前注册的人,可以像下面这样:

1
2
> start = new Date("01/01/2007")
> db.users.find({"registered":{"$lt":start}})

4.2.2 OR 查询

“$in” 可以查询一个键的多个值,”$or” 可以在多个键中查询任意的给定值。例如,抽奖活动的中奖号码是 725 、 542 和 390 ,要找出全部的中间文档的话,可以构建如下查询:

1
db.raffle.find({"ticket_no":{"$in":[725,542,390]}})

“$in” 后面的数组可以包含不同类型的值。”$nin” 将返回与数组中所有条件都不匹配的文档。

4.3.1 null

null 不仅可以匹配自身,而且还会匹配不包含这个键的文档。。如果既要检查该键的值是否为 null,还要通过 “$exists” 条件判定键值已存在:

1
db.c.find({"z":{"$in":[null],"$exists":true}})

4.3.2 正则表达式

MongoDB 使用 Perl 兼容的正则表达式库(PCRE)来匹配正则表达式,任何 PCRE 支持的正则表达式语法都能被 MongoDB 接受。

MongoDB 可以为前缀型正则表达式(比如 /^joey/)查询创建索引,所以这种类型的查询会非常高效。

4.3.3 查询数组

1.$all
使用 $all 通过多个元素来匹配数组。例如,有以下数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1
{
"_id": 1,
"fruit": [
"apple",
"banana",
"peach"
]
}

// 2
{
"_id": 2,
"fruit": [
"apple",
"kumquat",
"orange"
]
}

// 3
{
"_id": 3,
"fruit": [
"cherry",
"banana",
"apple"
]
}

想从中找到既有 “apple” 又有 “banana” 的文档,可以使用 $all :

1
db.food.find({fruit:{$all:["apple","banana"]}})

要是想查询数组特定位置的元素,需使用 key.index 语法指定的下标:

1
db.food.find({"fruit.2":"peach"})

数组下标都是从 0 开始的,所以上面的表达式会用数组的第 3 个元素和 “peach” 进行匹配。

2.$size
可以使用 $size 查询特定长度的数组。

1
db.food.find({"fruit":{"$size":3}})

2.$size
可以使用 $size 查询特定长度的数组。例如:

1
db.food.find({"fruit":{"$size":3})

3.$slice 操作符
$slice 操作符可以返回某个键匹配的数组元素的一个子集。例如,返回博客文章的文档的前 10 条评论:

1
db.blog.posts.findOne(criteria,{"comments":{$slice:10}})

其中,使用 -10 可以返回后 10 条评论。
也可以指定偏移值以及希望返回的元素数量。如:

1
db.blog.posts.findOne(criteria,{"comments":{$slice:[23,10]}})

该操作会跳过前 23 个元素,返回第 24 ~ 33 个元素。
除非特别说明,否则使用 $slice 将返回文档中的所有键。

返回一个匹配的数组元素:

1
db.blog.posts.find({"comments.name":"bob",{"comments.$":1}})

该查询只返回 Bob 在这篇博客文章下写过的评论的第一条。

第 5 章 索引

5.1 索引简介

5.1.5 索引基数

基数(cardinality)就是集合中某个字段拥有不同值的数量。通常,一个字段的基数越高,这个键上的索引就越有用。一般来说,应该在基数比较高的键上建立索引,或者至少应该把基数较高的键放在复合索引的前面(低基数的键之前)。

5.4 索引类型

5.4.1 唯一索引

唯一索引可以确保集合的每一个文档的指定键都有唯一值。唯一索引会把 null 看做值,所以无法将多个缺少唯一索引中的键的文档插入到集合中。例如,如果想保证不同文档的“username”键拥有不同的值,创建一个唯一索引就好了:

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

1.复合唯一索引
也可以创建复合的唯一索引。创建复合唯一索引时,单个键的值可以相同,但所有键的组合值必须是唯一的。

2.去除重复
创建索引时使用“dropDups”选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会被删除。

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

对于比较重要的数据,千万不要使用“dropDups”。

5.4.2 稀疏索引

创建稀疏索引时,只对键有值的情况进行唯一性约束。

1
db.ensureIndex({"email":1},{"unique":true,"sparse":true})

稀疏索引不必是唯一的,只要去掉 unique 选型,就可以创建一个非唯一的稀疏索引。

5.5.1 标识索引

创建索引时指定索引的名称:

1
db.foo.ensureIndex({"a":1,"b":1,"c":1,...,"z":1},{"name":"alphabet"})

5.5.2 删除索引

使用 dropIndex 命令删除不再需要的索引:

1
db.people.dropIndex("x_1_y_1")

第 6 章 特殊的索引和集合

6.1 固定集合

固定集合需要事先创建好,且大小是固定的。固定集合类似于循环队列,当固定集合被沾满时,如果再插入新文档,固定集合会自动将最老的文档从集合中删除。

固定集合不能被分片。可以用于记录日志。

6.1.1 创建固定集合

size 参数可以指定集合的固定大小,以字节为单位:

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

也可以通过 max 参数指定固定集合的文档数量。指定文档数量限制时,必须同时指定固定集合的大小。

可以使用 convertToCapped 命令将常规集合转换为固定集合:

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

固定集合无法转换为非固定集合。

6.1.2 自然排序

对固定集合可以进行 自然排序(natural sort)。自然排序返回结果集中文档的顺序就是文档在磁盘上的顺序。

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

6.1.3 循环游标

循环游标(tailable cursor):当循环游标的结果集被取光后,游标不会被关闭。如果超过 10 分钟没有新的结果,循环游标就会被释放。

6.2 TTL 索引

TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间后或在特定时钟时间自动从集合中删除文档。数据到期对于某些类型的信息非常有用,例如机器生成的事件数据,日志和会话信息,这些信息只需要在数据库中持续有限的时间。

1
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )

6.4 地理空间索引

MongoDB 支持 2dsphere 索引(用于地球表面类型的地图)和 2d 索引(用于平面地图和时间连续的数据)。

2dsphere 允许使用 GeoJSON 格式指定点、线和多边形。

1
2
3
4
5
6
7
{
"name":"New York City",
"loc":{
"type":"Point",
"coordinates":[50,2]
}
}

线可以用一个有点组成的数组来表示:

1
2
3
4
5
6
7
{
"name":"Hudson River",
"loc":{
"type":"Line",
"coordinates":[[0,1],[0,2],[1,2]]
}
}

多边形的 ”type“ 不同:

1
2
3
4
5
6
7
{
"name":"New England",
"loc":{
"type":"Polygon",
"coordinates":[[0,1],[0,2],[1,2]]
}
}

在 ensureIndex 中使用 “2dsphere” 选型可以创建一个地理空间索引:

1
db.world.ensureIndex({"loc":"2dsphere"})

6.4.1 地理空间查询的类型

可以使用多种不同的地理空间查询:交集(intersection)、包含(within)以及接近(nearness)。查询时,需要将希望查找的内容指定为形如 {“$geometry”:geoJsonDesc} 的 GeoJSON 对象。
例如,使用 “$geoIntersects” 操作符找出与查询位置相交的文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.places.find(
{
loc: {
$geoIntersects: {
$geometry: {
type: "Polygon" ,
coordinates: [
[ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ]
]
}
}
}
}
)

6.4.3 2D 索引

对于非球面地图(游戏地图、时间连续的数据等),可以使用 2d 索引。

1
db.hyrule.ensureIndex({title:"2d"})

6.5 使用 GridFS 存储文件

6.5.1 GridFS 入门

在下面这个会话中,首先用 mongofiles 从文件系统中上传一个文件到 GridFS ,然后列出 GridFS 中的所有文件,最后再将之前上传过的文件从 GridFS 中下载下来:

1
2
3
4
echo "Hello,world" > foo.txt
mongofiles put foo.txt
mongofiles list
mongofiles get foo.txt

第 7 章 聚合

7.1 聚合框架

例如,有一个保存着杂志文章的集合,假设每篇文章被保存为 MongoDB 中的一个文档,则可以构建如下查询:
1.将 “author” 从每个文档中投射出来

1
{"$project":{"author":1}}

2.将作者按照名字排序,统计每个名字出现的次数

1
{$group:{"_id":"$author","count":{"$sum":1}}}

3.将作者按照名字出现次数降序排列

1
{"$sort":{"count":-1}}

4.将返回结果限制为前 5 个

1
{"$limit":5}

在 MongoDB 中实际运行时,要将这些操作分别传给 aggregate() 函数:

1
2
3
4
db.aticles.aggregate({"$project":{"author":1}},
{$group:{"_id":"$author","count":{"$sum":1}}},
{"$sort":{"count":-1}},
{"$limit":5})

7.2 管道操作符