MongoDB 入门与常用操作

本文整合了 MongoDB 的入门笔记:先理清文档、集合、数据库三级概念,再在 mongo shell 中完整走一遍增删改查,随后给出对照 SQL 的常用命令速查表,最后用 Python 的 pymongo 完成同样的操作,并附上批量生成测试数据的脚本。

1. 概念与安装

MongoDB 是文档型数据库,数据按「文档 → 集合 → 数据库」三级组织,理解这三个概念之后,后面的命令基本都能对号入座。

文档

  1. 形如 {"foo": 3, "greeting": "Hello world!"} 的一组键值对,是 MongoDB 中最小的数据单元
  2. 区分大小写
  3. 键在同一文档内唯一
  4. 值可以继续嵌套文档
  5. 键值对是有序的

文档相当于关系型数据库中的一行记录,但它不需要事先定义结构。

集合

  1. 集合就是一组文档
  2. 文档类似关系库里的行,集合类似关系库里的表
  3. 集合中的文档无需固定的结构

数据库

  1. 多个文档组成集合,多个集合组成数据库
  2. 一个实例可以承载多个数据库(即一个 MongoDB 可以有多个数据库)
  3. 每个数据库都有独立的权限
  4. adminlocalconfig 是保留的数据库名称

安装与连接

安装可以直接使用发行版的包管理器或官方仓库提供的 mongodb 包,装好后用客户端连接:

1
2
mongo                       # 连接本机数据库
mongo <MONGO_HOST>:27017 # 连接指定主机的数据库

⚠️ 注:官方客户端自 4.4 起逐步由 mongo 迁移到 mongosh,旧的 mongo shell 已在 6.0 中移除。本文的语句在两者中基本通用,只有个别旧 API 有差异,遇到时会单独提示。

mongo shell 兼容 JavaScript

mongo shell 本身就是一个 JavaScript 解释器,可以直接做运算、定义函数,这也是后面能用变量承接查询结果的原因:

1
2
3
4
5
6
7
> 1+2
3
> function add(r1,r2){
... return r1 + r2;
... }
> add(8,2)
10

2. 库、集合与文档的基本操作

查看与切换数据库

1
2
3
4
5
6
7
> show dbs
admin 0.078GB
local 0.078GB
> use students // 切换到 students 库,库不存在时会在首次写入数据时创建
switched to db students
> db // 查看当前数据库
students

新增数据

常用的三个写入方法:db.collection.insert()db.collection.insertOne()db.collection.insertMany()

1
2
3
4
5
6
> stu = { name : 'weilai', age : 22 }
{ "name" : "weilai", "age" : 22 }
> db.students.insert(stu)
WriteResult({ "nInserted" : 1 })
> db.students.insert({ "name" : "weilai1", "age" : 22, "sex": "male" })
WriteResult({ "nInserted" : 1 })

批量写入用 insertMany(),传入一个文档数组,返回所有新文档的 _id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> use test
switched to db test
> db.students.insertMany(
... [{"name": "测试-0", "age": 63, "sex": "female", "grade": 1},{"name": "测试-1", "age": 32, "sex": "male", "grade": 7},{"name": "测试-2", "age": 20, "sex": "female", "grade": 8},{"name": "测试-3", "age": 55, "sex": "female", "grade": 58},{"name": "测试-4", "age": 84, "sex": "male", "grade": 65},{"name": "测试-5", "age": 27, "sex": "female", "grade": 57},{"name": "测试-6", "age": 86, "sex": "female", "grade": 61},{"name": "测试-7", "age": 18, "sex": "female", "grade": 93},{"name": "测试-8", "age": 50, "sex": "female", "grade": 71},{"name": "测试-9", "age": 81, "sex": "male", "grade": 42},{"name": "测试-10", "age": 26, "sex": "male", "grade": 25},{"name": "测试-11", "age": 91, "sex": "female", "grade": 14},{"name": "测试-12", "age": 6, "sex": "female", "grade": 54},{"name": "测试-13", "age": 73, "sex": "male", "grade": 24},{"name": "测试-14", "age": 51, "sex": "male", "grade": 14},{"name": "测试-15", "age": 53, "sex": "female", "grade": 19},{"name": "测试-16", "age": 3, "sex": "female", "grade": 48},{"name": "测试-17", "age": 13, "sex": "male", "grade": 11},{"name": "测试-18", "age": 40, "sex": "female", "grade": 97},{"name": "测试-19", "age": 97, "sex": "male", "grade": 96}])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5de8b35c75deb540ecfc5332"),
ObjectId("5de8b35c75deb540ecfc5333"),
ObjectId("5de8b35c75deb540ecfc5334"),
ObjectId("5de8b35c75deb540ecfc5335"),
ObjectId("5de8b35c75deb540ecfc5336"),
ObjectId("5de8b35c75deb540ecfc5337"),
ObjectId("5de8b35c75deb540ecfc5338"),
ObjectId("5de8b35c75deb540ecfc5339"),
ObjectId("5de8b35c75deb540ecfc533a"),
ObjectId("5de8b35c75deb540ecfc533b"),
ObjectId("5de8b35c75deb540ecfc533c"),
ObjectId("5de8b35c75deb540ecfc533d"),
ObjectId("5de8b35c75deb540ecfc533e"),
ObjectId("5de8b35c75deb540ecfc533f"),
ObjectId("5de8b35c75deb540ecfc5340"),
ObjectId("5de8b35c75deb540ecfc5341"),
ObjectId("5de8b35c75deb540ecfc5342"),
ObjectId("5de8b35c75deb540ecfc5343"),
ObjectId("5de8b35c75deb540ecfc5344"),
ObjectId("5de8b35c75deb540ecfc5345")
]
}

