MongoDB

Stone大约 191 分钟

MongoDB

注意:

此文档对应的 MongoDB 版本为 7.0.2。

概述

MongoDBopen in new window 是一个开源,高性能,无模式的文档型数据库,由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是功能最丰富,最像关系数据库的非关系数据库。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

基本概念

与 MySQL 相比,MongoDB 的基本概念如下:

MySQLMongoDB描述
databasedatabase数据库
tablecollection表/集合
rowdocument行/文档
columnfield字段
indexindex索引
table join$lookup, embedded documents表连接/嵌套文档
primary keyprimary key主键,MongoDB 自动将 _id 字段设置为主键

相关程序对比:

MongoDBMySQLOracle
Database Servermongodopen in new windowmysqldoracle
Database Clientmongoshopen in new windowmysqlsqlplus

数据类型

MongoDB 的常用数据类型如下:

数据类型描述
String字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Boolean布尔值。用于存储布尔值(真/假)。
Double双精度浮点值。用于存储浮点值。
Min/Max keys将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。
Array用于将数组或列表或多个值存储为一个键。
Timestamp时间戳。记录文档修改或添加的具体时间。
Object用于内嵌文档。
Null用于创建空值。
Symbol符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。
Date日期时间。用 UNIX 时间格式来存储当前日期或时间。可以指定日期时间:创建 Date 对象,传入年月日信息。
Object ID对象 ID。用于创建文档的 ID。
Binary Data二进制数据。用于存储二进制数据。
Code代码类型。用于在文档中存储 JavaScript 代码。
Regular expression正则表达式类型。用于存储正则表达式。

部署

服务器

下面介绍如何在 CentOS 7open in new window 环境安装 MongoDB。

配置 IP 和主机名映射:

[root@linux ~]# vi /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.92.128 linux

关闭防火墙和 SELinux:

[root@linux ~]# systemctl stop firewalld
[root@linux ~]# systemctl disable firewalld
[root@linux ~]# sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
[root@linux ~]# init 6
[root@linux ~]# getenforce
Disabled

关闭 Transparent Huge Pages (THP) :

[root@linux ~]# cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
[root@linux ~]# grubby --update-kernel=ALL --args="transparent_hugepage=never"
[root@linux ~]# init 6
[root@linux ~]# cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]

调整内核参数:

[root@linux ~]# vi /etc/sysctl.conf
kernel.threads-max=64000
vm.max_map_count=128000
[root@linux ~]# sysctl -p

调整资源限制:

[root@linux ~]# vi /etc/security/limits.conf
* soft memlock unlimited
* hard memlock unlimited
* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535
[root@linux ~]# logout
[root@linux ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7183
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 65535
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

官方仓库open in new window下载安装包,包括:

[root@linux ~]# curl -C - -O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-database-tools-100.8.0.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-mongosh-2.0.1.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-org-7.0.2-1.el7.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-org-database-7.0.2-1.el7.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-org-database-tools-extra-7.0.2-1.el7.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-org-mongos-7.0.2-1.el7.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-org-server-7.0.2-1.el7.x86_64.rpm \
-O https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/RPMS/mongodb-org-tools-7.0.2-1.el7.x86_64.rpm

[root@linux ~]# ll -rth mongodb-*
-rw-r--r-- 1 root root  52M Aug 17 03:18 mongodb-database-tools-100.8.0.x86_64.rpm
-rw-r--r-- 1 root root  50M Sep 14 22:26 mongodb-mongosh-2.0.1.x86_64.rpm
-rw-r--r-- 1 root root  12K Sep 30 06:57 mongodb-org-database-tools-extra-7.0.2-1.el7.x86_64.rpm
-rw-r--r-- 1 root root 6.4K Sep 30 06:57 mongodb-org-database-7.0.2-1.el7.x86_64.rpm
-rw-r--r-- 1 root root 6.3K Sep 30 06:57 mongodb-org-7.0.2-1.el7.x86_64.rpm
-rw-r--r-- 1 root root 6.3K Sep 30 06:57 mongodb-org-tools-7.0.2-1.el7.x86_64.rpm
-rw-r--r-- 1 root root  37M Sep 30 06:57 mongodb-org-server-7.0.2-1.el7.x86_64.rpm
-rw-r--r-- 1 root root  25M Sep 30 06:57 mongodb-org-mongos-7.0.2-1.el7.x86_64.rpm

安装:

[root@linux ~]# yum -y localinstall mongodb-*.rpm

调整配置:

[root@linux ~]# sed -i "s/bindIp: 127.0.0.1/bindIp: 0.0.0.0/g" /etc/mongod.conf

启动:

[root@linux ~]# systemctl daemon-reload
[root@linux ~]# systemctl start mongod
[root@linux ~]# systemctl status mongod
● mongod.service - MongoDB Database Server
   Loaded: loaded (/usr/lib/systemd/system/mongod.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2023-10-11 11:42:24 CST; 4s ago
     Docs: https://docs.mongodb.org/manual
 Main PID: 1759 (mongod)
   CGroup: /system.slice/mongod.service
           └─1759 /usr/bin/mongod -f /etc/mongod.conf

Oct 11 11:42:24 linux systemd[1]: Started MongoDB Database Server.
Oct 11 11:42:24 linux mongod[1759]: {"t":{"$date":"2023-10-11T03:42:24.182Z"},"s":"I",  "c":"CONTROL",  "id":7484500, "ctx":"m...false"}

客户端

使用前面安装的命令行客户端 mongosh 访问 MongoDB:

[root@linux ~]# mongosh
test> show dbs
admin   40.00 KiB
config  60.00 KiB
local   72.00 KiB

官方网站open in new window下载客户端软件 Compass 并进行安装,指定 URI 后点击 Connect 即可连接到 MongoDB。

image-20231011134406572

数据库

创建或选择数据库

使用 use database 命令创建或选择数据库。如果数据库不存在,则创建并选择该数据库,如果数据库存在,则选择该数据库。

MongoDB 中默认的数据库为 test,如果没有选择数据库,集合将存放在 test 数据库中。

[root@linux ~]# mongosh
test> 

创建 articledb 数据库:

test> use articledb
switched to db articledb
articledb> 

查看数据库

使用 show dbs 命令查看数据库。

articledb> show dbs
admin    40.00 KiB
config  108.00 KiB
local    40.00 KiB

在 MongoDB 中,集合只有在内容插入后才会创建。也就是说,创建集合(表)后要再插入一个文档(记录),集合才会真正创建。虽然此时没有显示 articledb,但是数据库的确已经在内存中创建了。

查看当前数据库

使用 db 命令查看当前数据库。

articledb> db
articledb

删除数据库

使用 db.dropDatabase() 删除数据库。

集合

集合类似于关系数据库中的表。

创建集合

使用 db.createCollection(name) 创建集合,其中 name 为集合名称。

articledb> db.createCollection("myCollection")
{ ok: 1 }

查看集合

使用 show collections 或者 show tables 查看集合。

articledb> show collections
myCollection
articledb> show tables
myCollection

删除集合

使用 db.collection.drop() 删除集合,其中 collection 为集合名称。

articledb> db.myCollection.drop()
true

固定集合

  • 固定集合(Capped Collection)是一种特殊类型的集合,它的大小是固定的,当达到最大值时,最旧的数据将被自动删除。
  • 固定集合主要用于存储日志、时间序列数据等需要定期清理的应用场景。
  • 固定集合默认有 _id 字段和 _id 字段上的索引。
  • 不能对固定集合进行分片。
  • 使用 find() 查询固定集合,如果没有指定排序方向,则结果的顺序与插入顺序相同。类似于 tailtail -f 命令功能。
  • 使用 isCapped() 方法查看集合是否为固定集合。
  • 使用 convertToCapped 命令将集合转换为固定集合。

使用 db.createCollection() 创建固定集合:

db.createCollection("collectionName", {capped: true, size: 1024 * 1024 * 100, max: 1000})

其中:

  • collectionName 是要创建的固定集合的名称。
  • capped 设置为 true 表示创建一个固定集合。
  • size 指定固定集合的最大大小,单位为字节,范围为 1 B 到 1 PB,圆整为 256 整数倍,必填字段。
  • max 指定固定集合中允许存储的最大文档数量,非必填字段。

使用 convertToCapped 命令将集合转换为固定集合:

db.runCommand({"convertToCapped": "mycoll", size: 100000});

使用 collMod 命令的 cappedSize 选项更改固定集合的大小:

db.runCommand( { collMod: "log", cappedSize: 100000 } )

使用 collMod 命令的 cappedMax 选项更改固定集合中的最大文档数:

db.runCommand( { collMod: "log", cappedMax: 500 } )

如果 cappedMax 小于或等于 0 ,则没有最大文档限制。如果 cappedMax 小于集合中的当前文档数,MongoDB将在下一次插入操作中删除多余的文档。

聚簇集合

从 MongoDB 5.3 开始,可以创建具有聚簇索引的集合,称之为聚簇集合。

使用 create 命令创建聚簇集合:

db.runCommand( {
   create: "products",
   clusteredIndex: { "key": { _id: 1 }, "unique": true, "name": "products clustered key" }
} )

其中:

  • "key": { _id: 1 } 设置 _id 字段为聚簇索引键。
  • "unique": true 表示聚簇索引键唯一。
  • "name": "products clustered key" 设置聚簇索引键名称。

使用 db.createCollection 创建聚簇集合:

db.createCollection(
   "stocks",
   { clusteredIndex: { "key": { _id: 1 }, "unique": true, "name": "stocks clustered key" } }
)

其中:

  • "key": { _id: 1 } 设置 _id 字段为聚簇索引键。
  • "unique": true 表示聚簇索引键唯一。
  • "name": "stocks clustered key" 设置聚簇索引键名称。

使用 create 命令创建聚簇集合:

db.createCollection(
   "orders",
   { clusteredIndex: { "key": { _id: 1 }, "unique": true, "name": "orders clustered key" } }
)

其中:

  • "key": { _id: 1 } 设置 _id 字段为聚簇索引键。
  • "unique": true 表示聚簇索引键唯一。
  • "name": "orders clustered key" 设置聚簇索引键名称。

将以下文档添加到集合 orders 中:

db.orders.insertMany( [
   { _id: ISODate( "2022-03-18T12:45:20Z" ), "quantity": 50, "totalOrderPrice": 500 },
   { _id: ISODate( "2022-03-18T12:47:00Z" ), "quantity": 5, "totalOrderPrice": 50 },
   { _id: ISODate( "2022-03-18T12:50:00Z" ), "quantity": 1, "totalOrderPrice": 10 }
] )

其中 _id 存储订单日期,如果在范围查询中使用 _id 字段,则会提供性能:

db.orders.find( { _id: { $gt: ISODate( "2022-03-18T12:47:00.000Z" ) } } )

执行结果:

[
  {
    _id: ISODate("2022-03-18T12:50:00.000Z"),
    quantity: 1,
    totalOrderPrice: 10
  }
]

查看集合是否为聚簇集合:

db.runCommand( { listCollections: 1, filter: { name: "orders" } } )

执行结果:

{
  cursor: {
    id: Long("0"),
    ns: 'test.$cmd.listCollections',
    firstBatch: [
      {
        name: 'orders',
        type: 'collection',
        options: {
          clusteredIndex: {
            v: 2,
            key: { _id: 1 },
            name: 'orders clustered key',
            unique: true
          }
        },
        info: {
          readOnly: false,
          uuid: new UUID("160db9cf-780f-4b66-8709-c9a3f27ec9fe")
        }
      }
    ]
  },
  ok: 1
}

文档

  • 文档的数据结构和 JSON 基本一样,所有存储在集合中的数据都是 BSON 格式。
  • 文档最大为 16 MB。
  • MongoDB 本身不支持 SQLopen in new window,需要使用方法来进行增删改查操作。

插入文档

插入单个文档

使用 db.collection.insertOne() 插入单个文档到集合,其中 collection 为集合名称,如果该集合不存在,则会自动创建。

语法:

db.collection.insertOne(
    <document>,
    {
      writeConcern: <document>
    }
)
ParameterTypeDescription
documentdocumentA document to insert into the collection.
writeConcerndocumentOptional. A document expressing the write concernopen in new window. Omit to use the default write concern. .. include:: /includes/extracts/transactions-operations-write-concern.rst

其中:

  • 如果未在文档中指定 _id 字段,则会在插入之前为文档增加 _id 字段并赋予一个唯一的 ObjectId()
  • 如果在文档中指定了 _id 字段,则需要确保 _id 字段唯一以避免重复键错误。

例子:插入不包含 _id 字段的文档

test> db.inventory.insertOne({ item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } })
{
  acknowledged: true,
  insertedId: ObjectId("6528bbc93cbe0deda1f3d786")
}

test> db.inventory.find( { item: "canvas" } )
[
  {
    _id: ObjectId("6528bbc93cbe0deda1f3d786"),
    item: 'canvas',
    qty: 100,
    tags: [ 'cotton' ],
    size: { h: 28, w: 35.5, uom: 'cm' }
  }
]

例子:插入不包含 _id 字段的文档,并捕获异常

test> try {
       db.products.insertOne( { item: "card", qty: 15 } );
    } catch (e) {
       print (e);
    };
{
  acknowledged: true,
  insertedId: ObjectId("6528e0e03cbe0deda1f3d787")
}

例子:插入包含 _id 字段的文档,并捕获异常

test> try {
       db.products.insertOne( { _id: 10, item: "box", qty: 20 } );
    } catch (e) {
       print (e);
    }
{ acknowledged: true, insertedId: 10 }

例子:插入包含相同 _id 字段的文档,并捕获异常

test> try {
       db.products.insertOne( { _id: 10, "item" : "packing peanuts", "qty" : 200 } );
    } catch (e) {
       print (e);
    }
MongoServerError: E11000 duplicate key error collection: test.products index: _id_ dup key: { _id: 10 }
    at InsertOneOperation.execute (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:2:2621871)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:2:2608448
    at async Proxy.insertOne (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:88:204792)
    at async Proxy.insertOne (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:88:275419)
    at async Proxy.<anonymous> (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:88:269665)
    at async Proxy.<anonymous> (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:88:270100)
    at async REPL32:30:27
    at async ShellEvaluator.innerEval (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:88:386453)
    at async ShellEvaluator.customEval (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:88:386592)
    at async MongoshNodeRepl.eval (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:10:67741)
    at async h.eval (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:10:19517) {
  index: 0,
  code: 11000,
  keyPattern: { _id: 1 },
  keyValue: { _id: 10 },
  [Symbol(errorLabels)]: Set(0) {}
}

插入多个文档

使用 db.collection.insertMany() 插入多个文档到集合,其中 collection 为集合名称,如果该集合不存在,则会自动创建。

语法:

db.collection.insertMany(
   [ <document 1> , <document 2>, ... ],
   {
      writeConcern: <document>,
      ordered: <boolean>
   }
)
ParameterTypeDescription
documentdocumentAn array of documents to insert into the collection.
writeConcerndocumentOptional. A document expressing the write concernopen in new window. Omit to use the default write concern.Doopen in new window not explicitly set the write concern for the operation if run in a transaction. To use write concern with transactions, see Transactions and Write Concern.open in new window
orderedbooleanOptional. A boolean specifying whether the mongodopen in new window instance should perform an ordered or unordered insert. Defaults to true.

其中:

  • 如果未在文档中指定 _id 字段,则会在插入之前为文档增加 _id 字段并赋予一个唯一的 ObjectId()
  • 如果在文档中指定了 _id 字段,则需要确保 _id 字段唯一以避免重复键错误。
  • 插入默认是有序的。

例子:插入不包含 _id 字段的多个文档

test> 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" } }
    ])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("6528e7f93cbe0deda1f3d788"),
    '1': ObjectId("6528e7f93cbe0deda1f3d789"),
    '2': ObjectId("6528e7f93cbe0deda1f3d78a")
  }
}

例子:插入不包含 _id 字段的多个文档,并捕获异常

test> try {
       db.products.insertMany( [
          { item: "card", qty: 15 },
          { item: "envelope", qty: 20 },
          { item: "stamps" , qty: 30 }
       ] );
    } catch (e) {
       print (e);
    }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("6528e83e3cbe0deda1f3d78b"),
    '1': ObjectId("6528e83e3cbe0deda1f3d78c"),
    '2': ObjectId("6528e83e3cbe0deda1f3d78d")
  }
}

例子:插入包含 _id 字段的多个文档,并捕获异常

test> try {
       db.products.insertMany( [
          { _id: 9, item: "large box", qty: 20 },
          { _id: 11, item: "small box", qty: 55 },
          { _id: 12, item: "medium box", qty: 30 }
       ] );
    } catch (e) {
       print (e);
    }
{ acknowledged: true, insertedIds: { '0': 9, '1': 11, '2': 12 } }

例子:插入包含重复 _id 字段的多个文档,并捕获异常

test> try {
       db.products.insertMany( [
          { _id: 13, item: "envelopes", qty: 60 },
          { _id: 13, item: "stamps", qty: 110 },
          { _id: 14, item: "packing tape", qty: 38 }
       ] );
    } catch (e) {
       print (e);
    }
MongoBulkWriteError: E11000 duplicate key error collection: test.products index: _id_ dup key: { _id: 13 }
    at OrderedBulkOperation.handleWriteError (/tmp/m/boxednode/mongosh/node-v16.20.0/out/Release/node:2:2072022)
    at c (/tmp/m/boxednode/mongosh/node-v16.20.0/out/Release/node:2:2063561)
    at /tmp/m/boxednode/mongosh/node-v16.20.0/out/Release/node:2:2377687
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  code: 11000,
  writeErrors: [
    WriteError {
      err: {
        index: 1,
        code: 11000,
        errmsg: 'E11000 duplicate key error collection: test.products index: _id_ dup key: { _id: 13 }',
        errInfo: undefined,
        op: { _id: 13, item: 'stamps', qty: 110 }
      }
    }
  ],
  result: BulkWriteResult {
    insertedCount: 1,
    matchedCount: 0,
    modifiedCount: 0,
    deletedCount: 0,
    upsertedCount: 0,
    upsertedIds: {},
    insertedIds: { '0': 13, '1': 13, '2': 14 }
  },
  [Symbol(errorLabels)]: Set(0) {}
}

有序插入时,重复文档及其后面的文档都没有插入。

例子:无序插入包含重复 _id 字段的多个文档,并捕获异常

test> db.products.drop()
true

test> try {
       db.products.insertMany( [
          { _id: 10, item: "large box", qty: 20 },
          { _id: 11, item: "small box", qty: 55 },
          { _id: 11, item: "medium box", qty: 30 },
          { _id: 12, item: "envelope", qty: 100},
          { _id: 13, item: "stamps", qty: 125 },
          { _id: 13, item: "tape", qty: 20},
          { _id: 14, item: "bubble wrap", qty: 30}
       ], { ordered: false } );
    } catch (e) {
       print (e);
    }
MongoBulkWriteError: E11000 duplicate key error collection: test.products index: _id_ dup key: { _id: 11 }
    at UnorderedBulkOperation.handleWriteError (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:2:2371689)
    at UnorderedBulkOperation.handleWriteError (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:2:2374148)
    at l (/tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:2:2365689)
    at /tmp/m/boxednode/mongosh/node-v20.6.1/out/Release/node:2:2706152
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 11000,
  writeErrors: [
    WriteError {
      err: {
        index: 2,
        code: 11000,
        errmsg: 'E11000 duplicate key error collection: test.products index: _id_ dup key: { _id: 11 }',
        errInfo: undefined,
        op: { _id: 11, item: 'medium box', qty: 30 }
      }
    },
    WriteError {
      err: {
        index: 5,
        code: 11000,
        errmsg: 'E11000 duplicate key error collection: test.products index: _id_ dup key: { _id: 13 }',
        errInfo: undefined,
        op: { _id: 13, item: 'tape', qty: 20 }
      }
    }
  ],
  result: BulkWriteResult {
    insertedCount: 5,
    matchedCount: 0,
    modifiedCount: 0,
    deletedCount: 0,
    upsertedCount: 0,
    upsertedIds: {},
    insertedIds: { '0': 10, '1': 11, '2': 11, '3': 12, '4': 13, '5': 13, '6': 14 }
  },
  [Symbol(errorLabels)]: Set(0) {}
}

无序插入时,只有重复 _id 的文档没有插入。

查询文档

使用 db.collection.find() 查询文档,其中 collection 为集合名称。

语法:

db.collection.find( <query>, <filter>, <options> )
ParameterTypeDescription
querydocumentOptional. Specifies selection filter using query operatorsopen in new window. To return all documents in a collection, omit this parameter or pass an empty document ({}).
projectiondocumentOptional. Specifies the fields to return in the documents that match the query filter. To return all fields in the matching documents, omit this parameter. For details, see Projection.open in new window
optionsdocumentOptional. Specifies additional options for the query. These options modify query behavior and how results are returned. To see available options, see FindOptions.open in new window

创建用于查询的集合并插入文档:

db.inventory.insertMany([
   { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" },
   { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
   { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
   { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" }
]);

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652ccd074e9c03eca4e4905e"),
    '1': ObjectId("652ccd074e9c03eca4e4905f"),
    '2': ObjectId("652ccd074e9c03eca4e49060"),
    '3': ObjectId("652ccd074e9c03eca4e49061"),
    '4': ObjectId("652ccd074e9c03eca4e49062")
  }
}

查询所有文档

使用过滤谓词 {} 查询集合所有文档。

db.inventory.find( {} )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e4905f"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49060"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'D'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49061"),
    item: 'planner',
    qty: 75,
    size: { h: 22.85, w: 30, uom: 'cm' },
    status: 'D'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49062"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    status: 'A'
  }
]

对比 SQL:

SELECT * FROM inventory

指定相等条件

使用 <field>:<value> 表达式指定相等条件。

语法:

{ <field1>: <value1>, ... }

例子:查询 status 等于 "D" 的文档

db.inventory.find( { status: "D" } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e49060"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'D'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49061"),
    item: 'planner',
    qty: 75,
    size: { h: 22.85, w: 30, uom: 'cm' },
    status: 'D'
  }
]

对比 SQL:

SELECT * FROM inventory WHERE status = "D"

使用查询运算符

使用查询运算符指定条件。

语法:

{ <field1>: { <operator1>: <value1> }, ... }

查询运算符包括:

  • Comparison
NameDescription
$eqopen in new windowMatches values that are equal to a specified value.
$gtopen in new windowMatches values that are greater than a specified value.
$gteopen in new windowMatches values that are greater than or equal to a specified value.
$inopen in new windowMatches any of the values specified in an array.
$ltopen in new windowMatches values that are less than a specified value.
$lteopen in new windowMatches values that are less than or equal to a specified value.
$neopen in new windowMatches all values that are not equal to a specified value.
$ninopen in new windowMatches none of the values specified in an array.
  • Logical
NameDescription
$andopen in new windowJoins query clauses with a logical AND returns all documents that match the conditions of both clauses.
$notopen in new windowInverts the effect of a query expression and returns documents that do not match the query expression.
$noropen in new windowJoins query clauses with a logical NOR returns all documents that fail to match both clauses.
$oropen in new windowJoins query clauses with a logical OR returns all documents that match the conditions of either clause.
  • Element
NameDescription
$existsopen in new windowMatches documents that have the specified field.
$typeopen in new windowSelects documents if a field is of the specified type.
  • Evaluation
NameDescription
$expropen in new windowAllows use of aggregation expressions within the query language.
$jsonSchemaopen in new windowValidate documents against the given JSON Schema.
$modopen in new windowPerforms a modulo operation on the value of a field and selects documents with a specified result.
$regexopen in new windowSelects documents where values match a specified regular expression.
$textopen in new windowPerforms text search.
$whereopen in new windowMatches documents that satisfy a JavaScript expression.
  • Geospatial
NameDescription
$geoIntersectsopen in new windowSelects geometries that intersect with a GeoJSONopen in new window geometry. The 2dsphereopen in new window index supports $geoIntersects.open in new window
$geoWithinopen in new windowSelects geometries within a bounding GeoJSON geometryopen in new window. The 2dsphereopen in new window and 2dopen in new window indexes support $geoWithin.open in new window
$nearopen in new windowReturns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphereopen in new window and 2dopen in new window indexes support $near.open in new window
$nearSphereopen in new windowReturns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The 2dsphereopen in new window and 2dopen in new window indexes support $nearSphere.open in new window
  • Array
NameDescription
$allopen in new windowMatches arrays that contain all elements specified in the query.
$elemMatchopen in new windowSelects documents if element in the array field matches all the specified $elemMatchopen in new window conditions.
$sizeopen in new windowSelects documents if the array field is a specified size.
  • Bitwise
NameDescription
$bitsAllClearopen in new windowMatches numeric or binary values in which a set of bit positions all have a value of 0.
$bitsAllSetopen in new windowMatches numeric or binary values in which a set of bit positions all have a value of 1.
$bitsAnyClearopen in new windowMatches numeric or binary values in which any bit from a set of bit positions has a value of 0.
$bitsAnySetopen in new windowMatches numeric or binary values in which any bit from a set of bit positions has a value of 1.
  • Projection Operators
NameDescription
$open in new windowProjects the first element in an array that matches the query condition.
$elemMatchopen in new windowProjects the first element in an array that matches the specified $elemMatchopen in new window condition.
$metaopen in new windowProjects the document's score assigned during $textopen in new window operation.
$sliceopen in new windowLimits the number of elements projected from an array. Supports skip and limit slices.
  • Miscellaneous Operators
NameDescription
$commentopen in new windowAdds a comment to a query predicate.
$randopen in new windowGenerates a random float between 0 and 1.

例子:查询 status 等于 "A" 或者 "D" 的文档

db.inventory.find( { status: { $in: [ "A", "D" ] } } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e4905f"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49060"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'D'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49061"),
    item: 'planner',
    qty: 75,
    size: { h: 22.85, w: 30, uom: 'cm' },
    status: 'D'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49062"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    status: 'A'
  }
]

对比 SQL:

SELECT * FROM inventory WHERE status in ("A", "D")

指定与条件

使用 $and 获取同时满足多个条件的文档。

例子:查询 status 等于 "A"qty 小于 30 的文档

db.inventory.find( { status: "A", qty: { $lt: 30 } } )
db.inventory.find( { $and: [ { status: "A" }, { qty: { $lt: 30 } } ] } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  }
]

对比 SQL:

SELECT * FROM inventory WHERE status = "A" AND qty < 30

指定或条件

使用 $or获取满足任一条件的文档。

例子:查询 status 等于 "A"qty 小于 30 的文档

db.inventory.find( { $or: [ { status: "A" }, { qty: { $lt: 30 } } ] } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e4905f"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49062"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    status: 'A'
  }
]

对比 SQL:

SELECT * FROM inventory WHERE status = "A" OR qty < 30

例子:查询 status 等于 "A" 且 满足 qty 小于 30 或 itemp 开头的任一条件的文档

db.inventory.find( {
     status: "A",
     $or: [ { qty: { $lt: 30 } }, { item: /^p/ } ]
} )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49062"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    status: 'A'
  }
]

对比 SQL:

SELECT * FROM inventory WHERE status = "A" AND ( qty < 30 OR item LIKE "p%")

提示:

MongoDB 使用正则表达式 $regex 进行字符串匹配。

查询嵌套字段

使用 "field.nestedField" 查询嵌套字段,需要使用双引号包裹。

例子:查询 "size.uom" 等于 "in" 的文档

db.inventory.find( { "size.uom": "in" } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905f"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49060"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'D'
  }
]

例子:查询 "size.h" 小于 15 的文档

db.inventory.find( { "size.h": { $lt: 15 } } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e4905f"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'A'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49060"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'D'
  },
  {
    _id: ObjectId("652ccd074e9c03eca4e49062"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    status: 'A'
  }
]

例子:查询 "size.h" 小于 15 且 "size.uom" 等于 "in"status 等于 "D" 的文档

db.inventory.find( { "size.h": { $lt: 15 }, "size.uom": "in", status: "D" } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e49060"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    status: 'D'
  }
]

例子:查询 size{ h: 14, w: 21, uom: "cm" } 的文档

db.inventory.find( { size: { h: 14, w: 21, uom: "cm" } } )

执行结果:

[
  {
    _id: ObjectId("652ccd074e9c03eca4e4905e"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    status: 'A'
  }
]

注意:

对于嵌套字段使用 { <field>: <value> } 进行匹配时, <value> 需要与文档完全匹配,包括字段的顺序。

以下语句将不会有结果返回:

db.inventory.find(  { size: { w: 21, h: 14, uom: "cm" } }  )

查询数组

先插入数据:

db.inventory.insertMany([
   { item: "journal", qty: 25, tags: ["blank", "red"], dim_cm: [ 14, 21 ] },
   { item: "notebook", qty: 50, tags: ["red", "blank"], dim_cm: [ 14, 21 ] },
   { item: "paper", qty: 100, tags: ["red", "blank", "plain"], dim_cm: [ 14, 21 ] },
   { item: "planner", qty: 75, tags: ["blank", "red"], dim_cm: [ 22.85, 30 ] },
   { item: "postcard", qty: 45, tags: ["blue"], dim_cm: [ 10, 15.25 ] }
]);

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652ce2d94e9c03eca4e49063"),
    '1': ObjectId("652ce2d94e9c03eca4e49064"),
    '2': ObjectId("652ce2d94e9c03eca4e49065"),
    '3': ObjectId("652ce2d94e9c03eca4e49066"),
    '4': ObjectId("652ce2d94e9c03eca4e49067")
  }
}

使用查询文档 { <field>: <value> } ,当 <value> 为数组时,进行相等条件匹配查询,同时顺序也要一致。

例子:查询 tags 匹配 ["red", "blank"] 的文档

db.inventory.find( { tags: ["red", "blank"] } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49064"),
    item: 'notebook',
    qty: 50,
    tags: [ 'red', 'blank' ],
    dim_cm: [ 14, 21 ]
  }
]

例子:使用 $all 操作符查询 tags 中包含 "red""blank" 的文档,不论顺序

db.inventory.find( { tags: { $all: ["red", "blank"] } } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49063"),
    item: 'journal',
    qty: 25,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49064"),
    item: 'notebook',
    qty: 50,
    tags: [ 'red', 'blank' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49065"),
    item: 'paper',
    qty: 100,
    tags: [ 'red', 'blank', 'plain' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49066"),
    item: 'planner',
    qty: 75,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 22.85, 30 ]
  }
]

例子:查询 tags 包含 "red" 的文档

db.inventory.find( { tags: "red" } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49063"),
    item: 'journal',
    qty: 25,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49064"),
    item: 'notebook',
    qty: 50,
    tags: [ 'red', 'blank' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49065"),
    item: 'paper',
    qty: 100,
    tags: [ 'red', 'blank', 'plain' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49066"),
    item: 'planner',
    qty: 75,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 22.85, 30 ]
  }
]

例子:查询 dim_cm 中任一元素大于 25 的文档

db.inventory.find( { dim_cm: { $gt: 25 } } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49066"),
    item: 'planner',
    qty: 75,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 22.85, 30 ]
  }
]

例子:查询 dim_cm 中存在大于 15 和小于 20 的元素的文档

db.inventory.find( { dim_cm: { $gt: 15, $lt: 20 } } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49063"),
    item: 'journal',
    qty: 25,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49064"),
    item: 'notebook',
    qty: 50,
    tags: [ 'red', 'blank' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49065"),
    item: 'paper',
    qty: 100,
    tags: [ 'red', 'blank', 'plain' ],
    dim_cm: [ 14, 21 ]
  },
  {
    _id: ObjectId("652ce2d94e9c03eca4e49067"),
    item: 'postcard',
    qty: 45,
    tags: [ 'blue' ],
    dim_cm: [ 10, 15.25 ]
  }
]

例子:使用 $elemMatch 查询 dim_cm 中某个元素满足大于 22 且小于 30 的文档

db.inventory.find( { dim_cm: { $elemMatch: { $gt: 22, $lt: 30 } } } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49066"),
    item: 'planner',
    qty: 75,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 22.85, 30 ]
  }
]

例子:使用数组索引查询 dim_cm 中第二个元素大于 25 的文档

db.inventory.find( { "dim_cm.1": { $gt: 25 } } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49066"),
    item: 'planner',
    qty: 75,
    tags: [ 'blank', 'red' ],
    dim_cm: [ 22.85, 30 ]
  }
]

例子:使用 $size 查询 tags 中包含三个元素的文档

db.inventory.find( { "tags": { $size: 3 } } )

执行结果:

[
  {
    _id: ObjectId("652ce2d94e9c03eca4e49065"),
    item: 'paper',
    qty: 100,
    tags: [ 'red', 'blank', 'plain' ],
    dim_cm: [ 14, 21 ]
  }
]

对于数组元素为文档情况,先插入以下数据:

db.inventory.insertMany( [
   { item: "journal", instock: [ { warehouse: "A", qty: 5 }, { warehouse: "C", qty: 15 } ] },
   { item: "notebook", instock: [ { warehouse: "C", qty: 5 } ] },
   { item: "paper", instock: [ { warehouse: "A", qty: 60 }, { warehouse: "B", qty: 15 } ] },
   { item: "planner", instock: [ { warehouse: "A", qty: 40 }, { warehouse: "B", qty: 5 } ] },
   { item: "postcard", instock: [ { warehouse: "B", qty: 15 }, { warehouse: "C", qty: 35 } ] }
]);

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652cf23e4e9c03eca4e49068"),
    '1': ObjectId("652cf23e4e9c03eca4e49069"),
    '2': ObjectId("652cf23e4e9c03eca4e4906a"),
    '3': ObjectId("652cf23e4e9c03eca4e4906b"),
    '4': ObjectId("652cf23e4e9c03eca4e4906c")
  }
}

例子:查询 instock 中有 { warehouse: "A", qty: 5 } 的文档

db.inventory.find( { "instock": { warehouse: "A", qty: 5 } } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  }
]

例子:查询 instock 中元素包含 qty 且其值小于等于 20 的文档

