来自阅读 《MongoDB 权威指南(第二版)》1~8章的整理。
写在前面的话
此文章可能不适合没有基础的读者。此书写作时对应的版本应当很低,所以文章中所示可能会和当下使用的方法有出入,但 MongoDB 所拥有的功能差不多类似,请读者注意。
第一章
(无)
第二章 基础知识
文档
- 文档就是键值对的集合,对应于关系型数据库中的行。
- 文档的键不能含有
\0
空字符。这个字符用于表示键的结尾。 -
.
和$
具有特殊意义。不可用于键的命名。 - MongoDB 中的文档不能有重复的键。非法。
- 文档的键/值对是有序的。某些特殊情况下,字段顺序非常重要。
集合
- 集合是文档的一个集合体,一个集合下的文档字段结构类似。
- 集合名不可以是空字符串。
- 集合名不可以包含
\0
空字符。这个字符用于表示集合名的结束。 - 集合名不能以
system.
开头。这是为系统集合保留的前缀。 - 用户创建的集合名中不能包含
$
。因为部分驱动程序生成的集合中使用了它。 - 子集合建议用
.
来分隔不同的命名空间。如db.blog.authors
数据库
- 一个 MongoDB 实例可以承载多个数据库。每个数据库有独立的权限。不同的数据库在磁盘上存放的文件不同。
- 数据库最终会变成文件系统中的文件,而数据库名就是相应的文件名。故命名规范略。
- 数据库名区分大小写,即便在不区分大小写的文件系统中也一样。故建议全部小写。
- 数据库名最多 64 字节。
- 保留的数据库:
admin
,local
,config
。-
admin
数据库。从身份验证角度来说,此为root
库。这个库下的用户拥有所有数据库权限。一些特定的服务器端命令也能能从admin
数据库运行。 -
local
数据库。永远不可以复制(9章详解) -
config
数据库。分片信息存储在此库。
-
启动 MongoDB
- 数据库目录
/data/db
(Windows 系统中为C:\data\db
)。启动前应保证已创建且得分写的权限。 - 默认监听端口
27017
。 - 默认启动一个 HTTP 服务器,监听端口号比主端口号高 1000(不知书中此句是否有深意),即
28017
。可通过浏览器访问此端口获取数据库管理信息。
数据类型
- 查询和文档中可以包括任意的 JavaScript 代码。如
{"x": function() { /* ... */ }}
。(待验证) - 创建日期类型应用
new Date(...)
而不是Date(...)
。因为后者(将构造函数作为函数的调用)返回的是日期的字符串表示,而非日期对象。 - 数组技能作为无序对象,也可作为有序对象(列表、栈或队列)来操作(因为有一些操作符)。
-
ObjectId
是_id
的默认类型。不同的机器能全局唯一地生成它。因为设计 MongoDB 的初中就是用作分布式数据库。 -
ObjectId
由 12 个字节的存储空间,24 个十六进制数字组成。前四位为秒级时间戳,再三位为主机名的散列值(hash),再两位为进程号(PID),最后三位为累加的计数器。(即一秒钟最多允许每个进程拥有 2563 个不同的 ObjectId)
MongoDB Shell
-
db.help()
查看数据库级别的帮助。 -
db.coll.help()
可查看集合级别的帮助。 -
db.coll.update
等直接输入函数名(不带括号)可以查看相应函数的 JavaScript 实现代码。
MongoDB Shell 执行脚本
-
使用 MongoDB Shell 执行脚本有三种方式
mongo script_1.js script_2.js script_3.js
load("script_1.js")
-
shell 辅助函数(
use db
、show collections
等)不可以再文件中使用。这些辅助函数有对应的 JavaScript 函数。(load 辅助函数是否可以在 js 中使用)-
use foo
对应函数db.getSisterDB("foo")
-
show dbs
对应函数db.getMongo().getDBs()
-
show collections
对应函数db.getCollectionNames()
-
run()
函数可以执行命令行程序。如run("ls", "-l", "/home/myUser/my-scripts/")
。此函数无法解析~
符号,局限性比较大。-
可以定义
.monogorc.js
文件,这个文件在启动 shell 时可以自动运行。- 可以重写内置函数,来限制一些操作。如
db.dropDatabase = DB.prototype.dropDatabase = function() { print('Deny.') };
- 要确保同时对 db 变量和 DB 原型进行改变(正如上例)。否则可能 db 变量没有改变,或这些改变在新使用的数据库(
use anothgerDB
)中不生效。
- 可以重写内置函数,来限制一些操作。如
定制 shell 提示可以重写
prompt
变量,指定为一个字符串或函数。在 shell 中定义的变量还可以再编辑,用
edit 变量名
命令来再编辑。但要提前设置编辑器,变量EDITOR
定义了编辑器的位置的指向。可以执行EDITOR="/usr/bin/vim"
第三章 CRUD 命令
插入-批量插入 batchInsert
- 快,但一次接受的最大消息长度为 48MB(当前版本)
- 默认在出错时会终止插入,可用
continueOnError
选项来忽略错误,继续执行。 - 插入校验,MongoDB 会进行基本检查(基本结构、_id)。插入的所有文档都必须小于 16MB(当前版本),此大小可以用
Object.bsonsize(doc)
来查看。(16MB 多大?整部《战争与和平》才 3.14MB)
删除
- 数据库的
drop
命令比remove()
(删除所有文档)速度更快(比如 1000000 的数据前者 1ms 后者 9676ms)。代价时所有元数据也都删除了,比如需要重建各项索引。
更新
-
$inc
操作符速度非常快,对性能几乎无影响。 - 操作符
$push
和$each
合用可以 push 多个元素。再加上$sort
、$slice
可以实现固定数组的大小。 - 操作符
$pop
可控制弹出首/尾的一个。(将数组用作 队列或栈) - 修改速度的问题:关键看是否改变了文档的大小。改变了大小就有可能变动文档的存储位置,就会慢些。
- 可以用
db.coll.stats()
查看 填充因子(padding factor)。(看文档在 3.0.0 版本后被废弃,所以也证明了此书所解释的版本在这之前。) - 填充因子表示了 MongoDB 为每个新文档预留的增长空间。如果为 1 就表示没有预留任何增长空间,即更新增长了 1 字节都会移动文档所在的存储位置。
- 这个填充因子在系统中会自动维护,如果文档移动频繁,就会持续变大,不再移动则会缓慢降低。
- (第八章有优化,记得整理)
- 频繁移动文档会产生大量空的数据文件,拥有太多的碎片,可能需要进行压缩
-
userPowerOf2Sizes
选项可以提高磁盘复用率。- 作用:空间分配分配得到的块的大小都是 2 的幂。
- 缺点:导致初始空间分配不再高效。在不需要增长存储空间的文档上使用会影响写入速度。
- 使用方法:
db.runCommand({"collMod": collectionName, "usePowerOf2Sizes": true})
.
- 可以用
写入安全(Write Concern)机制
- 写入安全是一种客户端设置,用于控制写入的安全级别。有
应答式写入
(acknowledged write)和非应答式写入
(unacknowledged write)两种方式。 -
应答式写入
是默认方式,写入操作会返回是否执行成功。 -
非应答式写入
不会返回任何响应。在不太重要的操作中(如批量操作)可以使用非应答式写入(如可以忽略Dup key
的操作中)。
第四章 查询
- 高效用法:
$and
应将限定数据最少的条件放在前面。$or
相反,应当将尽可能多的文档匹配放在前面。(这和boolClause_1 && boolClause_2
的使用类似,若boolClause_1
为 false 的话就不再判断boolClause_2
了。) - 尽量不用
$and
操作符,因为优化器不会优化它,效率更低。 - 查询条件
{x: null}
包含了无此键和此键对应的值为null
两种情况。- 准确地查出
无此键
可用{x: {$exists: false}}
来查询。 - 准确地查出
值为 null
的可用{x: {$exists: true, $in: [null]}}
来查询。这种方式不能用$eq
操作符。
- 准确地查出
- 查询数组,在不用
$xxx
的操作符时表示精确匹配,此时的顺序也时匹配的条件。即{x: [1, 2, 3]}
匹配不到{x: [3, 2, 1]}
的文档。 - 查询数组,可用
key.index
的语法,如:{"x.2": 2}
可以匹配到{x: [1, 2, 3]}
但匹配不到{x: [2, 1, 3]}
的文档。 - 查询数组,
$size
不能与其他查询条件组合使用(如$gt
)。 - 查询数组,
$slice
可以控制返回数组的部分子集(前、后、中间部分)。
书中说一个集合中存在
{x: 15}
和{x: [15, 25]}
两个文档,若要精确查到{x: 5}
可以在x
字段上创建索引,然后用cursor.min({x: 10}).max({x: 20})
来做。
min()
和max()
可以将限制查询条件遍历的索引。但注意,这个min/max
的使用必须指明索引的所有字段。
-
$where
指定一个funcion
来查询,所以可以在查询中做几乎任何事情(比如比较同一个文档的两个键是否相等)。但慢,不能利用索引,可能的话先用索引匹配,再用$where
进一步过滤。
游标
- 在调用
find
时,shell 不会立即查询 DB,而是等待真正开始要求获得结果时才发送查询。 -
cursor.hasNext()
,shell 会立即获取前 100 个结果或前 4MB 的数据(两者中的小者),这样在用cursor.next()
取数据时只从内存中取,直到用光了第一组数据,这时 shell 会再次联系 DB 使用 getMore 请求更多结果。 -
skip
跳过大量数据时会慢。所以分页设计最好限制页数,如只取到前 100 页的数据。 - 避免使用
skip
的拉取数据可以用某个游标来做,如_id
。第一次用db.foo.find().sort({_id: -1}).limit(100)
,第二次查询用db.foo.find({_id: {$lt: lastId}}).sort({_id: -1}).limit(100)
- 值类型的比较顺序,不同文档的某个键可能是多种类型的,他们的顺序由小到大是:
最小值-null-数字(整型、长整型、双精度)-字符串-对象/文档-数组-二进制数据-对象ID-布尔型-日期型-时间戳-正则表达式-最大值
。
高级查询
- 高级查询选项。
- 查询有两种类型:
简单查询
(plain query)和封装查询
(wrapped query)。上面我们常用的就是简单查询。 -
封装查询
是简单查询
的一种内部转换。如find({foo: "bar"}).sort({x: 1})
会转换成{$query: {foo: "bar"}}, $orderby: {x: 1}
。 - 上文有讲到
$min
、$max
,限定文档的开始条件。(这两个的查询必须与索引的键完全匹配) -
cursor._addSpecial("$maxscan, 20)
可以限定查询中扫描文档数量的上限。 -
cursor._addSpecial("$showDiskLoc", true)
可以显示该条结果在磁盘上的位置。
- 查询有两种类型:
-
snapshot
(快照,查询在_id
索引上便利执行),可以保证每个文档只被返回一次。(普通操作在文档内存变化后可能重复获取) -
cursor
的生命周期。三种情况会销毁。- 完成匹配自动销毁。
- 客户端的游标已不在作用域内,驱动程序会向服务器发送一条特别的消息,使其销毁。
- 在 10 分钟内没有被使用自动销毁。(???为什么感觉与有没有被使用无关)
immortal
函数或类似的机制可以避免其超时。
数据库命令
- 上面的所有例子在底层的内部实现是在
$cmd
集合上执行的。但客户端 shell 和服务器版本不一样时就无法使用,这时,不得不用runCommand()
或adminCommand()
。(adminCommand()
是执行管理员命令的操作(管理员权限)) -
db.runCommands()
接收一个文档,作为等价查询。文档的第一个字段必须为命令名称。- 如
db.runCommand({"drop": "test"})
,内部转换为db.$cmd.findOne({"drop": "test"})
。 - 再如:
db.runCommand({"getLastError": 1, "w": 2})
就有效命令,但db.runCommand({"w": 2, "getLastError": 1})
不是。
- 如
第五章 索引
简介
- 不用索引的查询称为
全表扫面
。cursor.explain()
中nscanned
就表示扫描的文档数。 - 由于机器性能和集合大小的不同,创建索引要花一些时间,期间可以在另一个 shell 中执行
db.currentOp()
来检查索引创建的进度。 - 索引让查询更快,但让增删改更慢。
- 一个集合上的索引最多为 64 个。
复合索引
- 使用
{"age" : 1, "username" : 1}
建立索引,这个索引大致会是这个样子:
// [age, username] -> 对应地址
[0, "user100309"] -> 0x0c965148
[0, "user100334"] -> 0xf51f818e
[0, "user100479"] -> 0x00fd7934
...
[0, "user99985" ] -> 0xd246648f
[1, "user100156"] -> 0xf78d5bdd
[1, "user100187"] -> 0x68ab28bd
[1, "user100192"] -> 0x5c7fb621
...
[1, "user999920"] -> 0x67ded4b7
[2, "user100141"] -> 0x3996dd46
[2, "user100149"] -> 0xfce68412
[2, "user100223"] -> 0x91106e23
...
- 点查询 (
point query
),用于查找单个值的查询。如:db.users.find({ age: 21}).sort({ username: -1 })
。 MongoDB 可以从{ age: 21 }
匹配的最后一个索引位置开始逆序遍历索引,所以非常高效。 - 多值查询(
multi-value query
),查找多个值相匹配的文档。如db.users.find({ age: { $get: 21, $lte: 30 }})
,查询 age 必须介于 21 到 30 之间。 - 不指定顺序时,返回的顺序是按照索引顺序排列的。如上一行的查询其顺序相当于
.sort({age: 1, username: 1})
的顺序。 -
db.users.find({ age: { $get: 21, $lte: 30 }}).sort({username: 1})
需要在内存中对结果进行排序。 - 如果在内存中进行排序的结果集大于 32MB,MongoDB就会报错。
- 此查询
db.users.find({ age: { $get: 21, $lte: 30 }}).sort({username: 1})
用{username: 1, age: 1}
的索引更好,因为不需要在内存中排序。但不得不在扫描整个索引。 - 在上述情况下,将排序键放在第一位是一个非常好的策略。:
{sortKey: 1, queryCriteria: 1}
- 在一个寒冰数组的字段上做索引,这个索引永远也无法覆盖查询。(??? 5.1.4 会介绍)
$
查询符与索引效率
- 低效率的操作符:
$ne
、$not
-
$ne
低效,它可以使用索引,但必须要查看这个字段上所有的索引条目。 -
$not
有时能使用索引,但通常它不知道如何使用索引,因而退化为全表扫描。
-
- 无效率的操作符:
$where
、$exists
、$nin
-
$nin
总是进行全表扫描 -
$where
和$exists
完全无法使用索引。 -
$exists: false
,在索引中不存在的字段和 null 字段的存储方式是一样的,查询必须遍历所有文档检查才能判断是 null 还是字段不存在。
-
范围查询与索引
-
{精确: 1, 范围: 1}
的索引效率更优。如db.users.find({age: 47, username: {$gt: "user4", $lt: "user10"}})
的查询中,用{age: 1, username: 1}
扫描的索引条目数(nscanned)将近{usrname: 1, age: 1}
的十分之一。
OR 操作
-
$or
低效,因为他的每个子句都使用索引(多次查询),然后将结果集合并(检查并去重)。 - 尽可能地用
$in
代替$or
。
索引嵌套文档
- 在嵌套文档本身上建立索引,只在查询嵌套文档本身的完全匹配上可用。
多键索引
- 多键索引(mutilkey index),若某个键在某个(不一定是所有)文档上是数组,那么这个索引就会被标记为多键索引。
- 要想将多键索引回复为非多键索引,唯一的方法就是删除在重建这个索引。
- 多键索引比非多键索引更慢。因为 MongoDB 要对查询去重。
索引数组
- 对数组建立索引,实际上对数组的每个元素都建立一个索引条目。
- 数组索引的代价比单值索引高:对单次插入、更新或删除,每一个索引条目可能都需要更新(可能有上千个)。
- 可精确地在数组的某一个下标位置的元素建立索引。如:
{comment.4: 1}
。但此索引也只能在查找此元素(第5个)时被使用。 - 一个索引中的数组字段最多智能有一个,否则创建非法。这是为了避免索引条目的爆炸性增长:如一个 m 个元素,一个 n 个元素,就有
m*n
个索引条目。
索引基数
- 基数(cardinality),就是集合中某个字段拥有不同值的数量。
- 一个字段的基数越高,这个键上的索引就越高效(更精准、更能排除更多文档)。(如
createdAt
就比isDeleted
高效)
使用 explain 和 hint
- 游标类型
-
BasicCursor
,基本游标,如不使用索引的查询。 -
BtreeCursor
,B 树游标,大部分使用索引的都使用此游标 - 其他游标,比如地理空间索引,使用他们自己的游标。
-
-
cursor.explain()
字段名词解释:
// cursor.explain()
{
"cursor" : "BtreeCursor age_1_username_1", // 游标类型已经索引名称
"isMultiKey" : false, // 是否为多键索引
"n" : 83484, // 匹配的文档数目
"nscannedObjects" : 83484, // 索引指针去磁盘上查找实际文档的次数
"nscanned" : 83484, // 文档查询次数
"nscannedObjectsAllPlans" : 83484,
"nscannedAllPlans" : 83484,
"scanAndOrder" : true, // 是否在内存中进行排序了
"indexOnly" : false, // 是否只使用索引就能完成此查询
"nYields" : 0, // 查询暂停的次数,(为了让写入顺利执行,如果有写入请求需要处理,查询会周期性地释放他们的锁)
"nChunkSkips" : 0,
"millis" : 2766, // 耗时 ms
"indexBounds" : { // 索引的使用情况,索引的遍历范围
"age" : [
[
21,
30
]
],
"username" : [
[
{
"$minElement" : 1 // 负无穷
},
{
"$maxElement" : 1 // 正无穷
}
]
]
},
"server" : "spock:27017"
}
查询优化器
- MongoDB 并行执行一个查询的多个索引计划,并认为最早返回 100 个文档的计划就是胜者,其他查询计划就会被中止。
- 查询计划是会被缓存的,这个查询会一直使用它。
- 集合数据发生了较大变动,会重新挑选可行的查询计划。
- 建立索引,或者没执行 1000 此查询后,查询优化器会重新评估查询计划。
第六章
何时不应该用索引
- 索引的查找:先查索引 -> 根据索引再查文档
- 不用索引的查找: 直接查文档
- 如上两条原则,如果结果集在原集合中所占的比例越大,索引的速度越慢。
- 例如:一个集合中只有一条数据,创建了索引后的查询会走两步,而不建立索引的查询只需要一步。
-
cursor.hint({$natural: 1})
强制进行全表扫描($natural
:按文档在磁盘上的顺序排列)。
索引类型
- 唯一索引(
{unique: true}
)- 文档中所有字段都必须小于 1024 字节,此字段才能包含到索引中。所以超过此大小的键不会受到唯一索引的约束。
- 在有文档的集合上创建唯一索引,默认遇到重复的会报错,创建失败。但可以用
dropDups
选项来跳过错误。
- 稀疏索引(
{sparse: true}
),只对有此键的文档进行索引。
索引管理
- 数据库的索引信息都存储在
system.indexes
集合中。此集合只能通过ensureIndex
和dropIndexes
对其进行操作。 - 更改索引名:
db.foo.ensureIndex({...}, {name: "xxx"})
- 默认情况下,MongoDB 为了索引创建快,会阻塞所有数据库的读和写请求,但可以用
background
选项将创建过程放到后台,在有请求需要处理时创建过程会暂停一下(后台创建索引比前台创建慢得多)。 - 在已有文档上创建索引比先创建索引再插入这些文档快一点。