这 20 条测试文档是用一小段 Python 脚本随机生成的,脚本见文末「批量生成测试数据」一节,下面的查询与更新示例都基于这批数据。

查询数据

db.collection.find() 返回全部匹配文档,db.collection.findOne() 只返回第一条:

1
2
3
4
5
6
7
8
9
10
11
12
13
> db.students.find()
{ "_id" : ObjectId("5de8b35c75deb540ecfc5332"), "name" : "测试-0", "age" : 63, "sex" : "female", "grade" : 1 }
{ "_id" : ObjectId("5de8b35c75deb540ecfc5333"), "name" : "测试-1", "age" : 32, "sex" : "male", "grade" : 7 }
{ "_id" : ObjectId("5de8b35c75deb540ecfc5334"), "name" : "测试-2", "age" : 20, "sex" : "female", "grade" : 8 }
...(其余文档省略,数据太多时输入 it 回车翻页)
> db.students.findOne() // 查询第一条数据
{
"_id" : ObjectId("5de8b35c75deb540ecfc5332"),
"name" : "测试-0",
"age" : 63,
"sex" : "female",
"grade" : 1
}

find() 的第一个参数是查询条件,第二个参数是投影(字段为 1 表示返回,0 表示不返回):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> db.students.find({sex:'male'},{age:1,name:1,_id:0})     // 只看 male,且只返回 name 与 age
{ "name" : "测试-1", "age" : 32 }
{ "name" : "测试-4", "age" : 84 }
{ "name" : "测试-9", "age" : 81 }
{ "name" : "测试-10", "age" : 26 }
{ "name" : "测试-13", "age" : 73 }
{ "name" : "测试-14", "age" : 51 }
{ "name" : "测试-17", "age" : 13 }
{ "name" : "测试-19", "age" : 97 }
> db.students.find({grade:{'$gte':60}},{_id:0}) // grade 大于等于 60
{ "name" : "测试-4", "age" : 84, "sex" : "male", "grade" : 65 }
{ "name" : "测试-6", "age" : 86, "sex" : "female", "grade" : 61 }
{ "name" : "测试-7", "age" : 18, "sex" : "female", "grade" : 93 }
{ "name" : "测试-8", "age" : 50, "sex" : "female", "grade" : 71 }
{ "name" : "测试-18", "age" : 40, "sex" : "female", "grade" : 97 }
{ "name" : "测试-19", "age" : 97, "sex" : "male", "grade" : 96 }
> db.students.find({'$or':[{sex:'female',age:18},{sex:'male',age:81}]},{_id:0}) // 两组条件取或
{ "name" : "测试-7", "age" : 18, "sex" : "female", "grade" : 93 }
{ "name" : "测试-9", "age" : 81, "sex" : "male", "grade" : 42 }

排序用 sort()1 为升序、-1 为倒序:

1
2
3
4
5
6
7
8
9
10
> db.students.find({},{_id:0}).sort({age:1})     // 不显示 _id,按 age 升序
{ "name" : "测试-16", "age" : 3, "sex" : "female", "grade" : 48 }
{ "name" : "测试-12", "age" : 6, "sex" : "female", "grade" : 54 }
{ "name" : "测试-17", "age" : 13, "sex" : "male", "grade" : 11 }
...(其余文档省略)
> db.students.find().sort({age:-1}) // 按 age 倒序
{ "_id" : ObjectId("5de8b35c75deb540ecfc5345"), "name" : "测试-19", "age" : 97, "sex" : "male", "grade" : 96 }
{ "_id" : ObjectId("5de8b35c75deb540ecfc533d"), "name" : "测试-11", "age" : 91, "sex" : "female", "grade" : 14 }
{ "_id" : ObjectId("5de8b35c75deb540ecfc5338"), "name" : "测试-6", "age" : 86, "sex" : "female", "grade" : 61 }
...(其余文档省略)

修改数据

db.collection.update() 的第二个参数如果直接给一个文档,是整文档替换而不是只改其中一个字段:

1
2
3
4
5
> db.students.update({name:'weilai'},{name:'cool'})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.students.find()
{ "_id" : ObjectId("5de8a491ff626673ec5503c9"), "name" : "cool" } // age、sex 一起被替换掉了
{ "_id" : ObjectId("5de8a554ff626673ec5503ca"), "name" : "weilai1", "age" : 22, "sex" : "male" }

想保留其他字段,一种笨办法是先取出文档、改完再整体写回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> s_obj = db.students.findOne({'name':'weilai1'})
{
"_id" : ObjectId("5de8a554ff626673ec5503ca"),
"name" : "weilai1",
"age" : 22,
"sex" : "male"
}
> s_obj.name = 'amy'
amy
> db.students.update({name:'weilai1'},s_obj)
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.students.find()
{ "_id" : ObjectId("5de8a491ff626673ec5503c9"), "name" : "cool" }
{ "_id" : ObjectId("5de8a554ff626673ec5503ca"), "name" : "amy", "age" : 22, "sex" : "male" }

更常规的做法是用修改器:$set 设置字段值,$inc 让数值字段自增;默认只改第一条匹配的文档,加上 {multi:true} 才会改全部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> db.students.update({},{'$set':{class:'class 1'}})               // 仅修改第一条数据
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.students.update({},{'$set':{class:'class 2'}},{multi:true}) // 全部文档都加上 class 字段
WriteResult({ "nMatched" : 20, "nUpserted" : 0, "nModified" : 20 })
> db.students.update({},{'$inc':{age:1}},{multi:true}) // 所有人的 age 加 1
WriteResult({ "nMatched" : 20, "nUpserted" : 0, "nModified" : 20 })
> db.students.update({sex:'male'},{'$inc':{grade:-1}},{multi:true}) // male 的 grade 减 1
WriteResult({ "nMatched" : 8, "nUpserted" : 0, "nModified" : 8 })
> db.students.find({},{_id:0})
{ "name" : "测试-0", "age" : 64, "sex" : "female", "grade" : 1, "class" : "class 2" }
{ "name" : "测试-1", "age" : 33, "sex" : "male", "grade" : 6, "class" : "class 2" }
{ "name" : "测试-2", "age" : 21, "sex" : "female", "grade" : 8, "class" : "class 2" }
{ "name" : "测试-3", "age" : 56, "sex" : "female", "grade" : 58, "class" : "class 2" }
{ "name" : "测试-4", "age" : 85, "sex" : "male", "grade" : 64, "class" : "class 2" }
...(其余文档省略)

回显里 nMatchednModified 是两个数:前者是条件匹配到的条数,后者是真正被改动的条数。如果 $set 写进去的值和原值完全一样,这条文档只计入 nMatched 而不计入 nModified。上面第二条命令把第一个文档的 class 1 改成了 class 2、其余 19 个文档是新增字段,20 条都发生了实际变化,所以两个数相等;要是把值也写成 class 1,就会看到 nMatched: 20, nModified: 19

删除数据

1
2
3
4
5
6
> db.students.remove({name:'cool'})     // 删除匹配的数据
WriteResult({ "nRemoved" : 1 })
> db.students.remove({}) // 删除集合内所有数据
WriteResult({ "nRemoved" : 1 }) // 上一条已经删掉 cool,集合里只剩 amy 一条
> db.students.find()
>

3. 常用命令速查

这一节按 SQL 的习惯把命令归了类,方便查阅。

数据库

1
2
3
4
5
6
7
8
9
10
db / db.getName()      // 查看当前数据库(db 就是 Database)
use demo // 切换或创建数据库(demo 为数据库名)
show dbs // 查看所有数据库
show tables // 查看所有集合
db.dropDatabase() // 删除当前操作的数据库
db.version() // 当前 db 版本
db.stats() // 显示当前 db 状态
db.getMongo() // 查看当前 db 的连接机器地址
db.copyDatabase("mydb", "temp", "127.0.0.1") // 把指定机器上的 mydb 复制到 temp 数据库
db.repairDatabase() // 修复当前数据库