db.inventory.find( { "instock.qty": { $lte: 20 } } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e49069"),
    item: 'notebook',
    instock: [ { warehouse: 'C', qty: 5 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906a"),
    item: 'paper',
    instock: [ { warehouse: 'A', qty: 60 }, { warehouse: 'B', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906b"),
    item: 'planner',
    instock: [ { warehouse: 'A', qty: 40 }, { warehouse: 'B', qty: 5 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906c"),
    item: 'postcard',
    instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
  }
]

例子:查询 instock 中第一个元素包含 qty 且其值小于等于 20 的文档

db.inventory.find( { "instock.0.qty": { $lte: 20 } } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e49069"),
    item: 'notebook',
    instock: [ { warehouse: 'C', qty: 5 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906c"),
    item: 'postcard',
    instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
  }
]

例子:查询 instock 中某个元素包含 qty 为 5 和 warehouse"A" 的文档

db.inventory.find( { "instock": { $elemMatch: { qty: 5, warehouse: "A" } } } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  }
]

例子:查询 instock 中某个元素包含 qty 大于 10 且小于等于 20 的文档

db.inventory.find( { "instock": { $elemMatch: { qty: { $gt: 10, $lte: 20 } } } } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906a"),
    item: 'paper',
    instock: [ { warehouse: 'A', qty: 60 }, { warehouse: 'B', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906c"),
    item: 'postcard',
    instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
  }
]

例子:查询 instock 中元素包含 qty 大于 10 和元素包含 qty 小于等于 20 的文档,此时不使用 $elemMatch

db.inventory.find( { "instock.qty": { $gt: 10,  $lte: 20 } } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906a"),
    item: 'paper',
    instock: [ { warehouse: 'A', qty: 60 }, { warehouse: 'B', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906b"),
    item: 'planner',
    instock: [ { warehouse: 'A', qty: 40 }, { warehouse: 'B', qty: 5 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906c"),
    item: 'postcard',
    instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
  }
]

例子:查询 instock 中元素包含 qty 等于 5 和元素包含 warehouse 等于 "A" 的文档

db.inventory.find( { "instock.qty": 5, "instock.warehouse": "A" } )

执行结果:

[
  {
    _id: ObjectId("652cf23e4e9c03eca4e49068"),
    item: 'journal',
    instock: [ { warehouse: 'A', qty: 5 }, { warehouse: 'C', qty: 15 } ]
  },
  {
    _id: ObjectId("652cf23e4e9c03eca4e4906b"),
    item: 'planner',
    instock: [ { warehouse: 'A', qty: 40 }, { warehouse: 'B', qty: 5 } ]
  }
]

指定返回字段

先插入示例数据:

db.inventory.insertMany( [
  { item: "journal", status: "A", size: { h: 14, w: 21, uom: "cm" }, instock: [ { warehouse: "A", qty: 5 } ] },
  { item: "notebook", status: "A",  size: { h: 8.5, w: 11, uom: "in" }, instock: [ { warehouse: "C", qty: 5 } ] },
  { item: "paper", status: "D", size: { h: 8.5, w: 11, uom: "in" }, instock: [ { warehouse: "A", qty: 60 } ] },
  { item: "planner", status: "D", size: { h: 22.85, w: 30, uom: "cm" }, instock: [ { warehouse: "A", qty: 40 } ] },
  { item: "postcard", status: "A", size: { h: 10, w: 15.25, uom: "cm" }, instock: [ { warehouse: "B", qty: 15 }, { warehouse: "C", qty: 35 } ] }
]);

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652cfba44e9c03eca4e4906d"),
    '1': ObjectId("652cfba44e9c03eca4e4906e"),
    '2': ObjectId("652cfba44e9c03eca4e4906f"),
    '3': ObjectId("652cfba44e9c03eca4e49070"),
    '4': ObjectId("652cfba44e9c03eca4e49071")
  }
}

例子:默认返回满足条件的文档的所有字段

db.inventory.find( { status: "A" } )

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    status: 'A',
    size: { h: 14, w: 21, uom: 'cm' },
    instock: [ { warehouse: 'A', qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    status: 'A',
    size: { h: 8.5, w: 11, uom: 'in' },
    instock: [ { warehouse: 'C', qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    status: 'A',
    size: { h: 10, w: 15.25, uom: 'cm' },
    instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
  }
]

对比 SQL:

SELECT * from inventory WHERE status = "A"

通过在投影文档中设置 <field>1 来指定返回的文档的字段,默认返回 _id 字段。

例子:返回满足条件的文档的 _iditemstatus 字段

db.inventory.find( { status: "A" }, { item: 1, status: 1 } )

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    status: 'A'
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    status: 'A'
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    status: 'A'
  }
]

对比 SQL:

SELECT _id, item, status from inventory WHERE status = "A"

例子:在投影文档中指定 _id0 不返回 _id 字段

db.inventory.find( { status: "A" }, { item: 1, status: 1, _id: 0 } )

执行结果:

[
  { item: 'journal', status: 'A' },
  { item: 'notebook', status: 'A' },
  { item: 'postcard', status: 'A' }
]

对比 SQL:

SELECT item, status from inventory WHERE status = "A"

例子:在投影文档中指定 statusinstock0 则不返回这两个字段

db.inventory.find( { status: "A" }, { status: 0, instock: 0 } )

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    size: { h: 14, w: 21, uom: 'cm' }
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    size: { h: 8.5, w: 11, uom: 'in' }
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    size: { h: 10, w: 15.25, uom: 'cm' }
  }
]

例子:在投影文档中指定显示嵌套文档的字段

db.inventory.find(
   { status: "A" },
   { item: 1, status: 1, "size.uom": 1 }
)

MongoDB 4.4 版本起:

db.inventory.find(
   { status: "A" },
   { item: 1, status: 1, size: { uom: 1 } }
)

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    status: 'A',
    size: { uom: 'cm' }
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    status: 'A',
    size: { uom: 'in' }
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    status: 'A',
    size: { uom: 'cm' }
  }
]

例子:在投影文档中指定不显示嵌套文档的字段

db.inventory.find(
   { status: "A" },
   { "size.uom": 0 }
)

MongoDB 4.4 版本起:

db.inventory.find(
   { status: "A" },
   { size: { uom: 0 } }
)

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    status: 'A',
    size: { h: 14, w: 21 },
    instock: [ { warehouse: 'A', qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    status: 'A',
    size: { h: 8.5, w: 11 },
    instock: [ { warehouse: 'C', qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    status: 'A',
    size: { h: 10, w: 15.25 },
    instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
  }
]

例子:在投影文档中指定显示数组中的元素

db.inventory.find( { status: "A" }, { item: 1, status: 1, "instock.qty": 1 } )

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    status: 'A',
    instock: [ { qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    status: 'A',
    instock: [ { qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    status: 'A',
    instock: [ { qty: 15 }, { qty: 35 } ]
  }
]

例子:在投影文档中使用 $slice 操作符返回数组 instock 最后一个元素

db.inventory.find( { status: "A" }, { item: 1, status: 1, instock: { $slice: -1 } } )

执行结果:

[
  {
    _id: ObjectId("652cfba44e9c03eca4e4906d"),
    item: 'journal',
    status: 'A',
    instock: [ { warehouse: 'A', qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e4906e"),
    item: 'notebook',
    status: 'A',
    instock: [ { warehouse: 'C', qty: 5 } ]
  },
  {
    _id: ObjectId("652cfba44e9c03eca4e49071"),
    item: 'postcard',
    status: 'A',
    instock: [ { warehouse: 'C', qty: 35 } ]
  }
]

查询空值或不存在的字段

先插入示例数据:

db.inventory.insertMany([
   { _id: 1, item: null },
   { _id: 2 }
])

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }

例子:使用 { item : null } 查询包含 item 字段但其值为 null 或者不包含 item 字段的文档

db.inventory.find( { item: null } )

执行结果:

[ { _id: 1, item: null }, { _id: 2 } ]

例子:使用 { item : { $type: 10 } } 查询只包含 item 字段但其值为 null 的文档

db.inventory.find( { item : { $type: 10 } } )

执行结果:

[ { _id: 1, item: null } ]

例子:使用 { item : { $exists: false } } 查询不包含 item 字段的文档

db.inventory.find( { item : { $exists: false } } )

执行结果:

[ { _id: 2 } ]

更新文档

更新的方法有:

  • db.collection.updateOne(<filter>, <update>, <options>)
  • db.collection.updateMany(<filter>, <update>, <options>)
  • db.collection.replaceOne(<filter>, <update>, <options>)

语法:

{
  <update operator>: { <field1>: <value1>, ... },
  <update operator>: { <field2>: <value2>, ... },
  ...
}

更新操作符包括:

  • Fields
NameDescription
$currentDateopen in new windowSets the value of a field to current date, either as a Date or a Timestamp.
$incopen in new windowIncrements the value of the field by the specified amount.
$minopen in new windowOnly updates the field if the specified value is less than the existing field value.
$maxopen in new windowOnly updates the field if the specified value is greater than the existing field value.
$mulopen in new windowMultiplies the value of the field by the specified amount.
$renameopen in new windowRenames a field.
$setopen in new windowSets the value of a field in a document.
$setOnInsertopen in new windowSets the value of a field if an update results in an insert of a document. Has no effect on update operations that modify existing documents.
$unsetopen in new windowRemoves the specified field from a document.
  • Array Operators
NameDescription
$open in new windowActs as a placeholder to update the first element that matches the query condition.
$[]open in new windowActs as a placeholder to update all elements in an array for the documents that match the query condition.
$[<identifier>]open in new windowActs as a placeholder to update all elements that match the arrayFilters condition for the documents that match the query condition.
$addToSetopen in new windowAdds elements to an array only if they do not already exist in the set.
$popopen in new windowRemoves the first or last item of an array.
$pullopen in new windowRemoves all array elements that match a specified query.
$pushopen in new windowAdds an item to an array.
$pullAllopen in new windowRemoves all matching values from an array.
  • Array Modifiers
NameDescription
$eachopen in new windowModifies the $pushopen in new window and $addToSetopen in new window operators to append multiple items for array updates.
$positionopen in new windowModifies the $pushopen in new window operator to specify the position in the array to add elements.
$sliceopen in new windowModifies the $pushopen in new window operator to limit the size of updated arrays.
$sortopen in new windowModifies the $pushopen in new window operator to reorder documents stored in an array.
  • Bitwise
NameDescription
$bitopen in new windowPerforms bitwise AND, OR, and XOR updates of integer values.

先插入示例数据:

db.inventory.insertMany( [
   { item: "canvas", qty: 100, size: { h: 28, w: 35.5, uom: "cm" }, status: "A" },
   { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "mat", qty: 85, size: { h: 27.9, w: 35.5, uom: "cm" }, status: "A" },
   { item: "mousepad", qty: 25, size: { h: 19, w: 22.85, uom: "cm" }, status: "P" },
   { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "P" },
   { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
   { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
   { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" },
   { item: "sketchbook", qty: 80, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "sketch pad", qty: 95, size: { h: 22.85, w: 30.5, uom: "cm" }, status: "A" }
] );

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652e02dc4479ad978c309aa6"),
    '1': ObjectId("652e02dc4479ad978c309aa7"),
    '2': ObjectId("652e02dc4479ad978c309aa8"),
    '3': ObjectId("652e02dc4479ad978c309aa9"),
    '4': ObjectId("652e02dc4479ad978c309aaa"),
    '5': ObjectId("652e02dc4479ad978c309aab"),
    '6': ObjectId("652e02dc4479ad978c309aac"),
    '7': ObjectId("652e02dc4479ad978c309aad"),
    '8': ObjectId("652e02dc4479ad978c309aae"),
    '9': ObjectId("652e02dc4479ad978c309aaf")
  }
}

更新单个文档

例子:使用 db.collection.updateOne(<filter>, <update>, <options>) 更新 inventory 集合中 item"paper" 的第一个文档

db.inventory.updateOne(
   { item: "paper" },
   {
     $set: { "size.uom": "cm", status: "P" },
     $currentDate: { lastModified: true }
   }
)

其中:

  • 使用 $set 操作符更新字段 size.uom 的值为 "cm",更新字段 status 的值为 "P"
  • 使用 $currentDate 操作符更新字段 lastModified 的值为当前时间,如果字段 lastModified 不存在,则创建该字段。

执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

更新多个文档

例子:使用 db.collection.updateMany(<filter>, <update>, <options>) 更新 inventory 集合中 qty 小于 50 的所有文档

db.inventory.updateMany(
   { "qty": { $lt: 50 } },
   {
     $set: { "size.uom": "in", status: "P" },
     $currentDate: { lastModified: true }
   }
)

其中:

  • 使用 $set 操作符更新字段 size.uom 的值为 "in",更新字段 status 的值为 "P"
  • 使用 $currentDate 操作符更新字段 lastModified 的值为当前时间,如果字段 lastModified 不存在,则创建该字段。

执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 3,
  modifiedCount: 3,
  upsertedCount: 0
}

例子:为所有文档增加一个字段

db.inventory.updateMany(
    { },
    { $set: { join_date: new Date() } }
)

执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 10,
  modifiedCount: 10,
  upsertedCount: 0
}

对比 SQL:

ALTER TABLE inventory ADD join_date DATETIME

例子:为所有文档删除一个字段

db.inventory.updateMany(
    { },
    { $unset: { "join_date": "" } }
)

执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 10,
  modifiedCount: 10,
  upsertedCount: 0
}

对比 SQL:

ALTER TABLE inventory DROP COLUMN join_date

替换一个文档

例子:使用 db.collection.replaceOne(<filter>, <update>, <options>) 更新 inventory 集合中 item"paper" 的第一个文档

db.inventory.replaceOne(
   { item: "paper" },
   { item: "paper", instock: [ { warehouse: "A", qty: 60 }, { warehouse: "B", qty: 40 } ] }
)

其中:

  • <update> 参数不能包含更新操作符,不要指定 _id 字段。如果包含了 _id 字段,则其值需与原值一致。

执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

删除文档

删除的方法有:

  • db.collection.deleteMany()
  • db.collection.deleteOne()

先插入示例数据:

db.inventory.insertMany( [
   { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "P" },
   { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
   { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
   { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" },
] );

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652e1dc94479ad978c309ab0"),
    '1': ObjectId("652e1dc94479ad978c309ab1"),
    '2': ObjectId("652e1dc94479ad978c309ab2"),
    '3': ObjectId("652e1dc94479ad978c309ab3"),
    '4': ObjectId("652e1dc94479ad978c309ab4")
  }
}

删除所有文档

例子:传递空过滤文档 {}db.collection.deleteMany() 以删除集合中所有文档

db.inventory.deleteMany({})

执行结果:

{ acknowledged: true, deletedCount: 5 }

对比 SQL:

DELETE FROM inventory

删除匹配文档

匹配文档的语法与查询相同:

{ <field1>: <value1>, ... }
{ <field1>: { <operator1>: <value1> }, ... }

例子:删除 inventory 集合中 status 字段为 "A" 的所有文档

db.inventory.deleteMany({ status : "A" })

执行结果:

{ acknowledged: true, deletedCount: 2 }

对比 SQL:

DELETE FROM inventory WHERE status = "A"

删除匹配的一个文档

例子:删除 inventory 集合中 status 字段为 "A" 的第一个文档

db.inventory.deleteOne( { status: "D" } )

执行结果:

{ acknowledged: true, deletedCount: 1 }

批量写操作

使用 db.collection.bulkWrite() 对单个集合进行批量写操作。

语法:

db.collection.bulkWrite(
    [ <operation 1>, <operation 2>, ... ],
    {
      writeConcern : <document>,
      ordered : <boolean>
    }
)

其中:

  • ordered : true:表示顺序执行,性能差,出现错误时,后续操作将不会执行。
  • ordered : false:表示无序执行,性能好,出现错误时,后续操作将继续执行。

db.collection.bulkWrite() 支持以下写操作:

  • insertOne
  • updateOne
  • updateMany
  • replaceOne
  • deleteOne
  • deleteMany

先插入示例数据:

db.pizzas.insertMany( [
   { _id: 0, type: "pepperoni", size: "small", price: 4 },
   { _id: 1, type: "cheese", size: "medium", price: 7 },
   { _id: 2, type: "vegan", size: "large", price: 8 }
] )

执行结果:

{ acknowledged: true, insertedIds: { '0': 0, '1': 1, '2': 2 } }

例子:在 db.collection.bulkWrite() 中使用 insertOne 增加两个文档, updateOne 更新一个文档, deleteOne 删除一个文档, replaceOne 替换一个文档

try {
   db.pizzas.bulkWrite( [
      { insertOne: { document: { _id: 3, type: "beef", size: "medium", price: 6 } } },
      { insertOne: { document: { _id: 4, type: "sausage", size: "large", price: 10 } } },
      { updateOne: {
         filter: { type: "cheese" },
         update: { $set: { price: 8 } }
      } },
      { deleteOne: { filter: { type: "pepperoni"} } },
      { replaceOne: {
         filter: { type: "vegan" },
         replacement: { type: "tofu", size: "small", price: 4 }
      } }
   ] )
} catch( error ) {
   print( error )
}

执行结果:

{
  acknowledged: true,
  insertedCount: 2,
  insertedIds: { '0': 3, '1': 4 },
  matchedCount: 2,
  modifiedCount: 2,
  deletedCount: 1,
  upsertedCount: 0,
  upsertedIds: {}
}

SQL 对照

SQL Terms/ConceptsMongoDB Terms/Concepts
databasedatabaseopen in new window
tablecollectionopen in new window
rowdocumentopen in new window or BSONopen in new window document
columnfieldopen in new window
indexindexopen in new window
table joins$lookupopen in new window, embedded documents
primary keySpecify any unique column or column combination as primary key.primary keyopen in new windowIn MongoDB, the primary key is automatically set to the _idopen in new window field.
aggregation (e.g. group by)aggregation pipelineSee the SQL to Aggregation Mapping Chart.open in new window
SELECT INTO NEW_TABLE$outopen in new windowSee the SQL to Aggregation Mapping Chart.open in new window
MERGE INTO TABLE$mergeopen in new window (Available starting in MongoDB 4.2)See the SQL to Aggregation Mapping Chart.open in new window
UNION ALL$unionWithopen in new window (Available starting in MongoDB 4.4)
transactionstransactionsopen in new window

假设:

  • SQL 数据库表名为 people
  • MongoDB 有名称为 people 集合,其中一个文档内容如下:
{
  _id: ObjectId("509a8fb2f3f4948bd2f983a0"),
  user_id: "abc123",
  age: 55,
  status: 'A'
}
  1. 创建表

SQL:

CREATE TABLE people (
    id MEDIUMINT NOT NULL AUTO_INCREMENT,
    user_id Varchar(30),
    age Number,
    status char(1),
    PRIMARY KEY (id)
)

MongoDB 使用 insertOne() 或者 insertMany() 隐式创建集合,如果没有指定 _id 则自动增加此字段:

db.people.insertOne( {
    user_id: "abc123",
    age: 55,
    status: "A"
 } )

还可以使用 db.createCollection(name) 显式创建集合:

db.createCollection("people")
  1. 增加字段

SQL:

ALTER TABLE people
ADD join_date DATETIME

MongoDB 中没有在集合级别的修改,但可以对已存在文档增加字段:

db.people.updateMany(
    { },
    { $set: { join_date: new Date() } }
)
  1. 删除字段

SQL:

ALTER TABLE people
DROP COLUMN join_date

MongoDB 中没有在集合级别的修改,但可以对已存在文档删除字段:

db.people.updateMany(
    { },
    { $unset: { "join_date": "" } }
)
  1. 创建索引

SQL:

CREATE INDEX idx_user_id_asc ON people(user_id)
CREATE INDEX idx_user_id_asc_age_desc ON people(user_id, age DESC)

MongoDB:

db.people.createIndex( { user_id: 1 } )
db.people.createIndex( { user_id: 1, age: -1 } )
  1. 删除表

SQL:

DROP TABLE people

MongoDB:

db.people.drop()
  1. 插入数据

SQL:

INSERT INTO people(user_id,age,status)
VALUES ("bcd001",45,"A")

MongoDB:

db.people.insertOne(
   { user_id: "bcd001", age: 45, status: "A" }
)
  1. 查询所有数据

SQL:

SELECT *
FROM people

MongoDB:

db.people.find()
  1. 查询指定字段所有数据

SQL:

SELECT id,
       user_id,
       status
FROM people;
	
db.people.find(
    { },
    { user_id: 1, status: 1 }
)

MongoDB:

db.people.find(
    { },
    { user_id: 1, status: 1 }
)

db.people.find(
    { },
    { user_id: 1, status: 1, _id: 0 }
)
  1. 查询满足条件的所有数据

SQL:

SELECT *
FROM people
WHERE status = "A"

SELECT *
FROM people
WHERE status != "A"

SELECT *
FROM people
WHERE status = "A"
AND age = 50

SELECT *
FROM people
WHERE status = "A"
OR age = 50

SELECT *
FROM people
WHERE age > 25

SELECT *
FROM people
WHERE age < 25

SELECT *
FROM people
WHERE age > 25
AND   age <= 50

SELECT *
FROM people
WHERE user_id like "%bc%"

SELECT *
FROM people
WHERE user_id like "bc%"

MongoDB:

db.people.find(
    { status: "A" }
)

db.people.find(
    { status: { $ne: "A" } }
)

db.people.find(
    { status: "A",
      age: 50 }
)

db.people.find(
    { $or: [ { status: "A" } , { age: 50 } ] }
)

db.people.find(
    { age: { $gt: 25 } }
)

db.people.find(
   { age: { $lt: 25 } }
)

db.people.find(
   { age: { $gt: 25, $lte: 50 } }
)

db.people.find( { user_id: /bc/ } )
db.people.find( { user_id: { $regex: /bc/ } } )

db.people.find( { user_id: /^bc/ } )
db.people.find( { user_id: { $regex: /^bc/ } } )
  1. 查询满足条件的指定字段的数据

SQL:

SELECT user_id, status
FROM people
WHERE status = "A"

MongoDB:

db.people.find(
    { status: "A" },
    { user_id: 1, status: 1, _id: 0 }
)
  1. 查询满足条件的数据并排序

SQL:

SELECT *
FROM people
WHERE status = "A"
ORDER BY user_id ASC

SELECT *
FROM people
WHERE status = "A"
ORDER BY user_id DESC

MongoDB:

db.people.find( { status: "A" } ).sort( { user_id: 1 } )
db.people.find( { status: "A" } ).sort( { user_id: -1 } )
  1. 查询记录数量

SQL:

SELECT COUNT(*)
FROM people

SELECT COUNT(user_id)
FROM people

SELECT COUNT(*)
FROM people
WHERE age > 30

MongoDB:

db.people.count()
db.people.find().count()

db.people.count( { user_id: { $exists: true } } )
db.people.find( { user_id: { $exists: true } } ).count()

db.people.count( { age: { $gt: 30 } } )
db.people.find( { age: { $gt: 30 } } ).count()
  1. 查询不同的记录

SQL:

SELECT DISTINCT(status)
FROM people

MongoDB:

db.people.aggregate( [ { $group : { _id : "$status" } } ] )
db.people.distinct( "status" )
  1. 限制返回的记录数

SQL:

SELECT *
FROM people
LIMIT 1

SELECT *
FROM people
LIMIT 5
SKIP 10

MongoDB:

db.people.findOne()
db.people.find().limit(1)

db.people.find().limit(5).skip(10)
  1. 查看执行计划

SQL:

EXPLAIN SELECT *
FROM people
WHERE status = "A"

MongoDB:

db.people.find( { status: "A" } ).explain()
  1. 更新记录

SQL:

UPDATE people
SET status = "C"
WHERE age > 25

UPDATE people
SET age = age + 3
WHERE status = "A"

MongoDB:

db.people.updateMany(
   { age: { $gt: 25 } },
   { $set: { status: "C" } }
)

db.people.updateMany(
   { status: "A" } ,
   { $inc: { age: 3 } }
)
  1. 删除记录

SQL:

DELETE FROM people
WHERE status = "D"

DELETE FROM people

MongoDB:

db.people.deleteMany( { status: "D" } )

db.people.deleteMany({})

地理空间查询

  • MongoDB 支持查询地理空间数据。
  • 在 MongoDB 中,可以存储地理空间数据为 GeoJSON 对象或坐标数据。
  • 可以使用地理空间索引提高查询性能。

GeoJSON 对象

要计算球面距离,使用嵌套文档指定 GeoJSON 对象,其中 type 字段为GeoJSON 对象类型,coordinates 字段为对象坐标,坐标包括经度和纬度,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90

语法:

<field>: { type: <GeoJSON type> , coordinates: <coordinates> }

例如:

location: {
      type: "Point",
      coordinates: [-73.856077, 40.848447]
}

坐标数据

要计算平面距离,存储位置数据为坐标并使用 2d 索引。可以使用数组(优先)或者嵌套文档指定坐标数据。

  • 数组
<field>: [ <x>, <y> ]

如果指定经度和纬度坐标,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90

<field>: [<longitude>, <latitude> ]
  • 嵌套文档
<field>: { <field1>: <x>, <field2>: <y> }

如果指定经度和纬度坐标,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90

<field>: { <field1>: <longitude>, <field2>: <latitude> }

地理空间查询操作符

MongoDB 支持以下地理空间查询运算符:

NameDescription
$geoIntersectsopen in new windowSelects geometries that intersect with a GeoJSONopen in new window geometry. The 2dsphere index supports $geoIntersects.open in new window
$geoWithinopen in new windowSelects geometries within a bounding GeoJSON geometryopen in new window. The 2dsphere and 2d indexes support $geoWithin.open in new window
$nearopen in new windowReturns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near.open in new window
$nearSphereopen in new windowReturns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The 2dsphere and 2d indexes support $nearSphere.open in new window

每种地理空间操作使用的地理空间查询运算符:

OperationSpherical/Flat QueryNotes
$nearopen in new window (GeoJSONopen in new window centroid point in this line and the following line, 2dsphereopen in new window index)SphericalSee also the $nearSphereopen in new window operator, which provides the same functionality when used with GeoJSONopen in new window and a 2dsphereopen in new window index.
$nearopen in new window (legacy coordinatesopen in new window, 2dopen in new window index)Flat
$nearSphereopen in new window (GeoJSONopen in new window point, 2dsphereopen in new window index)SphericalProvides the same functionality as $nearopen in new window operation that uses GeoJSONopen in new window point and a 2dsphereopen in new window index.For spherical queries, it may be preferable to use $nearSphereopen in new window which explicitly specifies the spherical queries in the name rather than $nearopen in new window operator.
$nearSphereopen in new window (legacy coordinatesopen in new window, 2dopen in new window index)SphericalUse GeoJSONopen in new window points instead.
$geoWithinopen in new window : { $geometryopen in new window: ... }Spherical
$geoWithinopen in new window : { $boxopen in new window: ... }Flat
$geoWithinopen in new window : { $polygonopen in new window: ... }Flat
$geoWithinopen in new window : { $centeropen in new window: ... }Flat
$geoWithinopen in new window : { $centerSphereopen in new window: ... }Spherical
$geoIntersectsopen in new windowSpherical
$geoNearopen in new window aggregation stage (2dsphereopen in new window index)Spherical
$geoNearopen in new window aggregation stage (2dopen in new window index)Flat

地理空间聚合

StageDescription
$geoNearopen in new windowReturns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $matchopen in new window, $sortopen in new window, and $limitopen in new window for geospatial data. The output documents include an additional distance field and can include a location identifier field.$geoNearopen in new window requires a geospatial index.open in new window

地理空间查询示例

  • 使用 $geoIntersects 查询附近位置
  • 使用 $geoWithin 查询附近位置的餐馆
  • 使用 $nearSphere 查询附近指定距离内餐馆

下载 Restaurantsopen in new windowNeighborhoodsopen in new window 数据,然后导入到数据库:

[root@linux ~]# mongoimport /root/restaurants.json -c=restaurants
2023-10-23T15:11:42.837+0800    connected to: mongodb://localhost/
2023-10-23T15:11:43.524+0800    25359 document(s) imported successfully. 0 document(s) failed to import.
[root@linux ~]# mongoimport /root/neighborhoods.json -c=neighborhoods
2023-10-23T15:11:23.492+0800    connected to: mongodb://localhost/
2023-10-23T15:11:24.235+0800    195 document(s) imported successfully. 0 document(s) failed to import.

创建 2dsphere 索引:

db.restaurants.createIndex({ location: "2dsphere" })
db.neighborhoods.createIndex({ geometry: "2dsphere" })

查询 restaurants 数据:

db.restaurants.findOne()

执行结果:

{
  _id: ObjectId("55cba2476c522cafdb053add"),
  location: { coordinates: [ -73.856077, 40.848447 ], type: 'Point' },
  name: 'Morris Park Bake Shop'
}

查询 neighborhoods 数据:

db.neighborhoods.findOne()

执行结果:

{
  _id: ObjectId("55cb9c666c522cafdb053a1a"),
  geometry: {
    coordinates: [
      [
        [ -73.94193078816193, 40.70072523469547 ],
        ...
        [ -73.957167301054, 40.69970786791901 ],
        ... 23 more items
      ]
    ],
    type: 'Polygon'
  },
  name: 'Bedford'
}

假设用户位于经度 -73.93414657,经度 40.82302903,使用 $geoIntersects 查找附近位置:

db.neighborhoods.findOne({ geometry: { $geoIntersects: { $geometry: { type: "Point", coordinates: [ -73.93414657, 40.82302903 ] } } } })

执行结果:

{
  _id: ObjectId("55cb9c666c522cafdb053a68"),
  geometry: {
    coordinates: [
      [
        [ -73.93383000695911, 40.81949109558767 ],
        ...
        [ -73.9398049898395, 40.831069790159276 ],
        ... 223 more items
      ]
    ],
    type: 'Polygon'
  },
  name: 'Central Harlem North-Polo Grounds'
}

查找附近位置的餐馆数量:

var neighborhood = db.neighborhoods.findOne( { geometry: { $geoIntersects: { $geometry: { type: "Point", coordinates: [ -73.93414657, 40.82302903 ] } } } } )
db.restaurants.find( { location: { $geoWithin: { $geometry: neighborhood.geometry } } } ).count()

执行结果:

127

使用 $geoWithin$centerSphere 在圆形区域内查询 5 英里范围内的餐馆,无序:

db.restaurants.find({ location:
   { $geoWithin:
      { $centerSphere: [ [ -73.93414657, 40.82302903 ], 5 / 3963.2 ] } } })

其中 3963.2 英里为地球半径。

使用 $nearSphere$maxDistance(以米为单位),返回 5 英里范围内的餐馆,从近到远排序:

var METERS_PER_MILE = 1609.34
db.restaurants.find({ location: { $nearSphere: { $geometry: { type: "Point", coordinates: [ -73.93414657, 40.82302903 ] }, $maxDistance: 5 * METERS_PER_MILE } } })

聚合

MongoDB 使用聚合操作处理多个文档,类似于 SQL 中的分组和组函数。

执行聚合操作,可以使用:

  • 聚合管道(Aggregation Pipelines):执行聚合的首选方法。
  • 单一用途聚合方法(Single purpose aggregation method):用于聚合单个集合中的文档。

单一用途聚合方法包括:

MethodDescription
db.collection.estimatedDocumentCount()open in new windowReturns an approximate count of the documents in a collection or a view.
db.collection.count()open in new windowReturns a count of the number of documents in a collection or a view.
db.collection.distinct()open in new windowReturns an array of documents that have distinct values for the specified field.

聚合管道

聚合管道由一个或多个处理文档的阶段组成,每个阶段对输入文档执行操作后,将输出的文档传递到下一阶段。

例子:聚合管道

先插入示例数据:

db.orders.insertMany( [
   { _id: 0, name: "Pepperoni", size: "small", price: 19,
     quantity: 10, date: ISODate( "2021-03-13T08:14:30Z" ) },
   { _id: 1, name: "Pepperoni", size: "medium", price: 20,
     quantity: 20, date : ISODate( "2021-03-13T09:13:24Z" ) },
   { _id: 2, name: "Pepperoni", size: "large", price: 21,
     quantity: 30, date : ISODate( "2021-03-17T09:22:12Z" ) },
   { _id: 3, name: "Cheese", size: "small", price: 12,
     quantity: 15, date : ISODate( "2021-03-13T11:21:39.736Z" ) },
   { _id: 4, name: "Cheese", size: "medium", price: 13,
     quantity:50, date : ISODate( "2022-01-12T21:23:13.331Z" ) },
   { _id: 5, name: "Cheese", size: "large", price: 14,
     quantity: 10, date : ISODate( "2022-01-12T05:08:13Z" ) },
   { _id: 6, name: "Vegan", size: "small", price: 17,
     quantity: 10, date : ISODate( "2021-01-13T05:08:13Z" ) },
   { _id: 7, name: "Vegan", size: "medium", price: 18,
     quantity: 10, date : ISODate( "2021-01-13T05:10:13Z" ) }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7 }
}

计算 sizemedium 的文档,按照 name 分组的计算 quantity 总计(中等尺寸披萨中,每种披萨的总数量):

db.orders.aggregate( [

   // Stage 1: Filter pizza order documents by pizza size
   {
      $match: { size: "medium" }
   },

   // Stage 2: Group remaining documents by pizza name and calculate total quantity
   {
      $group: { _id: "$name", totalQuantity: { $sum: "$quantity" } }
   }

] )

其中:

  • db.orders.aggregate() 运行聚合管道。

  • $match 阶段过滤出 sizemedium 的文档,并传递给 $group 阶段。

  • $group 阶段按照 name 分组,使用 $sum 计算每种 namequantity 总计,计算结果存在 totalQuantity 中。

执行结果:

[
  { _id: 'Pepperoni', totalQuantity: 20 },
  { _id: 'Cheese', totalQuantity: 50 },
  { _id: 'Vegan', totalQuantity: 10 }
]

计算指定时间段订单总金额和平均订单数量:

db.orders.aggregate( [

   // Stage 1: Filter pizza order documents by date range
   {
      $match:
      {
         "date": { $gte: new ISODate( "2020-01-30" ), $lt: new ISODate( "2022-01-30" ) }
      }
   },

   // Stage 2: Group remaining documents by date and calculate results
   {
      $group:
      {
         _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
         totalOrderValue: { $sum: { $multiply: [ "$price", "$quantity" ] } },
         averageOrderQuantity: { $avg: "$quantity" }
      }
   },

   // Stage 3: Sort documents by totalOrderValue in descending order
   {
      $sort: { totalOrderValue: -1 }
   }

 ] )

其中:

  • $match 阶段过滤出指定时间段的文档,并传递给 $group 阶段。
  • $group 阶段使用 $dateToString 根据 date 分组,使用 $multiply 计算单个文档的金额,使用 $sum 计算每组的总金额,使用 $avg 计算每组的平均订单数量,然后传递给 $sort 阶段。
  • $sort 阶段按照每组的总金额 totalOrderValue 倒序排序并返回。

执行结果:

[
  { _id: '2022-01-12', totalOrderValue: 790, averageOrderQuantity: 30 },
  { _id: '2021-03-13', totalOrderValue: 770, averageOrderQuantity: 15 },
  { _id: '2021-03-17', totalOrderValue: 630, averageOrderQuantity: 30 },
  { _id: '2021-01-13', totalOrderValue: 350, averageOrderQuantity: 10 }
]

例子:查询人口超过 1000 万的州

下载 Zipsopen in new window 数据,然后导入到数据库:

[root@linux ~]# mongoimport /root/zips.json -c=zipcodes
2023-10-26T15:18:36.372+0800    connected to: mongodb://localhost/
2023-10-26T15:18:37.287+0800    29353 document(s) imported successfully. 0 document(s) failed to import.

查询一个文档:

db.zipcodes.findOne()

执行结果:

{
  _id: '01001',
  city: 'AGAWAM',
  loc: [ -72.622739, 42.070206 ],
  pop: 15338,
  state: 'MA'
}

执行查询:

db.zipcodes.aggregate( [
   { $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
   { $match: { totalPop: { $gte: 10*1000*1000 } } }
] )

其中:

  • $group 阶段使用 state 字段分组,计算每组内的 pop 值的总和($sum),保存在 totalPop 中。
  • $match 阶段只输出 totalPop 大于等于($gte)指定值的文档。

执行结果:

[
  { _id: 'IL', totalPop: 11427576 },
  { _id: 'NY', totalPop: 17990402 },
  { _id: 'TX', totalPop: 16984601 },
  { _id: 'CA', totalPop: 29754890 },
  { _id: 'PA', totalPop: 11881643 },
  { _id: 'FL', totalPop: 12686644 },
  { _id: 'OH', totalPop: 10846517 }
]

对比 SQL:

SELECT state, SUM(pop) AS totalPop
FROM zipcodes
GROUP BY state
HAVING totalPop >= (10*1000*1000)

例子:查询每个州每个城市的平均人口

db.zipcodes.aggregate( [
   { $group: { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } },
   { $group: { _id: "$_id.state", avgCityPop: { $avg: "$pop" } } }
] )

执行结果:

[
  { _id: 'MN', avgCityPop: 5372.21375921376 },
  { _id: 'IL', avgCityPop: 9954.334494773519 },
  ...
  { _id: 'UT', avgCityPop: 9518.508287292818 }
  ...
]

例子:查询每个州人口最多和最少的城市

db.zipcodes.aggregate( [
   { $group:
      {
        _id: { state: "$state", city: "$city" },
        pop: { $sum: "$pop" }
      }
   },
   { $sort: { pop: 1 } },
   { $group:
      {
        _id : "$_id.state",
        biggestCity:  { $last: "$_id.city" },
        biggestPop:   { $last: "$pop" },
        smallestCity: { $first: "$_id.city" },
        smallestPop:  { $first: "$pop" }
      }
   },

  // the following $project is optional, and
  // modifies the output format.

  { $project:
    { _id: 0,
      state: "$_id",
      biggestCity:  { name: "$biggestCity",  pop: "$biggestPop" },
      smallestCity: { name: "$smallestCity", pop: "$smallestPop" }
    }
  }
] )

执行结果:

[
  {
    biggestCity: { name: 'HONOLULU', pop: 396643 },
    smallestCity: { name: 'NINOLE', pop: 0 },
    state: 'HI'
  },
  ...
  {
    biggestCity: { name: 'JACKSON', pop: 204788 },
    smallestCity: { name: 'CHUNKY', pop: 79 },
    state: 'MS'
  }
  ...
]

SQL 对照

SQL Terms, Functions, and ConceptsMongoDB Aggregation Operators
WHERE$matchopen in new window
GROUP BY$groupopen in new window
HAVING$matchopen in new window
SELECT$projectopen in new window
ORDER BY$sortopen in new window
LIMIT$limitopen in new window
SUM()$sumopen in new window
COUNT()$sumopen in new window$sortByCountopen in new window
join$lookupopen in new window
SELECT INTO NEW_TABLE$outopen in new window
MERGE INTO TABLE$mergeopen in new window (Available starting in MongoDB 4.2)
UNION ALL$unionWithopen in new window (Available starting in MongoDB 4.4)

假设:

  • SQL 数据库有表 ordersorder_lineitem,通过 order_lineitem.order_idorders.id 字段进行关联。
  • MongoDB 有名称为 orders 集合,其中一个文档内容如下:
{
  cust_id: "abc123",
  ord_date: ISODate("2012-11-02T17:04:11.102Z"),
  status: 'A',
  price: 50,
  items: [ { sku: "xxx", qty: 25, price: 1 },
           { sku: "yyy", qty: 25, price: 1 } ]
}
  1. 计算记录总数

SQL:

SELECT COUNT(*) AS count
FROM orders

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: null,
        count: { $sum: 1 }
     }
   }
] )
  1. 计算金额总计

SQL:

SELECT SUM(price) AS total
FROM orders

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: null,
        total: { $sum: "$price" }
     }
   }
] )
  1. 计算每个用户的金额合计

SQL:

SELECT cust_id,
       SUM(price) AS total
FROM orders
GROUP BY cust_id

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: "$cust_id",
        total: { $sum: "$price" }
     }
   }
] )
  1. 计算每个用户的金额合计并排序

SQL:

SELECT cust_id,
       SUM(price) AS total
FROM orders
GROUP BY cust_id
ORDER BY total

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: "$cust_id",
        total: { $sum: "$price" }
     }
   },
   { $sort: { total: 1 } }
] )
  1. 计算每个用户每天的金额合计

SQL:

SELECT cust_id,
       ord_date,
       SUM(price) AS total
FROM orders
GROUP BY cust_id,
         ord_date

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: {
           cust_id: "$cust_id",
           ord_date: { $dateToString: {
              format: "%Y-%m-%d",
              date: "$ord_date"
           }}
        },
        total: { $sum: "$price" }
     }
   }
] )
  1. 计算每个用户的订单数量

SQL:

SELECT cust_id,
       count(*)
FROM orders
GROUP BY cust_id
HAVING count(*) > 1

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: "$cust_id",
        count: { $sum: 1 }
     }
   },
   { $match: { count: { $gt: 1 } } }
] )
  1. 计算每个用户每天的订单总金额,只返回总金额大于 250 的记录

SQL:

SELECT cust_id,
       ord_date,
       SUM(price) AS total
FROM orders
GROUP BY cust_id,
         ord_date
HAVING total > 250

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: {
           cust_id: "$cust_id",
           ord_date: { $dateToString: {
              format: "%Y-%m-%d",
              date: "$ord_date"
           }}
        },
        total: { $sum: "$price" }
     }
   },
   { $match: { total: { $gt: 250 } } }
] )
  1. 对于状态为 A 的订单记录,计算每个用户的订单总金额

SQL:

SELECT cust_id,
       SUM(price) as total
FROM orders
WHERE status = 'A'
GROUP BY cust_id

MongoDB:

db.orders.aggregate( [
   { $match: { status: 'A' } },
   {
     $group: {
        _id: "$cust_id",
        total: { $sum: "$price" }
     }
   }
] )
  1. 对于状态为 A 的订单记录,计算每个用户的订单总金额,只返回总金额大于 250 的记录

SQL:

SELECT cust_id,
       SUM(price) as total
FROM orders
WHERE status = 'A'
GROUP BY cust_id
HAVING total > 250

MongoDB:

db.orders.aggregate( [
   { $match: { status: 'A' } },
   {
     $group: {
        _id: "$cust_id",
        total: { $sum: "$price" }
     }
   },
   { $match: { total: { $gt: 250 } } }
] )
  1. 关联查询每个用户的商品数量

SQL:

