MongoDB
MongoDB
注意:
此文档对应的 MongoDB 版本为 7.0.2。
概述
MongoDB 是一个开源,高性能,无模式的文档型数据库,由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是功能最丰富,最像关系数据库的非关系数据库。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。
基本概念
与 MySQL 相比,MongoDB 的基本概念如下:
MySQL | MongoDB | 描述 |
---|---|---|
database | database | 数据库 |
table | collection | 表/集合 |
row | document | 行/文档 |
column | field | 字段 |
index | index | 索引 |
table join | $lookup , embedded documents | 表连接/嵌套文档 |
primary key | primary key | 主键,MongoDB 自动将 _id 字段设置为主键 |
相关程序对比:
MongoDB | MySQL | Oracle | |
---|---|---|---|
Database Server | mongod | mysqld | oracle |
Database Client | mongosh | mysql | sqlplus |
数据类型
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 7 环境安装 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
在官方仓库下载安装包,包括:
[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
在官方网站下载客户端软件 Compass 并进行安装,指定 URI 后点击 Connect 即可连接到 MongoDB。
数据库
创建或选择数据库
使用 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()
查询固定集合,如果没有指定排序方向,则结果的顺序与插入顺序相同。类似于tail
和tail -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 本身不支持 SQL,需要使用方法来进行增删改查操作。
插入文档
插入单个文档
使用 db.collection.insertOne()
插入单个文档到集合,其中 collection
为集合名称,如果该集合不存在,则会自动创建。
语法:
db.collection.insertOne(
<document>,
{
writeConcern: <document>
}
)
Parameter | Type | Description |
---|---|---|
document | document | A document to insert into the collection. |
writeConcern | document | Optional. A document expressing the write concern. 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>
}
)
Parameter | Type | Description |
---|---|---|
document | document | An array of documents to insert into the collection. |
writeConcern | document | Optional. A document expressing the write concern. Omit to use the default write concern.Do 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. |
ordered | boolean | Optional. A boolean specifying whether the mongod 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> )
Parameter | Type | Description |
---|---|---|
query | document | Optional. Specifies selection filter using query operators. To return all documents in a collection, omit this parameter or pass an empty document ({} ). |
projection | document | Optional. 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. |
options | document | Optional. Specifies additional options for the query. These options modify query behavior and how results are returned. To see available options, see FindOptions. |
创建用于查询的集合并插入文档:
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
Name | Description |
---|---|
$eq | Matches values that are equal to a specified value. |
$gt | Matches values that are greater than a specified value. |
$gte | Matches values that are greater than or equal to a specified value. |
$in | Matches any of the values specified in an array. |
$lt | Matches values that are less than a specified value. |
$lte | Matches values that are less than or equal to a specified value. |
$ne | Matches all values that are not equal to a specified value. |
$nin | Matches none of the values specified in an array. |
- Logical
Name | Description |
---|---|
$and | Joins query clauses with a logical AND returns all documents that match the conditions of both clauses. |
$not | Inverts the effect of a query expression and returns documents that do not match the query expression. |
$nor | Joins query clauses with a logical NOR returns all documents that fail to match both clauses. |
$or | Joins query clauses with a logical OR returns all documents that match the conditions of either clause. |
- Element
Name | Description |
---|---|
$exists | Matches documents that have the specified field. |
$type | Selects documents if a field is of the specified type. |
- Evaluation
Name | Description |
---|---|
$expr | Allows use of aggregation expressions within the query language. |
$jsonSchema | Validate documents against the given JSON Schema. |
$mod | Performs a modulo operation on the value of a field and selects documents with a specified result. |
$regex | Selects documents where values match a specified regular expression. |
$text | Performs text search. |
$where | Matches documents that satisfy a JavaScript expression. |
- Geospatial
Name | Description |
---|---|
$geoIntersects | Selects geometries that intersect with a GeoJSON geometry. The 2dsphere index supports $geoIntersects . |
$geoWithin | Selects geometries within a bounding GeoJSON geometry. The 2dsphere and 2d indexes support $geoWithin . |
$near | Returns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near . |
$nearSphere | Returns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The 2dsphere and 2d indexes support $nearSphere . |
- Array
Name | Description |
---|---|
$all | Matches arrays that contain all elements specified in the query. |
$elemMatch | Selects documents if element in the array field matches all the specified $elemMatch conditions. |
$size | Selects documents if the array field is a specified size. |
- Bitwise
Name | Description |
---|---|
$bitsAllClear | Matches numeric or binary values in which a set of bit positions all have a value of 0 . |
$bitsAllSet | Matches numeric or binary values in which a set of bit positions all have a value of 1 . |
$bitsAnyClear | Matches numeric or binary values in which any bit from a set of bit positions has a value of 0 . |
$bitsAnySet | Matches numeric or binary values in which any bit from a set of bit positions has a value of 1 . |
- Projection Operators
Name | Description |
---|---|
$ | Projects the first element in an array that matches the query condition. |
$elemMatch | Projects the first element in an array that matches the specified $elemMatch condition. |
$meta | Projects the document's score assigned during $text operation. |
$slice | Limits the number of elements projected from an array. Supports skip and limit slices. |
- Miscellaneous Operators
Name | Description |
---|---|
$comment | Adds a comment to a query predicate. |
$rand | Generates 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 或 item
以 p
开头的任一条件的文档
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
字段。
例子:返回满足条件的文档的 _id
, item
和 status
字段
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"
例子:在投影文档中指定 _id
为 0
不返回 _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"
例子:在投影文档中指定 status
和 instock
为 0
则不返回这两个字段
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
Name | Description |
---|---|
$currentDate | Sets the value of a field to current date, either as a Date or a Timestamp. |
$inc | Increments the value of the field by the specified amount. |
$min | Only updates the field if the specified value is less than the existing field value. |
$max | Only updates the field if the specified value is greater than the existing field value. |
$mul | Multiplies the value of the field by the specified amount. |
$rename | Renames a field. |
$set | Sets the value of a field in a document. |
$setOnInsert | Sets 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. |
$unset | Removes the specified field from a document. |
- Array Operators
Name | Description |
---|---|
$ | Acts as a placeholder to update the first element that matches the query condition. |
$[] | Acts as a placeholder to update all elements in an array for the documents that match the query condition. |
$[<identifier>] | Acts as a placeholder to update all elements that match the arrayFilters condition for the documents that match the query condition. |
$addToSet | Adds elements to an array only if they do not already exist in the set. |
$pop | Removes the first or last item of an array. |
$pull | Removes all array elements that match a specified query. |
$push | Adds an item to an array. |
$pullAll | Removes all matching values from an array. |
- Array Modifiers
Name | Description |
---|---|
$each | Modifies the $push and $addToSet operators to append multiple items for array updates. |
$position | Modifies the $push operator to specify the position in the array to add elements. |
$slice | Modifies the $push operator to limit the size of updated arrays. |
$sort | Modifies the $push operator to reorder documents stored in an array. |
- Bitwise
Name | Description |
---|---|
$bit | Performs 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/Concepts | MongoDB Terms/Concepts |
---|---|
database | database |
table | collection |
row | document or BSON document |
column | field |
index | index |
table joins | $lookup , embedded documents |
primary keySpecify any unique column or column combination as primary key. | primary keyIn MongoDB, the primary key is automatically set to the _id field. |
aggregation (e.g. group by) | aggregation pipelineSee the SQL to Aggregation Mapping Chart. |
SELECT INTO NEW_TABLE | $out See the SQL to Aggregation Mapping Chart. |
MERGE INTO TABLE | $merge (Available starting in MongoDB 4.2)See the SQL to Aggregation Mapping Chart. |
UNION ALL | $unionWith (Available starting in MongoDB 4.4) |
transactions | transactions |
假设:
- SQL 数据库表名为
people
。 - MongoDB 有名称为
people
集合,其中一个文档内容如下:
{
_id: ObjectId("509a8fb2f3f4948bd2f983a0"),
user_id: "abc123",
age: 55,
status: 'A'
}
- 创建表
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")
- 增加字段
SQL:
ALTER TABLE people
ADD join_date DATETIME
MongoDB 中没有在集合级别的修改,但可以对已存在文档增加字段:
db.people.updateMany(
{ },
{ $set: { join_date: new Date() } }
)
- 删除字段
SQL:
ALTER TABLE people
DROP COLUMN join_date
MongoDB 中没有在集合级别的修改,但可以对已存在文档删除字段:
db.people.updateMany(
{ },
{ $unset: { "join_date": "" } }
)
- 创建索引
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 } )
- 删除表
SQL:
DROP TABLE people
MongoDB:
db.people.drop()
- 插入数据
SQL:
INSERT INTO people(user_id,age,status)
VALUES ("bcd001",45,"A")
MongoDB:
db.people.insertOne(
{ user_id: "bcd001", age: 45, status: "A" }
)
- 查询所有数据
SQL:
SELECT *
FROM people
MongoDB:
db.people.find()
- 查询指定字段所有数据
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 }
)
- 查询满足条件的所有数据
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/ } } )
- 查询满足条件的指定字段的数据
SQL:
SELECT user_id, status
FROM people
WHERE status = "A"
MongoDB:
db.people.find(
{ status: "A" },
{ user_id: 1, status: 1, _id: 0 }
)
- 查询满足条件的数据并排序
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 } )
- 查询记录数量
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()
- 查询不同的记录
SQL:
SELECT DISTINCT(status)
FROM people
MongoDB:
db.people.aggregate( [ { $group : { _id : "$status" } } ] )
db.people.distinct( "status" )
- 限制返回的记录数
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)
- 查看执行计划
SQL:
EXPLAIN SELECT *
FROM people
WHERE status = "A"
MongoDB:
db.people.find( { status: "A" } ).explain()
- 更新记录
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 } }
)
- 删除记录
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 支持以下地理空间查询运算符:
Name | Description |
---|---|
$geoIntersects | Selects geometries that intersect with a GeoJSON geometry. The 2dsphere index supports $geoIntersects . |
$geoWithin | Selects geometries within a bounding GeoJSON geometry. The 2dsphere and 2d indexes support $geoWithin . |
$near | Returns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near . |
$nearSphere | Returns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The 2dsphere and 2d indexes support $nearSphere . |
每种地理空间操作使用的地理空间查询运算符:
Operation | Spherical/Flat Query | Notes |
---|---|---|
$near (GeoJSON centroid point in this line and the following line, 2dsphere index) | Spherical | See also the $nearSphere operator, which provides the same functionality when used with GeoJSON and a 2dsphere index. |
$near (legacy coordinates, 2d index) | Flat | |
$nearSphere (GeoJSON point, 2dsphere index) | Spherical | Provides the same functionality as $near operation that uses GeoJSON point and a 2dsphere index.For spherical queries, it may be preferable to use $nearSphere which explicitly specifies the spherical queries in the name rather than $near operator. |
$nearSphere (legacy coordinates, 2d index) | Spherical | Use GeoJSON points instead. |
$geoWithin : { $geometry : ... } | Spherical | |
$geoWithin : { $box : ... } | Flat | |
$geoWithin : { $polygon : ... } | Flat | |
$geoWithin : { $center : ... } | Flat | |
$geoWithin : { $centerSphere : ... } | Spherical | |
$geoIntersects | Spherical | |
$geoNear aggregation stage (2dsphere index) | Spherical | |
$geoNear aggregation stage (2d index) | Flat |
地理空间聚合
Stage | Description |
---|---|
$geoNear | Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match , $sort , and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field.$geoNear requires a geospatial index. |
地理空间查询示例
- 使用
$geoIntersects
查询附近位置 - 使用
$geoWithin
查询附近位置的餐馆 - 使用
$nearSphere
查询附近指定距离内餐馆
下载 Restaurants 和 Neighborhoods 数据,然后导入到数据库:
[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):用于聚合单个集合中的文档。
单一用途聚合方法包括:
Method | Description |
---|---|
db.collection.estimatedDocumentCount() | Returns an approximate count of the documents in a collection or a view. |
db.collection.count() | Returns a count of the number of documents in a collection or a view. |
db.collection.distinct() | Returns 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 }
}
计算 size
为 medium
的文档,按照 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
阶段过滤出size
为medium
的文档,并传递给$group
阶段。$group
阶段按照name
分组,使用$sum
计算每种name
的quantity
总计,计算结果存在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 万的州
下载 Zips 数据,然后导入到数据库:
[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 Concepts | MongoDB Aggregation Operators |
---|---|
WHERE | $match |
GROUP BY | $group |
HAVING | $match |
SELECT | $project |
ORDER BY | $sort |
LIMIT | $limit |
SUM() | $sum |
COUNT() | $sum $sortByCount |
join | $lookup |
SELECT INTO NEW_TABLE | $out |
MERGE INTO TABLE | $merge (Available starting in MongoDB 4.2) |
UNION ALL | $unionWith (Available starting in MongoDB 4.4) |
假设:
- SQL 数据库有表
orders
和order_lineitem
,通过order_lineitem.order_id
和orders.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 } ]
}
- 计算记录总数
SQL:
SELECT COUNT(*) AS count
FROM orders
MongoDB:
db.orders.aggregate( [
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
] )
- 计算金额总计
SQL:
SELECT SUM(price) AS total
FROM orders
MongoDB:
db.orders.aggregate( [
{
$group: {
_id: null,
total: { $sum: "$price" }
}
}
] )
- 计算每个用户的金额合计
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" }
}
}
] )
- 计算每个用户的金额合计并排序
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 } }
] )
- 计算每个用户每天的金额合计
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" }
}
}
] )
- 计算每个用户的订单数量
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 } } }
] )
- 计算每个用户每天的订单总金额,只返回总金额大于 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 } } }
] )
- 对于状态为
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" }
}
}
] )
- 对于状态为
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 } } }
] )
- 关联查询每个用户的商品数量
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" }
}
}
] )
- 对订单表按照每个用户每天进行分组后,计算总记录数
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):将相关数据存储在单个文档结构中,这种非规范化的数据模型允许应用程序在单个操作中检索和操作关联数据,为读取操作提供了更好的性能。在很多情况下,此种模型最佳。适用于一对一或一对多场景。
- 引用(References):通过包含从一个文档到另一个文档的链接或引用来存储数据之间的关系,这是一种规范化的数据模型。适用于多对多场景。
写操作的原子性
- 在 MongoDB 中,在单个文档上的写操作是原子性的,即使该写操作修改了单个文档中的多个嵌套文档。
- 当单个写操作(例如
db.collection.updateMany()
)修改多个文档时,每个文档的修改是原子性的,但整个操作不是原子性的。 - MongoDB 使用多文档事务以支持多文档写操作的原子性,但需要更多的性能成本。
模式验证
- 可以使用模式验证为字段创建验证规则,例如允许的数据类型和数值范围。
- MongoDB 会在更新和插入数据时进行验证。如果文档不符合要求,则操作会被拒绝并返回错误信息。
- 向现有集合添加验证不会对现有文档强制执行验证。
- 不能为
admin
,local
和config
数据库中的集合指定模式验证。
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
的值只能是 France
,United Kingdom
或 United States
。
插入数据:
db.shipping.insertOne( {
item: "sweater",
size: "medium",
country: "Germany"
} )
由于 country
为 Germany
,不在允许的列表中,则会报错:
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
字段,则只会拒绝除了 _id
和 storeLocation
外还有其他字段的文档:
{
"$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
,则此文档必须匹配新的验证规则,在每次更新文档时都会进行验证。如果 validationLevel
为 moderate
,则此文档无需匹配新的验证规则。
指定现有文档的验证级别
使用 validationLevel
指定现有文档的验证级别:
Validation Level | Behavior |
---|---|
strict | (Default) MongoDB applies validation rules to all inserts and updates. |
moderate | MongoDB 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"
} )
由于 validationLevel
为 strict
,当更新任何文档时,会检查该文档进行验证。
使用以下不满足验证规则的数据更新文档:
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"
} )
由于 validationLevel
为 moderate
:
- 如果更新
_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 Action | Behavior |
---|---|
error | (Default) MongoDB rejects any insert or update that violates the validation criteria. |
warn | MongoDB 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' }
}
]
文档之间的模型关系
使用嵌套文档对一对一关系进行建模
对于以下 patron
和 address
文档,在规范化数据模型中,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")
}
}
应用程序只需要读取较少的数据就可以满足最常见的请求,提高了读取性能。
使用嵌套文档对一对多关系进行建模
对于以下 patron
和 address
文档,在规范化数据模型中,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"
}
模型树结构
具有父引用的模型树结构
该模型通过将父节点的引用存储在子节点中来描述文档的树状结构。
对于以下层次结构:
使用父引用(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' }
]
具有子引用的模型树结构
该模型通过将子节点的引用存储在父节点中来描述文档的树状结构。
对于以下层次结构:
使用子引用(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' ] } ]
具有祖先数组的模型树结构
该模型使用对父节点的引用和存储所有祖先的数组来描述文档的树状结构。
对于以下层次结构:
使用父引用(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'
}
]
具有物化路径的模型树结构
该模型通过存储文档之间的完整关系路径来描述文档的树状结构。
对于以下层次结构:
使用物化路径(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
具有嵌套集合的模型树结构
该模型使用对父节点的引用和左右遍历位置来描述文档的树状结构,适用于不变的静态树。
对于以下层次结构:
使用嵌套集合(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>" }
)
- 索引名称必须唯一。
- 不能重命名现有索引,只能删除并重建。
默认索引名称由索引键及其值组成,使用下划线连接,例如:
Index | Default 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 |
删除索引
使用以下方法删除索引或终止正在创建的索引:
Method | Description |
---|---|
db.collection.dropIndex() | Drops a specific index from the collection. |
db.collection.dropIndexes() | Drops 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> } )
先插入示例数据:
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>
} )
例子:在字段 name
和 gpa
上创建索引
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")
}
}
在字段 score
和 username
上创建复合索引,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> } )
例子:创建多键索引
先插入示例数据:
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 } }
在 item
和 ratings
字段上创建复合多键索引:
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.score
和 ratings.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.d
,a.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.score
和 ratings.by
创建复合索引:
db.survey2.createIndex( { "ratings.score": 1, "ratings.by": 1 } )
字段 ratings.score
和 ratings.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.q1
和 ratings.scores.q2
创建复合索引:
db.survey3.createIndex( { "ratings.scores.q1": 1, "ratings.scores.q2": 1 } )
字段 ratings.scores.q1
和 ratings.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' ]
}
]
例子:在 about
和 keywords
字段上创建复合文本索引
db.blog.createIndex(
{
"about": "text",
"keywords": "text"
}
)
执行结果:
about_text_keywords_text
索引 about_text_keywords_text
支持在 about
和 keywords
字段上进行文本搜索查询,例如以下查询返回 about
或 keywords
字段包含 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
支持对集合中所有字段进行文本搜索查询。
- 查询集合中任意字段包含字符串
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' ]
}
]
- 查询集合中任意字段包含
poll
或coffee
字符串的文档:
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' ]
}
]
- 查询集合中任意字段包含
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
- 查询集合中任意字段包含
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
}
]
- 查询集合中任意字段包含
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
}
]
- 查询集合中任意字段包含
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
等于 kitchen
且 description
字段包含 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
字段时,才支持同时针对0
和1
。
先插入示例数据:
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.size
和 attributes.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
and180
,纬度在后,范围为-90
and90
。 - 当指定范围
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
and180
,纬度在后,范围为-90
and90
。 - 在
$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
and180
,纬度在后,范围为-90
and90
。 - 可以创建 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
and180
,纬度在后,范围为-90
and90
。 - 在
$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。位置精度会影响插入和读取操作的性能。较低的精度可提高插入和更新操作的性能,并减少使用的存储空间。更高的精度可提高读取操作的性能,因为查询扫描索引的小部分数据即可返回结果。位置精度不会影响查询准确性。
- 可以使用
min
和max
指定 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
例子:创建索引时使用 min
和 max
指定 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
为 -75
,max
为 60
:
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
and180
,纬度在后,范围为-90
and90
。 - 在
$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
and180
,纬度在后,范围为-90
and90
。- 虽然
$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 索引,则该索引仍会使文档过期。
- 隐藏索引仍会出现在
listIndexes
和db.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 } } )
部分索引有以下限制:
- 不能同时指定
partialFilterExpression
和sparse
选项。 _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")
}
在 borough
和 cuisine
字段上创建部分索引,指定过滤条件为 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 值,范围为 0
到 2147483647
:
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )
如果 expireAfterSeconds
为 0
,则索引字段的值就是过期时间,如果该值为未来时间,则文档将在未来过期,如果该值为过去时间,则文档已过期。例如:
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 开始,可以为同一字段创建稀疏唯一索引和非稀疏唯一索引。
- 创建索引时指定
unique
为true
以创建唯一索引。
语法:
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 索引。 - 为不影响性能,在删除生产环境的索引之前,可以先创建一个包含相同字段的临时冗余索引。
例子:创建索引,然后修改为唯一索引
- 创建
siteAnalytics
集合,并在url
字段上创建索引
db.siteAnalytics.createIndex( { "url": 1 } )
执行结果:
url_1
- 在
url
字段上创建临时索引
db.siteAnalytics.createIndex( { "url": 1, "dummyField": 1 } )
执行结果:
url_1_dummyField_1
- 删除索引
url_1
,由于有临时索引,不会影响性能
db.siteAnalytics.dropIndex("url_1")
执行结果:
{ nIndexesWas: 3, ok: 1 }
- 重建索引为唯一索引
db.siteAnalytics.createIndex( { "url": 1 }, { "unique": true } )
执行结果:
url_1
- 删除临时索引
db.siteAnalytics.dropIndex( "url_1_dummyField_1" )
执行结果:
{ nIndexesWas: 3, ok: 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 } )
可以在 manufacturer
和 model
字段上创建索引以提高性能:
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 提供了一些内置角色用于不同级别的访问。
查看当前数据库的内置角色:
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
:提供执行管理任务的权限,包括:
Resource | Permitted Actions |
---|---|
system.profile | changeStream 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
:可以对数据库执行任何管理操作,包括 readWrite
,dbAdmin
和 userAdmin
角色授予的权限。
备份和恢复角色
数据库 admin
包括以下用于备份和恢复数据的角色:
backup
:提供备份数据所需权限。restore
:提供恢复数据所需权限。
所有数据库角色
为除 local
and config
数据库之外的所有数据库提高权限。
readAnyDatabase
:对于除local
andconfig
数据库之外的所有数据库,提供与read
角色相同的权限,及对整个群集的listDatabases
操作。readWriteAnyDatabase
:对于除local
andconfig
数据库之外的所有数据库,提供与readWrite
角色相同的权限,compactStructuredEncryptionData
操作及对整个群集的listDatabases
操作。userAdminAnyDatabase
:对于除local
andconfig
数据库之外的所有数据库,提供与userAdmin
角色相同的权限,及对集群的authSchemaUpgrade
,invalidateUserCache
和listDatabases
操作。授予该角色的用户为超级用户。dbAdminAnyDatabase
:对于除local
andconfig
数据库之外的所有数据库,提供与dbAdmin
角色相同的权限,及对整个群集的listDatabases
操作。
超级用户角色
可以为任何用户分配对任何数据库的任何权限,包括:
dbOwner
:限定为admin
数据库。userAdmin
:限定为admin
数据库。userAdminAnyDatabase
以下角色提供对所有资源的所有权限:
root
:包括以下角色:readWriteAnyDatabase
dbAdminAnyDatabase
userAdminAnyDatabase
clusterAdmin
restore
backup
还提供了对 system.
集合的 validate
权限。
集群管理角色
数据库 admin
包括以下集群管理角色:
clusterAdmin
:提供集群管理权限,包括clusterManager
、clusterMonitor
和hostManager
角色及dropDatabase
权限。
自定义角色
- 使用
db.createRole()
方法自定义角色。 - 可以在特定数据库中创建角色。MongoDB 使用数据库和角色名称的组合来唯一定义角色。每个角色的范围限定为其所在的数据库。
- 除了在
admin
数据库中创建的角色,角色只能包含应用于其所在数据库中的权限,也只能继承自其所在数据库的其他角色。 - 创建在
admin
数据库中的角色可以包括应用于admin
数据库,其他数据库或者集群资源的权限,可以继承自admin
数据库和其他数据库的角色。 - 所有角色信息存储在
admin
数据库system.roles
集合中。不要直接访问该集合,应使用角色管理命令查看和编辑自定义角色。 - 在数据库中创建角色,需要
createRole
和grantRole
权限,内置的userAdmin
和userAdminAnyDatabase
角色包含这两个权限。
例子:创建两个自定义角色
角色 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
集合中。不要直接访问该集合,应使用用户管理命令管理用户。
例子:创建用户管理员,启用访问控制,创建普通用户,授予角色
- 创建用户管理员
use admin
db.createUser(
{
user: "myUserAdmin",
pwd: passwordPrompt(), // or cleartext password
roles: [
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" }
]
}
)
- 修改配置文件,启用访问控制
[root@linux ~]# vi /etc/mongod.conf
security:
authorization: enabled
[root@linux ~]# systemctl restart mongod
[root@linux ~]# mongosh --authenticationDatabase "admin" -u "myUserAdmin" -p
- 创建普通用户并授予角色
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"} ]
}
)
- 以普通用户连接到 MongoDB
[root@linux ~]# mongosh --authenticationDatabase "test" -u "myTester" -p
test> show dbs
test 4.86 MiB
其中,--authenticationDatabase
指定用户创建时所在的数据库。
- 查看所有用户
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' } ]
}
]
- 授予用户角色
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' }
]
}
]
- 回收用户角色
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' } ]
}
]
- 修改密码
use test
db.updateUser(
"myTester",
{
pwd: passwordPrompt()
}
)
- 删除用户
use test
db.runCommand( {
dropUser: "myTester"
} )
- 创建超级用户
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 }
]
例子:创建角色,用户及视图,以便限制用户访问特定的数据
- 创建角色
Billing
和Provider
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: [ ] } )
- 创建用户
James
和Michelle
,并授予用户角色
db.createUser( {
user: "James",
pwd: "js008",
roles: [
{ role: "Billing", db: "test" }
]
} )
db.createUser( {
user: "Michelle",
pwd: "me009",
roles: [
{ role: "Provider", db: "test" }
]
} )
- 创建集合并插入数据
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"
}
] )
- 创建视图,使用
$$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
字段。
- 使用
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'
用户 James
有 Billing
角色,可以看到 creditCard
字段。
- 使用
Michelle
用户登录查询
db.auth( "Michelle", "me009" )
db.medicalView.find()
执行结果:
[
{ _id: 0, patientName: 'Jack Jones', diagnosisCode: 'CAS 17' },
{ _id: 1, patientName: 'Mary Smith', diagnosisCode: 'ACH 01' }
]
用户 Michelle
有 Provider
角色,可以看到 diagnosisCode
字段。
例子:将角色存储在文档中,以此判断用户是否可以访问
- 创建角色
db.createRole( { role: "Marketing", roles: [], privileges: [] } )
db.createRole( { role: "Sales", roles: [], privileges: [] } )
db.createRole( { role: "Development", roles: [], privileges: [] } )
db.createRole( { role: "Operations", roles: [], privileges: [] } )
- 创建用户并指定角色
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" }
]
} )
- 创建集合并插入数据
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
}
] )
- 创建视图
db.createView(
"budgetView", "budget",
[ {
$match: {
$expr: {
$not: {
$eq: [ { $setIntersection: [ "$allowedRoles", "$$USER_ROLES.role" ] }, [] ]
}
}
}
} ]
)
使用 $setIntersection
返回文档中 allowedRoles
和当前用户的角色 $$USER_ROLES
的交集,如果结果不为空,说明当前用户的角色满足该文档对角色的要求,则返回该文档。
- 使用
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
}
]
- 使用
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
}
]
例子:创建视图,角色和用户,将视图的查询权限授予给角色,将角色授予给用户
- 创建视图
db.createView(
"secondYears",
"students",
[ { $match: { year: 2 } } ]
)
- 创建角色
db.createRole(
{
role: "secondYearsRole",
privileges: [
{ resource: { db: "test", collection: "secondYears" }, actions: [ "find" ] }
],
roles: []
}
)
- 创建用户
db.createUser(
{
user: "secondYearsUser",
pwd: "sy001",
roles: [ { role: "secondYearsRole", db: "test" } ]
}
)
- 使用
secondYearsUser
用户登录查询
db.auth( "secondYearsUser", "sy001" )
db.secondYears.find()
执行结果:
[
{
_id: ObjectId("6544667149301affab2878c6"),
sID: 21001,
name: 'bernie',
year: 2,
score: 3.7
}
]
创建两个集合连接的视图
使用 $lookup
创建两个集合连接的视图。
创建 inventory
和 orders
集合:
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()
视图支持的操作
数据库命令:
mongosh
方法
按需物化视图
- 按需物化视图是预先计算的聚合管道结果,存储在磁盘上,通常是
$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") },
] );
- 定义按需物化视图,使用
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
进行匹配,如果存在则替换,如果不存在则插入。
- 执行初始化
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")
}
]
- 集合
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
)。从节点复制主节点的操作日志,并将其异步应用于从节点。
可以将从节点配置为特殊用途:
- 优先级为 0(
priority 0
)的从节点,表示不能被选举为主节点的成员,例如处于较远的数据中心中的节点。
- 隐藏的从节点,表示对客户端程序不可见,例如用于备份的节点。隐藏节点必须始终是优先级为 0 的节点,因此不能成为主节点。
- 延迟的从节点,表示从节点的数据落后主节点指定时间。延迟节点必须是隐藏节点。
副本集的最低建议配置是一个主节点和两个从节点。副本集最多可以有 50 个成员节点,但只有 7 个成员有投票权。
如果主节点与其他节点之间超过 electionTimeoutMillis
(默认 10 秒)未通信后,复合条件的从节点将会被选举为主节点。
在选举完成前,副本集不能处理写操作,但可以继续提供查询操作。
副本集操作日志
操作日志(oplog)是一个特殊的固定集合,用于滚动记录数据库中的所有修改操作。从MongoDB 4.4 开始,支持以小时为单位指定操作日志保留时间,则 MongoDB 只会在操作日志达到最大配置大小且超过了保留时间才会被删除。
操作日志保存在 local.oplog.rs
集合中。首次启动副本集成员时,如果没有指定操作日志大小,则 MongoDB 会创建默认大小的操作日志。
Storage Engine | Default Oplog Size | Lower Bound | Upper Bound |
---|---|---|---|
In-Memory Storage Engine | 5% of physical memory | 50 MB | 50 GB |
WiredTiger Storage Engine | 5% of free disk space | 990 MB | 50 GB |
在大多数情况下,默认的操作日志大小就足够了。可以使用 oplogSizeMB
选项在创建操作日志前指定大小。首次启动副本集成员后,使用 replSetResizeOplog
管理命令更改操作日志大小。
使用 rs.printReplicationInfo()
查看操作日志状态,包括大小和时间范围。
副本集写入
副本集的 Write Concern 是指在写入操作返回前,需要确认写入成功的数据节点数量。
w: "majority"
表示需要大多数具有投票权利的数据节点确认,此为默认设置。对于 3 节点的副本集,需要 2 个节点确认,对于 5 节点的副本集,需要 3 个节点确认。可以使用wtimeout
指定超时时间。
w: 1
表示只需要主节点确认。
副本集读取
默认情况下,客户端从主节点读取,但也可以设置为从从节点读取。异步复制意味着从节点的数据有可能和主节点的数据不一致。
MongoDB 使用读取首选项(Read Preference)描述客户端如何路由读取操作到副本集成员。读取首选项包括读取首选项模式,读取首选项模式有以下几种:
Read Preference Mode | Description |
---|---|
primary | 默认模式,从主节点读取。包含读取操作的多文档事务必须使用 primary 。 |
primaryPreferred | 多数情况下从主节点读取,但如果主节点不可用,则从从节点读取。 |
secondary | 从从节点读取。 |
secondaryPreferred | 通常从从节点读取,如果副本集只剩一个主节点,则从主机点读取。 |
nearest | 根据指定的延迟阈值,从符合条件的节点中随机读取。 |
在从节点读取数据时,使用读取首选项的 maxStalenessSeconds
选项指定从节点与主节点之间的最大复制延迟,避免从不合适的从节点读取到太过时的数据。
可以将 maxStalenessSeconds
选项用于以下读取首选项模式:
当客户端使用 maxStalenessSeconds
选项为读操作选项节点时,通过对比主节点和从节点的最后一次写入时间来估计从节点的延迟,然后客户端会选项延迟小于或等于 maxStalenessSeconds
的从节点。
默认不使用 maxStalenessSeconds
选项,客户端不考虑主从之间的延迟。如果要使用,则必须指定 maxStalenessSeconds
大于 90 秒。
部署副本集
部署 3 节点副本集,环境如下:
No. | Hostname | IP | Role |
---|---|---|---|
1 | mongodb0.stonecoding.net | 192.168.92.150 | Primary |
2 | mongodb1.stonecoding.net | 192.168.92.151 | Secondary |
3 | mongodb2.stonecoding.net | 192.168.92.152 | Secondary |
参考部署在 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()
删除副本集成员:
- 关闭要删除成员的
mongod
实例
[root@mongodb2 ~]# systemctl stop mongod
- 使用
db.hello()
确认主节点后,连接到主节点
[root@mongodb0 ~]# mongosh
- 在主节点删除成员
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()
添加副本集成员:
- 启动要添加成员的
mongod
实例
[root@mongodb2 ~]# systemctl start mongod
- 使用
db.hello()
确认主节点后,连接到主节点
[root@mongodb0 ~]# mongosh
- 在主节点添加成员
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
,范围为 0
到 1000
。0
表示不参被选举为主节点,隐藏节点和延迟节点的 priority
为 0
。
优先级 members[n].priority
和投票权 members[n].votes
有以下关系:
- 无投票权(即
votes
为0
) 的成员的priority
必须为0
。 priority
大于为0
的成员的votes
不能为0
。
- 将副本集配置分配给变量
cfg = rs.conf()
- 修改每个成员的优先级值
cfg.members[0].priority = 0.5
cfg.members[1].priority = 2
cfg.members[2].priority = 2
- 应用新配置
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].priority
为 0
及 members[n].hidden
为 true
。
在主节点执行:
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].priority
为 0
,members[n].hidden
为 true
以及 members[n].secondaryDelaySecs
为延迟的秒数。延迟秒数应该小于操作日志保留时间。
在主节点执行:
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].votes
和 members[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
实例。
- 停止从节点
db.shutdownServer()
或者:
[root@mongodb2 ~]# systemctl stop mongod
- 在另一个端口以独立运行方式重新启动从节点
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
- 维护完成后以副本集成员身份重新启动
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
- 对所有从节点完成维护操作后,最后对主节点进行维护,先在主节点执行
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
:从节点
- 连接到
mongodb2.stonecoding.net
,将其冻结以防止其在 120 秒内尝试成为主节点
rs.freeze(120)
- 连接到
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
选项。
- 备份可用成员。
- 连接到可用成员,保存当前配置
cfg = rs.conf()
printjson(cfg)
- 从副本集中移除不可用成员,即设置
members
为可用成员
cfg.members = [cfg.members[0] , cfg.members[4] , cfg.members[7]]
- 使用
rs.reconfig()
方法的force
选项进行配置,强制可用成员使用新配置,选出新的主节点。
rs.reconfig(cfg, {force : true})
- 尽快关闭或停用已删除成员。
管理链式复制
从 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 秒)参数值以下。
在主节点使用以下命令查看流控状态:
- 使用
rs.printSecondaryReplicationInfo()
查看复制延迟
rs.printSecondaryReplicationInfo()
- 查看流控是否正在运行
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 小时。
副本集成员状态
副本集成员的状态有:
Number | Name | State Description |
---|---|---|
0 | STARTUP | Not yet an active member of any set. All members start up in this state. The mongod parses the replica set configuration document while in STARTUP . |
1 | PRIMARY | The member in state primary is the only member that can accept write operations. Eligible to vote. |
2 | SECONDARY | A member in state secondary is replicating the data store. Eligible to vote. |
3 | RECOVERING | Members either perform startup self-checks, or transition from completing a rollback or resync. Data is not available for reads from this member. Eligible to vote. |
5 | STARTUP2 | The member has joined the set and is running an initial sync. Not eligible to vote. |
6 | UNKNOWN | The member's state, as seen from another member of the set, is not yet known. |
7 | ARBITER | Arbiters do not replicate data and exist solely to participate in elections. Eligible to vote. |
8 | DOWN | The member, as seen from another member of the set, is unreachable. |
9 | ROLLBACK | This member is actively performing a rollback. 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 ROLLBACK state. |
10 | REMOVED | This member was once in a replica set but was subsequently removed. |
分片
MongoDB 使用分片(Sharding)进行水平扩展,支持对大数据集和高吞吐量操作。
概述
分片集群
分片集群(Sharded Cluster)由以下组件组成:
- 分片( shard):每个分片包含分片数据的子集。每个分片都可以部署为一个副本集。
mongos
:作为查询路由器,在客户端应用程序和分片集群之间提供接口。- 配置服务器(Config servers):存储集群的元数据和配置。
各组件交互如下:
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,但可以根据需求进行调整。较小的块大小会导致更频繁的数据迁移,但数据分布更均匀;而较大的块大小则会减少数据迁移的频率,但可能导致数据分布的不均衡。块的大小会影响每个要迁移的块的最大文档数,以及最大集合大小。
分片与非分片集合
数据库可以混合使用分片集合和非分片集合。分片集合在集群分片中分区和分布,非分片集合存储在主分片上。每个数据库都有自己的主分片。
连接到分片集群
必须连接到 mongos
路由器才能与分片集群中的集合进行交互,包括分片和未分片的集合。客户端不要连接到单个分片执行读取或写入操作。
分片策略
MongoDB 支持两种分片策略:
- 哈希分片(Hashed Sharding):根据分片键字段值的哈希值进行分片,数据均匀分布在分片上,不利于对分片键执行范围查询。
- 范围分片(Ranged sharding):根据分片键值将数据划分为多个范围,为每个区块分配一个范围。相近的值更可能位于同一分片上,利于对分片键执行范围查询。默认的分片策略。
分片集群组件
MongoDB 分片集群有以下组件:
- 分片( shard):每个分片包含分片数据的子集。从 MongoDB 3.6 开始,必须将分片部署为副本集。
- mongos:作为查询路由器,在客户端应用程序和分片集群之间提供接口。
- 配置服务器(Config servers):存储集群的元数据和配置。从 MongoDB 3.4 开始,配置服务器必须部署为副本集 。
对于生产分片群集部署,建议:
- 将配置服务器部署为 3 个成员的副本集。
- 将每个分片部署为 3 个成员的副本集。
- 部署一个或多个
mongos
路由器。
对于开发和测试分片集群,可以部署具有最少组件数的分片集群,包括:
- 一个
mongos
实例。 - 单个分片副本集。
- 配置服务器副本集。
分片
- 分片群集中的每个数据库都有一个主分片(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
实例则将查询广播到集合的所有分片,此时性能较差。
- 如果查询包含分片键或复合分片键前缀,
mongos
可以将查询定位到特定分片以提高性能。所有insertOne()
操作定位到一个分片,所有updateOne()
,replaceOne()
和deleteOne()
操作都必须包含分片键或_id
。
- 分片集群支持基于角色的访问控制(RBAC),必须使用
--auth
选项启动集群中每个mongod
服务器(包括配置服务器),启用 RBAC 后,客户端在连接时必须指定--username
、--password
和--authenticationDatabase
才能访问群集资源。 - 从 MongoDB 4.2 开始,增加了参数
ShardingTaskExecutorPoolReplicaSetMatching
以确定mongod
/mongos
实例到分片集群各个成员的连接池最小值。
分片键
分片键可以是单个索引字段,也可以是在复合索引中的多个字段,以确定集合文档在集群分片中的分布。
MongoDB 将整个分片键值(或哈希分片键值)的范围划分为不重叠的分片键值(或哈希分片键值)范围,每个范围都与一个块相关联,并尝试在集群的分片中均匀分布这些块。
分片集合必须有以分片键开头的索引,既可以是单字段索引,也可以是复合索引。
- 如果集合为空且还没有分片键索引,
sh.shardCollection()
则会在分片键上创建索引。 - 如果集合不为空,则需要在使用
sh.shardCollection()
之前先创建索引。
如果索引是唯一支持分片键的非隐藏索引,则无法删除或隐藏索引。
MongoDB 可以对范围分片键索引强制实施唯一约束,对于范围分片集合,只有以下索引可以是唯一的:
- 分片键上的索引。
- 分片键为前缀的复合索引。
- 如果
_id
字段不是分片键或分片键的前缀时,_id
索引仅会强制执行每个分片的唯一约束,而不是所有分片。
例如对于使用分片键为 {x: 1}
的分片集合,包含分片 A 和分片 B,由于 _id
不是分片键的一部分, 则该集合既可以在分片 A 中包含 _id
值为 1
的文档,也可以在分片 B 中包含 _id
值为 1
的文档,此时需要在应用程序中保障 _id
值的唯一性。
唯一索引约束意味着:
- 对于待分片集合,如果该集合有其他唯一索引,则无法对该集合进行分片。
- 对于已分片的集合,不能在其他字段上创建唯一索引。
- 对于缺少被索引字段的文档,该字段存储为 NULL 值。
将 unique
为 true
(默认为 false
)传递给 sh.shardCollection()
,为分片键增加唯一约束:
- 如果集合为空,
sh.shardCollection()
则在分片键上创建唯一索引。 - 如果集合不为空,则需要在使用
sh.shardCollection()
之前先创建索引。
不能对哈希索引指定唯一约束。
从 4.4 版本开始,分片集合中的文档可以没有分片键字段。在分片之间分发文档时,缺少的分片键字段会被视为具有 NULL 值。
Document Missing Shard Key | Falls 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
namespace | Specify the full namespace of the collection that you want to shard ( "<database>.<collection>" ). |
key | Specify a document { <shard key field1>: <1|"hashed">, ... } where1 indicates range-based sharding"hashed" indicates hashed sharding. |
缺少分片键字段:
- 从 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
值不是全局唯一的,则重新分片操作将失败。
步骤如下:
- 启动重新分片操作,连接到
mongos
时,使用reshardCollection
命令,指定要重新分片的集合和新的分片键:
db.adminCommand({
reshardCollection: "<database>.<collection>",
key: <shardkey>
})
- 监视重新分片操作:
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>,
...
},
...
]
- 完成重新分片操作
{
ok: 1,
'$clusterTime': {
clusterTime: <timestamp>,
signature: {
hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
keyId: <number>
}
},
operationTime: <timestamp>
}
- 可以使用
commitReshardCollection
命令提前阻止写入,强制完成重新分片操作
db.adminCommand({
commitReshardCollection: "<database>.<collection>"
})
- 如果长时间没有完成,可以中止重新分片操作
db.adminCommand({
abortReshardCollection: "<database>.<collection>"
})
更改文档的分片键值
从 MongoDB 4.2 开始,可以更改文档的分片键值,除非分片键字段是不可变的 _id
字段。
更改分片键值时,需要注意:
- 必须使用
mongos
,不要直接在分片上操作。 - 必须在事务中运行或作为可重试写入(Retryable Write)运行。
- 必须在查询过滤器中的完整分片键上包含相等条件。例如
messages
集合使用{ activityid: 1, userid : 1 }
作为分片键,若要更新文档的分片键值,必须在查询过滤器中包含activityid: <value>, userid: <value>
。您可以根据需要在查询中包含其他字段。
要更新分片键值,使用以下操作:
Command | Method |
---|---|
update with multi: false | db.collection.replaceOne() db.collection.updateOne() To set to a non- null value, the update must be performed either inside a transaction or as a retryable write. |
findAndModify | db.collection.findOneAndReplace() db.collection.findOneAndUpdate() db.collection.findAndModify() To set to a non- null value, the update must be performed either inside a transaction or as a retryable write.db.collection.bulkWrite() Bulk.find.updateOne() 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
,对于字段 _id
为 12345
和 location
为 ""
的文档,使用以下命令更新字段值:
db.sales.updateOne(
{ _id: 12345, location: "" },
{ $set: { location: "New York"} }
)
设置缺少的分片键字段
如果缺少分片键字段,可以将分片键字段设置为 null
。如果要将缺少的分片键字段设置为非空值,参考 Change a Document's Shard Key Value
可以在 mongos
使用以下命令和方法进行修改:
Command | Method |
---|---|
update withmulti: true | db.collection.updateMany() |
其中:
- 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.
Command | Method |
---|---|
update withmulti: false | db.collection.replaceOne() db.collection.updateOne() |
其中:
- 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. - For additional requirements, refer to the specific command/method.
Command | Method |
---|---|
findAndModify | db.collection.findOneAndReplace() db.collection.findOneAndUpdate() db.collection.findAndModify() |
其中:
- 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 isnull
, include additional query conditions as appropriate. - To set to a non-
null
value, refer to Change a Document's Shard Key Value. - For additional requirements, refer to the specific command/method.
Command | Method |
---|---|
findAndModify | db.collection.bulkWrite() Bulk.find.replaceOne() Bulk.find.updateOne() Bulk.find.update() |
其中:
- 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. - 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
更可能执行广播操作来完成范围查询。
复合哈希索引分片
MongoDB 4.4 可以使用单个哈希字段创建复合索引。在创建索引时指定 hashed
为任何单个索引键的值来创建复合哈希索引。
复合哈希索引计算索引中单个字段的哈希值,此值与索引中的其他字段一起用作分片键。
为防止冲突,请勿将 hashed
索引用于无法可靠地转换为 64 位整数的浮点数。且 hashed
索引不支持大于 253 的浮点值。
哈希分片键
作为哈希分片键的字段应具有大量不同的值,例如 ObjectId 或者时间戳,建议使用默认 _id
字段。
哈希分片与范围分片
假设集合使用单调递增字段 X
作为分片键,使用范围分片,插入数据后分布如下:
由于 X
单调递增,大多数数据都写入到了上限为 MaxKey
块,导致数据分片不均。
使用哈希分片,插入数据后分布如下:
此时数据均匀分布在整个集群中。
对集合进行分片
使用 sh.shardCollection()
方法,指定集合的完整命名空间和作为分片键的哈希索引。
sh.shardCollection( "database.collection", { <field> : "hashed" } )
若要对复合哈希索引上的集合进行分片,指定集合的完整命名空间和作为分片键的复合哈希索引。
sh.shardCollection(
"database.collection",
{ "fieldA" : 1, "fieldB" : 1, "fieldC" : "hashed" }
)
范围分片
范围分片(默认)将数据划分为由分片键值确定的连续范围,相近分片键值的文档可能位于同一块或分片中,有利于进行范围查询。
分片键选择
适用于范围分片的分片键应该:
- 分片键的基数大,即具有大量不同的值
- 分片键值出现的频率低
- 非单调变化的键值
对集合进行分片
使用 sh.shardCollection()
方法,指定集合的完整命名空间和作为分片键的索引或者复合索引。
sh.shardCollection( "database.collection", { <shard key> } )
部署分片集群
分片集群包括一个 mongos
,一个配置服务器副本集和两个分片副本集。
No. | Hostname | IP | Role |
---|---|---|---|
1 | mongos.stonecoding.net | 192.168.92.170 | mongos |
2 | cfg1.stonecoding.net | 192.168.92.171 | Config server |
3 | cfg2.stonecoding.net | 192.168.92.172 | Config server |
4 | cfg3.stonecoding.net | 192.168.92.173 | Config server |
5 | s1rs1.stonecoding.net | 192.168.92.174 | Shard Replica Set 1 |
6 | s1rs2.stonecoding.net | 192.168.92.175 | Shard Replica Set 1 |
7 | s1rs3.stonecoding.net | 192.168.92.176 | Shard Replica Set 1 |
8 | s2rs1.stonecoding.net | 192.168.92.177 | Shard Replica Set 2 |
9 | s2rs2.stonecoding.net | 192.168.92.178 | Shard Replica Set 2 |
10 | s2rs3.stonecoding.net | 192.168.92.179 | Shard Replica Set 2 |
参考部署在 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: []
}
}
}
]