⚠️ 注:db.copyDatabase()db.repairDatabase() 已在 MongoDB 4.2 移除,新版本中请分别改用 mongodump/mongorestore 迁移数据、用 mongod --repair 修复数据文件。

集合

1
2
3
4
5
6
db.createCollection("collName")   // 创建普通集合,不带参数就够了
// 创建固定集合(capped collection):写满后自动覆盖最旧的文档
db.createCollection("logs", {capped: true, size: 1048576, max: 1000})
db.getCollection("table") // 得到指定名称的集合
db.getCollectionNames() // 查询当前 db 的所有集合名
db.printCollectionStats() // 显示当前 db 所有集合索引的状态

固定集合这三个参数容易写错:capped 是布尔值而不是数字;size 是这个集合的字节上限(上面 1048576 即 1MB,给太小的值会被抬到 mongod 的最小值),而且开 capped 时它是必填的;max 是文档条数上限,只在 capped: true 下才生效,两个上限哪个先到就先触发覆盖。

查询数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
db.table.find()                          // select * from table;  数据太多时输入 it 回车翻页
db.table.find().pretty() // 输出格式化
db.table.find({}, {id: 0, "name": 0}) // 查询所有数据,id 和 name 字段不返回
db.table.find({}, {id: 1, "name": 1}) // 查询所有数据,只返回 id 和 name 字段
db.table.distinct("name") // select distinct name from table;
db.table.find({"name": "Bill"}) // select * from table where name = 'Bill';
db.table.find({id: {$gt: 1}}) // select * from table where id > 1;
db.table.find({id: {$gt: 1, $lte: 5}}) // where id > 1 and id <= 5;
db.table.find({name: /li/}) // where name like '%li%';
db.table.find({name: /^li/}) // where name like 'li%';
db.table.find({name: /li$/}) // where name like '%li';
db.table.find({}, {name: 1, work: 1}) // select name, work from table;
db.table.find({id: {$gt: 3}}, {name: 1, work: 1}) // select name, work from table where id > 3;
db.table.find().sort({number: 1}) // 按 number 升序,-1 为降序
db.table.find().limit(3) // select top 3 * from table;
db.table.find().skip(3) // 跳过前三条
db.table.find().limit(5).skip(3) // 前三条之后的 5 条
db.table.find({$or: [{id: 2}, {id: 5}]}) // where id = 2 or id = 5;
db.table.findOne() // select top 1 * from table;
db.table.find({id: {$gte: 5}}).count() // select count(*) from table where id >= 5;
db.table.find({name: {$exists: true}}).count() // select count(name) from table;

比较与集合类操作符:$gt 大于、$lt 小于、$gte 大于等于、$lte 小于等于、$ne 不等于、$or 或、$in 包含、$nin 不包含。

索引

索引用于加快查找速度,主要是降低 CPU 成本消耗。索引与排序的区别在于:排序会重新排列原数据、改变原数据的排列顺序,而索引只建立一个顺序表,由这个顺序表指出数据的顺序,不改变原数据的排列顺序。

索引本身也分方向(建索引时字段写 1-1),但单字段索引的方向其实无所谓——B 树可以正着扫也可以倒着扫,{name: 1} 这一个索引同时能满足 sort({name: 1})sort({name: -1})。方向真正起作用的是复合索引:只有索引的方向组合与 sort() 完全一致、或者完全相反(每个字段都取反),才能直接用索引出顺序而免掉内存里的排序。比如 {name: 1, ts: -1} 这个索引能支撑 sort({name: 1, ts: -1})sort({name: -1, ts: 1}),但支撑不了 sort({name: 1, ts: 1})

MongoDB 支持的索引有:_id 索引(默认建立)、单键索引、多键索引、复合索引、过期索引、全文索引、地理位置索引。

1
2
3
4
5
6
7
db.table.ensureIndex({name: 1})           // 创建索引
db.table.ensureIndex({name: 1, ts: -1}) // 创建复合索引
db.table.getIndexes() // 查询当前集合所有索引
db.table.totalIndexSize() // 查看总索引记录大小
db.table.reIndex() // 重建当前集合的所有索引
db.table.dropIndex("name_1") // 删除指定索引
db.table.dropIndexes() // 删除所有索引

⚠️ 注:ensureIndex() 自 3.0 起已是 createIndex() 的别名,并在新版 shell 中被移除,新代码请直接使用 createIndex()