SELECT cust_id,
       SUM(li.qty) as qty
FROM orders o,
     order_lineitem li
WHERE li.order_id = o.id
GROUP BY cust_id

MongoDB:

db.orders.aggregate( [
   { $unwind: "$items" },
   {
     $group: {
        _id: "$cust_id",
        qty: { $sum: "$items.qty" }
     }
   }
] )
  1. 对订单表按照每个用户每天进行分组后,计算总记录数

SQL:

SELECT COUNT(*)
FROM (SELECT cust_id,
             ord_date
      FROM orders
      GROUP BY cust_id,
               ord_date)
      as DerivedTable

MongoDB:

db.orders.aggregate( [
   {
     $group: {
        _id: {
           cust_id: "$cust_id",
           ord_date: { $dateToString: {
              format: "%Y-%m-%d",
              date: "$ord_date"
           }}
        }
     }
   },
   {
     $group: {
        _id: null,
        count: { $sum: 1 }
     }
   }
] )

数据模型

简介

与关系数据库必须先定义好表结构(表名称,字段名称和数据类型)才能插入数据不同,MongoDB 每个文档都是一个独立的记录,可以包含不同的字段和数据类型。

文档结构

MongoDB 的数据模型有两种主要形式:

  • 嵌套数据(Embedded Data):将相关数据存储在单个文档结构中,这种非规范化的数据模型允许应用程序在单个操作中检索和操作关联数据,为读取操作提供了更好的性能。在很多情况下,此种模型最佳。适用于一对一或一对多场景。

image-20231030132200542

  • 引用(References):通过包含从一个文档到另一个文档的链接或引用来存储数据之间的关系,这是一种规范化的数据模型。适用于多对多场景。

image-20231030132230662

写操作的原子性

  • 在 MongoDB 中,在单个文档上的写操作是原子性的,即使该写操作修改了单个文档中的多个嵌套文档。
  • 当单个写操作(例如 db.collection.updateMany() )修改多个文档时,每个文档的修改是原子性的,但整个操作不是原子性的。
  • MongoDB 使用多文档事务以支持多文档写操作的原子性,但需要更多的性能成本。

模式验证

  • 可以使用模式验证为字段创建验证规则,例如允许的数据类型和数值范围。
  • MongoDB 会在更新和插入数据时进行验证。如果文档不符合要求,则操作会被拒绝并返回错误信息。
  • 向现有集合添加验证不会对现有文档强制执行验证。
  • 不能为 admin, localconfig 数据库中的集合指定模式验证。

JSON 模式验证

使用 $jsonSchema 运算符设置模式验证规则:

db.createCollection("students", {
   validator: {
      $jsonSchema: {
         bsonType: "object",
         title: "Student Object Validation",
         required: [ "address", "major", "name", "year" ],
         properties: {
            name: {
               bsonType: "string",
               description: "'name' must be a string and is required"
            },
            year: {
               bsonType: "int",
               minimum: 2017,
               maximum: 3017,
               description: "'year' must be an integer in [ 2017, 3017 ] and is required"
            },
            gpa: {
               bsonType: [ "double" ],
               description: "'gpa' must be a double if the field exists"
            }
         }
      }
   }
} )

插入文档:

db.students.insertOne( {
   name: "Alice",
   year: Int32( 2019 ),
   major: "History",
   gpa: Int32(3),
   address: {
      city: "NYC",
      street: "33rd Street"
   }
} )

由于插入文档的 gpa 为整型,不满足验证中规定的 double,则会报错:

Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId("653f512bf5ad52e0b1ef05a6"),
  details: {
    operatorName: '$jsonSchema',
    title: 'Student Object Validation',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'gpa',
            description: "'gpa' must be a double if the field exists",
            details: [
              {
                operatorName: 'bsonType',
                specifiedAs: { bsonType: [ 'double' ] },
                reason: 'type did not match',
                consideredValue: 3,
                consideredType: 'int'
              }
            ]
          }
        ]
      }
    ]
  }
}

插入满足验证的文档:

db.students.insertOne( {
   name: "Alice",
   year: NumberInt(2019),
   major: "History",
   gpa: Double(3.0),
   address: {
      city: "NYC",
      street: "33rd Street"
   }
} )

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("653f51b5f5ad52e0b1ef05a7")
}

查询数据:

db.students.find()

执行结果:

[
  {
    _id: ObjectId("653f51b5f5ad52e0b1ef05a7"),
    name: 'Alice',
    year: 2019,
    major: 'History',
    gpa: 3,
    address: { city: 'NYC', street: '33rd Street' }
  }
]

JSON 模式验证还可以与查询运算符验证一起使用,例如:

 db.createCollection("sales", {
   validator: {
     "$and": [
       // Validation with query operators
       {
         "$expr": {
           "$lt": ["$lineItems.discountedPrice", "$lineItems.price"]
         }
       },
       // Validation with JSON Schema
       {
         "$jsonSchema": {
           "properties": {
             "items": { "bsonType": "array" }
           }
          }
        }
      ]
    }
  }
)

其中:

  • 使用 $lt 运算符指定 lineItems.discountedPrice 必须小于 lineItems.price
  • 使用 $jsonSchema 指定 items 字段必须是数组。
指定允许的字段值

在 JSON 模式验证中使用 enum 指定字段允许的值。

创建 shipping 集合并使用 $jsonSchema 运算符设置模式验证规则:

db.createCollection("shipping", {
   validator: {
      $jsonSchema: {
         bsonType: "object",
         title: "Shipping Country Validation",
         properties: {
            country: {
               enum: [ "France", "United Kingdom", "United States" ],
               description: "Must be either France, United Kingdom, or United States"
            }
         }
      }
   }
} )

其中 enum 字段指定 country 的值只能是 FranceUnited KingdomUnited States

插入数据:

db.shipping.insertOne( {
   item: "sweater",
   size: "medium",
   country: "Germany"
} )

由于 countryGermany,不在允许的列表中,则会报错:

Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId("653f56eff5ad52e0b1ef05a8"),
  details: {
    operatorName: '$jsonSchema',
    title: 'Shipping Country Validation',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'country',
            description: 'Must be either France, United Kingdom, or United States',
            details: [
              {
                operatorName: 'enum',
                specifiedAs: {
                  enum: [ 'France', 'United Kingdom', 'United States' ]
                },
                reason: 'value was not found in enum',
                consideredValue: 'Germany'
              }
            ]
          }
        ]
      }
    ]
  }
}

插入正确的数据:

db.shipping.insertOne( {
   item: "sweater",
   size: "medium",
   country: "France"
} )

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("653f5749f5ad52e0b1ef05a9")
}

查询数据:

db.shipping.find()

执行结果:

[
  {
    _id: ObjectId("653f5749f5ad52e0b1ef05a9"),
    item: 'sweater',
    size: 'medium',
    country: 'France'
  }
]
最佳实践

当在 JSON 模式验证中指定 additionalProperties: false 时,如果在 properties 中没有包含 _id 字段,则会拒绝所有文档,如果包含了 _id 字段,则只会拒绝包含了没有在 properties 中指定的字段的文档。

例如对于以下验证,在 properties 中没有包含 _id 字段,将会拒绝所有文档:

{
  "$jsonSchema": {
    "required": [ "_id", "storeLocation" ],
    "properties": {
      "storeLocation": { "bsonType": "string" }
    },
    "additionalProperties": false
  }
}

例如对于以下验证,在 properties 中包含了 _id 字段,则只会拒绝除了 _idstoreLocation 外还有其他字段的文档:

{
  "$jsonSchema": {
    "required": [ "_id", "storeLocation" ],
    "properties": {
      "_id": { "bsonType": "objectId" },
      "storeLocation": { "bsonType": "string" }
    },
    "additionalProperties": false
  }
}

如果插入时,指定了文档某个字段为 null

db.store.insertOne( { storeLocation: null } )

则必须在 JSON 模式验证中显式指定该字段允许 null

db.createCollection("store",
   {
      validator:
         {
            "$jsonSchema": {
               "properties": {
                  "storeLocation": { "bsonType": [ "null", "string" ] }
               }
            }
         }
    }
 )

也可以在插入时不指定该字段,则 MongoDB 不会验证此字段。

查询运算符验证

可以使用查询运算符创建动态验证规则对比多个字段值。

不能在 validator 中使用以下查询运算符:

例如对 orders 集合创建模式验证规则,只能插入 totalWithVAT 字段值等于 total * (1 + VAT) 的文档:

db.createCollection( "orders",
   {
      validator: {
         $expr:
            {
               $eq: [
                  "$totalWithVAT",
                  { $multiply: [ "$total", { $sum:[ 1, "$VAT" ] } ] }
               ]
            }
      }
   }
)

插入错误数据:

db.orders.insertOne( {
   total: NumberDecimal("141"),
   VAT: NumberDecimal("0.20"),
   totalWithVAT: NumberDecimal("169")
} )

由于不满足验证规则,报错:

Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId("653f611ff5ad52e0b1ef05ab"),
  details: {
    operatorName: '$expr',
    specifiedAs: {
      '$expr': {
        '$eq': [
          '$totalWithVAT',
          {
            '$multiply': [ '$total', { '$sum': [ 1, '$VAT' ] } ]
          }
        ]
      }
    },
    reason: 'expression did not match',
    expressionResult: false
  }
}

插入正确数据:

db.orders.insertOne( {
   total: NumberDecimal("141"),
   VAT: NumberDecimal("0.20"),
   totalWithVAT: NumberDecimal("169.2")
} )

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("653f6178f5ad52e0b1ef05ac")
}

查看验证规则

使用 db.getCollectionInfos()listCollections 查看集合验证规则。

db.getCollectionInfos( { name: "students" } )[0].options.validator

执行结果:

{
  '$jsonSchema': {
    bsonType: 'object',
    title: 'Student Object Validation',
    required: [ 'address', 'major', 'name', 'year' ],
    properties: {
      name: {
        bsonType: 'string',
        description: "'name' must be a string and is required"
      },
      year: {
        bsonType: 'int',
        minimum: 2017,
        maximum: 3017,
        description: "'year' must be an integer in [ 2017, 3017 ] and is required"
      },
      gpa: {
        bsonType: [ 'double' ],
        description: "'gpa' must be a double if the field exists"
      }
    }
  }
}

修改模式验证

使用 collMod 命令修改或增加模式验证。

创建 users 集合及其验证规则:

db.createCollection("users", {
   validator: {
      $jsonSchema: {
         bsonType: "object",
         required: [ "username", "password" ],
         properties: {
            username: {
               bsonType: "string",
               description: "must be a string and is required"
            },
            password: {
               bsonType: "string",
               minLength: 8,
               description: "must be a string at least 8 characters long, and is required"
            }
         }
      }
   }
} )

修改验证规则,将 password 字段的 minLength 从 8 修改为 12:

db.runCommand( { collMod: "users",
   validator: {
      $jsonSchema: {
         bsonType: "object",
         required: [ "username", "password" ],
         properties: {
            username: {
               bsonType: "string",
               description: "must be a string and is required"
            },
            password: {
               bsonType: "string",
               minLength: 12,
               description: "must be a string of at least 12 characters, and is required"
            }
         }
      }
   }
} )

插入无效数据:

db.users.insertOne(
   {
      "username": "salesAdmin01",
      "password": "kT9$j4wg#M"
   }
)

执行结果:

Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId("653f6506f5ad52e0b1ef05ad"),
  details: {
    operatorName: '$jsonSchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'password',
            description: 'must be a string of at least 12 characters, and is required',
            details: [
              {
                operatorName: 'minLength',
                specifiedAs: { minLength: 12 },
                reason: 'specified string length was not satisfied',
                consideredValue: 'kT9$j4wg#M'
              }
            ]
          }
        ]
      }
    ]
  }
}

插入有效数据:

db.users.insertOne(
   {
      "username": "salesAdmin01",
      "password": "8p&SQd7T90$KKx"
   }
)

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("653f6534f5ad52e0b1ef05ae")
}

对于之前有效,修改验证规则后无效的文档,如果 validationLevel 为默认值 strict,则此文档必须匹配新的验证规则,在每次更新文档时都会进行验证。如果 validationLevelmoderate,则此文档无需匹配新的验证规则。

指定现有文档的验证级别

使用 validationLevel 指定现有文档的验证级别:

Validation LevelBehavior
strict(Default) MongoDB applies validation rules to all inserts and updates.
moderateMongoDB only applies validation rules to existing valid documents. Updates to invalid documents which exist prior to the validation being added are not checked for validity.

创建 contacts 集合,插入数据:

db.contacts.insertMany([
   { "_id": 1, "name": "Anne", "phone": "+1 555 123 456", "city": "London", "status": "Complete" },
   { "_id": 2, "name": "Ivan", "city": "Vancouver" }
])

例子:指定具有 strict 验证级别的验证规则

db.runCommand( {
   collMod: "contacts",
   validator: { $jsonSchema: {
      bsonType: "object",
      required: [ "phone", "name" ],
      properties: {
         phone: {
            bsonType: "string",
            description: "phone must be a string and is required"
         },
         name: {
            bsonType: "string",
            description: "name must be a string and is required"
         }
      }
   } },
   validationLevel: "strict"
} )

由于 validationLevelstrict,当更新任何文档时,会检查该文档进行验证。

使用以下不满足验证规则的数据更新文档:

db.contacts.updateOne(
   { _id: 1 },
   { $set: { name: 10 } }
)

出现报错:

Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: 1,
  details: {
    operatorName: '$jsonSchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'name',
            description: 'name must be a string and is required',
            details: [
              {
                operatorName: 'bsonType',
                specifiedAs: { bsonType: 'string' },
                reason: 'type did not match',
                consideredValue: 10,
                consideredType: 'int'
              }
            ]
          }
        ]
      }
    ]
  }
}

例子:指定具有 moderate 验证级别的验证规则

db.runCommand( {
   collMod: "contacts",
   validator: { $jsonSchema: {
      bsonType: "object",
      required: [ "phone", "name" ],
      properties: {
         phone: {
            bsonType: "string",
            description: "phone must be a string and is required"
         },
         name: {
            bsonType: "string",
            description: "name must be a string and is required"
         }
      }
   } },
   validationLevel: "moderate"
} )

由于 validationLevelmoderate

  • 如果更新 _id: 1 文档,由于此文档满足验证规则,则会应用新的验证规则。
  • 如果更新 _id: 2 文档,由于此文档不满足验证规则,则不会应用新的验证规则。

对于 _id: 1 文档,插入不满足验证规则的文档:

db.contacts.updateOne(
   { _id: 1 },
   { $set: { name: 10 } }
)

会将验证规则应用于此文档,出现报错:

Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: 1,
  details: {
    operatorName: '$jsonSchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'name',
            description: 'name must be a string and is required',
            details: [
              {
                operatorName: 'bsonType',
                specifiedAs: { bsonType: 'string' },
                reason: 'type did not match',
                consideredValue: 10,
                consideredType: 'int'
              }
            ]
          }
        ]
      }
    ]
  }
}

对于 _id: 2 文档,插入不满足验证规则的文档:

db.contacts.updateOne(
   { _id: 2 },
   { $set: { name: 20 } }
)

不会将验证规则应用于此文档,执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

选择如何处理无效文档

使用 validationAction 指定如何处理违反验证规则的无效文档。

Validation ActionBehavior
error(Default) MongoDB rejects any insert or update that violates the validation criteria.
warnMongoDB allows the operation to proceed, but records the violation in the MongoDB log.

例子:拒绝无效文档

创建集合 contacts,指定验证规则中的 validationAction"error"

db.createCollection( "contacts", {
   validator: { $jsonSchema: {
      bsonType: "object",
      required: [ "phone" ],
      properties: {
         phone: {
            bsonType: "string",
            description: "must be a string and is required"
         },
         email: {
            bsonType : "string",
            pattern : "@mongodb\\.com$",
            description: "must be a string and end with '@mongodb.com'"
         }
      }
   } },
   validationAction: "error"
} )

validationAction"error" 将会拒绝任何无效文档的插入:

db.contacts.insertOne(
   { name: "Amanda", email: "amanda@xyz.com" }
)

此文档违反的验证规则有:

  • email 字段不匹配正则表达式,结尾必须是 @mongodb.com
  • 缺少必须的 phone 字段。
Uncaught:
MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId("653f6ee0f5ad52e0b1ef05af"),
  details: {
    operatorName: '$jsonSchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'properties',
        propertiesNotSatisfied: [
          {
            propertyName: 'email',
            description: "must be a string and end with '@mongodb.com'",
            details: [
              {
                operatorName: 'pattern',
                specifiedAs: { pattern: '@mongodb\\.com$' },
                reason: 'regular expression did not match',
                consideredValue: 'amanda@xyz.com'
              }
            ]
          }
        ]
      },
      {
        operatorName: 'required',
        specifiedAs: { required: [ 'phone' ] },
        missingProperties: [ 'phone' ]
      }
    ]
  }
}

例子:允许无效文档,当记录到日志中

创建集合 contacts2,指定验证规则中的 validationAction"warn"

db.createCollection( "contacts2", {
   validator: { $jsonSchema: {
      bsonType: "object",
      required: [ "phone" ],
      properties: {
         phone: {
            bsonType: "string",
            description: "must be a string and is required"
         },
         email: {
            bsonType : "string",
            pattern : "@mongodb\\.com$",
            description: "must be a string and end with '@mongodb.com'"
         }
      }
   } },
   validationAction: "warn"
} )

validationAction"warn" 将会允许插入无效文档:

db.contacts2.insertOne(
   { name: "Amanda", email: "amanda@xyz.com" }
)

此文档违反的验证规则有:

  • email 字段不匹配正则表达式,结尾必须是 @mongodb.com
  • 缺少必须的 phone 字段。

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("653f7016f5ad52e0b1ef05b0")
}

查看日志:

db.adminCommand(
   { getLog:'global'} ).log.forEach(x => { print(x) }
)

查询和修改有效或无效的文档

在创建集合后添加模式验证规则或者修改当前模式验证规则,则之前有效的文档可能会变为无效文档。可以使用 $jsonSchema 查找满足或者不满足验证规则的文档,并对其更新或删除。

创建 inventory 集合:

db.inventory.insertMany( [
   { item: "journal", qty: NumberInt(25), size: { h: 14, w: 21, uom: "cm" }, instock: true },
   { item: "notebook", qty: NumberInt(50), size: { h: 8.5, w: 11, uom: "in" }, instock: true },
   { item: "paper", qty: NumberInt(100), size: { h: 8.5, w: 11, uom: "in" }, instock: 1 },
   { item: "planner", qty: NumberInt(75), size: { h: 22.85, w: 30, uom: "cm" }, instock: 1 },
   { item: "postcard", qty: NumberInt(45), size: { h: 10, w: 15.25, uom: "cm" }, instock: true },
   { item: "apple", qty: NumberInt(45), status: "A", instock: true },
   { item: "pears", qty: NumberInt(50), status: "A", instock: true }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65405370d195c45aeb75d334"),
    '1': ObjectId("65405370d195c45aeb75d335"),
    '2': ObjectId("65405370d195c45aeb75d336"),
    '3': ObjectId("65405370d195c45aeb75d337"),
    '4': ObjectId("65405370d195c45aeb75d338"),
    '5': ObjectId("65405370d195c45aeb75d339"),
    '6': ObjectId("65405370d195c45aeb75d33a")
  }
}

定义模式对象 myschema

let myschema =
{
   $jsonSchema: {
      required: [ "item", "qty", "instock" ],
      properties: {
         item: { bsonType: "string" },
         qty: { bsonType: "int" },
         size: {
            bsonType: "object",
            required: [ "uom" ],
            properties: {
               uom: { bsonType: "string" },
               h: { bsonType: "double" },
               w: { bsonType: "double" }
            }
          },
          instock: { bsonType: "bool" }
      }
   }
}

例子:查找匹配模式验证规则的文档

db.inventory.find(myschema)
db.inventory.aggregate( [ { $match: myschema } ] )

执行结果:

[
  {
    _id: ObjectId("65405370d195c45aeb75d339"),
    item: 'apple',
    qty: 45,
    status: 'A',
    instock: true
  },
  {
    _id: ObjectId("65405370d195c45aeb75d33a"),
    item: 'pears',
    qty: 50,
    status: 'A',
    instock: true
  }
]

例子:查找不匹配模式验证规则的文档

db.inventory.find( { $nor: [ myschema ] } )

执行结果:

