MongoDB 精粹

来自阅读 《MongoDB 权威指南(第二版)》1~8章的整理。

写在前面的话

此文章可能不适合没有基础的读者。此书写作时对应的版本应当很低,所以文章中所示可能会和当下使用的方法有出入,但 MongoDB 所拥有的功能差不多类似,请读者注意。

第一章

(无)

第二章 基础知识

文档
  • 文档就是键值对的集合,对应于关系型数据库中的行。
  • 文档的键不能含有 \0 空字符。这个字符用于表示键的结尾。
  • .$ 具有特殊意义。不可用于键的命名。
  • MongoDB 中的文档不能有重复的键。非法。
  • 文档的键/值对是有序的。某些特殊情况下,字段顺序非常重要。
集合
  • 集合是文档的一个集合体,一个集合下的文档字段结构类似。
  • 集合名不可以是空字符串。
  • 集合名不可以包含 \0 空字符。这个字符用于表示集合名的结束。
  • 集合名不能以 system. 开头。这是为系统集合保留的前缀。
  • 用户创建的集合名中不能包含 $。因为部分驱动程序生成的集合中使用了它。
  • 子集合建议用 . 来分隔不同的命名空间。如 db.blog.authors
数据库
  • 一个 MongoDB 实例可以承载多个数据库。每个数据库有独立的权限。不同的数据库在磁盘上存放的文件不同。
  • 数据库最终会变成文件系统中的文件,而数据库名就是相应的文件名。故命名规范略。
  • 数据库名区分大小写,即便在不区分大小写的文件系统中也一样。故建议全部小写。
  • 数据库名最多 64 字节。
  • 保留的数据库: adminlocalconfig
    • 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 dbshow 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 集合中。此集合只能通过 ensureIndexdropIndexes 对其进行操作。
  • 更改索引名: db.foo.ensureIndex({...}, {name: "xxx"})
  • 默认情况下,MongoDB 为了索引创建快,会阻塞所有数据库的读和写请求,但可以用 background 选项将创建过程放到后台,在有请求需要处理时创建过程会暂停一下(后台创建索引比前台创建慢得多)。
  • 在已有文档上创建索引比先创建索引再插入这些文档快一点。

你可能感兴趣的:(MongoDB 精粹)