添加数据与导入

1
2
db.table.insert({})
db.table.save({})

导入整个数据文件用 mongoimport

1
2
3
4
5
mongoimport -d demo -c product --file D:\MongoDB\data\product.json --jsonArray
# -d 数据库
# -c 集合(表)名
# --file 数据文件地址
# --jsonArray 导入 json 文件专用,否则导入失败;json 文件格式:[{},{},{},{},{}]

修改数据

1
2
3
4
5
6
7
8
// update table set number = '111' where name = 'baiqi';  第一个 false 表示不新增数据,最后的 true 表示批量
db.table.update({name: "baiqi"}, {$set: {number: "111"}}, false, true)

// update table set number = number + 111, id = id + 2 where name = 'mayun';
db.table.update({name: "mayun"}, {$inc: {number: 111, id: 2}}, false, true)

// 两种修改器混用:update table set id = id + 3, number = 113 where name = 'liuqiangdong';
db.table.update({name: 'liuqiangdong'}, {$inc: {id: 3}, $set: {number: 113}}, false, true)

删除数据

1
db.table.remove({_id: ObjectId("59a67e103cbae75d282fe46b")})

其他

1
2
db.getPrevError()     // 查询之前的错误信息
db.resetError() // 清除错误记录

⚠️ 注:insert()update()remove() 属于早期 API,在新版本的 mongosh 与各语言驱动中已标记废弃,建议改用 insertOne()/insertMany()updateOne()/updateMany()deleteOne()/deleteMany()

4. 使用 pymongo 操作 MongoDB

Python 侧用官方驱动 pymongo,方法名与 shell 中的新版 API 一一对应(insert_onefind_oneupdate_many……),下面把增删改查封装成一个类便于对照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from pymongo import MongoClient
from dataclasses import dataclass
from bson.objectid import ObjectId


@dataclass
class Test_Mongo():
client:dict = MongoClient()
db:dict = client['test'] # 连接到 test 数据库

def add_one(self):
post = {
'name': 'justin',
'age': 18,
'sex': 'male',
'grade': 80
}
return self.db.students.insert_one(post)

def add_many(self):
return self.db.students.insert_many([{'name':i} for i in range(0,10)])


def get_one(self):
return self.db.students.find_one({'name':'justin'})

def get_more(self):
return self.db.students.find({'sex':'male'})

def get_one_from_oid(self, oid):
obj = ObjectId(oid)
return self.db.students.find_one({'_id': obj})

def get_count(self):
return self.db.students.estimated_document_count()


def update_one(self):
return self.db.students.update_one({'name':'justin'},{'$inc':{'age':10}})

def update_many(self):
return self.db.students.update_many({},{'$inc':{'age':5}})

def delete_one(self):
return self.db.students.delete_one({'name':'justin'})

def delete_many(self):
return self.db.students.delete_many({'name':'justin'})


def main():
obj = Test_Mongo()
print(obj.add_one().inserted_id)
print(obj.get_one())
print(obj.add_many().inserted_ids)
print(obj.get_count())
for item in obj.get_more():
print(item['_id'])
print(obj.get_one_from_oid(obj.add_one().inserted_id))
print(obj.update_one().matched_count)
print(obj.update_one().matched_count)
print(obj.update_many().matched_count)
print(obj.update_many().matched_count)
print(obj.delete_one().deleted_count)
print(obj.delete_many().deleted_count)


if __name__ == '__main__':
main()

几个返回值的用法值得留意:插入单条后用 inserted_id 拿到新文档的 _id,批量插入用 inserted_ids;更新看 matched_count,删除看 deleted_count;按 _id 查询时字符串要先用 ObjectId() 包一层。

5. 批量生成测试数据

第 2 节 insertMany() 用到的 20 条文档就是这段脚本输出的:用 random 随机生成字段,再用 json 拼成 [{},{}...] 形式的数组,可以直接粘进 insertMany(),也可以存成文件交给 mongoimport --jsonArray 导入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import random
import json

a = {
'name' : 'weilai',
'age' : 18,
'sex': 'male',
'grade' : 45
}
list_json = []
for i in range(0,20):
a['name'] = f"测试-{i}"
a['age'] = random.randint(0, 100)
a['sex'] = random.choice(['male', 'female'])
a['grade'] = random.randint(0, 100)
j = json.dumps(a, ensure_ascii=False)
list_json.append(j)

list_json = ','.join(list_json)
list_json = '[' + list_json +']'
print(list_json)