[
  {
    _id: ObjectId("65405370d195c45aeb75d334"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    instock: true
  },
  {
    _id: ObjectId("65405370d195c45aeb75d335"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    instock: true
  },
  {
    _id: ObjectId("65405370d195c45aeb75d336"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    instock: 1
  },
  {
    _id: ObjectId("65405370d195c45aeb75d337"),
    item: 'planner',
    qty: 75,
    size: { h: 22.85, w: 30, uom: 'cm' },
    instock: 1
  },
  {
    _id: ObjectId("65405370d195c45aeb75d338"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    instock: true
  }
]

例子:更新不匹配模式验证规则的文档,将文档 isValid 字段设置为 false

db.inventory.updateMany(
   {
      $nor: [ myschema ]
   },
   {
      $set: { isValid: false }
   }
)

执行结果:

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 5,
  modifiedCount: 5,
  upsertedCount: 0
}

查看集合:

db.inventory.find()

执行结果:

[
  {
    _id: ObjectId("65405370d195c45aeb75d334"),
    item: 'journal',
    qty: 25,
    size: { h: 14, w: 21, uom: 'cm' },
    instock: true,
    isValid: false
  },
  {
    _id: ObjectId("65405370d195c45aeb75d335"),
    item: 'notebook',
    qty: 50,
    size: { h: 8.5, w: 11, uom: 'in' },
    instock: true,
    isValid: false
  },
  {
    _id: ObjectId("65405370d195c45aeb75d336"),
    item: 'paper',
    qty: 100,
    size: { h: 8.5, w: 11, uom: 'in' },
    instock: 1,
    isValid: false
  },
  {
    _id: ObjectId("65405370d195c45aeb75d337"),
    item: 'planner',
    qty: 75,
    size: { h: 22.85, w: 30, uom: 'cm' },
    instock: 1,
    isValid: false
  },
  {
    _id: ObjectId("65405370d195c45aeb75d338"),
    item: 'postcard',
    qty: 45,
    size: { h: 10, w: 15.25, uom: 'cm' },
    instock: true,
    isValid: false
  },
  {
    _id: ObjectId("65405370d195c45aeb75d339"),
    item: 'apple',
    qty: 45,
    status: 'A',
    instock: true
  },
  {
    _id: ObjectId("65405370d195c45aeb75d33a"),
    item: 'pears',
    qty: 50,
    status: 'A',
    instock: true
  }
]

例子:删除不匹配模式验证规则的文档

db.inventory.deleteMany( { $nor: [ myschema ] } )

执行结果:

{ acknowledged: true, deletedCount: 5 }

查看集合:

db.inventory.find()

执行结果:

[
  {
    _id: ObjectId("65405370d195c45aeb75d339"),
    item: 'apple',
    qty: 45,
    status: 'A',
    instock: true
  },
  {
    _id: ObjectId("65405370d195c45aeb75d33a"),
    item: 'pears',
    qty: 50,
    status: 'A',
    instock: true
  }
]

绕过模式验证

在某些时候,需要绕过模式验证,以便插入不满足规则的数据。

创建 students 集合,使用 $jsonSchema 运算符设置模式验证规则:

db.createCollection("students", {
   validator: {
      $jsonSchema: {
         bsonType: "object",
         required: [ "name", "year", "major", "address" ],
         properties: {
            name: {
               bsonType: "string",
               description: "must be a string and is required"
            },
            year: {
               bsonType: "int",
               minimum: 2017,
               maximum: 3017,
               description: "must be an integer in [ 2017, 3017 ] and is required"
            }
         }
      }
   }
} )

bypassDocumentValidation 设置 true 绕过验证插入无效文档:

db.runCommand( {
   insert: "students",
   documents: [
      {
         name: "Alice",
         year: Int32( 2016 ),
         major: "History",
         gpa: Double(3.0),
         address: {
            city: "NYC",
            street: "33rd Street"
         }
      }
   ],
   bypassDocumentValidation: true
} )

执行结果:

{ n: 1, ok: 1 }

查询集合:

db.students.find()

执行结果:

[
  {
    _id: ObjectId("65405a837f89031cf9ad7b58"),
    name: 'Alice',
    year: 2016,
    major: 'History',
    gpa: 3,
    address: { city: 'NYC', street: '33rd Street' }
  }
]

文档之间的模型关系

使用嵌套文档对一对一关系进行建模

对于以下 patronaddress 文档,在规范化数据模型中,address 文档包含对 patron 文档的引用。

// patron document
{
   _id: "joe",
   name: "Joe Bookreader"
}

// address document
{
   patron_id: "joe", // reference to patron document
   street: "123 Fake Street",
   city: "Faketon",
   state: "MA",
   zip: "12345"
}

如果经常使用 name 检索 address 数据,更好的模型是将 address 数据嵌套到 patron 数据中:

{
   _id: "joe",
   name: "Joe Bookreader",
   address: {
              street: "123 Fake Street",
              city: "Faketon",
              state: "MA",
              zip: "12345"
            }
}

此时通过一个查询操作就可以获取所需信息。

对于包含较多字段和数据的集合,可以使用子集模式,将集合进行拆分。例如对于以下 movie 集合:

{
  "_id": 1,
  "title": "The Arrival of a Train",
  "year": 1896,
  "runtime": 1,
  "released": ISODate("01-25-1896"),
  "poster": "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg",
  "plot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...",
  "fullplot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.",
  "lastupdated": ISODate("2015-08-15T10:06:53"),
  "type": "movie",
  "directors": [ "Auguste Lumière", "Louis Lumière" ],
  "imdb": {
    "rating": 7.3,
    "votes": 5043,
    "id": 12
  },
  "countries": [ "France" ],
  "genres": [ "Documentary", "Short" ],
  "tomatoes": {
    "viewer": {
      "rating": 3.7,
      "numReviews": 59
    },
    "lastUpdated": ISODate("2020-01-09T00:02:53")
  }
}

可以拆分为需要经常访问的集合 movie

// movie collection

{
  "_id": 1,
  "title": "The Arrival of a Train",
  "year": 1896,
  "runtime": 1,
  "released": ISODate("1896-01-25"),
  "type": "movie",
  "directors": [ "Auguste Lumière", "Louis Lumière" ],
  "countries": [ "France" ],
  "genres": [ "Documentary", "Short" ],
}

和很少访问的集合 movie_details

// movie_details collection

{
  "_id": 156,
  "movie_id": 1, // reference to the movie collection
  "poster": "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg",
  "plot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...",
  "fullplot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.",
  "lastupdated": ISODate("2015-08-15T10:06:53"),
  "imdb": {
    "rating": 7.3,
    "votes": 5043,
    "id": 12
  },
  "tomatoes": {
    "viewer": {
      "rating": 3.7,
      "numReviews": 59
    },
    "lastUpdated": ISODate("2020-01-29T00:02:53")
  }
}

应用程序只需要读取较少的数据就可以满足最常见的请求,提高了读取性能。

使用嵌套文档对一对多关系进行建模

对于以下 patronaddress 文档,在规范化数据模型中,address 文档包含对 patron 文档的引用。

// patron document
{
   _id: "joe",
   name: "Joe Bookreader"
}

// address documents
{
   patron_id: "joe", // reference to patron document
   street: "123 Fake Street",
   city: "Faketon",
   state: "MA",
   zip: "12345"
}

{
   patron_id: "joe",
   street: "1 Some Other Street",
   city: "Boston",
   state: "MA",
   zip: "12345"
}

如果经常使用 name 检索 address 数据,更好的模型是将 address 数据嵌套到 patron 数据中:

{
   "_id": "joe",
   "name": "Joe Bookreader",
   "addresses": [
                {
                  "street": "123 Fake Street",
                  "city": "Faketon",
                  "state": "MA",
                  "zip": "12345"
                },
                {
                  "street": "1 Some Other Street",
                  "city": "Boston",
                  "state": "MA",
                  "zip": "12345"
                }
              ]
 }

此时通过一个查询操作就可以获取所需信息。

对于包含较多字段和数据的集合,可以使用子集模式,将集合进行拆分。例如对于以下 product 集合:

{
  "_id": 1,
  "name": "Super Widget",
  "description": "This is the most useful item in your toolbox.",
  "price": { "value": NumberDecimal("119.99"), "currency": "USD" },
  "reviews": [
    {
      "review_id": 786,
      "review_author": "Kristina",
      "review_text": "This is indeed an amazing widget.",
      "published_date": ISODate("2019-02-18")
    },
    {
      "review_id": 785,
      "review_author": "Trina",
      "review_text": "Nice product. Slow shipping.",
      "published_date": ISODate("2019-02-17")
    },
    ...
    {
      "review_id": 1,
      "review_author": "Hans",
      "review_text": "Meh, it's okay.",
      "published_date": ISODate("2017-12-06")
    }
  ]
}

集合中包含了很多的评论信息,评论按时间倒序排序。当用户访问产品页面时,应用程序会加载 10 条最新的评论。

可以将集合拆分为只保存 10 条最新评论的 product 集合:

{
  "_id": 1,
  "name": "Super Widget",
  "description": "This is the most useful item in your toolbox.",
  "price": { "value": NumberDecimal("119.99"), "currency": "USD" },
  "reviews": [
    {
      "review_id": 786,
      "review_author": "Kristina",
      "review_text": "This is indeed an amazing widget.",
      "published_date": ISODate("2019-02-18")
    }
    ...
    {
      "review_id": 777,
      "review_author": "Pablo",
      "review_text": "Amazing!",
      "published_date": ISODate("2019-02-16")
    }
  ]
}

以及存储所有评论的 review 集合,集合中每个文档都包含对产品的引用:

{
  "review_id": 786,
  "product_id": 1,
  "review_author": "Kristina",
  "review_text": "This is indeed an amazing widget.",
  "published_date": ISODate("2019-02-18")
}
{
  "review_id": 785,
  "product_id": 1,
  "review_author": "Trina",
  "review_text": "Nice product. Slow shipping.",
  "published_date": ISODate("2019-02-17")
}
...
{
  "review_id": 1,
  "product_id": 1,
  "review_author": "Hans",
  "review_text": "Meh, it's okay.",
  "published_date": ISODate("2017-12-06")
}

应用程序只需要读取较少的数据就可以满足最常见的请求,提高了读取性能。

使用文档引用对一对多关系进行建模

将出版商文档嵌套到图书文档中将导致出版商数据重复,如下所示:

{
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",
   publisher: {
              name: "O'Reilly Media",
              founded: 1980,
              location: "CA"
            }
}

{
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",
   publisher: {
              name: "O'Reilly Media",
              founded: 1980,
              location: "CA"
            }
}

为避免出版商数据重复,使用引用将出版商信息保存在不同的集合中:

{
   _id: "oreilly",
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA"
}

{
   _id: 123456789,
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",
   publisher_id: "oreilly"
}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",
   publisher_id: "oreilly"
}

模型树结构

具有父引用的模型树结构

该模型通过将父节点的引用存储在子节点中来描述文档的树状结构。

对于以下层次结构:

image-20231031141909377

使用父引用(Parent References)进行建模,将文档的父节点存储在字段 parent 中:

db.categories.insertMany( [
   { _id: "MongoDB", parent: "Databases" },
   { _id: "dbm", parent: "Databases" },
   { _id: "Databases", parent: "Programming" },
   { _id: "Languages", parent: "Programming" },
   { _id: "Programming", parent: "Books" },
   { _id: "Books", parent: null }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': 'MongoDB',
    '1': 'dbm',
    '2': 'Databases',
    '3': 'Languages',
    '4': 'Programming',
    '5': 'Books'
  }
}
  • 可以快速直接查询到文档的父节点:
db.categories.findOne( { _id: "MongoDB" } ).parent

执行结果:

Databases
  • 可以在 parent 字段上创建索引以加快对父节点的查询:
db.categories.createIndex( { parent: 1 } )

执行结果:

parent_1
  • 可以指定 parent 字段值获取其直接子节点:
db.categories.find( { parent: "Databases" } )

执行结果:

[
  { _id: 'MongoDB', parent: 'Databases' },
  { _id: 'dbm', parent: 'Databases' }
]

具有子引用的模型树结构

该模型通过将子节点的引用存储在父节点中来描述文档的树状结构。

对于以下层次结构:

image-20231031141909377

使用子引用(Child References)进行建模,将文档的子节点存储在字段 children 中:

db.categories.insertMany( [
   { _id: "MongoDB", children: [] },
   { _id: "dbm", children: [] },
   { _id: "Databases", children: [ "MongoDB", "dbm" ] },
   { _id: "Languages", children: [] },
   { _id: "Programming", children: [ "Databases", "Languages" ] },
   { _id: "Books", children: [ "Programming" ] }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': 'MongoDB',
    '1': 'dbm',
    '2': 'Databases',
    '3': 'Languages',
    '4': 'Programming',
    '5': 'Books'
  }
}
  • 可以快速直接查询到文档的子节点:
db.categories.findOne( { _id: "Databases" } ).children

执行结果:

[ 'MongoDB', 'dbm' ]
  • 可以在 children 字段上创建索引以加快对子节点的查询:
db.categories.createIndex( { children: 1 } )

执行结果:

children_1
  • 可以指定 children 字段值获取直接父节点及其同级节点:
db.categories.find( { children: "MongoDB" } )

执行结果:

[ { _id: 'Databases', children: [ 'MongoDB', 'dbm' ] } ]

具有祖先数组的模型树结构

该模型使用对父节点的引用和存储所有祖先的数组来描述文档的树状结构。

对于以下层次结构:

image-20231031141909377

使用父引用(Parent References)和祖先数组(Array of Ancestors)进行建模,将文档的父节点存储在字段 parent 中:

db.categories.insertMany( [
  { _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" },
  { _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" },
  { _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" },
  { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" },
  { _id: "Programming", ancestors: [ "Books" ], parent: "Books" },
  { _id: "Books", ancestors: [ ], parent: null }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': 'MongoDB',
    '1': 'dbm',
    '2': 'Databases',
    '3': 'Languages',
    '4': 'Programming',
    '5': 'Books'
  }
}
  • 可以快速直接查询到文档的祖先:
db.categories.findOne( { _id: "MongoDB" } ).ancestors

执行结果:

[ 'Books', 'Programming', 'Databases' ]
  • 可以在 ancestors 字段上创建索引以加快对祖先节点的查询:
db.categories.createIndex( { ancestors: 1 } )

执行结果:

ancestors_1
  • 可以指定 ancestors 字段值获取其所有后代:
db.categories.find( { ancestors: "Programming" } )

执行结果:

[
  {
    _id: 'MongoDB',
    ancestors: [ 'Books', 'Programming', 'Databases' ],
    parent: 'Databases'
  },
  {
    _id: 'dbm',
    ancestors: [ 'Books', 'Programming', 'Databases' ],
    parent: 'Databases'
  },
  {
    _id: 'Databases',
    ancestors: [ 'Books', 'Programming' ],
    parent: 'Programming'
  },
  {
    _id: 'Languages',
    ancestors: [ 'Books', 'Programming' ],
    parent: 'Programming'
  }
]

具有物化路径的模型树结构

该模型通过存储文档之间的完整关系路径来描述文档的树状结构。

对于以下层次结构:

image-20231031141909377

使用物化路径(Materialized Paths)进行建模,将路径存储在字段 path 中,路径字符串使用逗号 , 作为分隔符:

db.categories.insertMany( [
   { _id: "Books", path: null },
   { _id: "Programming", path: ",Books," },
   { _id: "Databases", path: ",Books,Programming," },
   { _id: "Languages", path: ",Books,Programming," },
   { _id: "MongoDB", path: ",Books,Programming,Databases," },
   { _id: "dbm", path: ",Books,Programming,Databases," }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': 'Books',
    '1': 'Programming',
    '2': 'Databases',
    '3': 'Languages',
    '4': 'MongoDB',
    '5': 'dbm'
  }
}
  • 可以检索整个树,按字段 path 排序:
db.categories.find().sort( { path: 1 } )

执行结果:

[
  { _id: 'Books', path: null },
  { _id: 'Programming', path: ',Books,' },
  { _id: 'Databases', path: ',Books,Programming,' },
  { _id: 'Languages', path: ',Books,Programming,' },
  { _id: 'MongoDB', path: ',Books,Programming,Databases,' },
  { _id: 'dbm', path: ',Books,Programming,Databases,' }
]
  • 可以在 path 字段上使用正则表达式来查找 Programming 的后代:
db.categories.find( { path: /,Programming,/ } )

执行结果:

[
  { _id: 'Databases', path: ',Books,Programming,' },
  { _id: 'Languages', path: ',Books,Programming,' },
  { _id: 'MongoDB', path: ',Books,Programming,Databases,' },
  { _id: 'dbm', path: ',Books,Programming,Databases,' }
]
  • 可以检索 Books 的后代:
db.categories.find( { path: /^,Books,/ } )

执行结果:

[
  { _id: 'Programming', path: ',Books,' },
  { _id: 'Databases', path: ',Books,Programming,' },
  { _id: 'Languages', path: ',Books,Programming,' },
  { _id: 'MongoDB', path: ',Books,Programming,Databases,' },
  { _id: 'dbm', path: ',Books,Programming,Databases,' }
]
  • 可以在 path 字段上创建索引:
db.categories.createIndex( { path: 1 } )

执行结果:

path_1

具有嵌套集合的模型树结构

该模型使用对父节点的引用和左右遍历位置来描述文档的树状结构,适用于不变的静态树。

对于以下层次结构:

image-20231031151837897

使用嵌套集合(Nested Sets)进行建模:

db.categories.insertMany( [
   { _id: "Books", parent: 0, left: 1, right: 12 },
   { _id: "Programming", parent: "Books", left: 2, right: 11 },
   { _id: "Languages", parent: "Programming", left: 3, right: 4 },
   { _id: "Databases", parent: "Programming", left: 5, right: 10 },
   { _id: "MongoDB", parent: "Databases", left: 6, right: 7 },
   { _id: "dbm", parent: "Databases", left: 8, right: 9 }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': 'Books',
    '1': 'Programming',
    '2': 'Languages',
    '3': 'Databases',
    '4': 'MongoDB',
    '5': 'dbm'
  }
}

检索节点的后代:

var databaseCategory = db.categories.findOne( { _id: "Databases" } );
db.categories.find( { left: { $gt: databaseCategory.left }, right: { $lt: databaseCategory.right } } );

执行结果:

[
  { _id: 'MongoDB', parent: 'Databases', left: 6, right: 7 },
  { _id: 'dbm', parent: 'Databases', left: 8, right: 9 }
]

数据库引用

MongoDB 有两种方式来关联文档:

  • 手动引用(Manual Reference):将一个文档的 _id 字段值保存到另一个文档中作为引用,适用于两个文档之间的引用。
  • DBRefs:通过指定引用的集合名称,_id 字段值和数据库名称(可选)进行引用,适用于单个集合中的文档与多个集合中的文档进行关联。

手动引用

创建 places 集合并插入数据:

original_id = ObjectId()
db.places.insertOne({
    "_id": original_id,
    "name": "Broadway Center",
    "url": "bc.example.net"
})

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("6541c3c9385928a78cac7f08")
}

查看 places 集合:

db.places.find()

执行结果:

[
  {
    _id: ObjectId("6541c3c9385928a78cac7f08"),
    name: 'Broadway Center',
    url: 'bc.example.net'
  }
]

创建 people 集合并插入数据,包含对 places 集合刚刚插入文档的引用:

db.people.insertOne({
    "name": "Erin",
    "places_id": original_id,
    "url":  "bc.example.net/Erin"
})

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("6541c42c385928a78cac7f09")
}

查看 people 集合:

db.people.find()

执行结果:

[
  {
    _id: ObjectId("6541c42c385928a78cac7f09"),
    name: 'Erin',
    places_id: ObjectId("6541c3c9385928a78cac7f08"),
    url: 'bc.example.net/Erin'
  }
]

关联查询指定地址的人员信息:

db.places.aggregate([
  {
    $lookup: {
      from: "people",
      localField: "_id",
      foreignField: "places_id",
      as: "people"
    }
  }
])

执行结果:

[
  {
    _id: ObjectId("6541c3c9385928a78cac7f08"),
    name: 'Broadway Center',
    url: 'bc.example.net',
    people: [
      {
        _id: ObjectId("6541c42c385928a78cac7f09"),
        name: 'Erin',
        places_id: ObjectId("6541c3c9385928a78cac7f08"),
        url: 'bc.example.net/Erin'
      }
    ]
  }
]

关联查询指定人员的地址信息:

db.people.aggregate([
  {
    $lookup: {
      from: "places",
      localField: "places_id",
      foreignField: "_id",
      as: "places"
    }
  }
])

执行结果:

[
  {
    _id: ObjectId("6541c42c385928a78cac7f09"),
    name: 'Erin',
    places_id: ObjectId("6541c3c9385928a78cac7f08"),
    url: 'bc.example.net/Erin',
    places: [
      {
        _id: ObjectId("6541c3c9385928a78cac7f08"),
        name: 'Broadway Center',
        url: 'bc.example.net'
      }
    ]
  }
]

DBRefs

DBRefs 的语法为:

{ "$ref" : <value>, "$id" : <value>, "$db" : <value> }

包括:

  • $ref:引用文档所在的集合名称。
  • $id:引用文档中 _id 字段的值。
  • $db:引用文档所在的数据库名称,可选。

使用 DBRefs 插入文档到集合中:

db.people.insertOne({
    "name": "Tom",
    "url":  "bc.example.net/Tom",
	"place" : {
                  "$ref" : "places",
                  "$id" : ObjectId("6541c3c9385928a78cac7f08"),
                  "$db" : "test",
                  "extraField" : "anything"
               }
})

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("6541eda3385928a78cac7f17")
}

查看插入文档:

db.people.find({"name":"Tom"})

执行结果:

[
  {
    _id: ObjectId("6541eda3385928a78cac7f17"),
    name: 'Tom',
    url: 'bc.example.net/Tom',
    place: DBRef("places", ObjectId("6541c3c9385928a78cac7f08"), 'test')
  }
]

索引

  • MongoDB 可以使用索引来进行高效查询,应在经常查询的字段上创建索引。
  • MongoDB 索引使用 B-Tree 数据结构。
  • MongoDB 默认在创建集合时在 _id 字段上创建唯一索引,不能删除该索引。
  • 索引的默认名称由索引键及其值组成,使用下划线连接,不能重命名。
  • 从 MongoDB 4.2 开始,仅在创建索引开始和结束时对集合获取排他锁。
  • 使用 db.currentOp() 监控索引创建的进度。
  • 使用参数 maxNumActiveUserIndexBuilds 指定索引并发创建数量,默认为 3。
  • 使用参数 maxIndexBuildMemoryUsageMegabytes 指定创建索引可使用的最大内存,默认为 200 MB。达到限制后,将会使用 --dbpath 目录下的 _tmp 子目录下的临时磁盘文件。
  • 使用 db.collection.totalIndexSize() 查看集合索引大小。

创建索引

语法:

db.collection.createIndex( <key and index type specification>, <options> )

例子:在 name 字段上创建降序索引

db.inventory.createIndex( { item: -1 } )

执行结果:

item_-1

查询索引:

db.inventory.getIndexes()

执行结果:

[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  { v: 2, key: { item: -1 }, name: 'item_-1' }
]

指定索引名称

语法:

db.<collection>.createIndex(
   { <field>: <value> },
   { name: "<indexName>" }
)
  • 索引名称必须唯一。
  • 不能重命名现有索引,只能删除并重建。

默认索引名称由索引键及其值组成,使用下划线连接,例如:

IndexDefault Name
{ score : 1 }score_1
{ content : "text", "description.tags": "text" }content_text_description.tags_text
{ category : 1, locale : "2dsphere"}category_1_locale_2dsphere
{ "fieldA" : 1, "fieldB" : "hashed", "fieldC" : -1 }fieldA_1_fieldB_hashed_fieldC_-1

删除索引

使用以下方法删除索引或终止正在创建的索引:

MethodDescription
db.collection.dropIndex()open in new windowDrops a specific index from the collection.
db.collection.dropIndexes()open in new windowDrops all removable indexes from the collection or an array of indexes, if specified.
  • 除了创建在 _id 字段上的默认索引, 可以删除任何索引。
  • 要删除创建在 _id 字段上的默认索引,必须删除整个集合。
  • 删除索引前,建议先隐藏索引,以评估删除索引的潜在影响。

例子:删除一个索引

db.inventory.dropIndex("item_-1")

执行结果:

{ nIndexesWas: 2, ok: 1 }

索引类型

单字段索引

  • 单字段索引对集合文档中的单个字段收集和排序数据。
  • 默认所有集合都有一个在 _id 字段上的单字段索引。
  • 可以在文档的任意字段创建单字段索引。
  • 创建索引时,需要指定字段及排序方向,1 表示升序,-1 表示降序。
  • 对于单字段索引,索引键的排序无关紧要,因为 MongoDB 可以在任一方向上遍历索引。

语法:

db.<collection>.createIndex( { <field>: <sortOrder> } )

image-20231018094401850

先插入示例数据:

db.students.insertMany( [
   {
      "name": "Alice",
      "gpa": 3.6,
      "location": { city: "Sacramento", state: "California" }
   },
   {
      "name": "Bob",
      "gpa": 3.2,
      "location": { city: "Albany", state: "New York" }
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652f3972090f91a5ab877384"),
    '1': ObjectId("652f3972090f91a5ab877385")
  }
}

例子:在单字段 gpa 上创建索引

db.students.createIndex( { gpa: 1 } )

执行结果:

gpa_1

执行以下查询将会使用索引 gpa_1

db.students.find( { gpa: 3.6 } )
db.students.find( { gpa: { $lt: 3.4 } } )

例子:在嵌套文档 location 上创建索引

db.students.createIndex( { location: 1 } )

执行结果:

location_1

执行以下查询将会使用索引 location_1,需要注意查询语句的嵌套文档中的字段顺序应该与原数据一致,且查询整个嵌套文档才会使用索引:

db.students.find( { location: { city: "Sacramento", state: "California" } } )

查询嵌套文档中字段不会使用索引 location_1

db.students.find( { "location.city": "Sacramento" } )
db.students.find( { "location.state": "New York" } )

例子:在嵌套文档字段 location.state 上创建索引

db.students.createIndex( { "location.state": 1 } )

执行结果:

location.state_1

执行以下查询将会使用索引 location.state_1

db.students.find( { "location.state": "California" } )
db.students.find( { "location.city": "Albany", "location.state": "New York" } )

复合索引

  • 复合索引对集合文档中的多个字段收集和排序数据。
  • 通过使用复合索引进行覆盖查询,只需要从索引中而无需访问任何文档即可获取所需数据。
  • 如果经常执行的查询语句包含多个字段,可以在多个字段上创建复合索引以提高查询性能。
  • 复合索引最多包含 32 个字段。
  • 遵循 ESR 规则,创建高效复合索引。
  • 从 MongoDB 4.4 起,复合索引可以包含一个单哈希索引字段。
  • 复合索引满足最左匹配原则,查询语句至少应包含索引的第一个字段,才会使用索引。
  • 查询中的排序操作是否可以使用索引取决于索引中各个字段的排序顺序。排序方向需要与所有索引字段定义的方向都相同或者都相反。

语法:

db.<collection>.createIndex( {
   <field1>: <sortOrder>,
   <field2>: <sortOrder>,
   ...
   <fieldN>: <sortOrder>
} )

image-20231018101805715

例子:在字段 namegpa 上创建索引

db.students.createIndex( {
   name: 1,
   gpa: -1
} )

执行结果:

name_1_gpa_-1

执行以下查询将会使用索引 name_1_gpa_-1

db.students.find( { name: "Alice", gpa: 3.6 } )
db.students.find( { name: "Bob" } )

执行以下查询将不会使用索引 name_1_gpa_-1

db.students.find( { gpa: { $gt: 3.5 } } )

例子:复合索引对排序操作的支持

先插入示例数据:

db.leaderboard.insertMany( [
   {
      "score": 50,
      "username": "Alex Martin",
      "date": ISODate("2022-03-01T00:00:00Z")
   },
   {
      "score": 55,
      "username": "Laura Garcia",
      "date": ISODate("2022-03-02T00:00:00Z")
   },
   {
      "score": 60,
      "username": "Alex Martin",
      "date": ISODate("2022-03-03T00:00:00Z")
   },
   {
      "score": 60,
      "username": "Riya Patel",
      "date": ISODate("2022-03-04T00:00:00Z")
   },
   {
      "score": 50,
      "username": "Laura Garcia",
      "date": ISODate("2022-03-05T00:00:00Z")
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652f4ee6090f91a5ab877386"),
    '1': ObjectId("652f4ee6090f91a5ab877387"),
    '2': ObjectId("652f4ee6090f91a5ab877388"),
    '3': ObjectId("652f4ee6090f91a5ab877389"),
    '4': ObjectId("652f4ee6090f91a5ab87738a")
  }
}

在字段 scoreusername 上创建复合索引,score 为升序,username 为降序:

db.leaderboard.createIndex( { score: -1, username: 1 } )

执行结果:

score_-1_username_1

则以下 SQL 的排序顺序与索引 score_-1_username_1 一致, 可以使用索引来提升性能:

db.leaderboard.createIndex( { score: -1, username: 1 } )

同时也支持与索引相反方向的排序操作:

db.leaderboard.find().sort( { score: 1, username: -1 } )

对于与索引排序方向或者反方向不同的排序,则不能使用索引,例如:

db.leaderboard.find().sort( { score: 1, username: 1 } )
db.leaderboard.find().sort( { username: 1, score: -1, } )

多键索引

  • 多键索引从包含数组值的字段收集和排序数据。
  • 当在包含数组值的字段上创建索引,则自动创建为多键索引。
  • 对于复合多键索引,只能包含最多一个数组字段。

语法:

db.<collection>.createIndex( { <arrayField>: <sortOrder> } )

image-20231018141227790

例子:创建多键索引

先插入示例数据:

db.inventory.insertMany( [
   { _id: 5, type: "food", item: "apple", ratings: [ 5, 8, 9 ] },
   { _id: 6, type: "food", item: "banana", ratings: [ 5, 9 ] },
   { _id: 7, type: "food", item: "chocolate", ratings: [ 9, 5, 8 ] },
   { _id: 8, type: "food", item: "fish", ratings: [ 9, 5 ] },
   { _id: 9, type: "food", item: "grapes", ratings: [ 5, 9, 5 ] }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: { '0': 5, '1': 6, '2': 7, '3': 8, '4': 9 }
}

创建多键索引:

db.inventory.createIndex( { ratings: 1 } )

执行结果:

ratings_1

执行以下查询将会使用多键索引 ratings_1,先找到 ratings 数组中任意位置有 5 的文档,再查找满足条件的文档:

db.inventory.find( { ratings: [ 5, 9 ] } )

执行结果:

[ { _id: 6, type: 'food', item: 'banana', ratings: [ 5, 9 ] } ]

例子:在数组字段创建多键索引

先插入示例数据:

db.students.insertMany( [
   {
      "name": "Andre Robinson",
      "test_scores": [ 88, 97 ]
   },
   {
      "name": "Wei Zhang",
      "test_scores": [ 62, 73 ]
   },
   {
      "name": "Jacob Meyer",
      "test_scores": [ 92, 89 ]
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652f8516090f91a5ab87738b"),
    '1': ObjectId("652f8516090f91a5ab87738c"),
    '2': ObjectId("652f8516090f91a5ab87738d")
  }
}

test_scores 字段上创建升序多键索引:

db.students.createIndex( { test_scores: 1 } )

执行结果:

test_scores_1

索引 test_scores_1 以升序存储 test_scores 字段的不同值,如: [ 62, 73, 88, 89, 92, 97 ]

当查询 test_scores 字段时会使用该索引:

db.students.find(
   {
      test_scores: { $elemMatch: { $gt: 90 } }
   }
)

执行结果:

[
  {
    _id: ObjectId("652f8516090f91a5ab87738d"),
    name: 'Jacob Meyer',
    test_scores: [ 92, 89 ]
  },
  {
    _id: ObjectId("652f8516090f91a5ab87738b"),
    name: 'Andre Robinson',
    test_scores: [ 88, 97 ]
  }
]

例子:在数组中的嵌套文档字段创建多键索引

先插入示例数据:

db.inventory.insertMany( [
   {
      "item": "t-shirt",
      "stock": [
         {
            "size": "small",
            "quantity": 8
         },
         {
            "size": "large",
            "quantity": 10
         },
       ]
   },
   {
      "item": "sweater",
      "stock": [
         {
            "size": "small",
            "quantity": 4
         },
         {
            "size": "large",
            "quantity": 7
         },
       ]
   },
   {
      "item": "vest",
      "stock": [
         {
            "size": "small",
            "quantity": 6
         },
         {
            "size": "large",
            "quantity": 1
         }
       ]
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("652f8d6a090f91a5ab87738e"),
    '1': ObjectId("652f8d6a090f91a5ab87738f"),
    '2': ObjectId("652f8d6a090f91a5ab877390")
  }
}

stock.quantity 字段上创建升序多键索引:

db.inventory.createIndex( { "stock.quantity": 1 } )

执行结果:

stock.quantity_1

索引 stock.quantity_1 以升序存储 stock.quantity 字段的不同值,如:[ 1, 4, 6, 7, 8, 10 ]

当查询 stock.quantity 字段时会使用该索引,例如以下查询 stock 数组中 quantity 小于 5 的文档 :

db.inventory.find(
   {
      "stock.quantity": { $lt: 5 }
   }
)

执行结果:

[
  {
    _id: ObjectId("652f8d6a090f91a5ab877390"),
    item: 'vest',
    stock: [ { size: 'small', quantity: 6 }, { size: 'large', quantity: 1 } ]
  },
  {
    _id: ObjectId("652f8d6a090f91a5ab87738f"),
    item: 'sweater',
    stock: [ { size: 'small', quantity: 4 }, { size: 'large', quantity: 7 } ]
  }
]

还可以在 stock.quantity 字段上排序时使用该索引:

db.inventory.find().sort( { "stock.quantity": -1 } )

执行结果:

[
  {
    _id: ObjectId("652f8d6a090f91a5ab87738e"),
    item: 't-shirt',
    stock: [ { size: 'small', quantity: 8 }, { size: 'large', quantity: 10 } ]
  },
  {
    _id: ObjectId("652f8d6a090f91a5ab87738f"),
    item: 'sweater',
    stock: [ { size: 'small', quantity: 4 }, { size: 'large', quantity: 7 } ]
  },
  {
    _id: ObjectId("652f8d6a090f91a5ab877390"),
    item: 'vest',
    stock: [ { size: 'small', quantity: 6 }, { size: 'large', quantity: 1 } ]
  }
]

例子:使用边界交集来定义要查询的较小范围的值,从而提高了查询性能

先插入示例数据:

db.students.insertMany(
   [
      { _id: 1, name: "Shawn", grades: [ 70, 85 ] },
	  { _id: 2, item: "Elena", grades: [ 92, 84 ] },
      { _id: 3, item: "Elena", grades: [ 102, 88 ] }
   ]
)

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }

grades 数组字段上创建多键索引:

db.students.createIndex( { grades: 1 } )

执行结果:

grades_1

使用 $elemMatch 查询 grades 数组,至少要有一个元素满足大于等于 90 且小于等于 99

db.students.find( { grades : { $elemMatch: { $gte: 90, $lte: 99 } } } )

对于谓词 $gte: 90 的边界为 [ [ 90, Infinity ] ],对于谓词 $lte: 99 的边界为 [ [ -Infinity, 99 ] ],由于使用 $elemMatch 联接这两个谓词,边界交集为 [ [ 90, 99 ] ]

执行结果:

[ { _id: 2, item: 'Elena', grades: [ 92, 84 ] } ]

不使用 $elemMatch 查询 grades 数组,存在某个元素满足大于等于 90 以及其他元素小于等于 99

db.students.find( { grades: { $gte: 90, $lte: 99 } } )

对于谓词 $gte: 90 的边界为 [ [ 90, Infinity ] ],对于谓词 $lte: 99 的边界为 [ [ -Infinity, 99 ] ],由于没有使用 $elemMatch 联接这两个谓词,不对边界进行交集,使用这两个谓词的边界。

执行结果:

[
  { _id: 2, item: 'Elena', grades: [ 92, 84 ] },
  { _id: 3, item: 'Elena', grades: [ 102, 88 ] }
]

例子:对于创建在非数组字段和数组字段上的复合索引,使用组合边界定义更高效的查询约束以提高查询性能

先插入示例数据:

db.survey.insertMany(
   [
      { _id: 1, item: "ABC", ratings: [ 2, 9 ] },
      { _id: 2, item: "XYZ", ratings: [ 4, 3 ] }
   ]
)

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }

itemratings 字段上创建复合多键索引:

db.survey.createIndex( { item: 1, ratings: 1 } )

执行结果:

item_1_ratings_1

执行针对两个索引字段的查询:

db.survey.find( { item: "XYZ", ratings: { $gte: 3 } } )

对于谓词 item: "XYZ" 的边界为 [ [ "XYZ", "XYZ" ]],对于谓词 ratings: { $gte: 3 } 的边界为 [ [ 3, Infinity ] ],组合边界为 { item: [ [ "XYZ", "XYZ" ] ], ratings: [ [ 3, Infinity ] ] }

执行结果:

[ { _id: 2, item: 'XYZ', ratings: [ 4, 3 ] } ]

例子:对于创建在一个非数组字段和多个数组字段上的复合索引,使用组合边界定义更高效的查询约束以提高查询性能

先插入示例数据:

db.survey2.insertMany( [
   {
      _id: 1,
      item: "ABC",
      ratings: [ { score: 2, by: "mn" }, { score: 9, by: "anon" } ]
   },
   {
      _id: 2,
      item: "XYZ",
      ratings: [ { score: 5, by: "anon" }, { score: 7, by: "wv" } ]
   }
] )

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }

在非数组字段 item 和数组字段 ratings.scoreratings.by 创建复合多键索引:

db.survey2.createIndex(
   {
      "item": 1,
      "ratings.score": 1,
      "ratings.by": 1
   }
)

执行结果:

item_1_ratings.score_1_ratings.by_1

执行针对三个索引字段的查询:

db.survey2.find(
   {
      item: "XYZ",
      "ratings.score": { $lte: 5 },
      "ratings.by": "anon"
   }
)

对于谓词 item: "XYZ" 的边界为 [ [ "XYZ", "XYZ" ]],对于谓词 score: { $lte: 5 } 的边界为 [ [ -Infinity, 5] ],对于谓词 by: "anon" 的边界为 [ "anon", "anon" ]

根据查询谓词和索引键值,对 item"ratings.score" 或者 item"ratings.by" 组合边界。即通过以下方式之一完成查询:

  • item"ratings.score" 组合边界
{
   "item" : [ [ "XYZ", "XYZ" ] ],
   "ratings.score" : [ [ -Infinity, 5 ] ],
   "ratings.by" : [ [ MinKey, MaxKey ] ]
}
  • item"ratings.by" 组合边界
{
   "item" : [ [ "XYZ", "XYZ" ] ],
   "ratings.score" : [ [ MinKey, MaxKey ] ],
   "ratings.by" : [ [ "anon", "anon" ] ]
}

要对 "ratings.score""ratings.by" 组合边界,则必须使用 $elemMatch 进行查询。

执行结果:

[
  {
    _id: 2,
    item: 'XYZ',
    ratings: [ { score: 5, by: 'anon' }, { score: 7, by: 'wv' } ]
  }
]

例子:对于创建在同一数组多个字段上的复合索引,使用组合边界定义更高效的查询约束以提高查询性能,需要满足:

  • 索引字段的路径必须相同,例如对于字段 a.b.c.da.b.c 为字段路径。
  • 查询必须在该路径上使用 $elemMatch

先插入示例数据:

db.survey2.insertMany( [
   {
      _id: 1,
      item: "ABC",
      ratings: [ { score: 2, by: "mn" }, { score: 9, by: "anon" } ]
   },
   {
      _id: 2,
      item: "XYZ",
      ratings: [ { score: 5, by: "anon" }, { score: 7, by: "wv" } ]
   }
] )

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }

在数组字段 ratings.scoreratings.by 创建复合索引:

db.survey2.createIndex( { "ratings.score": 1, "ratings.by": 1 } )

字段 ratings.scoreratings.by 有相同的字段路径 ratings

执行结果:

ratings.score_1_ratings.by_1

在相同的字段路径 ratings 上使用 $elemMatch 进行查询:

db.survey2.find( { ratings: { $elemMatch: { score: { $lte: 5 }, by: "anon" } } } )

查询 ratings 数组中的某个元素需同时满足 score: { $lte: 5 }by: "anon"

对于谓词 score: { $lte: 5 } 的边界为 [ [ -Infinity, 5 ] ],对于谓词 by: "anon" 的边界为 [ [ "anon", "anon" ] ],组合边界为 { "ratings.score" : [ [ -Infinity, 5 ] ], "ratings.by" : [ [ "anon", "anon" ] ] }

执行结果:

[
  {
    _id: 2,
    item: 'XYZ',
    ratings: [ { score: 5, by: 'anon' }, { score: 7, by: 'wv' } ]
  }
]

例子:字段路径和 $elemMatch

先插入示例数据:

db.survey3.insertMany( [
   {
      _id: 1,
      item: "ABC",
      ratings: [
         { scores: [ { q1: 2, q2: 4 }, { q1: 3, q2: 8 } ], loc: "A" },
         { scores: [ { q1: 2, q2: 5 } ], loc: "B" }
      ]
   },
   {
      _id: 2,
      item: "XYZ",
      ratings: [
         { scores: [ { q1: 7 }, { q1: 2, q2: 8 } ], loc: "B" }
      ]
   }
] )

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }

在数组字段 ratings.scores.q1ratings.scores.q2 创建复合索引:

db.survey3.createIndex( { "ratings.scores.q1": 1, "ratings.scores.q2": 1 } )

字段 ratings.scores.q1ratings.scores.q2 有相同的字段路径 ratings.scores

执行结果:

ratings.scores.q1_1_ratings.scores.q2_1

对于以下查询,$elemMatch 没有用于相同的字段路径 ratings.scores 上,故不能组合索引边界:

db.survey3.find( { ratings: { $elemMatch: { 'scores.q1': 2, 'scores.q2': 8 } } } )

执行结果:

[
  {
    _id: 1,
    item: 'ABC',
    ratings: [
      { scores: [ { q1: 2, q2: 4 }, { q1: 3, q2: 8 } ], loc: 'A' },
      { scores: [ { q1: 2, q2: 5 } ], loc: 'B' }
    ]
  },
  {
    _id: 2,
    item: 'XYZ',
    ratings: [
      { scores: [ { q1: 7 }, { q1: 2, q2: 8 } ], loc: 'B' }
    ]
  }
]

对于以下查询,$elemMatch 用于相同的字段路径 ratings.scores 上,故会组合索引边界:

db.survey3.find( { 'ratings.scores': { $elemMatch: { 'q1': 2, 'q2': 8 } } } )

执行结果:

[
  {
    _id: 2,
    item: 'XYZ',
    ratings: [
      { scores: [ { q1: 7 }, { q1: 2, q2: 8 } ], loc: 'B' }
    ]
  }
]

文本索引

  • 文本索引用于对包含字符串内容的字段进行文本搜索查询。
  • 一个集合只能有一个文本索引,但该索引可以包含多个字段。
  • 文本索引支持 $text 查询操作。
  • 文本索引会占用大量内存,影响写入性能。
  • 文本索引的默认语言为 english,可以在创建索引时使用 default_language 指定其他语言。
  • 当返回文本搜索结果时,会为每个返回的文档分配一个分数,指示文档与给定搜索查询的相关性,可以按分数对返回的文档进行排序,以使最相关的文档首先出现在结果集中。
  • 如果文本索引包含多个索引字段,则可以为每个索引字段指定不同的权重,默认为 1,表示字段相对于其他索引字段的重要性,权重越高,文本搜索分数越高。
  • 建议创建包含文本索引的复合索引,通过等值匹配限制扫描的文本索引条目数。
  • 文本索引大小写不敏感。
  • 文本索引无法提高排序操作的性能。

语法:

db.<collection>.createIndex(
   {
     <field1>: "text",
     <field2>: "text",
     ...
   },
   {
     weights: {
       <field1>: <weight>,
       <field2>: <weight>,
       ...
     },
     name: <indexName>
   }
 )

先插入示例数据:

db.blog.insertMany( [
   {
     _id: 1,
     content: "This morning I had a cup of coffee.",
     about: "beverage",
     keywords: [ "coffee" ]
   },
   {
     _id: 2,
     content: "Who likes chocolate ice cream for dessert?",
     about: "food",
     keywords: [ "poll" ]
   },
   {
     _id: 3,
     content: "My favorite flavors are strawberry and coffee",
     about: "ice cream",
     keywords: [ "food", "dessert" ]
   }
] )

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }

例子:在 content 字段上创建单字段文本索引

db.blog.createIndex( { "content": "text" } )

执行结果:

content_text

索引 content_text 支持在 content 字段上进行文本搜索查询,例如以下查询返回 content 字段包含 coffee 字符串的文档:

db.blog.find(
   {
      $text: { $search: "coffee" }
   }
)

执行结果:

[
  {
    _id: 1,
    content: 'This morning I had a cup of coffee.',
    about: 'beverage',
    keywords: [ 'coffee' ]
  },
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ]
  }
]

例子:在 aboutkeywords 字段上创建复合文本索引

db.blog.createIndex(
   {
      "about": "text",
      "keywords": "text"
   }
)

执行结果:

about_text_keywords_text

索引 about_text_keywords_text 支持在 aboutkeywords 字段上进行文本搜索查询,例如以下查询返回 aboutkeywords 字段包含 food 字符串的文档:

db.blog.find(
   {
      $text: { $search: "food" }
   }
)

执行结果:

[
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ]
  },
  {
    _id: 2,
    content: 'Who likes chocolate ice cream for dessert?',
    about: 'food',
    keywords: [ 'poll' ]
  }
]

例子:创建通配符文本索引,用于未知的索引字段

db.blog.createIndex( { "$**": "text" } )

执行结果:

 $**_text

索引 $**_text 支持对集合中所有字段进行文本搜索查询。

  1. 查询集合中任意字段包含字符串 coffee 的文档:
db.blog.find( { $text: { $search: "coffee" } } )

执行结果:

[
  {
    _id: 1,
    content: 'This morning I had a cup of coffee.',
    about: 'beverage',
    keywords: [ 'coffee' ]
  },
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ]
  }
]
  1. 查询集合中任意字段包含 pollcoffee 字符串的文档:
db.blog.find( { $text: { $search: "poll coffee" } } )

执行结果:

[
  {
    _id: 1,
    content: 'This morning I had a cup of coffee.',
    about: 'beverage',
    keywords: [ 'coffee' ]
  },
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ]
  },
  {
    _id: 2,
    content: 'Who likes chocolate ice cream for dessert?',
    about: 'food',
    keywords: [ 'poll' ]
  }
]
  1. 查询集合中任意字段包含 chocolate ice cream 语句的文档:
db.blog.find( { $text: { $search: "\"chocolate ice cream\"" } } )

执行结果:

[
  {
    _id: 2,
    content: 'Who likes chocolate ice cream for dessert?',
    about: 'food',
    keywords: [ 'poll' ]
  }
]

例子:创建文本索引时指定权重

先插入示例数据:

db.blog.insertMany( [
   {
     _id: 1,
     content: "This morning I had a cup of coffee.",
     about: "beverage",
     keywords: [ "coffee" ]
   },
   {
     _id: 2,
     content: "Who likes chocolate ice cream for dessert?",
     about: "food",
     keywords: [ "poll" ]
   },
   {
     _id: 3,
     content: "My favorite flavors are strawberry and coffee",
     about: "ice cream",
     keywords: [ "food", "dessert" ]
   }
] )

执行结果:

{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }

创建文本索引,为被索引字段指定权重:

db.blog.createIndex(
   {
     content: "text",
     keywords: "text",
     about: "text"
   },
   {
     weights: {
       content: 10,
       keywords: 5
     },
     name: "BlogTextIndex"
   }
 )

其中:

  • content 权重为 10。
  • keywords 权重为 5。
  • about 权重为默认值 1。

执行结果:

BlogTextIndex
  1. 查询集合中任意字段包含 ice cream 语句的文档:
db.blog.find(
   {
      $text: { $search: "ice cream" }
   },
   {
      score: { $meta: "textScore" }
   }
).sort( { score: { $meta: "textScore" } } )

其中:

  • score: { $meta: "textScore" } 表示文档分数。

执行结果:

[
  {
    _id: 2,
    content: 'Who likes chocolate ice cream for dessert?',
    about: 'food',
    keywords: [ 'poll' ],
    score: 12
  },
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ],
    score: 1.5
  }
]
  1. 查询集合中任意字段包含 food 字符串的文档:
db.blog.find(
   {
      $text: { $search: "food" }
   },
   {
      score: { $meta: "textScore" }
   }
).sort( { score: { $meta: "textScore" } } )

执行结果:

[
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ],
    score: 5.5
  },
  {
    _id: 2,
    content: 'Who likes chocolate ice cream for dessert?',
    about: 'food',
    keywords: [ 'poll' ],
    score: 1.1
  }
]
  1. 查询集合中任意字段包含 coffee 字符串的文档:
db.blog.find(
   {
      $text: { $search: "coffee" }
   },
   {
      score: { $meta: "textScore" }
   }
).sort( { score: { $meta: "textScore" } } )

执行结果:

[
  {
    _id: 1,
    content: 'This morning I had a cup of coffee.',
    about: 'beverage',
    keywords: [ 'coffee' ],
    score: 11.666666666666666
  },
  {
    _id: 3,
    content: 'My favorite flavors are strawberry and coffee',
    about: 'ice cream',
    keywords: [ 'food', 'dessert' ],
    score: 6.25
  }
]

例子:创建包含文本索引的复合索引

先插入示例数据:

db.inventory.insertMany( [
   { _id: 1, department: "tech", description: "lime green computer" },
   { _id: 2, department: "tech", description: "wireless red mouse" },
   { _id: 3, department: "kitchen", description: "green placemat" },
   { _id: 4, department: "kitchen", description: "red peeler" },
   { _id: 5, department: "food", description: "green apple" },
   { _id: 6, department: "food", description: "red potato" }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
}

创建包含文本索引的复合索引:

db.inventory.createIndex(
   {
     department: 1,
     description: "text"
   }
)

执行结果:

department_1_description_text

查询 department 等于 kitchendescription 字段包含 green 字符串的文档:

db.inventory.find( { department: "kitchen", $text: { $search: "green" } } )

执行结果:

[ { _id: 3, department: 'kitchen', description: 'green placemat' } ]

获取上面查询的执行统计信息以查看扫描的索引条目数:

db.inventory.find(
   {
      department: "kitchen", $text: { $search: "green" }
   }
).explain("executionStats")

执行结果:

{
  explainVersion: '2',
  queryPlanner: {
    namespace: 'test.inventory',
    indexFilterSet: false,
    parsedQuery: {
      '$and': [
        { department: { '$eq': 'kitchen' } },
        {
          '$text': {
            '$search': 'green',
            '$language': 'english',
            '$caseSensitive': false,
            '$diacriticSensitive': false
          }
        }
      ]
    },
    queryHash: '1514D243',
    planCacheKey: 'F8F0D498',
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {
      queryPlan: {
        stage: 'TEXT_MATCH',
        planNodeId: 3,
        indexPrefix: { department: 'kitchen' },
        indexName: 'department_1_description_text',
        parsedTextQuery: {
          terms: [ 'green' ],
          negatedTerms: [],
          phrases: [],
          negatedPhrases: []
        },
        textIndexVersion: 2,
        inputStage: {
          stage: 'FETCH',
          planNodeId: 2,
          inputStage: {
            stage: 'IXSCAN',
            planNodeId: 1,
            keyPattern: { department: 1, _fts: 'text', _ftsx: 1 },
            indexName: 'department_1_description_text',
            isMultiKey: true,
            isUnique: false,
            isSparse: false,
            isPartial: false,
            indexVersion: 2,
            direction: 'backward',
            indexBounds: {}
          }
        }
      },
      slotBasedPlan: {
        slots: '$$RESULT=s9 env: { s2 = Nothing (SEARCH_META), s3 = 1697701345208 (NOW), s1 = TimeZoneDatabase(Asia/Tbilisi...Pacific/Nauru) (timeZoneDB), s8 = {"department" : 1, "_fts" : "text", "_ftsx" : 1} }',
        stages: '[3] filter {\n' +
          '    if isObject(s9) \n' +
          '    then ftsMatch(FtsMatcher({"terms" : ["green"], "negatedTerms" : [], "phrases" : [], "negatedPhrases" : []}), s9) \n' +
          '    else fail(4623400, "textmatch requires input to be an object") \n' +
          '} \n' +
          '[2] nlj inner [] [s4, s5, s6, s7, s8] \n' +
          '    left \n' +
          '        [1] unique [s4] \n' +
          '        [1] ixseek KS(3C6B69746368656E003C677265656E002E77359400FE04) KS(3C6B69746368656E003C677265656E00290104) s7 s4 s5 s6 [] @"8eca139a-dc26-4895-a20b-edd19b510d39" @"department_1_description_text" false \n' +
          '    right \n' +
          '        [2] limit 1 \n' +
          '        [2] seek s4 s9 s10 s5 s6 s7 s8 [] @"8eca139a-dc26-4895-a20b-edd19b510d39" true false \n'
      }
    },
    rejectedPlans: []
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 0,
    totalKeysExamined: 1,
    totalDocsExamined: 1,
    executionStages: {
      stage: 'filter',
      planNodeId: 3,
      nReturned: 1,
      executionTimeMillisEstimate: 0,
      opens: 1,
      closes: 1,
      saveState: 0,
      restoreState: 0,
      isEOF: 1,
      numTested: 1,
      filter: '\n' +
        '    if isObject(s9) \n' +
        '    then ftsMatch(FtsMatcher({"terms" : ["green"], "negatedTerms" : [], "phrases" : [], "negatedPhrases" : []}), s9) \n' +
        '    else fail(4623400, "textmatch requires input to be an object") \n',
      inputStage: {
        stage: 'nlj',
        planNodeId: 2,
        nReturned: 1,
        executionTimeMillisEstimate: 0,
        opens: 1,
        closes: 1,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        totalDocsExamined: 1,
        totalKeysExamined: 1,
        collectionScans: 0,
        collectionSeeks: 1,
        indexScans: 0,
        indexSeeks: 1,
        indexesUsed: [ 'department_1_description_text' ],
        innerOpens: 1,
        innerCloses: 1,
        outerProjects: [],
        outerCorrelated: [ Long("4"), Long("5"), Long("6"), Long("7"), Long("8") ],
        outerStage: {
          stage: 'unique',
          planNodeId: 1,
          nReturned: 1,
          executionTimeMillisEstimate: 0,
          opens: 1,
          closes: 1,
          saveState: 0,
          restoreState: 0,
          isEOF: 1,
          dupsTested: 1,
          dupsDropped: 0,
          keySlots: [ Long("4") ],
          inputStage: {
            stage: 'ixseek',
            planNodeId: 1,
            nReturned: 1,
            executionTimeMillisEstimate: 0,
            opens: 1,
            closes: 1,
            saveState: 0,
            restoreState: 0,
            isEOF: 1,
            indexName: 'department_1_description_text',
            keysExamined: 1,
            seeks: 1,
            numReads: 2,
            indexKeySlot: 7,
            recordIdSlot: 4,
            snapshotIdSlot: 5,
            indexIdentSlot: 6,
            outputSlots: [],
            indexKeysToInclude: '00000000000000000000000000000000',
            seekKeyLow: 'KS(3C6B69746368656E003C677265656E002E77359400FE04) ',
            seekKeyHigh: 'KS(3C6B69746368656E003C677265656E00290104) '
          }
        },
        innerStage: {
          stage: 'limit',
          planNodeId: 2,
          nReturned: 1,
          executionTimeMillisEstimate: 0,
          opens: 1,
          closes: 1,
          saveState: 0,
          restoreState: 0,
          isEOF: 1,
          limit: 1,
          inputStage: {
            stage: 'seek',
            planNodeId: 2,
            nReturned: 1,
            executionTimeMillisEstimate: 0,
            opens: 1,
            closes: 1,
            saveState: 0,
            restoreState: 0,
            isEOF: 0,
            numReads: 1,
            recordSlot: 9,
            recordIdSlot: 10,
            seekKeySlot: 4,
            snapshotIdSlot: 5,
            indexIdentSlot: 6,
            indexKeySlot: 7,
            indexKeyPatternSlot: 8,
            fields: [],
            outputSlots: []
          }
        }
      }
    }
  },
  command: {
    find: 'inventory',
    filter: { department: 'kitchen', '$text': { '$search': 'green' } },
    '$db': 'test'
  },
  serverInfo: {
    host: 'linux',
    port: 27017,
    version: '7.0.2',
    gitVersion: '02b3c655e1302209ef046da6ba3ef6749dd0b62a'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeEngine'
  },
  ok: 1
}

其中:

  • totalKeysExamined: 1 表示扫描的索引条目数为 1。

通配符索引

  • 由于可以在同一集合的不同文档指定不同的字段名,可以使用通配符索引支持对任意或未知字段的查询。
  • 使用 "$**" 作为索引键来创建通配符索引。
  • 可以在集合中创建多个通配符索引。
  • 通配符索引可以覆盖与集合中其他索引相同的字段。
  • 通配符索引默认忽略 _id 字段。要在通配符索引中包含 _id 字段,必须通过指定{ "_id" : 1 } 将其显式包含在 wildcardProjection 文档中。
  • 通配符索引不同于通配符文本索引,并且与通配符文本索引不兼容。通配符索引不支持使用 $text 运算符的查询。
  • 当通配符索引遇到嵌套字段时,会为嵌套字段中的字段创建索引。
  • 当通配符索引遇到数组字段时,不会为数组字段中的数组字段创建索引。
  • 仅在索引的字段未知或可能更改时才使用通配符索引,更好的方式是使用统一的文档字段,创建固定字段的索引,以获得更好的性能。

语法:

db.collection.createIndex( { "$**": <sortOrder> } )

只有满足以下所有条件,通配符索引才能支持覆盖查询:

  • 根据查询谓词,执行计划选择通配符索引。
  • 查询谓词只涉及通配符索引中的一个字段。
  • 返回的字段只有 _id 和谓词字段。
  • 查询的字段不是数组。

例如对于以下 employees 集合创建通配符索引:

db.employees.createIndex( { "$**" : 1 } )

下面对单个字段 lastName 的查询,如果 lastName 不是数组,则会使用通配符索引以支持覆盖查询:

db.employees.find(
  { "lastName" : "Doe" },
  { "_id" : 0, "lastName" : 1 }
)

例子:在单个字段上创建通配符索引,用于对索引字段的任何子字段的查询

语法:

db.collection.createIndex( { "<field>.$**": <sortOrder> } )

先插入示例数据:

db.products.insertMany( [
   {
      "product_name" : "Spy Coat",
      "attributes" : {
         "material" : [ "Tweed", "Wool", "Leather" ],
         "size" : {
            "length" : 72,
            "units" : "inches"
         }
      }
   },
   {
      "product_name" : "Spy Pen",
      "attributes" : {
         "colors" : [ "Blue", "Black" ],
         "secret_feature" : {
            "name" : "laser",
            "power" : "1000",
            "units" : "watts",
         }
      }
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("6531eec2227f90e63e4d1478"),
    '1': ObjectId("6531eec2227f90e63e4d1479")
  }
}

attributes 字段上创建通配符索引:

db.products.createIndex( { "attributes.$**" : 1 } )

执行结果:

attributes.$**_1

通配符索引 attributes.$**_1 支持对 attributes 及其子字段进行单字段查询:

db.products.find( { "attributes.size.length" : { $gt : 60 } } )

执行结果:

[
  {
    _id: ObjectId("6531eec2227f90e63e4d1478"),
    product_name: 'Spy Coat',
    attributes: {
      material: [ 'Tweed', 'Wool', 'Leather' ],
      size: { length: 72, units: 'inches' }
    }
  }
]

查询:

db.products.find( { "attributes.material" : "Leather" } )

执行结果:

[
  {
    _id: ObjectId("6531eec2227f90e63e4d1478"),
    product_name: 'Spy Coat',
    attributes: {
      material: [ 'Tweed', 'Wool', 'Leather' ],
      size: { length: 72, units: 'inches' }
    }
  }
]

查询:

db.products.find(
   { "attributes.secret_feature.name" : "laser" },
   { "_id": 0, "product_name": 1, "attributes.colors": 1 }
)

执行结果:

[
  {
    product_name: 'Spy Pen',
    attributes: { colors: [ 'Blue', 'Black' ] }
  }
]

例子:在通配符索引中指定包含或者排除的字段

语法:

db.<collection>.createIndex(
   {
      "$**" : <sortOrder>
   },
   {
      "wildcardProjection" : {
         "<field1>" : < 0 | 1 >,
         "<field2>" : < 0 | 1 >,
         ...
         "<fieldN>" : < 0 | 1 >
      },
      name: "IndexName"
   }
)

其中:

  • wildcardProjection 文档中:

    • 0 表示排除字段

    • 1 表示包含字段

  • 使用 wildcardProjection 选项时,索引键必须为 $**

  • 只有在 wildcardProjection 显式指定 _id 字段时,才支持同时针对 01

先插入示例数据:

db.products.insertMany( [
   {
      "item": "t-shirt",
      "price": "29.99",
      "attributes": {
         "material": "cotton",
         "color": "blue",
         "size": {
            "units": "cm",
            "length": 74
         }
      }
   },
   {
      "item": "milk",
      "price": "3.99",
      "attributes": {
         "sellBy": "02-06-2023",
         "type": "oat"
      }
   },
   {
      "item": "laptop",
      "price": "339.99",
      "attributes": {
         "memory": "8GB",
         "size": {
            "units": "inches",
            "height": 10,
            "width": 15
         }
      }
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("6531fab9227f90e63e4d147a"),
    '1': ObjectId("6531fab9227f90e63e4d147b"),
    '2': ObjectId("6531fab9227f90e63e4d147c")
  }
}

为通配符索引指定只包含 attributes.sizeattributes.color 字段:

db.products.createIndex(
   {
      "$**" : 1
   },
   {
      "wildcardProjection" : {
         "attributes.size" : 1,
         "attributes.color" : 1
      }
   }
)

执行结果:

$**_1

该索引支持以下查询:

db.products.find( { "attributes.size.height" : 10 } )
db.products.find( { "attributes.color" : "blue" } )

为通配符索引指定排除的 attributes.memory 字段,该字段很少被查询:

db.products.createIndex(
   {
      "$**" : 1
   },
   {
      "wildcardProjection" : {
         "attributes.memory" : 0
      }
   }
)

执行结果:

$**_1

该索引支持以下查询:

db.products.find( { "attributes.color" : "blue" } )
db.products.find( { "attributes.size.height" : 10 } )

例子:在所有字段上创建通配符索引,除了 _id 字段。

语法:

db.<collection>.createIndex( { "$**": <sortOrder> } )

先插入示例数据:

db.artwork.insertMany( [
   {
      "name": "The Scream",
      "artist": "Edvard Munch",
      "style": "modern",
      "themes": [ "humanity", "horror" ]
   },
   {
      "name": "Acrobats",
      "artist": {
         "name": "Raoul Dufy",
         "nationality": "French",
         "yearBorn": 1877
      },
      "originalTitle": "Les acrobates",
      "dimensions": [ 65, 49 ]
   },
   {
      "name": "The Thinker",
      "type": "sculpture",
      "materials": [ "bronze" ],
      "year": 1904
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65321529227f90e63e4d147d"),
    '1': ObjectId("65321529227f90e63e4d147e"),
    '2': ObjectId("65321529227f90e63e4d147f")
  }
}

创建对所有字段(除了 _id)的通配符索引:

db.artwork.createIndex( { "$**" : 1 } )

执行结果:

$**_1

此索引支持对集合中的任何字段进行单字段查询。如果文档包含嵌套文档或数组,则通配符索引将遍历文档或数组,并存储其中所有字段的值。

索引支持以下查询:

db.artwork.find( { "style": "modern" } )

执行结果:

[
  {
    _id: ObjectId("65321529227f90e63e4d147d"),
    name: 'The Scream',
    artist: 'Edvard Munch',
    style: 'modern',
    themes: [ 'humanity', 'horror' ]
  }
]

查询:

db.artwork.find( { "artist.nationality": "French" } )

执行结果:

[
  {
    _id: ObjectId("65321529227f90e63e4d147e"),
    name: 'Acrobats',
    artist: { name: 'Raoul Dufy', nationality: 'French', yearBorn: 1877 },
    originalTitle: 'Les acrobates',
    dimensions: [ 65, 49 ]
  }
]

查询:

db.artwork.find( { "materials": "bronze" } )

执行结果:

[
  {
    _id: ObjectId("65321529227f90e63e4d147f"),
    name: 'The Thinker',
    type: 'sculpture',
    materials: [ 'bronze' ],
    year: 1904
  }
]

地理空间索引

  • 地理空间索引支持对存储在 GeoJSON 对象或坐标数据的查询。

  • MongoDB 提供两种类型的地理空间索引:

    • 2dsphere:用于球面上的查询,也可以用于平面上的查询。

    • 2d:用于平面上的查询,不能用于球面上的查询。

  • 如果使用 $near$nearSphere 或者 $geoNear 进行查询,则必须创建地理空间索引。

2dsphere 索引

2dSphere 索引支持类地球体上的地理空间查询。可以:

  • 确定指定区域内的点。

  • 计算与指定点的邻近度。

  • 返回指定坐标的文档。

例子:使用 "2dsphere" 作为索引类型创建 2dsphere 索引

语法:

db.<collection>.createIndex( { <location field> : "2dsphere" } )

先插入示例数据:

db.places.insertMany( [
   {
      loc: { type: "Point", coordinates: [ -73.97, 40.77 ] },
      name: "Central Park",
      category : "Park"
   },
   {
      loc: { type: "Point", coordinates: [ -73.88, 40.78 ] },
      name: "La Guardia Airport",
      category: "Airport"
   },
   {
      loc: { type: "Point", coordinates: [ -1.83, 51.18 ] },
      name: "Stonehenge",
      category : "Monument"
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65362c267b1c57d3a419dbc2"),
    '1': ObjectId("65362c267b1c57d3a419dbc3"),
    '2': ObjectId("65362c267b1c57d3a419dbc4")
  }
}

loc 字段上创建 2dsphere 索引:

db.places.createIndex( { loc : "2dsphere" } )

执行结果:

loc_2dsphere

例子:使用 $geoWithin 查询在指定范围(Polygon)内的数据

语法:

db.<collection>.find( {
   <location field> : {
      $geoWithin : {
         $geometry : {
            type : "Polygon",
            coordinates : [ <coordinates> ]
         }
       }
    }
 } )

其中:

  • 使用 $geoWithin 运算符查询字段的值必须为 GeoJSON 格式。
  • 当指定经度和纬度坐标时,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90
  • 当指定范围 coordinates,数组中的第一个坐标和最后一个坐标必须相同,以形成封闭的范围。
  • 最好创建 2dsphere 以支持 $geoWithin 查询,提高性能。

先插入示例数据:

db.places.insertMany( [
   {
      loc: { type: "Point", coordinates: [ -73.97, 40.77 ] },
      name: "Central Park",
      category : "Park"
   },
   {
      loc: { type: "Point", coordinates: [ -73.88, 40.78 ] },
      name: "La Guardia Airport",
      category: "Airport"
   },
   {
      loc: { type: "Point", coordinates: [ -1.83, 51.18 ] },
      name: "Stonehenge",
      category : "Monument"
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653632d87b1c57d3a419dbc8"),
    '1': ObjectId("653632d87b1c57d3a419dbc9"),
    '2': ObjectId("653632d87b1c57d3a419dbca")
  }
}

使用 $geoWithin 查询在指定范围内的数据:

db.places.find( {
   loc: {
      $geoWithin: {
         $geometry: {
            type: "Polygon",
            coordinates: [ [
               [ -73.95, 40.80 ],
               [ -73.94, 40.79 ],
               [ -73.97, 40.76 ],
               [ -73.98, 40.76 ],
               [ -73.95, 40.80 ]
            ] ]
          }
      }
   }
} )

执行结果:

[
  {
    _id: ObjectId("653632d87b1c57d3a419dbc8"),
    loc: { type: 'Point', coordinates: [ -73.97, 40.77 ] },
    name: 'Central Park',
    category: 'Park'
  }
]

例子:使用 $near 查询球面某点的附近位置数据

语法:

db.<collection>.find( {
   <location field> : {
      $near : {
         $geometry : {
            type : "Point",
            coordinates : [ <longitude>, <latitude> ]
         },
         $maxDistance : <distance in meters>
      }
    }
 } )

其中:

  • 当指定经度和纬度坐标时,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90
  • $maxDistance 字段以米为单位指定距离

先插入示例数据:

db.places.insertMany( [
   {
      loc: { type: "Point", coordinates: [ -73.97, 40.77 ] },
      name: "Central Park",
      category : "Park"
   },
   {
      loc: { type: "Point", coordinates: [ -73.88, 40.78 ] },
      name: "La Guardia Airport",
      category: "Airport"
   },
   {
      loc: { type: "Point", coordinates: [ -1.83, 51.18 ] },
      name: "Stonehenge",
      category : "Monument"
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653634be7b1c57d3a419dbcb"),
    '1': ObjectId("653634be7b1c57d3a419dbcc"),
    '2': ObjectId("653634be7b1c57d3a419dbcd")
  }
}

loc 字段上创建 2dsphere 索引:

db.places.createIndex( { "loc": "2dsphere" } )

执行结果:

loc_2dsphere

使用 $near 查询 loc[ -73.92, 40.78 ] 附近 5000 米内的文档:

db.places.find( {
   loc: {
      $near: {
         $geometry: {
            type: "Point",
            coordinates: [ -73.92, 40.78 ]
         },
         $maxDistance : 5000
      }
   }
} )

执行结果:

[
  {
    _id: ObjectId("653634be7b1c57d3a419dbcc"),
    loc: { type: 'Point', coordinates: [ -73.88, 40.78 ] },
    name: 'La Guardia Airport',
    category: 'Airport'
  },
  {
    _id: ObjectId("653634be7b1c57d3a419dbcb"),
    loc: { type: 'Point', coordinates: [ -73.97, 40.77 ] },
    name: 'Central Park',
    category: 'Park'
  }
]

例子:使用 $geoIntersects 查询与 GeoJSON 对象相交的位置数据

语法:

db.<collection>.find( {
   <location field> : {
      $geoIntersects : {
         $geometry : {
            type : "<GeoJSON object type>",
            coordinates : [ <coordinates> ]
         }
       }
    }
 } )

其中:

  • 当指定经度和纬度坐标时,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90
  • 可以创建 2dsphere 索引以提高性能。

先插入示例数据:

db.gasStations.insertMany( [
   {
      loc: { type: "Point", coordinates: [ -106.31, 35.65 ] },
      state: "New Mexico",
      country: "United States",
      name: "Horizons Gas Station"
   },
   {
      loc: { type: "Point", coordinates: [ -122.62, 40.75 ] },
      state: "California",
      country: "United States",
      name: "Car and Truck Rest Area"
   },
   {
      loc: { type: "Point", coordinates: [ -72.71, 44.15 ] },
      state: "Vermont",
      country: "United States",
      name: "Ready Gas and Snacks"
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653638237b1c57d3a419dbce"),
    '1': ObjectId("653638237b1c57d3a419dbcf"),
    '2': ObjectId("653638237b1c57d3a419dbd0")
  }
}

使用 $geoIntersects 查询与 LineString 位置相交的数据:

db.gasStations.find( {
   loc: {
      $geoIntersects: {
         $geometry: {
            type: "LineString",
            coordinates: [
               [ -105.82, 33.87 ],
               [ -106.01, 34.09 ],
               [ -106.31, 35.65 ],
               [ -107.39, 35.98 ]
            ]
          }
      }
   }
} )

执行结果:

[
  {
    _id: ObjectId("653638237b1c57d3a419dbce"),
    loc: { type: 'Point', coordinates: [ -106.31, 35.65 ] },
    state: 'New Mexico',
    country: 'United States',
    name: 'Horizons Gas Station'
  }
]

例子:使用 $geoWithin$centerSphere 查询球体上圆形内的位置数据

语法:

db.<collection>.find( {
   <location field> : {
      $geoWithin : {
         $centerSphere: [
            [ <longitude>, <latitude> ],
            <radius>
         ]
       }
    }
 } )

其中:

  • 当指定经度和纬度坐标时,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90
  • $centerSphere 中,以弧度为单位指定圆的半径。
  • 将千米除以 6378.1 转换为弧度
  • 可以创建 2dsphere 索引以提高性能。

先插入示例数据:

db.places.insertMany( [
   {
      loc: { type: "Point", coordinates: [ -73.97, 40.77 ] },
      name: "Central Park",
      category : "Park"
   },
   {
      loc: { type: "Point", coordinates: [ -73.88, 40.78 ] },
      name: "La Guardia Airport",
      category: "Airport"
   },
   {
      loc: { type: "Point", coordinates: [ -1.83, 51.18 ] },
      name: "Stonehenge",
      category : "Monument"
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65371894d93ca20a4c3159ea"),
    '1': ObjectId("65371894d93ca20a4c3159eb"),
    '2': ObjectId("65371894d93ca20a4c3159ec")
  }
}

使用 $geoWithin$centerSphere 运算符查询位于经度 -1.76,纬度 51.16,10 公里范围的文档数据

db.places.find( {
   loc: {
      $geoWithin: {
         $centerSphere: [
            [ -1.76, 51.16 ],
            10 / 6378.1
         ]
      }
   }
} )

执行结果:

[
  {
    _id: ObjectId("65371894d93ca20a4c3159ec"),
    loc: { type: 'Point', coordinates: [ -1.83, 51.18 ] },
    name: 'Stonehenge',
    category: 'Monument'
  }
]
2d 索引
  • 2d 索引用于二维平面上的坐标查询,不能用于查询 GeoJSON 对象。
  • 可以修改 2d 索引的位置精度,默认为 26(约等于 2 英尺,60 厘米),范围为 1 到 32。位置精度会影响插入和读取操作的性能。较低的精度可提高插入和更新操作的性能,并减少使用的存储空间。更高的精度可提高读取操作的性能,因为查询扫描索引的小部分数据即可返回结果。位置精度不会影响查询准确性。
  • 可以使用 minmax 指定 2d 索引位置范围。为 2d 索引定义较小的位置范围可减少索引中存储的数据量,提高查询性能。如果集合包含索引位置范围之外的坐标数据,则无法创建 2d 索引。创建 2d 索引后,无法插入索引位置范围之外的坐标数据。
  • 要将球面查询运算符( $centerSphere$geoNear$near$nearSphere)与 2d 索引配合使用,必须将距离转换为弧度,即将距离除以球体的半径,地球的半径为 3963.2 英里或 6378.1 公里。

例子:使用 "2d" 作为索引类型创建 2d 索引

语法:

db.<collection>.createIndex( { <location field> : "2d" } )

先插入示例数据:

db.contacts.insertMany( [
   {
      name: "Evander Otylia",
      phone: "202-555-0193",
      address: [ 55.5, 42.3 ]
   },
   {
      name: "Georgine Lestaw",
      phone: "714-555-0107",
      address: [ -74, 44.74 ]
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65371e2cd93ca20a4c3159ed"),
    '1': ObjectId("65371e2cd93ca20a4c3159ee")
  }
}

address 字段上创建 2d 索引:

db.contacts.createIndex( { address : "2d" } )

执行结果:

address_2d

例子:创建索引时使用 bits 指定 2d 索引的默认位置精度

语法:

db.<collection>.createIndex(
   { <location field>: "2d" },
   { bits: <bit precision> }
)

先插入示例数据:

db.contacts.insertMany( [
   {
      name: "Evander Otylia",
      phone: "202-555-0193",
      address: [ 55.5, 42.3 ]
   },
   {
      name: "Georgine Lestaw",
      phone: "714-555-0107",
      address: [ -74, 44.74 ]
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653722ced93ca20a4c3159ef"),
    '1': ObjectId("653722ced93ca20a4c3159f0")
  }
}

address 字段上创建 2d 索引,指定位置精度为 32 位:

db.contacts.createIndex(
   { address: "2d" },
   { bits: 32 }
)

执行结果:

address_2d

例子:创建索引时使用 minmax 指定 2d 索引位置范围

语法:

db.<collection>.createIndex(
   {
      <location field>: "2d"
   },
   {
      min: <lower bound>,
      max: <upper bound>
   }
)

先插入示例数据:

db.contacts.insertMany( [
   {
      name: "Evander Otylia",
      phone: "202-555-0193",
      address: [ 55.5, 42.3 ]
   },
   {
      name: "Georgine Lestaw",
      phone: "714-555-0107",
      address: [ -74, 44.74 ]
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65372589d93ca20a4c3159f1"),
    '1': ObjectId("65372589d93ca20a4c3159f2")
  }
}

address 字段上创建 2d 索引,指定位置范围 min-75max60

db.contacts.createIndex(
   {
      address: "2d"
   },
   {
      min: -75,
      max: 60
   }
)

执行结果:

address_2d

相比默认位置范围的 2d 索引,该索引覆盖较小的位置范围,提高了性能。但是不能插入该位置范围之外的数据:

db.contacts.insertOne(
   {
      name: "Paige Polson",
      phone: "402-555-0190",
      address: [ 70, 42.3 ]
   }
)

执行结果:

MongoServerError: point not in interval of [ -75, 60 ] :: caused by :: { _id: ObjectId('653726f5d93ca20a4c3159f3'), name: "Paige Polson", phone: "402-555-0190", address: [ 70, 42.3 ] }

例子:使用 $near 查询平面上指定坐标附近位置

语法:

db.<collection>.find( {
   <location field> : {
      $near : {
         [ <longitude>, <latitude> ],
         $maxDistance : <distance in meters>
      }
    }
 } )

其中:

  • 当指定经度和纬度坐标时,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90
  • $maxDistance 字段中以米为单位指定距离。

先插入示例数据:

db.contacts.insertMany( [
   {
      name: "Evander Otylia",
      phone: "202-555-0193",
      address: [ 55.5, 42.3 ]
   },
   {
      name: "Georgine Lestaw",
      phone: "714-555-0107",
      address: [ -74, 44.74 ]
   }
] )

如果使用 $near 运算符进行查询,则必须在包含位置数据的字段上创建地理空间索引,在 address 字段上创建 2d 索引:

db.contacts.createIndex( { address: "2d" } )

执行结果:

address_2d

使用 $near 查询 address 在坐标 [ -73.92, 40.78 ] 50 米范围内的文档:

db.contacts.find( {
   address: {
      $near: [ -73.92, 40.78 ],
      $maxDistance : 50
   }
} )

执行结果:

[
  {
    _id: ObjectId("65372c72d93ca20a4c3159f5"),
    name: 'Georgine Lestaw',
    phone: '714-555-0107',
    address: [ -74, 44.74 ]
  }
]

例子:使用 $geoWithin 查询在某种形状范围内的位置数据

语法:

db.<collection>.find( {
   <location field> : {
      $geoWithin : {
         <shape operator> : <coordinates>
      }
    }
 } )

其中:

  • <shape operator> 可以是:
    • $box
    • $polygon
    • $center
  • <coordinates> 为形状边缘的坐标,当指定经度和纬度坐标时,经度在前,范围为 -180 and 180,纬度在后,范围为 -90 and 90
  • 虽然 $geoWithin 不强制需要地理空间索引,但可以创建地理空间索引以提高查询性能。

先插入示例数据:

db.contacts.insertMany( [
   {
      name: "Evander Otylia",
      phone: "202-555-0193",
      address: [ 55.5, 42.3 ]
   },
   {
      name: "Georgine Lestaw",
      phone: "714-555-0107",
      address: [ -74, 44.74 ]
   }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65373169d93ca20a4c3159f6"),
    '1': ObjectId("65373169d93ca20a4c3159f7")
  }
}

使用 $geoWithin$box 查询位于一个矩形内的数据:

db.contacts.find( {
   address: {
      $geoWithin: {
         $box: [ [ 49, 40 ], [ 60, 60 ] ]
      }
   }
} )

执行结果:

[
  {
    _id: ObjectId("65373169d93ca20a4c3159f6"),
    name: 'Evander Otylia',
    phone: '202-555-0193',
    address: [ 55.5, 42.3 ]
  }
]

哈希索引

  • 哈希索引收集和保存索引字段的哈希值。
  • 哈希索引支持使用哈希分片键进行分片,分片之间的数据分布更均匀。
  • 应在具有较多不同值的字段上创建哈希索引。
  • 哈希函数不支持多键索引,故不能在包含数组的字段上创建哈希索引,也不能将数组插入到哈希索引字段中。
  • 不能对哈希索引指定唯一约束。

创建单字段哈希索引语法:

db.<collection>.createIndex(
   {
      <field>: "hashed"
   }
)

创建复合哈希索引语法:

db.<collection>.createIndex(
   {
      <field1>: "hashed",
      <field2>: "<sort order>",
      <field3>: "<sort order>",
      ...
   }
)

索引属性

不区分大小写索引

  • 创建索引时,使用 collation 指定索引是否支持不区分大小写的查询。
  • 在没有默认排序规则的集合上使用不区分大小写的索引,则需要创建一个带有 collation(排序规则)并指定 strength 参数为 1 或者 2 的索引。要使用该索引的排序规则,则必须在查询级别指定相同的排序规则。
  • 如果集合有默认排序规则,则索引和查询会继承该默认排序规则。

例如:

db.collection.createIndex( { "key" : 1 },
                           { collation: {
                               locale : <locale>,
                               strength : <strength>
                             }
                           } )

其中:

  • locale:指定语言。
  • strength:对比规则,值为 1 或者 2 表示不区分大小写。

例子:创建没有默认排序规则的集合 fruit,并在 type 字段增加一个不区分大小写排序规则的索引

创建集合:

db.createCollection("fruit")

执行结果:

{ ok: 1 }

创建索引:

db.fruit.createIndex( { type: 1},
                      { collation: { locale: 'en', strength: 2 } } )

执行结果:

type_1

插入数据:

db.fruit.insertMany( [
   { type: "apple" },
   { type: "Apple" },
   { type: "APPLE" }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("6535e24bb4a7c326d20a72d7"),
    '1': ObjectId("6535e24bb4a7c326d20a72d8"),
    '2': ObjectId("6535e24bb4a7c326d20a72d9")
  }
}

直接查询,不指定排序规则,则不会使用索引,结果只有 1 条数据:

db.fruit.find( { type: "apple" } )

查询结果:

[ { _id: ObjectId("6535e24bb4a7c326d20a72d7"), type: 'apple' } ]

指定排序规则,使用索引,结果有 3 条数据:

db.fruit.find( { type: "apple" } ).collation( { locale: 'en', strength: 2 } )

执行结果:

[
  { _id: ObjectId("6535e24bb4a7c326d20a72d7"), type: 'apple' },
  { _id: ObjectId("6535e24bb4a7c326d20a72d8"), type: 'Apple' },
  { _id: ObjectId("6535e24bb4a7c326d20a72d9"), type: 'APPLE' }
]

指定排序规则,不使用索引,结果有 3 条数据:

db.fruit.find( { type: "apple" } ).collation( { locale: 'en', strength: 1 } )

执行结果:

[
  { _id: ObjectId("6535e24bb4a7c326d20a72d7"), type: 'apple' },
  { _id: ObjectId("6535e24bb4a7c326d20a72d8"), type: 'Apple' },
  { _id: ObjectId("6535e24bb4a7c326d20a72d9"), type: 'APPLE' }
]

例子:创建带有默认排序规则的集合 names,并在 first_name 字段创建索引

创建集合:

db.createCollection("names", { collation: { locale: 'en_US', strength: 2 } } )

执行结果:

{ ok: 1 }

创建索引,会继承默认的排序规则:

db.names.createIndex( { first_name: 1 } )

执行结果:

first_name_1

插入数据:

db.names.insertMany( [
   { first_name: "Betsy" },
   { first_name: "BETSY"},
   { first_name: "betsy"}
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("6535e681b4a7c326d20a72da"),
    '1': ObjectId("6535e681b4a7c326d20a72db"),
    '2': ObjectId("6535e681b4a7c326d20a72dc")
  }
}

查询:

db.names.find( { first_name: "betsy" } )

执行结果,会继承默认的排序规则,使用索引,结果有 3 条数据:

[
  { _id: ObjectId("6535e681b4a7c326d20a72da"), first_name: 'Betsy' },
  { _id: ObjectId("6535e681b4a7c326d20a72db"), first_name: 'BETSY' },
  { _id: ObjectId("6535e681b4a7c326d20a72dc"), first_name: 'betsy' }
]

不使用默认的排序规则,指定不同的排序规则,不指定 strength,执行区分大小写的查询,不使用索引,结果有 1 条数据:

db.names.find( { first_name: "betsy" } ).collation( { locale: 'en_US' } )

执行结果:

[ { _id: ObjectId("6535e681b4a7c326d20a72dc"), first_name: 'betsy' } ]

隐藏索引

  • 隐藏索引对查询计划不可见,用于评估不使用该索引时对性能的影响。
  • 不能隐藏 _id 索引。
  • 如果隐藏索引为唯一索引,则该索引仍将其唯一约束应用于文档。
  • 如果隐藏索引为 TTL 索引,则该索引仍会使文档过期。
  • 隐藏索引仍会出现在 listIndexesdb.collection.getIndexes() 结果中。
  • 在对集合更新时,也会更新隐藏索引,并消耗磁盘空间。
  • db.collection.stats()$indexStats 统计信息操作会包含隐藏索引。
  • 隐藏一个未隐藏的索引及显式一个隐藏的索引会重置其 $indexStats

例子:使用 db.collection.createIndex() 创建索引时,将 hidden 设置为 true,创建隐藏索引

db.addresses.createIndex(
   { borough: 1 },
   { hidden: true }
);

执行结果:

borough_1

查看索引:

db.addresses.getIndexes()

执行结果:

[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  { v: 2, key: { borough: 1 }, name: 'borough_1', hidden: true }
]

例子:使用 db.collection.hideIndex() 隐藏索引

先创建一个索引:

db.restaurants.createIndex( { borough: 1, ratings: 1 } );

执行结果:

borough_1_ratings_1

可以通过指定索引名称来隐藏索引:

db.restaurants.hideIndex( "borough_1_ratings_1" );

执行结果:

{ hidden_old: false, hidden_new: true, ok: 1 }

也可以通过指定索引对应的文档来隐藏索引:

db.restaurants.hideIndex( { borough: 1, ratings: 1 } );

执行结果:

{ hidden_old: false, hidden_new: true, ok: 1 }

查看索引:

db.restaurants.getIndexes()

执行结果:

[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  {
    v: 2,
    key: { borough: 1, ratings: 1 },
    name: 'borough_1_ratings_1',
    hidden: true
  }
]

例子:使用 db.collection.unhideIndex() 显示索引

可以通过指定索引名称来显示索引:

db.restaurants.unhideIndex( "borough_1_ratings_1" );

执行结果:

{ hidden_old: true, hidden_new: false, ok: 1 }

也可以通过指定索引对应的文档来显示索引:

db.restaurants.unhideIndex( { borough: 1, ratings: 1 } );

执行结果:

{ hidden_old: true, hidden_new: false, ok: 1 }

查看索引:

db.restaurants.getIndexes()

执行结果:

[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  {
    v: 2,
    key: { borough: 1, ratings: 1 },
    name: 'borough_1_ratings_1'
  }
]

部分索引

部分索引仅索引集合中满足指定过滤表达式的文档,可以节约存储,降低索引创建和维护的性能成本。

创建索引时使用 partialFilterExpression 选项指定过滤表达式,可以使用:

  • 等值表达式,例如 field: value 或者使用 $eq 运算符。
  • $exists: true 表达式
  • $gt$gte$lt$lte 表达式
  • $type 表达式
  • $and 运算符
  • $or 运算符
  • $in 运算符

若要使用部分索引,查询必须包含过滤表达式(或者其子集),作为其查询条件的一部分。

例如以下部分索引:

db.restaurants.createIndex(
   { cuisine: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)

可以用于以下查询:

db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )

不能用于以下查询:

db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
db.restaurants.find( { cuisine: "Italian" } )

相比稀疏索引,部分索引能够更好控制为哪些文档创建索引,为稀疏索引的超集。例如通过部分索引实现稀疏索引功能:

db.contacts.createIndex(
   { name: 1 },
   { partialFilterExpression: { name: { $exists: true } } }
)

部分索引可以在非索引字段上指定过滤表达式,例如:

db.contacts.createIndex(
   { name: 1 },
   { partialFilterExpression: { email: { $exists: true } } }
)

查询语句要使用该索引,谓词条件必须包含索引字段并满足过滤表达式。例如下面查询谓词条件包含了 name 字段和在 email 字段上的非空匹配,则可以使用该部分索引:

db.contacts.find( { name: "xyz", email: { $regex: /\.org$/ } } )

下面查询不满足部分索引的过滤表达式,则不能使用该部分索引:

db.contacts.find( { name: "xyz", email: { $exists: false } } )

部分索引有以下限制:

  • 不能同时指定 partialFilterExpressionsparse 选项。
  • _id 索引不能是部分索引。
  • 分片键索引不能是部分索引。

例子:创建部分索引

先插入示例数据:

db.restaurants.insertOne(
{
   "address" : {
      "building" : "1007",
      "coord" : [
         -73.856077,
         40.848447
      ],
      "street" : "Morris Park Ave",
      "zipcode" : "10462"
   },
   "borough" : "Bronx",
   "cuisine" : "Bakery",
   "rating" : { "date" : ISODate("2014-03-03T00:00:00Z"),
                "grade" : "A",
                "score" : 2
              },
   "name" : "Morris Park Bake Shop",
   "restaurant_id" : "30075445"
}
)

执行结果:

{
  acknowledged: true,
  insertedId: ObjectId("653766ced93ca20a4c3159fa")
}

boroughcuisine 字段上创建部分索引,指定过滤条件为 rating.grade 字段值为 A

db.restaurants.createIndex(
   { borough: 1, cuisine: 1 },
   { partialFilterExpression: { 'rating.grade': { $eq: "A" } } }
)

执行结果:

borough_1_cuisine_1

以下查询可以使用该部分索引:

db.restaurants.find( { borough: "Bronx", 'rating.grade': "A" } )

执行结果:

[
  {
    _id: ObjectId("653766ced93ca20a4c3159fa"),
    address: {
      building: '1007',
      coord: [ -73.856077, 40.848447 ],
      street: 'Morris Park Ave',
      zipcode: '10462'
    },
    borough: 'Bronx',
    cuisine: 'Bakery',
    rating: { date: ISODate("2014-03-03T00:00:00.000Z"), grade: 'A', score: 2 },
    name: 'Morris Park Bake Shop',
    restaurant_id: '30075445'
  }
]

以下查询不包含 rating.grade 字段,不能使用该部分索引:

db.restaurants.find( { borough: "Bronx", cuisine: "Bakery" } )

稀疏索引

  • 稀疏索引仅包含具有索引字段的文档,将跳过缺少索引字段的文档。相比之下,非稀疏索引包含集合中的所有文档,为不包含索引字段的文档存储 Null 值。
  • 从 MongoDB 5.0 开始,可以为同一字段创建稀疏和非稀疏索引,也可以为同一字段创建稀疏唯一索引和非稀疏唯一索引。
  • 下列索引始终为稀疏索引:
    • 2d

    • 2dsphere (version 2)

    • Text

    • Wildcard

创建索引时将 sparse 设置为 true 即创建稀疏索引,例如:

db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )

例子:创建稀疏索引

先插入示例数据:

db.scores.insertMany([
   { "userid" : "newbie" },
   { "userid" : "abby", "score" : 82 },
   { "userid" : "nina", "score" : 90 }
])

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653774f4d93ca20a4c3159fe"),
    '1': ObjectId("653774f4d93ca20a4c3159ff"),
    '2': ObjectId("653774f4d93ca20a4c315a00")
  }
}

score 字段上创建稀疏索引:

db.scores.createIndex( { score: 1 } , { sparse: true } )

执行结果:

score_1

以下查询会用到稀疏索引:

db.scores.find( { score: { $lt: 90 } } )

执行结果:

[
  {
    _id: ObjectId("653774f4d93ca20a4c3159ff"),
    userid: 'abby',
    score: 82
  }
]

例子:稀疏索引不能返回完整结果

先插入示例数据:

db.scores.insertMany([
   { "userid" : "newbie" },
   { "userid" : "abby", "score" : 82 },
   { "userid" : "nina", "score" : 90 }
])

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653774f4d93ca20a4c3159fe"),
    '1': ObjectId("653774f4d93ca20a4c3159ff"),
    '2': ObjectId("653774f4d93ca20a4c315a00")
  }
}

score 字段上创建稀疏索引:

db.scores.createIndex( { score: 1 } , { sparse: true } )

执行结果:

score_1

查询集合中所有文档,并以 score 字段排序:

db.scores.find().sort( { score: -1 } )

执行结果:

[
  {
    _id: ObjectId("653774f4d93ca20a4c315a00"),
    userid: 'nina',
    score: 90
  },
  {
    _id: ObjectId("653774f4d93ca20a4c3159ff"),
    userid: 'abby',
    score: 82
  },
  { _id: ObjectId("653774f4d93ca20a4c3159fe"), userid: 'newbie' }
]

为返回完整的数据,此时 MongoDB 不会使用该稀疏索引来完成查询。

可以通过指定 hint() 来使用稀疏索引:

db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )

执行结果:

[
  {
    _id: ObjectId("653774f4d93ca20a4c315a00"),
    userid: 'nina',
    score: 90
  },
  {
    _id: ObjectId("653774f4d93ca20a4c3159ff"),
    userid: 'abby',
    score: 82
  }
]

使用稀疏索引后,只会返回有 score 字段的文档。

例子:带唯一约束的稀疏索引

先插入示例数据:

db.scores.insertMany([
   { "userid" : "newbie" },
   { "userid" : "abby", "score" : 82 },
   { "userid" : "nina", "score" : 90 }
])

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("653777dbd93ca20a4c315a01"),
    '1': ObjectId("653777dbd93ca20a4c315a02"),
    '2': ObjectId("653777dbd93ca20a4c315a03")
  }
}

score 字段上创建带唯一约束的稀疏索引:

db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )

执行结果:

score_1

此索引允许 score 字段有唯一值的文档或没有 score 字段的文档插入到集合中:

db.scores.insertMany( [
   { "userid": "newbie", "score": 43 },
   { "userid": "abby", "score": 34 },
   { "userid": "nina" }
] )

执行结果:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId("65377886d93ca20a4c315a04"),
    '1': ObjectId("65377886d93ca20a4c315a05"),
    '2': ObjectId("65377886d93ca20a4c315a06")
  }
}

不允许插入相同的 score 值:

db.scores.insertMany( [
   { "userid": "newbie", "score": 82 },
   { "userid": "abby", "score": 90 }
] )

执行结果:

MongoBulkWriteError: E11000 duplicate key error collection: test.scores index: score_1 dup key: { score: 82 }
Result: BulkWriteResult {
  insertedCount: 0,
  matchedCount: 0,
  modifiedCount: 0,
  deletedCount: 0,
  upsertedCount: 0,
  upsertedIds: {},
  insertedIds: {
    '0': ObjectId("653778d4d93ca20a4c315a07"),
    '1': ObjectId("653778d4d93ca20a4c315a08")
  }
}
Write Errors: [
  WriteError {
    err: {
      index: 0,
      code: 11000,
      errmsg: 'E11000 duplicate key error collection: test.scores index: score_1 dup key: { score: 82 }',
      errInfo: undefined,
      op: {
        userid: 'newbie',
        score: 82,
        _id: ObjectId("653778d4d93ca20a4c315a07")
      }
    }
  }
]

TTL 索引

  • TTL 索引是特殊的单字段索引,用于在特定时间后自动删除文档。
  • 为避免创建 TTL 索引后删除大量满足条件的文档而导致的性能问题,应该在系统空闲时间创建索引或提前删除满足条件的文档。
  • 从被索引字段的键值时间算起,如果字段为包含多个时间值的数组,则使用最早的时间,在指定的过期时间后,TTL 索引会将文档过期。
  • 删除过期文档的后台任务每 60 秒运行一次,因此过期文档会在 0 到 60 秒之间开始被删除。
  • 如果索引字段不是日期类型或包含一个或多个日期值的数组,则文档不会过期。
  • 如果文档不包含 TTL 索引字段,则文档不会过期。
  • _id 字段不支持 TTL 索引。
  • 如果字段已存在非 TTL 单字段索引,则无法在同一字段上创建 TTL 索引。

创建 TTL 索引时,使用 expireAfterSeconds 以秒为单位指定 TTL 值,范围为 02147483647

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

如果 expireAfterSeconds0,则索引字段的值就是过期时间,如果该值为未来时间,则文档将在未来过期,如果该值为过去时间,则文档已过期。例如:

db.log_events.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )

对于包含 expireAfterSeconds 字段的时间序列集合,可以在带有 partialFilterExpression 的部分 TTL 索引中设置一个更短的过期时间。

例如对于集合:

db.createCollection(
    "weather24h",
    {
       timeseries: {
          timeField: "timestamp",
          metaField: "sensor",
          granularity: "hours"
       },
       expireAfterSeconds: 86400})

创建以下 TTL 索引,会删除 1 小时前的文档:

db.eventlog.createIndex(
  { "timestamp": 1 },
  { partialFilterExpression: { "sensor": { $eq: "40.761873, -73.984287" } } },
  { expireAfterSeconds: 3600 } )

从 MongoDB 5.1 开始,可以使用 collMod 数据库命令为现有的单字段索引增加 expireAfterSeconds 选项,将非 TTL 单字段索引修改为 TTL 索引,语法:

db.runCommand({
  "collMod": <collName>,
  "index": {
    "keyPattern": <keyPattern>,
    "expireAfterSeconds": <number>
  }
})

例如:

db.runCommand({
  "collMod": "tickets",
  "index": {
    "keyPattern": { "lastModifiedDate": 1 },
    "expireAfterSeconds": 100
  }
})

可以使用 collMod 数据库命令修改 TTL 索引的 expireAfterSeconds 值,语法:

db.runCommand({
  "collMod": <collName>,
  "index": {
    "keyPattern": <keyPattern>,
    "expireAfterSeconds": <number>
  }
})

例如:

db.runCommand({
  "collMod": "tickets",
  "index": {
    "keyPattern": { "lastModifiedDate": 1 },
    "expireAfterSeconds": 100
  }
})

唯一索引

  • 唯一索引确保索引字段不存储重复值。
  • 默认在 _id 字段上创建一个唯一索引。
  • 对于唯一单字段索引,集合中只能有一个不包含索引字段的文档。
  • 对于唯一复合索引,集合中只能有一个不包含所有索引字段的文档。
  • 对于要分片的集合,如有其他唯一索引,则无法对该集合进行分片。对于已分片的集合,无法在其他字段上创建唯一索引。
  • 从 MongoDB 5.0 开始,可以为同一字段创建稀疏唯一索引和非稀疏唯一索引。
  • 创建索引时指定 uniquetrue 以创建唯一索引。

语法:

db.collection.createIndex( <key and index type specification>, { unique: true } )

在单个字段上创建唯一索引:

db.members.createIndex( { "user_id": 1 }, { unique: true } )

唯一复合索引:

db.members.createIndex( { groupNumber: 1, lastname: 1, firstname: 1 }, { unique: true } )

管理索引

查看索引

查看集合所有索引:

db.people.getIndexes()

查看数据库的所有索引:

db.getCollectionNames().forEach(function(collection) {
    indexes = db[collection].getIndexes();
    print("Indexes for " + collection + ":");
    printjson(indexes);
});

查询数据库的某种索引:

// The following finds all text indexes

db.adminCommand("listDatabases").databases.forEach(function(d){
    let mdb = db.getSiblingDB(d.name);
    mdb.getCollectionInfos({ type: "collection" }).forEach(function(c){
      let currentCollection = mdb.getCollection(c.name);
      currentCollection.getIndexes().forEach(function(idx){
        let idxValues = Object.values(Object.assign({}, idx.key));

        if (idxValues.includes("text")) {
          print("text index: " + idx.name + " on " + d.name + "." + c.name);
          printjson(idx);
        };
      });
    });
});

修改索引

  • 修改现有索引,需要删除并重建索引。但可以通过 collMod 命令修改 TTL 索引。
  • 为不影响性能,在删除生产环境的索引之前,可以先创建一个包含相同字段的临时冗余索引。

例子:创建索引,然后修改为唯一索引

  1. 创建 siteAnalytics 集合,并在 url 字段上创建索引
db.siteAnalytics.createIndex( { "url": 1 } )

执行结果:

url_1
  1. url 字段上创建临时索引
db.siteAnalytics.createIndex( { "url": 1, "dummyField": 1 } )

执行结果:

url_1_dummyField_1
  1. 删除索引 url_1,由于有临时索引,不会影响性能
db.siteAnalytics.dropIndex("url_1")

执行结果:

{ nIndexesWas: 3, ok: 1 }
  1. 重建索引为唯一索引
db.siteAnalytics.createIndex( { "url": 1 }, { "unique": true } )

执行结果:

url_1
  1. 删除临时索引
db.siteAnalytics.dropIndex( "url_1_dummyField_1" )

执行结果:

{ nIndexesWas: 3, ok: 1 }
  1. 查看索引
db.siteAnalytics.getIndexes()

执行结果:

[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  { v: 2, key: { url: 1 }, name: 'url_1', unique: true }
]

度量索引

  • 使用 $indexStats 获取集合中各个索引使用情况的统计信息。
db.scores.aggregate( [ { $indexStats: { } } ] )

执行结果:

[
  {
    name: '_id_',
    key: { _id: 1 },
    host: 'linux:27017',
    accesses: { ops: Long("0"), since: ISODate("2023-10-25T01:02:50.946Z") },
    spec: { v: 2, key: { _id: 1 }, name: '_id_' }
  },
  {
    name: 'score_1',
    key: { score: 1 },
    host: 'linux:27017',
    accesses: { ops: Long("0"), since: ISODate("2023-10-25T01:02:50.946Z") },
    spec: {
      v: 2,
      unique: true,
      key: { score: 1 },
      name: 'score_1',
      sparse: true
    }
  }
]
  • 使用 explain() 查看语句是否使用索引。
db.scores.find({"score":34}).explain()

执行结果:

{
  explainVersion: '2',
  queryPlanner: {
    namespace: 'test.scores',
    indexFilterSet: false,
    parsedQuery: { score: { '$eq': 34 } },
    queryHash: '3921E39F',
    planCacheKey: 'C2B2F39E',
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {
      queryPlan: {
        stage: 'FETCH',
        planNodeId: 2,
        inputStage: {
          stage: 'IXSCAN',
          planNodeId: 1,
          keyPattern: { score: 1 },
          indexName: 'score_1',
          isMultiKey: false,
          multiKeyPaths: { score: [] },
          isUnique: true,
          isSparse: true,
          isPartial: false,
          indexVersion: 2,
          direction: 'forward',
          indexBounds: { score: [ '[34, 34]' ] }
        }
      },
      slotBasedPlan: {
        slots: '$$RESULT=s11 env: { s1 = TimeZoneDatabase(Europe/Istanbul...Asia/Atyrau) (timeZoneDB), s10 = {"score" : 1}, s2 = Nothing (SEARCH_META), s5 = KS(2B440104), s3 = 1698214246280 (NOW), s6 = KS(2B44FE04) }',
        stages: '[2] nlj inner [] [s4, s7, s8, s9, s10] \n' +
          '    left \n' +
          '        [1] cfilter {(exists(s5) && exists(s6))} \n' +
          '        [1] ixseek s5 s6 s9 s4 s7 s8 [] @"3916c703-3a62-4f1b-a9a0-e270b0595fc9" @"score_1" true \n' +
          '    right \n' +
          '        [2] limit 1 \n' +
          '        [2] seek s4 s11 s12 s7 s8 s9 s10 [] @"3916c703-3a62-4f1b-a9a0-e270b0595fc9" true false \n'
      }
    },
    rejectedPlans: []
  },
  command: { find: 'scores', filter: { score: 34 }, '$db': 'test' },
  serverInfo: {
    host: 'linux',
    port: 27017,
    version: '7.0.2',
    gitVersion: '02b3c655e1302209ef046da6ba3ef6749dd0b62a'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeEngine'
  },
  ok: 1
}
  • 使用 hint() 强制使用索引。
db.people.find(
   { name: "John Doe", zipcode: { $gt: "63000" } }
).hint( { zipcode: 1 } )
  • hint() 中指定 $natural 运算符表示不使用任何索引。
db.people.find(
   { name: "John Doe", zipcode: { $gt: "63000" } }
).hint( { $natural: 1 } )

索引策略

应该根据应用程序的查询语句来创建合适的索引,以提供最佳性能。

ESR 规则

在创建复合索引时,使用 ESR(相等、排序、范围)规则来排列索引键,可创建出更高效的复合索引。

相等

相等(Equality)是指对单个值的完全匹配。例如以下查询中的 model 字段需要完全匹配 Cordoba

db.cars.find( { model: "Cordoba" } )
db.cars.find( { model: { $eq: "Cordoba" } } )

使用完全匹配可以大大减少需要访问的文档数,特别是可选择性高的字段,故将需要完全匹配的字段放在索引的首位。

排序

排序(Sort)决定了结果的顺序。在索引中,排序字段应在相等字段之后,只有排序字段前的所有相等字段都进行完全匹配后,才能使用索引进行排序。

例如对于以下查询:

db.cars.find( { manufacturer: "GM" } ).sort( { model: 1 } )

可以在 manufacturermodel 字段上创建索引以提高性能:

db.cars.createIndex( { manufacturer: 1, model: 1 } )

其中:

  • 由于 manufacturer 是等值匹配,故在首位。
  • 索引中 model 的顺序与查询一致。
范围

范围(Range)过滤扫描结果。为提高查询性能,尽可能缩小扫描范围。

范围查询如下:

db.cars.find( { price: { $gte: 15000} } )
db.cars.find( { age: { $lt: 10 } } )
db.cars.find( { priorAccidents: { $ne: null } } )

MongoDB 无法对范围过滤的结果进行索引排序。在索引中,将范围字段放在排序字段之后,以便可以使用非阻塞索引排序。

其他注意事项
  • 不等运算符,例如 $ne 或者 $nin,是范围运算符,而不是相等运算符。
  • $regex 是范围运算符。
  • $in 可以是相等运算符或者范围运算符。单独使用 $in 时,是相等运算符。 与 .sort() 一起使用时,类似于范围运算符。
示例

对于以下查询:

db.cars.find(
   {
       manufacturer: 'Ford',
       cost: { $gt: 15000 }
   } ).sort( { model: 1 } )

其中:

  • manufacturer: 'Ford' 为相等匹配。
  • cost: { $gt: 15000 } 为范围匹配。
  • model 用于排序。

根据 ESR 规则,该查询的最佳索引为:

{ manufacturer: 1, model: 1, cost: 1 }

创建索引支持查询

如果只查询集合中的单个字段,则只需为该集合创建一个单字段索引。例如:

db.products.createIndex( { "category": 1 } )

如果有时查询一个字段,有时查询该字段与第二个字段的组合,则创建复合索引比创建单字段索引更高效。例如:

db.products.createIndex( { "category": 1, "item": 1 } )

使用索引进行排序

由于索引包含有序数据,故可以从包含排序字段的索引中获取排序结果,以提高性能。

如果升序或降序索引位于单个字段上,则对字段进行升序或降序排序都可以使用索引。

例如对于以下升序索引:

db.records.createIndex( { a: 1 } )

既可以支持对字段 a 的升序排序:

db.records.find().sort( { a: 1 } )

也可以支持对字段 a 的降序排序:

db.records.find().sort( { a: -1 } )

复合索引支持对多个索引字段排序。排序字段的列出顺序必须与索引中字段的列出顺序一致。排序方向必须与索引字段都一致或者都相反。

复合索引开头的一个或者多个字段称为索引前缀,可以使用索引前缀进行排序。

例如对于以下复合索引:

db.data.createIndex( { a:1, b: 1, c: 1, d: 1 } )

该索引的前缀有:

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

以下查询可使用该索引前缀进行排序:

Example 例Index Prefix 索引前缀
db.data.find().sort( { a: 1 } ){ a: 1 }
db.data.find().sort( { a: -1 } ){ a: 1 }
db.data.find().sort( { a: 1, b: 1 } ){ a: 1, b: 1 }
db.data.find().sort( { a: -1, b: -1 } ){ a: 1, b: 1 }
db.data.find().sort( { a: 1, b: 1, c: 1 } ){ a: 1, b: 1, c: 1 }
db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } ){ a: 1, b: 1 }

如果排序字段为复合索引中的非前缀字段,则查询必须包含索引中该排序字段前面的所有字段的相同条件。

例如对于以下索引:

{ a: 1, b: 1, c: 1, d: 1 }

以下查询可以使用索引获取排序结果:

Example 例Index Prefix 索引前缀
db.data.find( { a: 5 } ).sort( { b: 1, c: 1 } ){ a: 1 , b: 1, c: 1 }
db.data.find( { b: 3, a: 4 } ).sort( { c: 1 } ){ a: 1, b: 1, c: 1 }
db.data.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } ){ a: 1, b: 1 }

安全

MongoDB 默认没有启用访问控制。在生产环境中,需要启用访问控制,创建角色和用户。

角色

  • MongoDB 使用基于角色的访问控制(RBAC)来管理对系统的访问。
  • 将权限或者角色授予给角色,再将角色授予给用户,用户即获得了角色的所有权限。
  • 角色可以继承其所在数据库中角色的权限,在 admin 数据库上创建的角色可以继承任何数据库中的角色权限。

内置角色

MongoDB 提供了一些内置角色用于不同级别的访问。

image-20231106120431102

查看当前数据库的内置角色:

db.runCommand({ rolesInfo: 1, showBuiltinRoles: true })

执行结果:

{
  roles: [
    {
      role: 'userAdmin',
      db: 'test',
      isBuiltin: true,
      roles: [],
      inheritedRoles: []
    },
    {
      role: 'dbAdmin',
      db: 'test',
      isBuiltin: true,
      roles: [],
      inheritedRoles: []
    },
    {
      role: 'read',
      db: 'test',
      isBuiltin: true,
      roles: [],
      inheritedRoles: []
    },
    {
      role: 'readWrite',
      db: 'test',
      isBuiltin: true,
      roles: [],
      inheritedRoles: []
    },
    {
      role: 'dbOwner',
      db: 'test',
      isBuiltin: true,
      roles: [],
      inheritedRoles: []
    },
    {
      role: 'enableSharding',
      db: 'test',
      isBuiltin: true,
      roles: [],
      inheritedRoles: []
    }
  ],
  ok: 1
}
数据库用户角色

每个数据库都包含以下客户端角色:

  • read:包括读取所有非系统集合和 system.js 集合上数据的权限,权限操作有:

    • changeStream
    • collStats
    • dbHash
    • dbStats
    • find
    • killCursors
    • listCollections
    • listIndexes
    • listSearchIndexes
  • readWrite:包括 read 角色所有权限以及修改所有非系统集合和 system.js 集合上数据的权限,权限操作有:

  • changeStream

  • collStats

  • convertToCapped

  • createCollection

  • createIndex

  • createSearchIndexes

  • dbHash

  • dbStats

  • dropCollection

  • dropIndex

  • dropSearchIndex

  • find

  • insert

  • killCursors

  • listCollections

  • listIndexes

  • listSearchIndexes

  • remove

  • renameCollectionSameDB

  • update

  • updateSearchIndex

数据库管理角色

每个数据库都包含以下数据库管理角色:

  • dbAdmin:提供执行管理任务的权限,包括:
ResourcePermitted Actions
system.profileopen in new windowchangeStream
collStats
convertToCapped
createCollection
dbHash
dbStats
dropCollection
find
killCursors
listCollections
listIndexes
listSearchIndexes
planCacheRead
所有非系统集合bypassDocumentValidation
collMod
collStats
compact
convertToCapped
createCollection
createIndex
createSearchIndexes
dbStats
dropCollection
dropDatabase
dropIndex
dropSearchIndex
enableProfiler
listCollections
listIndexes
listSearchIndexes
planCacheIndexFilter
planCacheRead
planCacheWrite
reIndex
renameCollectionSameDB
storageDetails
updateSearchIndex
validate
  • userAdmin:提供在当前数据库上创建和修改角色和用户的功能,该角色允许用户为任何用户授予任何权限。包括:

  • changeCustomData

  • changePassword

  • createRole

  • createUser

  • dropRole

  • dropUser

  • grantRole

  • revokeRole

  • setAuthenticationRestriction

  • viewRole

  • viewUser

dbOwner:可以对数据库执行任何管理操作,包括 readWritedbAdminuserAdmin 角色授予的权限。

备份和恢复角色

数据库 admin 包括以下用于备份和恢复数据的角色:

  • backup:提供备份数据所需权限。
  • restore:提供恢复数据所需权限。
所有数据库角色

为除 local and config 数据库之外的所有数据库提高权限。

  • readAnyDatabase:对于除 local and config 数据库之外的所有数据库,提供与 read 角色相同的权限,及对整个群集的 listDatabases 操作。
  • readWriteAnyDatabase:对于除 local and config 数据库之外的所有数据库,提供与 readWrite 角色相同的权限,compactStructuredEncryptionData 操作及对整个群集的 listDatabases 操作。
  • userAdminAnyDatabase:对于除 local and config 数据库之外的所有数据库,提供与 userAdmin 角色相同的权限,及对集群的 authSchemaUpgradeinvalidateUserCachelistDatabases 操作。授予该角色的用户为超级用户。
  • dbAdminAnyDatabase:对于除 local and config 数据库之外的所有数据库,提供与 dbAdmin 角色相同的权限,及对整个群集的 listDatabases 操作。
超级用户角色

可以为任何用户分配对任何数据库的任何权限,包括:

  • dbOwner:限定为 admin 数据库。
  • userAdmin:限定为 admin 数据库。
  • userAdminAnyDatabase

以下角色提供对所有资源的所有权限:

  • root:包括以下角色:

    • readWriteAnyDatabase

    • dbAdminAnyDatabase

    • userAdminAnyDatabase

    • clusterAdmin

    • restore

    • backup

还提供了对 system. 集合的 validate 权限。

集群管理角色

数据库 admin 包括以下集群管理角色:

  • clusterAdmin:提供集群管理权限,包括 clusterManagerclusterMonitorhostManager 角色及 dropDatabase 权限。

自定义角色

  • 使用 db.createRole() 方法自定义角色。
  • 可以在特定数据库中创建角色。MongoDB 使用数据库和角色名称的组合来唯一定义角色。每个角色的范围限定为其所在的数据库。
  • 除了在 admin 数据库中创建的角色,角色只能包含应用于其所在数据库中的权限,也只能继承自其所在数据库的其他角色。
  • 创建在 admin 数据库中的角色可以包括应用于 admin 数据库,其他数据库或者集群资源的权限,可以继承自 admin 数据库和其他数据库的角色。
  • 所有角色信息存储在 admin 数据库 system.roles 集合中。不要直接访问该集合,应使用角色管理命令查看和编辑自定义角色。
  • 在数据库中创建角色,需要 createRolegrantRole 权限,内置的 userAdminuserAdminAnyDatabase 角色包含这两个权限。

例子:创建两个自定义角色

角色 testRole1

db.createRole(
    {
        role: "testRole1",
        privileges: [
            // 给角色添加 test 数据库中 students 集合的 find 和 update 权限
            { resource: { db: "test", collection: "students" }, actions: ["find", "update"] },
            // 给角色添加 test 数据库中 scores 的find权限
            { resource: { db: "test", collection: "scores" }, actions: ["find"] },
        ],
        roles: []
    }
)

查看角色:

db.getRole( "testRole1", { showPrivileges: true } )

执行结果:

{
  _id: 'test.testRole1',
  role: 'testRole1',
  db: 'test',
  privileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    }
  ],
  roles: [],
  inheritedRoles: [],
  inheritedPrivileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    }
  ],
  isBuiltin: false
}

角色 testRole2

db.createRole(
    {
        role: "testRole2",
        privileges: [
            // 给角色添加 test 数据库中 categories 集合的 find 和 update 权限
            { resource: { db: "test", collection: "categories" }, actions: ["find", "update"] }
        ],
        roles: []
    }
)

查看角色:

db.getRole( "testRole2", { showPrivileges: true } )

执行结果:

{
  _id: 'test.testRole2',
  role: 'testRole2',
  db: 'test',
  privileges: [
    {
      resource: { db: 'test', collection: 'categories' },
      actions: [ 'find', 'update' ]
    }
  ],
  roles: [],
  inheritedRoles: [],
  inheritedPrivileges: [
    {
      resource: { db: 'test', collection: 'categories' },
      actions: [ 'find', 'update' ]
    }
  ],
  isBuiltin: false
}

例子:授予权限给角色 testRole1

db.runCommand(
   {
     grantPrivilegesToRole: "testRole1",
     privileges: [
         {
           resource: { db: "test", collection: "blog" }, actions: [ "find" ]
         }
     ]
   }
)

查看角色:

db.getRole( "testRole1", { showPrivileges: true } )

执行结果:

{
  _id: 'test.testRole1',
  role: 'testRole1',
  db: 'test',
  privileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    },
    {
      resource: { db: 'test', collection: 'blog' },
      actions: [ 'find' ]
    }
  ],
  roles: [],
  inheritedRoles: [],
  inheritedPrivileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    },
    {
      resource: { db: 'test', collection: 'blog' },
      actions: [ 'find' ]
    }
  ],
  isBuiltin: false
}

例子:授予角色 testRole2 给角色 testRole1

db.runCommand(
   { grantRolesToRole: "testRole1",
     roles: [ "testRole2" ]
   }
)

查看角色:

db.getRole( "testRole1", { showPrivileges: true } )

执行结果:

{
  _id: 'test.testRole1',
  role: 'testRole1',
  db: 'test',
  privileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    },
    {
      resource: { db: 'test', collection: 'blog' },
      actions: [ 'find' ]
    }
  ],
  roles: [ { role: 'testRole2', db: 'test' } ],
  inheritedRoles: [ { role: 'testRole2', db: 'test' } ],
  inheritedPrivileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    },
    {
      resource: { db: 'test', collection: 'blog' },
      actions: [ 'find' ]
    },
    {
      resource: { db: 'test', collection: 'categories' },
      actions: [ 'find', 'update' ]
    }
  ],
  isBuiltin: false
}

例子:从角色 testRole1 回收权限

db.runCommand(
   {
     revokePrivilegesFromRole: "testRole1",
     privileges:
      [
        {
          resource: { db: "test", collection: "blog" }, actions: [ "find" ]
        }
      ]
   }
)

查看角色:

db.getRole( "testRole1", { showPrivileges: true } )

执行结果:

{
  _id: 'test.testRole1',
  role: 'testRole1',
  db: 'test',
  privileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    }
  ],
  roles: [ { role: 'testRole2', db: 'test' } ],
  inheritedRoles: [ { role: 'testRole2', db: 'test' } ],
  inheritedPrivileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    },
    {
      resource: { db: 'test', collection: 'categories' },
      actions: [ 'find', 'update' ]
    }
  ],
  isBuiltin: false
}

例子:从角色 testRole1 回收角色

db.runCommand( 
   { 
     revokeRolesFromRole: "testRole1",
     roles: [ "testRole2" ]
    } 
 )

查看角色:

db.getRole( "testRole1", { showPrivileges: true } )

执行结果:

{
  _id: 'test.testRole1',
  role: 'testRole1',
  db: 'test',
  privileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    }
  ],
  roles: [],
  inheritedRoles: [],
  inheritedPrivileges: [
    {
      resource: { db: 'test', collection: 'students' },
      actions: [ 'find', 'update' ]
    },
    {
      resource: { db: 'test', collection: 'scores' },
      actions: [ 'find' ]
    }
  ],
  isBuiltin: false
}

例子:删除角色

db.dropRole("testRole2")

用户

  • 要在 MongoDB 中对客户端进行身份验证,必须添加相应的用户。
  • 使用 db.createUser() 方法添加用户。第一个用户必须具有创建其他用户的权限,即用户管理员。故需要拥有 userAdmin 或者 userAdminAnyDatabase 角色。
  • 在创建用户时为用户分配角色来授予用户权限。
  • 用户由用户名和创建用户的数据库(称之为身份验证数据库)唯一标识,每个用户有唯一的 userId
  • MongoDB 将所有用户信息,包括用户名,密码和身份验证数据库存储在 admin 数据库 system.users 集合中。不要直接访问该集合,应使用用户管理命令管理用户。

例子:创建用户管理员,启用访问控制,创建普通用户,授予角色

  1. 创建用户管理员
use admin
db.createUser(
  {
    user: "myUserAdmin",
    pwd: passwordPrompt(), // or cleartext password
    roles: [
      { role: "userAdminAnyDatabase", db: "admin" },
      { role: "readWriteAnyDatabase", db: "admin" }
    ]
  }
)
  1. 修改配置文件,启用访问控制
[root@linux ~]# vi /etc/mongod.conf 
security:
  authorization: enabled
  
[root@linux ~]# systemctl restart mongod
[root@linux ~]# mongosh --authenticationDatabase "admin" -u "myUserAdmin" -p
  1. 创建普通用户并授予角色
use test
db.createUser(
  {
    user: "myTester",
    pwd:  passwordPrompt(),   // or cleartext password
    roles: [ { role: "readWrite", db: "test" } ]
  }
)
db.runCommand(
   {
     grantRolesToUser: "myTester",
     roles: [ { role: "dbAdmin", db: "test"} ]
   }
)
  1. 以普通用户连接到 MongoDB
[root@linux ~]# mongosh --authenticationDatabase "test" -u "myTester" -p
test> show dbs
test  4.86 MiB

其中,--authenticationDatabase 指定用户创建时所在的数据库。

  1. 查看所有用户
use admin
db.system.users.find()

执行结果:

[
  {
    _id: 'admin.myUserAdmin',
    userId: new UUID("7693d6f5-71a7-4060-aba7-2aa4ef489c8b"),
    user: 'myUserAdmin',
    db: 'admin',
    credentials: {
      'SCRAM-SHA-1': {
        iterationCount: 10000,
        salt: 'OOCi3ZiZH9dp2NELTajq+Q==',
        storedKey: 'hP2/0+oe8d0Yhmgm7vtuKHUJ2IA=',
        serverKey: 'lDcDsQXjUt7CBDolkiSRon2LQU8='
      },
      'SCRAM-SHA-256': {
        iterationCount: 15000,
        salt: 'V+JyQkCRJJV5tfnolacEGr/Dj2W4UHuv/4vLxA==',
        storedKey: 'SiKUyBXxM814P+xFxPiFg6zCCTIo4ukzsrx2ZbgDclo=',
        serverKey: 'zgAJT8zAAqYgAWMRPD2ubKMHlISYTZTHCYsNQVQUW2Y='
      }
    },
    roles: [
      { role: 'userAdminAnyDatabase', db: 'admin' },
      { role: 'readWriteAnyDatabase', db: 'admin' }
    ]
  },
  {
    _id: 'test.myTester',
    userId: new UUID("d58ee9a2-76d7-4d2d-802d-ee3ac4bdd152"),
    user: 'myTester',
    db: 'test',
    credentials: {
      'SCRAM-SHA-1': {
        iterationCount: 10000,
        salt: 'RekiIIUlCt+VwagmcaUpNA==',
        storedKey: '+llZoCShjN3MgKvlTStDYwv5kZg=',
        serverKey: 'pXOnOlkh/xRE7QcQzXdLM721yKI='
      },
      'SCRAM-SHA-256': {
        iterationCount: 15000,
        salt: '/Envko51nIXcKwQxJXmwm+hw1aXZx1rHLw376w==',
        storedKey: 'A76hDDbNhEUnQUZwA6PTodNKoTKPsId4n4UkGRU6aCU=',
        serverKey: '/J/+uSyFqQsmNU8nv0LXgWswc330KTaJeN1Wk/zdtNA='
      }
    },
    roles: [ { role: 'readWrite', db: 'test' } ]
  }
]
  1. 授予用户角色
use test
db.runCommand(
   {
     grantRolesToUser: "myTester",
     roles: [ { role: "testRole1", db: "test"} ]
   }
)

查看用户:

use admin
db.system.users.find( { user: "myTester"},{roles: 1 } )

执行结果:

[
  {
    _id: 'test.myTester',
    roles: [
      { role: 'readWrite', db: 'test' },
      { role: 'testRole1', db: 'test' }
    ]
  }
]
  1. 回收用户角色
use test
db.runCommand(
   {
     revokeRolesFromUser: "myTester",
     roles: [ { role: "testRole1", db: "test"} ]
   }
)

查看用户:

use admin
db.system.users.find( { user: "myTester"},{roles: 1 } )

执行结果:

[
  {
    _id: 'test.myTester',
    roles: [ { role: 'readWrite', db: 'test' } ]
  }
]
  1. 修改密码
use test
db.updateUser(
   "myTester",
   {
      pwd: passwordPrompt()
   }
)
  1. 删除用户
use test
db.runCommand( {
   dropUser: "myTester"
} )
  1. 创建超级用户
db.createUser(
  {
    user: "root",
    pwd: "123456",
    roles: [
      { role: "root", db: "admin" }
    ]
  }
)

视图

创建和查询视图

  • 必须在与源集合相同的数据库中创建视图。
  • 视图定义 pipeline 中不能包含 $out$merge
  • 不能重命名视图。

创建视图的语法:

db.createCollection(
  "<viewName>",
  {
    "viewOn" : "<source>",
    "pipeline" : [<pipeline>],
    "collation" : { <collation> }
  }
)

或者:

db.createView(
  "<viewName>",
  "<source>",
  [<pipeline>],
  {
    "collation" : { <collation> }
  }
)

例子:创建视图

先插入数据:

db.students.insertMany( [
   { sID: 22001, name: "Alex", year: 1, score: 4.0 },
   { sID: 21001, name: "bernie", year: 2, score: 3.7 },
   { sID: 20010, name: "Chris", year: 3, score: 2.5 },
   { sID: 22021, name: "Drew", year: 1, score: 3.2 },
   { sID: 17301, name: "harley", year: 6, score: 3.1 },
   { sID: 21022, name: "Farmer", year: 1, score: 2.2 },
   { sID: 20020, name: "george", year: 3, score: 2.8 },
   { sID: 18020, name: "Harley", year: 5, score: 2.8 },
] )

使用 db.createView() 创建视图:

db.createView(
   "firstYears",
   "students",
   [ { $match: { year: 1 } } ]
)

其中:

  • firstYears 为视图名称。
  • students 为视图所基于的集合。
  • $match 为匹配表达式。

查询视图:

db.firstYears.find({}, { _id: 0 } )

执行结果:

[
  { sID: 22001, name: 'Alex', year: 1, score: 4 },
  { sID: 22021, name: 'Drew', year: 1, score: 3.2 },
  { sID: 21022, name: 'Farmer', year: 1, score: 2.2 }
]

注意:

对于视图上的 find() 操作不支持以下运算符:

  • $
  • $elemMatch
  • $slice
  • $meta

使用 db.createCollection() 创建视图:

db.createCollection(
   "graduateStudents",
   {
      viewOn: "students",
      pipeline: [ { $match: { $expr: { $gt: [ "$year", 4 ] } } } ],
      collation: { locale: "en", caseFirst: "upper" }
   }
)

注意:

  • 可以在创建视图时指定默认排序规则。如果未指定排序规则,则视图的默认排序规则是”简单”二进制比较排序规则。也就是说,视图不继承集合的默认排序规则。
  • 视图上的字符串比较使用视图的默认排序规则。尝试更改或覆盖视图的默认排序规则将失败并报错。
  • 如果从另一个视图创建视图,则不能指定与源视图不同的排序规则。
  • 如果对多个视图执行聚合操作,比如 $lookup$graphLookup,则这些视图必须有相同的排序规则。

查询视图:

db.graduateStudents.aggregate(
   [
      { $sort: { name: 1 } },
      { $unset: [ "_id" ] }
   ]
)

执行结果:

[
  { sID: 18020, name: 'Harley', year: 5, score: 2.8 },
  { sID: 17301, name: 'harley', year: 6, score: 3.1 }
]

例子:创建角色,用户及视图,以便限制用户访问特定的数据

  1. 创建角色 BillingProvider
db.createRole( { role: "Billing", privileges: [ { resource: { db: "test",
   collection: "medicalView" }, actions: [ "find" ] } ], roles: [ ] } )
db.createRole( { role: "Provider", privileges: [ { resource: { db: "test",
   collection: "medicalView" }, actions: [ "find" ] } ], roles: [ ] } )
  1. 创建用户 JamesMichelle,并授予用户角色
db.createUser( {
   user: "James",
   pwd: "js008",
   roles: [
      { role: "Billing", db: "test" }
   ]
} )

db.createUser( {
   user: "Michelle",
   pwd: "me009",
   roles: [
      { role: "Provider", db: "test" }
   ]
} )
  1. 创建集合并插入数据
db.medical.insertMany( [
   {
      _id: 0,
      patientName: "Jack Jones",
      diagnosisCode: "CAS 17",
      creditCard: "1234-5678-9012-3456"
   },
   {
      _id: 1,
      patientName: "Mary Smith",
      diagnosisCode: "ACH 01",
      creditCard: "6541-7534-9637-3456"
   }
] )
  1. 创建视图,使用 $$USER_ROLES 从系统变量 USER_ROLES 中读取当前用户角色,然后根据角色查看对应的数据
db.createView(
   "medicalView", "medical",
   [ {
      $set: {
         "diagnosisCode": {
            $cond: {
               if: { $in: [
                  "Provider", "$$USER_ROLES.role"
               ] },
               then: "$diagnosisCode",
               else: "$$REMOVE"
            }
      }
   },
   }, {
      $set: {
         "creditCard": {
            $cond: {
               if: { $in: [
                  "Billing", "$$USER_ROLES.role"
               ] },
               then: "$creditCard",
               else: "$$REMOVE"
            }
         }
      }
   } ]
)

其中:

  • 具有 Provider 角色的用户可以访问 diagnosisCode 字段。
  • 具有 Billing 角色的用户可以访问 creditCard 字段。
  1. 使用 James 用户登录查询
db.auth( "James", "js008" )
db.medicalView.find()

执行结果:

[
  {
    _id: 0,
    patientName: 'Jack Jones',
    creditCard: '1234-5678-9012-3456'
  },
  {
    _id: 1,
    patientName: 'Mary Smith',
    creditCard: '6541-7534-9637-3456'
 

用户 JamesBilling 角色,可以看到 creditCard 字段。

  1. 使用 Michelle 用户登录查询
db.auth( "Michelle", "me009" )
db.medicalView.find()

执行结果:

[
  { _id: 0, patientName: 'Jack Jones', diagnosisCode: 'CAS 17' },
  { _id: 1, patientName: 'Mary Smith', diagnosisCode: 'ACH 01' }
]

用户 MichelleProvider 角色,可以看到 diagnosisCode 字段。

例子:将角色存储在文档中,以此判断用户是否可以访问

  1. 创建角色
db.createRole( { role: "Marketing", roles: [], privileges: [] } )
db.createRole( { role: "Sales", roles: [], privileges: [] } )
db.createRole( { role: "Development", roles: [], privileges: [] } )
db.createRole( { role: "Operations", roles: [], privileges: [] } )
  1. 创建用户并指定角色
db.createUser( {
   user: "John",
   pwd: "jn008",
   roles: [
      { role: "Marketing", db: "test" },
      { role: "Development", db: "test" },
      { role: "Operations", db: "test" },
      { role: "read", db: "test" }
   ]
} )

db.createUser( {
   user: "Jane",
   pwd: "je009",
   roles: [
      { role: "Sales", db: "test" },
      { role: "Operations", db: "test" },
      { role: "read", db: "test" }
   ]
} )
  1. 创建集合并插入数据
db.budget.insertMany( [
   {
      _id: 0,
      allowedRoles: [ "Marketing" ],
      comment: "For marketing team",
      yearlyBudget: 15000
   },
   {
      _id: 1,
      allowedRoles: [ "Sales" ],
      comment: "For sales team",
      yearlyBudget: 17000,
      salesEventsBudget: 1000
   },
   {
      _id: 2,
      allowedRoles: [ "Operations" ],
      comment: "For operations team",
      yearlyBudget: 19000,
      cloudBudget: 12000
   },
   {
      _id: 3,
      allowedRoles: [ "Development" ],
      comment: "For development team",
      yearlyBudget: 27000
   }
] )
  1. 创建视图
db.createView(
   "budgetView", "budget",
   [ {
      $match: {
         $expr: {
            $not: {
               $eq: [ { $setIntersection: [ "$allowedRoles", "$$USER_ROLES.role" ] }, [] ]
            }
         }
      }
   } ]
)

使用 $setIntersection 返回文档中 allowedRoles 和当前用户的角色 $$USER_ROLES 的交集,如果结果不为空,说明当前用户的角色满足该文档对角色的要求,则返回该文档。

  1. 使用 John 用户登录查询
db.auth( "John", "jn008" )
db.budgetView.find()

执行结果:

[
  {
    _id: 0,
    allowedRoles: [ 'Marketing' ],
    comment: 'For marketing team',
    yearlyBudget: 15000
  },
  {
    _id: 2,
    allowedRoles: [ 'Operations' ],
    comment: 'For operations team',
    yearlyBudget: 19000,
    cloudBudget: 12000
  },
  {
    _id: 3,
    allowedRoles: [ 'Development' ],
    comment: 'For development team',
    yearlyBudget: 27000
  }
]
  1. 使用 Jane 用户登录查询
db.auth( "Jane", "je009" )
db.budgetView.find()

执行结果:

[
  {
    _id: 1,
    allowedRoles: [ 'Sales' ],
    comment: 'For sales team',
    yearlyBudget: 17000,
    salesEventsBudget: 1000
  },
  {
    _id: 2,
    allowedRoles: [ 'Operations' ],
    comment: 'For operations team',
    yearlyBudget: 19000,
    cloudBudget: 12000
  }
]

例子:创建视图,角色和用户,将视图的查询权限授予给角色,将角色授予给用户

  1. 创建视图
db.createView(
   "secondYears",
   "students",
   [ { $match: { year: 2 } } ]
)
  1. 创建角色
db.createRole(
    {
        role: "secondYearsRole",
        privileges: [
            { resource: { db: "test", collection: "secondYears" }, actions: [ "find" ] }
        ],
        roles: []
    }
)
  1. 创建用户
db.createUser(
  {
    user: "secondYearsUser",
    pwd:  "sy001",
    roles: [ { role: "secondYearsRole", db: "test" } ]
  }
)
  1. 使用 secondYearsUser 用户登录查询
db.auth( "secondYearsUser", "sy001" )
db.secondYears.find()

执行结果:

[
  {
    _id: ObjectId("6544667149301affab2878c6"),
    sID: 21001,
    name: 'bernie',
    year: 2,
    score: 3.7
  }
]

创建两个集合连接的视图

使用 $lookup 创建两个集合连接的视图。

创建 inventoryorders 集合:

db.inventory.insertMany( [
   { prodId: 100, price: 20, quantity: 125 },
   { prodId: 101, price: 10, quantity: 234 },
   { prodId: 102, price: 15, quantity: 432 },
   { prodId: 103, price: 17, quantity: 320 }
] )

db.orders.insertMany( [
   { orderId: 201, custid: 301, prodId: 100, numPurchased: 20 },
   { orderId: 202, custid: 302, prodId: 101, numPurchased: 10 },
   { orderId: 203, custid: 303, prodId: 102, numPurchased: 5 },
   { orderId: 204, custid: 303, prodId: 103, numPurchased: 15 },
   { orderId: 205, custid: 303, prodId: 103, numPurchased: 20 },
   { orderId: 206, custid: 302, prodId: 102, numPurchased: 1 },
   { orderId: 207, custid: 302, prodId: 101, numPurchased: 5 },
   { orderId: 208, custid: 301, prodId: 100, numPurchased: 10 },
   { orderId: 209, custid: 303, prodId: 103, numPurchased: 30 }
] )

创建连接视图:

db.createView( "sales", "orders", [
   {
      $lookup:
         {
            from: "inventory",
            localField: "prodId",
            foreignField: "prodId",
            as: "inventoryDocs"
         }
   },
   {
      $project:
         {
           _id: 0,
           prodId: 1,
           orderId: 1,
           numPurchased: 1,
           price: "$inventoryDocs.price"
         }
   },
      { $unwind: "$price" }
] )

查询视图:

db.sales.find()

执行结果:

[
  { orderId: 201, prodId: 100, numPurchased: 20, price: 20 },
  { orderId: 202, prodId: 101, numPurchased: 10, price: 10 },
  { orderId: 203, prodId: 102, numPurchased: 5, price: 15 },
  { orderId: 204, prodId: 103, numPurchased: 15, price: 17 },
  { orderId: 205, prodId: 103, numPurchased: 20, price: 17 },
  { orderId: 206, prodId: 102, numPurchased: 1, price: 15 },
  { orderId: 207, prodId: 101, numPurchased: 5, price: 10 },
  { orderId: 208, prodId: 100, numPurchased: 10, price: 20 },
  { orderId: 209, prodId: 103, numPurchased: 30, price: 17 }
]

查询视图,获取每种商品的订单总额:

db.sales.aggregate( [
   {
      $group:
         {
            _id: "$prodId",
            amountSold: { $sum: { $multiply: [ "$price", "$numPurchased" ] } }
         }
   }
] )

执行结果:

[
  { _id: 103, amountSold: 1105 },
  { _id: 101, amountSold: 150 },
  { _id: 102, amountSold: 90 },
  { _id: 100, amountSold: 600 }
]

修改视图

要修改视图,可以:

  • 删除再重建视图。
  • 使用 collMod 命令。

创建视图:

db.createView(
   "lowStock",
   "products",
   [ { $match: { quantity: { $lte: 20 } } } ]
)

删除再重建视图:

db.lowStock.drop()
db.createView(
   "lowStock",
   "products",
   [ { $match: { quantity: { $lte: 10 } } } ]
)

使用 collMod 命令修改视图:

db.runCommand( {
   collMod: "lowStock",
   viewOn: "products",
   "pipeline": [ { $match: { quantity: { $lte: 10 } } } ]
} )

删除视图

使用 db.collection.drop() 删除视图。

db.productView01.drop()

视图支持的操作

按需物化视图

  • 按需物化视图是预先计算的聚合管道结果,存储在磁盘上,通常是 $merge 或者 $out 阶段的结果。
  • 可以在按需物化视图上创建索引。

先创建集合:

db.bakesales.insertMany( [
   { date: new ISODate("2018-12-01"), item: "Cake - Chocolate", quantity: 2, amount: new NumberDecimal("60") },
   { date: new ISODate("2018-12-02"), item: "Cake - Peanut Butter", quantity: 5, amount: new NumberDecimal("90") },
   { date: new ISODate("2018-12-02"), item: "Cake - Red Velvet", quantity: 10, amount: new NumberDecimal("200") },
   { date: new ISODate("2018-12-04"), item: "Cookies - Chocolate Chip", quantity: 20, amount: new NumberDecimal("80") },
   { date: new ISODate("2018-12-04"), item: "Cake - Peanut Butter", quantity: 1, amount: new NumberDecimal("16") },
   { date: new ISODate("2018-12-05"), item: "Pie - Key Lime", quantity: 3, amount: new NumberDecimal("60") },
   { date: new ISODate("2019-01-25"), item: "Cake - Chocolate", quantity: 2, amount: new NumberDecimal("60") },
   { date: new ISODate("2019-01-25"), item: "Cake - Peanut Butter", quantity: 1, amount: new NumberDecimal("16") },
   { date: new ISODate("2019-01-26"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") },
   { date: new ISODate("2019-01-26"), item: "Cookies - Chocolate Chip", quantity: 12, amount: new NumberDecimal("48") },
   { date: new ISODate("2019-01-26"), item: "Cake - Carrot", quantity: 2, amount: new NumberDecimal("36") },
   { date: new ISODate("2019-01-26"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") },
   { date: new ISODate("2019-01-27"), item: "Pie - Chocolate Cream", quantity: 1, amount: new NumberDecimal("20") },
   { date: new ISODate("2019-01-27"), item: "Cake - Peanut Butter", quantity: 5, amount: new NumberDecimal("80") },
   { date: new ISODate("2019-01-27"), item: "Tarts - Apple", quantity: 3, amount: new NumberDecimal("12") },
   { date: new ISODate("2019-01-27"), item: "Cookies - Chocolate Chip", quantity: 12, amount: new NumberDecimal("48") },
   { date: new ISODate("2019-01-27"), item: "Cake - Carrot", quantity: 5, amount: new NumberDecimal("36") },
   { date: new ISODate("2019-01-27"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") },
   { date: new ISODate("2019-01-28"), item: "Cookies - Chocolate Chip", quantity: 20, amount: new NumberDecimal("80") },
   { date: new ISODate("2019-01-28"), item: "Pie - Key Lime", quantity: 3, amount: new NumberDecimal("60") },
   { date: new ISODate("2019-01-28"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") },
] );
  1. 定义按需物化视图,使用 updateMonthlySales 函数定义 monthlybakesales 物化视图,包含从指定日期开始的每月累计销售额信息。
updateMonthlySales = function(startDate) {
   db.bakesales.aggregate( [
      { $match: { date: { $gte: startDate } } },
      { $group: { _id: { $dateToString: { format: "%Y-%m", date: "$date" } }, sales_quantity: { $sum: "$quantity"}, sales_amount: { $sum: "$amount" } } },
      { $merge: { into: "monthlybakesales", whenMatched: "replace" } }
   ] );
};

其中:

  • 使用 $match 匹配大于等于指定日期的文档。
  • 使用 $group 按照年月进行分组。
  • 使用 $merge 将分组后数据与 monthlybakesales 按照 _id 进行匹配,如果存在则替换,如果不存在则插入。
  1. 执行初始化
updateMonthlySales(new ISODate("1970-01-01"));

初始化后查看物化视图 monthlybakesales

db.monthlybakesales.find().sort( { _id: 1 } )

执行结果:

[
  {
    _id: '2018-12',
    sales_quantity: 41,
    sales_amount: Decimal128("506")
  },
  {
    _id: '2019-01',
    sales_quantity: 86,
    sales_amount: Decimal128("896")
  }
]
  1. 集合 bakesales 增加了新数据后刷新物化视图
db.bakesales.insertMany( [
   { date: new ISODate("2019-01-28"), item: "Cake - Chocolate", quantity: 3, amount: new NumberDecimal("90") },
   { date: new ISODate("2019-01-28"), item: "Cake - Peanut Butter", quantity: 2, amount: new NumberDecimal("32") },
   { date: new ISODate("2019-01-30"), item: "Cake - Red Velvet", quantity: 1, amount: new NumberDecimal("20") },
   { date: new ISODate("2019-01-30"), item: "Cookies - Chocolate Chip", quantity: 6, amount: new NumberDecimal("24") },
   { date: new ISODate("2019-01-31"), item: "Pie - Key Lime", quantity: 2, amount: new NumberDecimal("40") },
   { date: new ISODate("2019-01-31"), item: "Pie - Banana Cream", quantity: 2, amount: new NumberDecimal("40") },
   { date: new ISODate("2019-02-01"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") },
   { date: new ISODate("2019-02-01"), item: "Tarts - Apple", quantity: 2, amount: new NumberDecimal("8") },
   { date: new ISODate("2019-02-02"), item: "Cake - Chocolate", quantity: 2, amount: new NumberDecimal("60") },
   { date: new ISODate("2019-02-02"), item: "Cake - Peanut Butter", quantity: 1, amount: new NumberDecimal("16") },
   { date: new ISODate("2019-02-03"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") }
] )

再次运行函数,刷新物化视图:

updateMonthlySales(new ISODate("2019-01-01"));

初始化后查看物化视图 monthlybakesales

db.monthlybakesales.find().sort( { _id: 1 } )

执行结果:

[
  {
    _id: '2018-12',
    sales_quantity: 41,
    sales_amount: Decimal128("506")
  },
  {
    _id: '2019-01',
    sales_quantity: 102,
    sales_amount: Decimal128("1142")
  },
  {
    _id: '2019-02',
    sales_quantity: 15,
    sales_amount: Decimal128("284")
  }
]

复制

类似于 MySQL 主从复制,MongoDB 使用位于不同服务器上的副本集(Replica sets)提供冗余和高可用性,副本集之间使用复制实现数据同步。

副本集成员

副本集是一组维护相同数据集的 mongod 实例,可以包含多个数据节点和一个可选的仲裁节点。数据节点包括一个主节点和多个从节点

一个副本集只能有一个主节点,主节点接收写入操作,将对数据集的变更记录到操作日志oplog)。从节点复制主节点的操作日志,并将其异步应用于从节点。

image-20231107194713867

可以将从节点配置为特殊用途:

  • 优先级为 0(priority 0)的从节点,表示不能被选举为主节点的成员,例如处于较远的数据中心中的节点。

image-20231107205410638

  • 隐藏的从节点,表示对客户端程序不可见,例如用于备份的节点。隐藏节点必须始终是优先级为 0 的节点,因此不能成为主节点。

image-20231107205922616

  • 延迟的从节点,表示从节点的数据落后主节点指定时间。延迟节点必须是隐藏节点。

image-20231107210209829

副本集的最低建议配置是一个主节点和两个从节点。副本集最多可以有 50 个成员节点,但只有 7 个成员有投票权。

image-20231107191613561

如果主节点与其他节点之间超过 electionTimeoutMillis(默认 10 秒)未通信后,复合条件的从节点将会被选举为主节点。

image-20231107192243990

在选举完成前,副本集不能处理写操作,但可以继续提供查询操作。

副本集操作日志

操作日志(oplog)是一个特殊的固定集合open in new window,用于滚动记录数据库中的所有修改操作。从MongoDB 4.4 开始,支持以小时为单位指定操作日志保留时间,则 MongoDB 只会在操作日志达到最大配置大小且超过了保留时间才会被删除。

操作日志保存在 local.oplog.rs 集合中。首次启动副本集成员时,如果没有指定操作日志大小,则 MongoDB 会创建默认大小的操作日志。

Storage EngineDefault Oplog SizeLower BoundUpper Bound
In-Memory Storage Engineopen in new window5% of physical memory50 MB50 GB
WiredTiger Storage Engineopen in new window5% of free disk space990 MB50 GB

在大多数情况下,默认的操作日志大小就足够了。可以使用 oplogSizeMB 选项在创建操作日志前指定大小。首次启动副本集成员后,使用 replSetResizeOplog 管理命令更改操作日志大小。

使用 rs.printReplicationInfo() 查看操作日志状态,包括大小和时间范围。

副本集写入

副本集的 Write Concern 是指在写入操作返回前,需要确认写入成功的数据节点数量。

  • w: "majority" 表示需要大多数具有投票权利的数据节点确认,此为默认设置。对于 3 节点的副本集,需要 2 个节点确认,对于 5 节点的副本集,需要 3 个节点确认。可以使用 wtimeout 指定超时时间。

image-20231107224759522

  • w: 1 表示只需要主节点确认。

副本集读取

默认情况下,客户端从主节点读取,但也可以设置为从从节点读取。异步复制意味着从节点的数据有可能和主节点的数据不一致。

image-20231107193759777

MongoDB 使用读取首选项(Read Preference)描述客户端如何路由读取操作到副本集成员。读取首选项包括读取首选项模式,读取首选项模式有以下几种:

Read Preference ModeDescription
primaryopen in new window默认模式,从主节点读取。包含读取操作的多文档事务必须使用 primary
primaryPreferredopen in new window多数情况下从主节点读取,但如果主节点不可用,则从从节点读取。
secondaryopen in new window从从节点读取。
secondaryPreferredopen in new window通常从从节点读取,如果副本集只剩一个主节点,则从主机点读取。
nearestopen in new window根据指定的延迟阈值,从符合条件的节点中随机读取。

在从节点读取数据时,使用读取首选项的 maxStalenessSeconds 选项指定从节点与主节点之间的最大复制延迟,避免从不合适的从节点读取到太过时的数据。

可以将 maxStalenessSeconds 选项用于以下读取首选项模式:

当客户端使用 maxStalenessSeconds 选项为读操作选项节点时,通过对比主节点和从节点的最后一次写入时间来估计从节点的延迟,然后客户端会选项延迟小于或等于 maxStalenessSeconds 的从节点。

默认不使用 maxStalenessSeconds 选项,客户端不考虑主从之间的延迟。如果要使用,则必须指定 maxStalenessSeconds 大于 90 秒。

部署副本集

部署 3 节点副本集,环境如下:

No.HostnameIPRole
1mongodb0.stonecoding.netopen in new window192.168.92.150Primary
2mongodb1.stonecoding.netopen in new window192.168.92.151Secondary
3mongodb2.stonecoding.netopen in new window192.168.92.152Secondary

参考部署open in new window在 3 台服务器上安装 MongoDB。

如果没有 DNS 服务,需要为 3 台服务器配置本地域名解析:

[root@mongodb0 ~]# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.92.150   mongodb0.stonecoding.net   mongodb0
192.168.92.151   mongodb1.stonecoding.net   mongodb1
192.168.92.152   mongodb2.stonecoding.net   mongodb2

修改 3 个节点的配置文件:

[root@mongodb0 ~]# vi /etc/mongod.conf 
net:
  port: 27017
  bindIp: 0.0.0.0
  
replication:
   replSetName: "rs0"

启动 3 个节点上的 MongoDB:

[root@mongodb0 ~]# systemctl daemon-reload
[root@mongodb0 ~]# systemctl start mongod
[root@mongodb0 ~]# systemctl status mongod
● mongod.service - MongoDB Database Server
   Loaded: loaded (/usr/lib/systemd/system/mongod.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2023-11-08 15:10:54 CST; 10min ago
     Docs: https://docs.mongodb.org/manual
 Main PID: 960 (mongod)
   CGroup: /system.slice/mongod.service
           └─960 /usr/bin/mongod -f /etc/mongod.conf

Nov 08 15:10:54 mongodb0.stonecoding.net systemd[1]: Started MongoDB Database Server.
Nov 08 15:10:56 mongodb0.stonecoding.net mongod[960]: {"t":{"$date":"2023-11-08T07:10:56.576Z"},"s":"I",  "c":"CONTROL",  "id"...lse"}
Hint: Some lines were ellipsized, use -l to show in full.

在一个节点上执行初始化:

[root@mongodb0 ~]# mongosh
rs.initiate( {
   _id : "rs0",
   members: [
      { _id: 0, host: "mongodb0.stonecoding.net:27017" },
      { _id: 1, host: "mongodb1.stonecoding.net:27017" },
      { _id: 2, host: "mongodb2.stonecoding.net:27017" }
   ]
})

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 1,
  term: 1,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

查看状态:

rs.status()

执行结果:

{
  set: 'rs0',
  date: ISODate("2023-11-08T07:43:59.264Z"),
  myState: 1,
  term: Long("1"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 3,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
    lastCommittedWallTime: ISODate("2023-11-08T07:43:57.880Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
    appliedOpTime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
    durableOpTime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
    lastAppliedWallTime: ISODate("2023-11-08T07:43:57.880Z"),
    lastDurableWallTime: ISODate("2023-11-08T07:43:57.880Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1699429377, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-11-08T07:36:17.737Z"),
    electionTerm: Long("1"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1699428967, i: 1 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1699428967, i: 1 }), t: Long("-1") },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    numCatchUpOps: Long("0"),
    newTermStartDate: ISODate("2023-11-08T07:36:17.799Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-11-08T07:36:18.339Z")
  },
  members: [
    {
      _id: 0,
      name: 'mongodb0.stonecoding.net:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 948,
      optime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-11-08T07:43:57.000Z"),
      lastAppliedWallTime: ISODate("2023-11-08T07:43:57.880Z"),
      lastDurableWallTime: ISODate("2023-11-08T07:43:57.880Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1699428977, i: 1 }),
      electionDate: ISODate("2023-11-08T07:36:17.000Z"),
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: 'mongodb1.stonecoding.net:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 471,
      optime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-11-08T07:43:57.000Z"),
      optimeDurableDate: ISODate("2023-11-08T07:43:57.000Z"),
      lastAppliedWallTime: ISODate("2023-11-08T07:43:57.880Z"),
      lastDurableWallTime: ISODate("2023-11-08T07:43:57.880Z"),
      lastHeartbeat: ISODate("2023-11-08T07:43:58.082Z"),
      lastHeartbeatRecv: ISODate("2023-11-08T07:43:59.155Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongodb0.stonecoding.net:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    },
    {
      _id: 2,
      name: 'mongodb2.stonecoding.net:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 471,
      optime: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1699429437, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-11-08T07:43:57.000Z"),
      optimeDurableDate: ISODate("2023-11-08T07:43:57.000Z"),
      lastAppliedWallTime: ISODate("2023-11-08T07:43:57.880Z"),
      lastDurableWallTime: ISODate("2023-11-08T07:43:57.880Z"),
      lastHeartbeat: ISODate("2023-11-08T07:43:58.084Z"),
      lastHeartbeatRecv: ISODate("2023-11-08T07:43:59.158Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongodb0.stonecoding.net:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699429437, i: 1 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699429437, i: 1 })
}

查看当前实例角色:

db.hello()

执行结果:

{
  topologyVersion: {
    processId: ObjectId("654b388b1b0e34913e7f5ed3"),
    counter: Long("6")
  },
  hosts: [
    'mongodb0.stonecoding.net:27017',
    'mongodb1.stonecoding.net:27017',
    'mongodb2.stonecoding.net:27017'
  ],
  setName: 'rs0',
  setVersion: 1,
  isWritablePrimary: true,
  secondary: false,
  primary: 'mongodb0.stonecoding.net:27017',
  me: 'mongodb0.stonecoding.net:27017',
  electionId: ObjectId("7fffffff0000000000000001"),
  lastWrite: {
    opTime: { ts: Timestamp({ t: 1699430447, i: 1 }), t: Long("1") },
    lastWriteDate: ISODate("2023-11-08T08:00:47.000Z"),
    majorityOpTime: { ts: Timestamp({ t: 1699430447, i: 1 }), t: Long("1") },
    majorityWriteDate: ISODate("2023-11-08T08:00:47.000Z")
  },
  maxBsonObjectSize: 16777216,
  maxMessageSizeBytes: 48000000,
  maxWriteBatchSize: 100000,
  localTime: ISODate("2023-11-08T08:00:56.865Z"),
  logicalSessionTimeoutMinutes: 30,
  connectionId: 4,
  minWireVersion: 0,
  maxWireVersion: 21,
  readOnly: false,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699430447, i: 1 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699430447, i: 1 })
}

删除副本集成员

使用 rs.remove() 删除副本集成员:

  1. 关闭要删除成员的 mongod 实例
[root@mongodb2 ~]# systemctl stop mongod
  1. 使用 db.hello() 确认主节点后,连接到主节点
[root@mongodb0 ~]# mongosh
  1. 在主节点删除成员
rs.remove("mongodb2.stonecoding.net:27017")

执行结果:

{
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699431014, i: 1 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699431014, i: 1 })
}

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 2,
  term: 1,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

添加副本集成员

使用 rs.add() 添加副本集成员:

  1. 启动要添加成员的 mongod 实例
[root@mongodb2 ~]# systemctl start mongod
  1. 使用 db.hello() 确认主节点后,连接到主节点
[root@mongodb0 ~]# mongosh
  1. 在主节点添加成员
rs.add( { host: "mongodb2.stonecoding.net:27017" } )

执行结果:

{
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699431288, i: 2 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699431288, i: 2 })
}

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 4,
  term: 1,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

配置副本集成员

调整副本集成员优先级

副本集成员的 priority 越大,优先级越高,越有可能被选举为主节点。默认为 1,范围为 010000 表示不参被选举为主节点,隐藏节点和延迟节点的 priority0

优先级 members[n].priority 和投票权 members[n].votes 有以下关系:

  • 无投票权(即 votes0) 的成员的 priority 必须为 0
  • priority 大于为 0 的成员的 votes 不能为 0
  1. 将副本集配置分配给变量
cfg = rs.conf()
  1. 修改每个成员的优先级值
cfg.members[0].priority = 0.5
cfg.members[1].priority = 2
cfg.members[2].priority = 2
  1. 应用新配置
rs.reconfig(cfg)

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 5,
  term: 2,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 0.5,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 2,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 2,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

隐藏副本集成员

要配置从节点为隐藏节点,需要设置 members[n].priority0members[n].hiddentrue

在主节点执行:

cfg = rs.conf()
cfg.members[2].priority = 0
cfg.members[2].hidden = true
rs.reconfig(cfg)

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 6,
  term: 2,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 0.5,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 2,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: true,
      priority: 0,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

配置延迟副本集成员

要配置从节点为延迟节点,需要设置 members[n].priority0members[n].hiddentrue 以及 members[n].secondaryDelaySecs 为延迟的秒数。延迟秒数应该小于操作日志open in new window保留时间。

在主节点执行:

cfg = rs.conf()
cfg.members[2].priority = 0
cfg.members[2].hidden = true
cfg.members[2].secondaryDelaySecs = 3600
rs.reconfig(cfg)

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 7,
  term: 2,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 0.5,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 2,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: true,
      priority: 0,
      tags: {},
      secondaryDelaySecs: Long("3600"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

配置无投票权副本集成员

要配置成员为无投票权节点,需要设置 members[n].votesmembers[n].priority 值为 0

在主节点执行:

cfg = rs.conf()
cfg.members[2].votes = 0;
cfg.members[2].priority = 0;
rs.reconfig(cfg)

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 8,
  term: 2,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 0.5,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 2,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: true,
      priority: 0,
      tags: {},
      secondaryDelaySecs: Long("3600"),
      votes: 0
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

维护副本集

修改操作日志大小

使用命令 replSetResizeOplog 修改副本集成员的操作日志大小,先修改所有从节点,再修改主节点。

先查看操作日志大小:

rs0 [direct: secondary] test> use local
switched to db local
rs0 [direct: secondary] local> db.oplog.rs.stats().maxSize
1038090240

使用命令 replSetResizeOplog 修改操作日志大小,其中 size 为 Double 类型,必须大于 990 MB。

例如修改操作日志为 16000 MB:

rs0 [direct: secondary] local> db.adminCommand({replSetResizeOplog: 1, size: Double(16000)})
{
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699434587, i: 1 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699434587, i: 1 })
}
rs0 [direct: secondary] local> db.oplog.rs.stats().maxSize
16777216000

维护副本集成员

当需要对副本集成员进行维护时,为了最大程度减少主节点的不可用时间,应该从从节点开始进行维护,最后维护主节点。

维护操作流程为:

  • 以独立运行方式重新启动 mongod 实例。
  • 执行维护操作。
  • 以副本集成员身份重新启动 mongod 实例。
  1. 停止从节点
db.shutdownServer()

或者:

[root@mongodb2 ~]# systemctl stop mongod
  1. 在另一个端口以独立运行方式重新启动从节点 mongod 实例,先调整从节点配置文件 /etc/mongod.conf
net:
   bindIp: 0.0.0.0
   port: 27018
#   port: 27017
#replication:
#   replSetName: "rs0"
setParameter:
   skipShardingConfigurationChecks: true
   disableLogicalSessionCacheRefresh: true

然后启动从节点 mongod 实例进行维护:

[root@mongodb2 ~]# systemctl start mongod
  1. 维护完成后以副本集成员身份重新启动 mongod 实例,先关闭从节点 mongod 实例:
[root@mongodb2 ~]# systemctl stop mongod

再调整从节点配置文件 /etc/mongod.conf

net:
   bindIp: 0.0.0.0
#   port: 27018
   port: 27017
replication:
   replSetName: "rs0"
#setParameter:
#   skipShardingConfigurationChecks: true
#   disableLogicalSessionCacheRefresh: true

然后启动从节点 mongod 实例:

[root@mongodb2 ~]# systemctl start mongod
  1. 对所有从节点完成维护操作后,最后对主节点进行维护,先在主节点执行 rs.stepDown(),将主节点降级以便允许其中一个从节点被选举为主节点,并指定 5 分钟超时时间以防止该节点再次被选举为主机点:
rs.stepDown(300)

然后关闭 mongod 实例:

[root@mongodb1 ~]# systemctl stop mongod

再调整配置文件 /etc/mongod.conf

net:
   bindIp: 0.0.0.0
   port: 27018
#   port: 27017
#replication:
#   replSetName: "rs0"
setParameter:
   skipShardingConfigurationChecks: true
   disableLogicalSessionCacheRefresh: true

以独立运行方式重新启动 mongod 实例进行维护操作:

[root@mongodb1 ~]# systemctl start mongod

完成维护操作后,关闭 mongod 实例:

[root@mongodb1 ~]# systemctl stop mongod

还原配置:

net:
   bindIp: 0.0.0.0
#   port: 27018
   port: 27017
replication:
   replSetName: "rs0"
#setParameter:
#   skipShardingConfigurationChecks: true
#   disableLogicalSessionCacheRefresh: true

再次启动

[root@mongodb1 ~]# systemctl start mongod

强制成员成为主节点

通过为成员的 members[n].priority 赋予一个比其他成员更大的值强制其成为主节点。

例子:通过调整成员优先级强制其为主节点

查看当前配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 8,
  term: 4,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 0.5,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 2,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: true,
      priority: 0,
      tags: {},
      secondaryDelaySecs: Long("3600"),
      votes: 0
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

当前主节点为 mongodb1.stonecoding.net:27017,在主节点执行:

cfg = rs.conf()
cfg.members[0].priority = 1
cfg.members[1].priority = 0.5
rs.reconfig(cfg)

将节点 mongodb0.stonecoding.net:27017 的优先级修改为 1,节点 mongodb1.stonecoding.net:27017 的优先级修改为 0.5,然后查看主节点:

db.hello().primary

执行结果:

mongodb0.stonecoding.net:27017

如果节点 mongodb0.stonecoding.net:27017 的数据远远落后于节点 mongodb1.stonecoding.net:27017,那么节点 mongodb1.stonecoding.net:27017 将等待 10 秒后再进行降级操作,以便减少没有主节点的时间。

如果节点 mongodb0.stonecoding.net:27017 的数据落后于节点 mongodb1.stonecoding.net:27017 10 秒以上,并且可以容忍 10 秒以上没有可用主节点,则可以使用以下命令强制节点 mongodb1.stonecoding.net:27017 降级:

db.adminCommand({replSetStepDown: 86400, force: 1})

以上命令可以在没有其他成员可以成为主节点的情况下,防止成员 mongodb1.stonecoding.net:27017 成为主节点,当节点 mongodb0.stonecoding.net:27017 的数据追上节点 mongodb1.stonecoding.net:27017 后,其会成为主节点。

在上面数据追赶的等待过程中,如果想让成员 mongodb1.stonecoding.net:27017 再次成为主节点,使用以下命令:

rs.freeze()

例子:使用数据库命令强制成员为主节点

对于 3 节点环境:

  • mongodb0.stonecoding.net:主节点
  • mongodb1.stonecoding.net:从节点
  • mongodb2.stonecoding.net:从节点
  1. 连接到 mongodb2.stonecoding.net,将其冻结以防止其在 120 秒内尝试成为主节点
rs.freeze(120)
  1. 连接到 mongodb0.stonecoding.net,将其降级以防止其在 120 秒内有资格成为主节点
rs.stepDown(120)

那么成员mongodb1.stonecoding.net 将会成为主节点。

配置副本集成员标签

可以为副本集成员指定标签,以便将读操作定向到特定标签的成员。

注意:

读取首选项模式primary 时,不能使用标签。

连接到主节点,添加标签:

conf = rs.conf()
conf.members[0].tags = { "dc": "east", "usage": "production" }
conf.members[1].tags = { "dc": "east", "usage": "reporting" }
conf.members[2].tags = { "dc": "west", "usage": "production" }
rs.reconfig(conf)

查看配置:

rs.conf()

执行结果:

{
  _id: 'rs0',
  version: 10,
  term: 5,
  members: [
    {
      _id: 0,
      host: 'mongodb0.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: { dc: 'east', usage: 'production' },
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 0.5,
      tags: { dc: 'east', usage: 'reporting' },
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: true,
      priority: 0,
      tags: { dc: 'west', usage: 'production' },
      secondaryDelaySecs: Long("3600"),
      votes: 0
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("654b3a671b0e34913e7f5f90")
  }
}

在读取首选项中指定标签:

  • 定位到同时具有 "dc": "east""usage": "production" 的节点
db.collection.find({}).readPref( "secondary", [ { "dc": "east", "usage": "production" } ] )
  • 先使用标签 "dc": "east" 查找节点,如果没有找到,再使用标签 "usage": "production" 查找
db.collection.find({}).readPref( "secondary", [ { "dc": "east"}, { "usage": "production" } ] )

重新配置只有少数成员的副本集

当副本集多数成员不可用时,连接到可用成员,使用 rs.reconfig() 方法的 force 选项进行配置。不要在主节点仍可用时使用 force 选项。

  1. 备份可用成员。
  2. 连接到可用成员,保存当前配置
cfg = rs.conf()
printjson(cfg)
  1. 从副本集中移除不可用成员,即设置 members 为可用成员
cfg.members = [cfg.members[0] , cfg.members[4] , cfg.members[7]]
  1. 使用 rs.reconfig() 方法的 force 选项进行配置,强制可用成员使用新配置,选出新的主节点。
rs.reconfig(cfg, {force : true})
  1. 尽快关闭或停用已删除成员。

管理链式复制

从 MongoDB 2.0 开始支持链式复制。从节点根据 Ping 时间选择复制目标,如果发现从另一个从节点复制操作日志比从主节点快,则会发生链式复制。链式复制可以减少主节点的负载,但会增加从节点的延迟。

默认启用链式复制,可以使用 settings.chainingAllowed 配置链式复制。

settings.chainingAllowed 设置为 false 禁用链式复制:

cfg = rs.config()
cfg.settings.chainingAllowed = false
rs.reconfig(cfg)

settings.chainingAllowed 设置为 true 启用链式复制:

cfg = rs.config()
cfg.settings.chainingAllowed = true
rs.reconfig(cfg)

配置从节点的同步目标

从节点既可以根据 Ping 时间选择复制目标,也可以手动指定同步目标。

使用 replSetSyncFrom 命令指定同步目标:

db.adminCommand( { replSetSyncFrom: "hostname<:port>" } );

使用 rs.syncFrom() 命令指定同步目标:

rs.syncFrom("hostname<:port>");

在以下情况将恢复为根据 Ping 时间选择复制目标:

  • 重启 mongod 实例。
  • 从节点与同步目标间的连接断开。
  • 同步目标比其他成员晚 30 秒以上。

复制参考

副本集排错

查看副本集状态

使用 rs.status() 查看副本集状态。

rs.status()

执行结果:

{
  set: 'rs0',
  date: ISODate("2023-11-09T06:01:49.084Z"),
  myState: 2,
  term: Long("5"),
  syncSourceHost: 'mongodb1.stonecoding.net:27017',
  syncSourceId: 1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 2,
  writableVotingMembersCount: 2,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1699509701, i: 1 }), t: Long("5") },
    lastCommittedWallTime: ISODate("2023-11-09T06:01:41.178Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1699506100, i: 1 }), t: Long("5") },
    appliedOpTime: { ts: Timestamp({ t: 1699506100, i: 1 }), t: Long("5") },
    durableOpTime: { ts: Timestamp({ t: 1699506100, i: 1 }), t: Long("5") },
    lastAppliedWallTime: ISODate("2023-11-09T05:01:40.629Z"),
    lastDurableWallTime: ISODate("2023-11-09T05:01:40.629Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1699506100, i: 1 }),
  members: [
    {
      _id: 0,
      name: 'mongodb0.stonecoding.net:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 16985,
      optime: { ts: Timestamp({ t: 1699509701, i: 1 }), t: Long("5") },
      optimeDurable: { ts: Timestamp({ t: 1699509701, i: 1 }), t: Long("5") },
      optimeDate: ISODate("2023-11-09T06:01:41.000Z"),
      optimeDurableDate: ISODate("2023-11-09T06:01:41.000Z"),
      lastAppliedWallTime: ISODate("2023-11-09T06:01:41.178Z"),
      lastDurableWallTime: ISODate("2023-11-09T06:01:41.178Z"),
      lastHeartbeat: ISODate("2023-11-09T06:01:48.332Z"),
      lastHeartbeatRecv: ISODate("2023-11-09T06:01:48.346Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1699495958, i: 1 }),
      electionDate: ISODate("2023-11-09T02:12:38.000Z"),
      configVersion: 10,
      configTerm: 5
    },
    {
      _id: 1,
      name: 'mongodb1.stonecoding.net:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 16985,
      optime: { ts: Timestamp({ t: 1699509701, i: 1 }), t: Long("5") },
      optimeDurable: { ts: Timestamp({ t: 1699509701, i: 1 }), t: Long("5") },
      optimeDate: ISODate("2023-11-09T06:01:41.000Z"),
      optimeDurableDate: ISODate("2023-11-09T06:01:41.000Z"),
      lastAppliedWallTime: ISODate("2023-11-09T06:01:41.178Z"),
      lastDurableWallTime: ISODate("2023-11-09T06:01:41.178Z"),
      lastHeartbeat: ISODate("2023-11-09T06:01:47.118Z"),
      lastHeartbeatRecv: ISODate("2023-11-09T06:01:47.145Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongodb0.stonecoding.net:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 10,
      configTerm: 5
    },
    {
      _id: 2,
      name: 'mongodb2.stonecoding.net:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 16987,
      optime: { ts: Timestamp({ t: 1699506100, i: 1 }), t: Long("5") },
      optimeDate: ISODate("2023-11-09T05:01:40.000Z"),
      lastAppliedWallTime: ISODate("2023-11-09T05:01:40.629Z"),
      lastDurableWallTime: ISODate("2023-11-09T05:01:40.629Z"),
      syncSourceHost: 'mongodb1.stonecoding.net:27017',
      syncSourceId: 1,
      infoMessage: '',
      configVersion: 10,
      configTerm: 5,
      self: true,
      lastHeartbeatMessage: ''
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699509701, i: 1 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699506100, i: 1 })
}
查看复制延迟

使用 rs.printSecondaryReplicationInfo() 查看复制延迟。

rs.printSecondaryReplicationInfo()

执行结果:

source: mongodb1.stonecoding.net:27017
{
  syncedTo: 'Thu Nov 09 2023 14:13:31 GMT+0800 (China Standard Time)',
  replLag: '0 secs (0 hrs) behind the primary '
}
---
source: mongodb2.stonecoding.net:27017
{
  syncedTo: 'Thu Nov 09 2023 13:13:30 GMT+0800 (China Standard Time)',
  replLag: '-3601 secs (-1 hrs) behind the primary '
}
流控

从 MongoDB 4.2 开始,默认启用流控以限制主节点的写入速率,将主从之间的延迟控制在 flowControlTargetLagSeconds(默认 10 秒)参数值以下。

在主节点使用以下命令查看流控状态:

  1. 使用 rs.printSecondaryReplicationInfo() 查看复制延迟
rs.printSecondaryReplicationInfo()
  1. 查看流控是否正在运行
db.runCommand( { serverStatus: 1 } ).flowControl.isLagged
查看操作日志大小

使用 rs.printReplicationInfo() 查看操作日志大小。

rs.printReplicationInfo()

执行结果:

actual oplog size
'16000 MB'
---
configured oplog size
'16000 MB'
---
log length start to end
'82834 secs (23.01 hrs)'
---
oplog first event time
'Wed Nov 08 2023 15:36:07 GMT+0800 (China Standard Time)'
---
oplog last event time
'Thu Nov 09 2023 14:36:41 GMT+0800 (China Standard Time)'
---
now
'Thu Nov 09 2023 14:36:49 GMT+0800 (China Standard Time)'

可以看到操作日志为 16000 MB,满足大约 23 小时(82834 秒)的操作。

建议操作日志大小应满足从节点预计的最长停机时间,至少 24 小时。

副本集成员状态

副本集成员的状态有:

NumberNameState Description
0STARTUPopen in new windowNot yet an active member of any set. All members start up in this state. The mongodopen in new window parses the replica set configuration documentopen in new window while in STARTUP.open in new window
1PRIMARYopen in new windowThe member in state primaryopen in new window is the only member that can accept write operations. Eligible to vote.
2SECONDARYopen in new windowA member in state secondaryopen in new window is replicating the data store. Eligible to vote.
3RECOVERINGopen in new windowMembers either perform startup self-checks, or transition from completing a rollbackopen in new window or resyncopen in new window. Data is not available for reads from this member. Eligible to vote.
5STARTUP2open in new windowThe member has joined the set and is running an initial sync. Not eligible to vote.
6UNKNOWNopen in new windowThe member's state, as seen from another member of the set, is not yet known.
7ARBITERopen in new windowArbitersopen in new window do not replicate data and exist solely to participate in elections. Eligible to vote.
8DOWNopen in new windowThe member, as seen from another member of the set, is unreachable.
9ROLLBACKopen in new windowThis member is actively performing a rollbackopen in new window. Eligible to vote. Data is not available for reads from this member.Starting in version 4.2, MongoDB kills all in-progress user operations when a member enters the ROLLBACKopen in new window state.
10REMOVEDopen in new windowThis member was once in a replica set but was subsequently removed.

分片

MongoDB 使用分片(Sharding)进行水平扩展,支持对大数据集和高吞吐量操作。

概述

分片集群

分片集群(Sharded Cluster)由以下组件组成:

  • 分片( shard):每个分片包含分片数据的子集。每个分片都可以部署为一个副本集。
  • mongos:作为查询路由器,在客户端应用程序和分片集群之间提供接口。
  • 配置服务器(Config servers):存储集群的元数据和配置。

各组件交互如下:

image-20231109152613799

MongoDB 在集合级别对数据进行分片,将集合数据分布在集群中的分片中。

分片键

MongoDB 使用分片键(Shard Keys)在分片之间分发集合的文档,分片键由文档中的一个或多个字段组成。

  • 从 4.4 版本开始,分片集合中的文档可以没有分片键字段。在分片之间分发文档时,缺少的分片键字段会被视为具有 NULL 值。
  • 在 4.2 版本及之前,分片集合的每个文档中都必须有分片键字段。

在对集合进行分片时选择分片键。

  • 从 MongoDB 5.0 开始,可以通过更改集合的分片键来重新分片集合。
  • 从 MongoDB 4.4 开始,可以通过向现有分片键添加一个或多个后缀字段来优化分片键。
  • 在 MongoDB 4.2 及之前,分片后无法更改分片键。

文档的分片键值决定了其在分片中的分布。

  • 从 MongoDB 4.2 开始,可以更新文档的分片键值,除非分片键字段是不可变的 _id 字段。
  • 在 MongoDB 4.0 及之前,文档的分片键字段值是不可变的。

分片集合必须有以分片键开头的索引。对于查询包含分片键或复合分片键前缀, mongos 可以将查询定位到特定分片以提高性能;如果查询不包含分片键或复合分片键前缀,则执行广播操作,查询集群中所有分片。

  • 在 MongoDB 分片环境中,块(Chunks)是数据的逻辑单元,代表了给定分片键特定范围内的文档集合。一个块只存储在一个分片上。
  • 块是进行数据分配和迁移的基本单位。当数据增长到一定程度,MongoDB 会自动将块拆分到不同的分片上,以实现数据的水平扩展。
  • 当块内的文档数量或大小增长到一定程度时,MongoDB 会自动将块拆分为两个较小的块,以确保数据的均衡分布。插入和更新操作都可能触发块的拆分。
  • 默认块大小为 64 MB,但可以根据需求进行调整。较小的块大小会导致更频繁的数据迁移,但数据分布更均匀;而较大的块大小则会减少数据迁移的频率,但可能导致数据分布的不均衡。块的大小会影响每个要迁移的块的最大文档数,以及最大集合大小。

分片与非分片集合

数据库可以混合使用分片集合和非分片集合。分片集合在集群分片中分区和分布,非分片集合存储在主分片上。每个数据库都有自己的主分片。

image-20231110095749984

连接到分片集群

必须连接到 mongos 路由器才能与分片集群中的集合进行交互,包括分片和未分片的集合。客户端不要连接到单个分片执行读取或写入操作。

image-20231110133511700

分片策略

MongoDB 支持两种分片策略:

  • 哈希分片(Hashed Sharding):根据分片键字段值的哈希值进行分片,数据均匀分布在分片上,不利于对分片键执行范围查询。

image-20231110133857412

  • 范围分片(Ranged sharding):根据分片键值将数据划分为多个范围,为每个区块分配一个范围。相近的值更可能位于同一分片上,利于对分片键执行范围查询。默认的分片策略。

image-20231110135138771

分片集群组件

MongoDB 分片集群有以下组件:

  • 分片( shard):每个分片包含分片数据的子集。从 MongoDB 3.6 开始,必须将分片部署为副本集。
  • mongos:作为查询路由器,在客户端应用程序和分片集群之间提供接口。
  • 配置服务器(Config servers):存储集群的元数据和配置。从 MongoDB 3.4 开始,配置服务器必须部署为副本集 。

对于生产分片群集部署,建议:

  • 将配置服务器部署为 3 个成员的副本集。
  • 将每个分片部署为 3 个成员的副本集。
  • 部署一个或多个 mongos 路由器。

image-20231110141024893

对于开发和测试分片集群,可以部署具有最少组件数的分片集群,包括:

  • 一个 mongos 实例。
  • 单个分片副本集。
  • 配置服务器副本集。

image-20231110141418024

分片

  • 分片群集中的每个数据库都有一个主分片(Primary Shard),用于保存该数据库的所有未分片集合。
  • 在创建新数据库时,mongos 选择集群中数据量最少的分片作为主分片。使用 listDatabases 命令返回的 totalSize 字段来判断数据量。
  • 使用 movePrimary 命令更改数据库的主分片。
  • mongosh 中使用 sh.status() 查看集群概览,包括数据库主分片以及分片的块分布。

配置服务器

  • 配置服务器存储分片集群的元数据。元数据反映了分片集群中所有数据和组件的状态和组织,包括每个分片上的块列表以及块范围。
  • mongos 实例缓存元数据,并使用它将读取和写入操作路由到正确的分片。当集群的元数据发生改变(例如添加分片)时,mongos 将更新缓存。分片还从配置服务器读取块元数据。
  • 配置服务器还存储身份验证配置信息,例如基于角色访问控制或集群内部身份验证设置。
  • MongoDB 还使用配置服务器管理分布式锁。
  • 每个分片集群都必须有自己的配置服务器。不要对不同的分片集群使用相同的配置服务器。
  • 在配置服务器上执行的管理操作可能会对分片集群的性能和可用性产生重大影响。根据受影响的配置服务器的数量,集群可能在一段时间内处于只读或脱机状态。
  • 作为配置服务器的副本集,必须使用 WiredTiger 存储引擎,没有仲裁节点,没有延迟成员,成员不能将 members[n].buildIndexes 设置设置为 false
  • 配置服务器包含 admin 数据库和 config 数据库:
    • admin 数据库包含与身份验证和授权相关的集合,以及供内部使用的其他 system.* collections 集合。
    • config 数据库包含分片集群元数据的集合,用户应避免在正常操作或维护过程中直接写入 config 数据库。在配置服务器上执行任何维护之前,务必备份 config 数据库。永远不要直接编辑 config 数据库的内容。
  • 如果配置服务器副本集丢失其主节点并且无法选举出主节点,则集群的元数据将变为只读。此时仍然可以从分片中读取和写入数据,但在副本集可以选择主节点之前,不会发生块迁移或块拆分。
  • 如果所有配置服务器都不可用,则集群不可用。故一定要为配置服务器创建备份。

mongos

  • MongoDB的 mongos 实例将查询和写入操作路由到分片集群中的分片,在客户端应用程序和分片集群之间提供唯一接口,应用程序不能直接连接到分片。

  • mongos 通过缓存配置服务器中的元数据来跟踪分片数据,以确认数据位于哪个分片上。

  • 最佳实践是在应用服务器上运行 mongos 实例。

  • mongos 根据查询确定需要访问哪些分片,并在对应分片上创建游标。

  • mongos 可以将 limit() 传递给分片,但不能将 skip() 传递给分片。

  • 当客户端连接到 mongos 时,使用 hello 命令,返回的 msg 字段值为 isdbgrid

  • 如果查询不包含分片键,mongos 实例则将查询广播到集合的所有分片,此时性能较差。

image-20231113092728631

  • 如果查询包含分片键或复合分片键前缀, mongos 可以将查询定位到特定分片以提高性能。所有 insertOne() 操作定位到一个分片,所有 updateOne()replaceOne()deleteOne() 操作都必须包含分片键或 _id

image-20231113093019030

  • 分片集群支持基于角色的访问控制(RBAC),必须使用 --auth 选项启动集群中每个 mongod 服务器(包括配置服务器),启用 RBAC 后,客户端在连接时必须指定 --username--password--authenticationDatabase 才能访问群集资源。
  • 从 MongoDB 4.2 开始,增加了参数 ShardingTaskExecutorPoolReplicaSetMatching 以确定 mongod / mongos 实例到分片集群各个成员的连接池最小值。

分片键

分片键可以是单个索引字段,也可以是在复合索引中的多个字段,以确定集合文档在集群分片中的分布。

MongoDB 将整个分片键值(或哈希分片键值)的范围划分为不重叠的分片键值(或哈希分片键值)范围,每个范围都与一个块相关联,并尝试在集群的分片中均匀分布这些块。

image-20231110135138771

分片集合必须有以分片键开头的索引,既可以是单字段索引,也可以是复合索引。

  • 如果集合为空且还没有分片键索引,sh.shardCollection() 则会在分片键上创建索引。
  • 如果集合不为空,则需要在使用 sh.shardCollection() 之前先创建索引。

如果索引是唯一支持分片键的非隐藏索引,则无法删除或隐藏索引。

MongoDB 可以对范围分片键索引强制实施唯一约束,对于范围分片集合,只有以下索引可以是唯一的:

  • 分片键上的索引。
  • 分片键为前缀的复合索引。
  • 如果 _id 字段不是分片键或分片键的前缀时, _id 索引仅会强制执行每个分片的唯一约束,而不是所有分片。

例如对于使用分片键为 {x: 1} 的分片集合,包含分片 A 和分片 B,由于 _id 不是分片键的一部分, 则该集合既可以在分片 A 中包含 _id 值为 1 的文档,也可以在分片 B 中包含 _id 值为 1 的文档,此时需要在应用程序中保障 _id 值的唯一性。

唯一索引约束意味着:

  • 对于待分片集合,如果该集合有其他唯一索引,则无法对该集合进行分片。
  • 对于已分片的集合,不能在其他字段上创建唯一索引。
  • 对于缺少被索引字段的文档,该字段存储为 NULL 值。

uniquetrue(默认为 false)传递给 sh.shardCollection(),为分片键增加唯一约束:

  • 如果集合为空,sh.shardCollection() 则在分片键上创建唯一索引。
  • 如果集合不为空,则需要在使用 sh.shardCollection() 之前先创建索引。

不能对哈希索引指定唯一约束。

从 4.4 版本开始,分片集合中的文档可以没有分片键字段。在分片之间分发文档时,缺少的分片键字段会被视为具有 NULL 值。

Document Missing Shard KeyFalls into Same Range As
{ x: "hello" }{ x: "hello", y: null }
{ y: "goodbye" }{ x: null, y: "goodbye" }
{ z: "oops" }{ x: null, y: null }

要定位缺少分片键字段的文档,可以对分片键字段使用 { $exists: false } 筛选条件。例如,如果分片键位于 { x: 1, y: 1 } 字段上,则可以通过运行以下查询来查找缺少分片键字段的文档:

db.shardedcollection.find( { $or: [ { x: { $exists: false } }, { y: { $exists: false } } ] } )

如果指定了与 NULL 相等的过滤条件({ x: null }),则会匹配缺少分片键字段的文档及分片键字段为 NULL 的文档。

对集合进行分片

若要对集合进行分片,必须指定要分片集合的完整命名空间和分片键。

语法:

sh.shardCollection(<namespace>, <key>) // Optional parameters omitted
namespaceSpecify the full namespace of the collection that you want to shard
("<database>.<collection>").
keySpecify a document { <shard key field1>: <1|"hashed">, ... } where
1 indicates range-based shardingopen in new window
"hashed" indicates hashed sharding.open in new window

缺少分片键字段:

  • 从 4.4 版本开始,分片集合中的文档可以没有分片键字段。在分片之间分发文档时,缺少的分片键字段会被视为具有 NULL 值。
  • 在 4.2 版本及之前,分片集合的每个文档中都必须有分片键字段。

更改文档的分片键值:

  • 从 MongoDB 4.2 开始,可以更新文档的分片键值,除非分片键字段是不可变的 _id 字段。
  • 在 MongoDB 4.0 及之前,文档的分片键字段值是不可变的。

更改集合的分片键:

  • 从 MongoDB 5.0 开始,可以通过更改集合的分片键来重新分片集合。
  • 从 MongoDB 4.4 开始,可以通过向现有分片键添加一个或多个后缀字段来优化分片键。
  • 在 MongoDB 4.2 及之前,分片后无法更改分片键。

选择分片键

分片键的选择会影响分片中块的创建和分布,进而影响分片集群内操作的效率和性能。

理想的分片键允许 MongoDB 在整个集群中均匀分布文档,故选择分片键时,需考虑:

  • 分片键的基数(Cardinality)
  • 分片键值出现的频率(Frequency)
  • 分片键是否单调增长(Monotonically)
  • 分片查询模式
  • 分片键限制

更改分片键

不合适的分片键会导致:

  • 巨型块
  • 负载分布不均
  • 查询性能随时间推移而下降

为了解决这些问题,MongoDB 允许更改分片键:

  • 从 MongoDB 5.0 开始,可以通过更改集合的分片键来重新分片集合。
  • 从 MongoDB 4.4 开始,可以通过向现有分片键添加一个或多个后缀字段来优化分片键。
  • 在 MongoDB 4.2 及之前,分片后无法更改分片键。
优化分片键

从 MongoDB 4.4 开始,可以通过向现有分片键添加一个或多个后缀字段来优化分片键。

例如,在 test 数据库的 orders 集合中,分片键为 { customer_id: 1 },使用 refineCollectionShardKey 命令修改分片键为 { customer_id: 1, order_id: 1 }

db.adminCommand( {
   refineCollectionShardKey: "test.orders",
   key: { customer_id: 1, order_id: 1 }
} )

注意:

不要修改分片类型,这将导致数据不一致。例如,不要将分片键从 { customer_id: 1 } 修改为 { customer_id: "hashed", order_id: 1 }

重新分片集合

从 MongoDB 5.0 开始,可以通过更改集合的分片键来重新分片集合。

在对集合进行重新分片之前,请确保满足以下要求:

  • 应用程序可以容忍两秒写阻塞。

  • 确保集群有足够的磁盘空间,如果重新分片后集合文档在集群中平均分布,则每个分片所需的磁盘空间约为集合大小除以分片数的 1.2 倍。

  • 确保 I/O 容量低于 50%。

  • 确保 CPU 负载低于 80%。

  • 使用 db.currentOp() 检查任何正在运行的索引构建。

 db.adminCommand(
    {
      currentOp: true,
      $or: [
        { op: "command", "command.createIndexes": { $exists: true }  },
        { op: "none", "msg" : /^Index Build/ }
      ]
    }
)

限制:

  • 一次只能对一个集合进行重新分片。
  • writeConcernMajorityJournalDefault 必须是 true
  • 不支持对有唯一约束的集合进行重新分片。
  • 新的分片键不能有唯一性约束。
  • 正在重新分片的集合不支持以下命令和方法:
    • collMod
    • convertToCapped
    • createIndexes
    • createIndex()
    • drop
    • drop()
    • dropIndexes
    • dropIndex()
    • renameCollection
    • renameCollection()
  • 在重新分片操作进行时,集群不支持以下命令和方法:
    • addShard
    • removeShard
    • db.createCollection()
    • dropDatabase
  • 无法对分片时序集合进行重新分片。
重新分片
  • 重新分片操作的最短持续时间始终为 5 分钟。
  • 如果 _id 值不是全局唯一的,则重新分片操作将失败。

步骤如下:

  1. 启动重新分片操作,连接到 mongos 时,使用 reshardCollection 命令,指定要重新分片的集合和新的分片键:
db.adminCommand({
  reshardCollection: "<database>.<collection>",
  key: <shardkey>
})
  1. 监视重新分片操作:
db.getSiblingDB("admin").aggregate([
  { $currentOp: { allUsers: true, localOps: false } },
  {
    $match: {
      type: "op",
      "originatingCommand.reshardCollection": "<database>.<collection>"
    }
  }
])
[
  {
    shard: '<shard>',
    type: 'op',
    desc: 'ReshardingRecipientService | ReshardingDonorService | ReshardingCoordinatorService <reshardingUUID>',
    op: 'command',
    ns: '<database>.<collection>',
    originatingCommand: {
      reshardCollection: '<database>.<collection>',
      key: <shardkey>,
      unique: <boolean>,
      collation: { locale: 'simple' }
    },
    totalOperationTimeElapsedSecs: <number>,
    remainingOperationTimeEstimatedSecs: <number>,
    ...
  },
  ...
]
  1. 完成重新分片操作
{
  ok: 1,
  '$clusterTime': {
    clusterTime: <timestamp>,
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: <number>
    }
  },
  operationTime: <timestamp>
}
  1. 可以使用 commitReshardCollection 命令提前阻止写入,强制完成重新分片操作
db.adminCommand({
  commitReshardCollection: "<database>.<collection>"
})
  1. 如果长时间没有完成,可以中止重新分片操作
db.adminCommand({
  abortReshardCollection: "<database>.<collection>"
})

更改文档的分片键值

从 MongoDB 4.2 开始,可以更改文档的分片键值,除非分片键字段是不可变的 _id 字段。

更改分片键值时,需要注意:

  • 必须使用 mongos,不要直接在分片上操作。
  • 必须在事务中运行或作为可重试写入(Retryable Write)运行。
  • 必须在查询过滤器中的完整分片键上包含相等条件。例如 messages 集合使用 { activityid: 1, userid : 1 } 作为分片键,若要更新文档的分片键值,必须在查询过滤器中包含 activityid: <value>, userid: <value>。您可以根据需要在查询中包含其他字段。

要更新分片键值,使用以下操作:

CommandMethod
updateopen in new window with multi: falsedb.collection.replaceOne()open in new window
db.collection.updateOne()open in new window

To set to a non-null value, the update must be performed either inside a transaction or as a retryable write.
findAndModifyopen in new windowdb.collection.findOneAndReplace()open in new window
db.collection.findOneAndUpdate()open in new window
db.collection.findAndModify()open in new window

To set to a non-null value, the update must be performed either inside a transaction or as a retryable write.

db.collection.bulkWrite()open in new window
Bulk.find.updateOne()open in new window

If the shard key modification results in moving the document to another shard, you cannot specify more than one shard key modification in the bulk operation; the batch size has to be 1.
If the shard key modification does not result in moving the document to another shard, you can specify multiple shard key modification in the bulk operation.
To set to a non-null value, the operation must be performed either inside a transaction or as a retryable write.

例如在 location 字段上分片的集合 sales,对于字段 _id12345location"" 的文档,使用以下命令更新字段值:

db.sales.updateOne(
  { _id: 12345, location: "" },
  { $set: { location: "New York"} }
)

设置缺少的分片键字段

如果缺少分片键字段,可以将分片键字段设置为 null 。如果要将缺少的分片键字段设置为非空值,参考 Change a Document's Shard Key Valueopen in new window

可以在 mongos 使用以下命令和方法进行修改:

CommandMethod
updateopen in new window withmulti: truedb.collection.updateMany()open in new window

其中:

  • Can be used to set the missing key value to null only.
  • Can be performed inside or outside a transaction.
  • Can be performed as a retryable write or not.
  • For additional requirements, refer to the specific command/method.
CommandMethod
updateopen in new window withmulti: falsedb.collection.replaceOne()open in new window
db.collection.updateOne()open in new window

其中:

  • Can be used to set the missing key value to null or any other value.
  • The update to set missing shard key fields must meet one of the following requirements:
    • the filter of the query contains an equality condition on the full shard key in the query
    • the filter of the query contains an exact match on _id
    • the update targets a single shard
  • To set to a non-null value, refer to Change a Document's Shard Key Value.open in new window
  • For additional requirements, refer to the specific command/method.
CommandMethod
findAndModifyopen in new windowdb.collection.findOneAndReplace()open in new window
db.collection.findOneAndUpdate()open in new window
db.collection.findAndModify()open in new window

其中:

  • Can be used to set the missing key value to null or any other value.
  • When setting missing shard key fields with a method that explicitly updates only one document, the update must meet one of the following requirements:
    • the filter of the query contains an equality condition on the full shard key in the query
    • the filter of the query contains an exact match on _id
    • the update targets a single shard
  • Missing key values are returned when matching on null. To avoid updating a key value that is null, include additional query conditions as appropriate.
  • To set to a non-null value, refer to Change a Document's Shard Key Value.open in new window
  • For additional requirements, refer to the specific command/method.
CommandMethod
findAndModifyopen in new windowdb.collection.bulkWrite()open in new window
Bulk.find.replaceOne()open in new window
Bulk.find.updateOne()open in new window
Bulk.find.update()open in new window

其中:

  • To set to a null value, you can specify multiple shard key modifications in the bulk operation.
  • When setting missing shard key fields with a method that explicitly updates only one document, the update must meet one of the following requirements:
    • the filter of the query contains an equality condition on the full shard key in the query
    • the filter of the query contains an exact match on _id
    • the update targets a single shard
  • To set to a non-null value, refer to Change a Document's Shard Key Value.open in new window
  • For additional requirements, refer to the underlying command/method.

例如在 location 字段上分片的集合 sales,集合中的某些文档没有 location 字段。缺少的字段被视为与字段的 null 值相同。运行以下命令将这些字段显式设置为 null

db.sales.updateOne(
  { _id: 12345, location: null },
  { $set: { location: null } }
)

查找键

每个分片集合都有一个分片键。要显示分片键,连接到 mongos 实例并运行 db.printShardingStatus() 方法:

db.printShardingStatus()

执行结果:

<dbname>.<collection>
   shard key: { <shard key> : <1 or hashed> }
   unique: <boolean>
   balancing: <boolean>
   chunks:
      <shard name1> <number of chunks>
      <shard name2> <number of chunks>
      ...
   { <shard key>: <min range1> } -->> { <shard key> : <max range1> } on : <shard name> <last modified timestamp>
   { <shard key>: <min range2> } -->> { <shard key> : <max range2> } on : <shard name> <last modified timestamp>
   ...
   tag: <tag1>  { <shard key> : <min range1> } -->> { <shard key> : <max range1> }
   ...

哈希分片

哈希分片(Hashed Sharding)使用单字段哈希索引或复合哈希索引(4.4 版本新增功能)作为分片键。

单字段哈希索引分片

哈希分片在分片集群中提供更均匀的数据分布,相近分片键值的文档不太可能位于同一块或分片上,此时 mongos 更可能执行广播操作来完成范围查询。

image-20231110133857412

复合哈希索引分片

MongoDB 4.4 可以使用单个哈希字段创建复合索引。在创建索引时指定 hashed 为任何单个索引键的值来创建复合哈希索引。

复合哈希索引计算索引中单个字段的哈希值,此值与索引中的其他字段一起用作分片键。

为防止冲突,请勿将 hashed 索引用于无法可靠地转换为 64 位整数的浮点数。且 hashed 索引不支持大于 253 的浮点值。

哈希分片键

作为哈希分片键的字段应具有大量不同的值,例如 ObjectId 或者时间戳,建议使用默认 _id 字段。

哈希分片与范围分片

假设集合使用单调递增字段 X 作为分片键,使用范围分片,插入数据后分布如下:

image-20231114103840422

由于 X 单调递增,大多数数据都写入到了上限为 MaxKey 块,导致数据分片不均。

使用哈希分片,插入数据后分布如下:

image-20231114104413458

此时数据均匀分布在整个集群中。

对集合进行分片

使用 sh.shardCollection() 方法,指定集合的完整命名空间和作为分片键的哈希索引。

sh.shardCollection( "database.collection", { <field> : "hashed" } )

若要对复合哈希索引上的集合进行分片,指定集合的完整命名空间和作为分片键的复合哈希索引。

sh.shardCollection(
  "database.collection",
  { "fieldA" : 1, "fieldB" : 1, "fieldC" : "hashed" }
)

范围分片

范围分片(默认)将数据划分为由分片键值确定的连续范围,相近分片键值的文档可能位于同一块或分片中,有利于进行范围查询。

image-20231110135138771

分片键选择

适用于范围分片的分片键应该:

  • 分片键的基数大,即具有大量不同的值
  • 分片键值出现的频率低
  • 非单调变化的键值

image-20231114113509131

对集合进行分片

使用 sh.shardCollection() 方法,指定集合的完整命名空间和作为分片键的索引或者复合索引。

sh.shardCollection( "database.collection", { <shard key> } )

部署分片集群

分片集群包括一个 mongos,一个配置服务器副本集和两个分片副本集。

No.HostnameIPRole
1mongos.stonecoding.netopen in new window192.168.92.170mongos
2cfg1.stonecoding.netopen in new window192.168.92.171Config server
3cfg2.stonecoding.netopen in new window192.168.92.172Config server
4cfg3.stonecoding.netopen in new window192.168.92.173Config server
5s1rs1.stonecoding.netopen in new window192.168.92.174Shard Replica Set 1
6s1rs2.stonecoding.netopen in new window192.168.92.175Shard Replica Set 1
7s1rs3.stonecoding.netopen in new window192.168.92.176Shard Replica Set 1
8s2rs1.stonecoding.netopen in new window192.168.92.177Shard Replica Set 2
9s2rs2.stonecoding.netopen in new window192.168.92.178Shard Replica Set 2
10s2rs3.stonecoding.netopen in new window192.168.92.179Shard Replica Set 2

参考部署open in new window在 10 台服务器上安装 MongoDB,如果是虚拟机,可以先部署好一台后再克隆。

如果没有 DNS 服务,需要为所有服务器配置本地域名解析:

[root@mongos ~]# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.92.170   mongos.stonecoding.net   mongos
192.168.92.171   cfg1.stonecoding.net     cfg1
192.168.92.172   cfg2.stonecoding.net     cfg2
192.168.92.173   cfg3.stonecoding.net     cfg3
192.168.92.174   s1rs1.stonecoding.net    s1rs1
192.168.92.175   s1rs2.stonecoding.net    s1rs2
192.168.92.176   s1rs3.stonecoding.net    s1rs3
192.168.92.177   s2rs1.stonecoding.net    s2rs1
192.168.92.178   s2rs2.stonecoding.net    s2rs2
192.168.92.179   s2rs3.stonecoding.net    s2rs3

创建配置服务器副本集

修改配置服务器副本集 3 个节点的配置文件:

[root@cfg1 ~]# vi /etc/mongod.conf 
net:
  port: 27017
  bindIp: 0.0.0.0
  
sharding:
  clusterRole: configsvr
replication:
  replSetName: cfg

启动 3 个节点上的 MongoDB:

[root@cfg1 ~]# systemctl daemon-reload
[root@cfg1 ~]# systemctl start mongod
[root@cfg1 ~]# systemctl status mongod
● mongod.service - MongoDB Database Server
   Loaded: loaded (/usr/lib/systemd/system/mongod.service; enabled; vendor preset: disabled)
   Active: active (running) since Tue 2023-11-14 21:05:26 CST; 7s ago
     Docs: https://docs.mongodb.org/manual
 Main PID: 1686 (mongod)
   CGroup: /system.slice/mongod.service
           └─1686 /usr/bin/mongod -f /etc/mongod.conf

Nov 14 21:05:26 cfg1.stone.com systemd[1]: Started MongoDB Database Server.
Nov 14 21:05:26 cfg1.stone.com mongod[1686]: {"t":{"$date":"2023-11-14T13:05:26.296Z"},"s":"I",  "c":"CONTROL",  "id":7484500, "ct...false"}
Hint: Some lines were ellipsized, use -l to show in full.

在一个节点上执行初始化:

[root@cfg1 ~]# mongosh
rs.initiate(
  {
    _id: "cfg",
    configsvr: true,
    members: [
      { _id : 0, host : "cfg1.stonecoding.net:27017" },
      { _id : 1, host : "cfg2.stonecoding.net:27017" },
      { _id : 2, host : "cfg3.stonecoding.net:27017" }
    ]
  }
)

其中:

  • _id 为副本集名称。
  • configsvr 设置为 true 表示配置服务器副本集。

查看配置:

rs.conf()

执行结果:

{
  _id: 'cfg',
  version: 1,
  term: 1,
  members: [
    {
      _id: 0,
      host: 'cfg1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'cfg2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'cfg3.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  configsvr: true,
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("6553738f84352abab549c4fc")
  }
}

创建分片副本集

修改第一个分片副本集 3 个节点的配置文件:

[root@s1rs1 ~]# vi /etc/mongod.conf 
net:
  port: 27017
  bindIp: 0.0.0.0
  
sharding:
    clusterRole: shardsvr
replication:
    replSetName: s1

启动 3 个节点上的 MongoDB:

[root@s1rs1 ~]# systemctl daemon-reload
[root@s1rs1 ~]# systemctl start mongod
[root@s1rs1 ~]# systemctl status mongod
● mongod.service - MongoDB Database Server
   Loaded: loaded (/usr/lib/systemd/system/mongod.service; enabled; vendor preset: disabled)
   Active: active (running) since Tue 2023-11-14 21:38:00 CST; 41s ago
     Docs: https://docs.mongodb.org/manual
 Main PID: 2359 (mongod)
   CGroup: /system.slice/mongod.service
           └─2359 /usr/bin/mongod -f /etc/mongod.conf

Nov 14 21:38:00 s1rs1.stonecoding.net systemd[1]: Started MongoDB Database Server.
Nov 14 21:38:00 s1rs1.stonecoding.net mongod[2359]: {"t":{"$date":"2023-11-14T13:38:00.945Z"},"s":"I",  "c":"CONTROL",  "id":748450...alse"}
Hint: Some lines were ellipsized, use -l to show in full.

在一个节点上执行初始化:

[root@s1rs1 ~]# mongosh
rs.initiate(
  {
    _id : "s1",
    members: [
      { _id : 0, host : "s1rs1.stonecoding.net:27017" },
      { _id : 1, host : "s1rs2.stonecoding.net:27017" },
      { _id : 2, host : "s1rs3.stonecoding.net:27017" }
    ]
  }
)

其中:

  • _id 为副本集名称。

查看配置:

rs.conf()

执行结果:

{
  _id: 's1',
  version: 1,
  term: 1,
  members: [
    {
      _id: 0,
      host: 's1rs1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 's1rs2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 's1rs3.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("65537943cd1788a4b9e5e390")
  }
}

修改第二个分片副本集 3 个节点的配置文件:

[root@s2rs1 ~]# vi /etc/mongod.conf 
net:
  port: 27017
  bindIp: 0.0.0.0
  
sharding:
    clusterRole: shardsvr
replication:
    replSetName: s2

启动 3 个节点上的 MongoDB:

[root@s2rs1 ~]# systemctl daemon-reload
[root@s2rs1 ~]# systemctl start mongod
[root@s2rs1 ~]# systemctl status mongod
● mongod.service - MongoDB Database Server
   Loaded: loaded (/usr/lib/systemd/system/mongod.service; enabled; vendor preset: disabled)
   Active: active (running) since Tue 2023-11-14 21:54:23 CST; 48s ago
     Docs: https://docs.mongodb.org/manual
 Main PID: 2224 (mongod)
   CGroup: /system.slice/mongod.service
           └─2224 /usr/bin/mongod -f /etc/mongod.conf

Nov 14 21:54:23 s2rs1.stonecoding.net systemd[1]: Started MongoDB Database Server.
Nov 14 21:54:23 s2rs1.stonecoding.net mongod[2224]: {"t":{"$date":"2023-11-14T13:54:23.779Z"},"s":"I",  "c":"CONTROL",  "id":748450...alse"}
Hint: Some lines were ellipsized, use -l to show in full.

在一个节点上执行初始化:

[root@s2rs1 ~]# mongosh
rs.initiate(
  {
    _id : "s2",
    members: [
      { _id : 0, host : "s2rs1.stonecoding.net:27017" },
      { _id : 1, host : "s2rs2.stonecoding.net:27017" },
      { _id : 2, host : "s2rs3.stonecoding.net:27017" }
    ]
  }
)

其中:

  • _id 为副本集名称。

查看配置:

rs.conf()

执行结果:

{
  _id: 's2',
  version: 1,
  term: 1,
  members: [
    {
      _id: 0,
      host: 's2rs1.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 's2rs2.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 's2rs3.stonecoding.net:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("65537c76d6974adc9673ea50")
  }
}

启动 mongos

修改 mongos 的配置文件:

[root@mongos ~]# vi /etc/mongos.conf
net:
  port: 27017
  bindIp: 0.0.0.0
  
sharding:
  configDB: cfg/cfg1.stonecoding.net:27017,cfg2.stonecoding.net:27017,cfg3.stonecoding.net:27017

其中:

  • sharding.configDB 需要设置为配置服务器副本集名称和至少一个副本集成员。

然后启动 mongos

[root@mongos ~]# nohup mongos --config /etc/mongos.conf > /var/log/mongodb/mongos.log 2>&1 &

添加分片到集群

使用 mongosh 连接到 mongos

[root@mongos ~]# mongosh

使用 sh.addShard() 方法添加所有分片副本集到集群:

[direct: mongos] test> sh.addShard( "s1/s1rs1.stonecoding.net:27017,s1rs2.stonecoding.net:27017,s1rs3.stonecoding.net:27017")
{
  shardAdded: 's1',
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699971596, i: 6 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699971596, i: 6 })
}

[direct: mongos] test> sh.addShard( "s2/s2rs1.stonecoding.net:27017,s2rs2.stonecoding.net:27017,s2rs3.stonecoding.net:27017")
{
  shardAdded: 's2',
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1699971642, i: 15 }),
    signature: {
      hash: Binary.createFromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1699971642, i: 5 })
}

使用 sh.status() 方法查看分片集群状态:

[direct: mongos] test> sh.status()
shardingVersion
{ _id: 1, clusterId: ObjectId("6553739984352abab549c586") }
---
shards
[
  {
    _id: 's1',
    host: 's1/s1rs1.stonecoding.net:27017,s1rs2.stonecoding.net:27017,s1rs3.stonecoding.net:27017',
    state: 1,
    topologyTime: Timestamp({ t: 1699971596, i: 3 })
  },
  {
    _id: 's2',
    host: 's2/s2rs1.stonecoding.net:27017,s2rs2.stonecoding.net:27017,s2rs3.stonecoding.net:27017',
    state: 1,
    topologyTime: Timestamp({ t: 1699971642, i: 3 })
  }
]
---
active mongoses
[ { '7.0.2': 1 } ]
---
autosplit
{ 'Currently enabled': 'yes' }
---
balancer
{
  'Currently running': 'no',
  'Currently enabled': 'yes',
  'Failed balancer rounds in last 5 attempts': 0,
  'Migration Results for the last 24 hours': 'No recent migrations'
}
---
databases
[
  {
    database: { _id: 'config', primary: 'config', partitioned: true },
    collections: {
      'config.system.sessions': {
        shardKey: { _id: 1 },
        unique: false,
        balancing: true,
        chunkMetadata: [ { shard: 's1', nChunks: 1 } ],
        chunks: [
          { min: { _id: MinKey() }, max: { _id: MaxKey() }, 'on shard': 's1', 'last modified': Timestamp({ t: 1, i: 0 }) }
        ],
        tags: []
      }
    }
  }
]
上次编辑于:
贡献者: stonebox,stone