MongoDB入门
- MongoDB 是一个开源的 NoSQL 数据库,用「文档」而不是「行」来存数据。文档长得像 JSON 对象,二进制存储格式叫 BSON(Binary JSON),比 JSON 多了日期、ObjectId、二进制、Decimal128 这些类型。
- 它和关系库最大的差别是没有固定的表结构:同一个集合里的文档可以有不同字段、不同类型,加字段不用
ALTER TABLE。代价是跨文档的一致性和关联查询要自己设计。
- MongoDB 用 C++ 编写,扩展能力靠驱动(Python、Java、Node.js、Go、C# 等官方与社区驱动都有)。
- 本文全部实测环境:WSL2 的 Ubuntu 26.04.1 + MongoDB 9.0.4(官方 apt 源),shell 是 mongosh 2.14.1,Python 驱动是 pymongo 4.19.0,客户端从 Windows 走
127.0.0.1:27017(WSL 的端口转发)。 - 版本口径:为什么 9.0 就是「长期维护的最新版」。MongoDB 官方的 Software Lifecycle Schedules 并不使用 LTS 这个词——它只给每个大版本标
Release Date与End of Life Date。现役几条(照抄那张表):版本 发布 End of Life MongoDB 9.0 September 2026 October 31, 2031 MongoDB 8.3 May 2026 October 31, 2029 MongoDB 8.0 October 2024 October 31, 2029 MongoDB 7.0 August 2023 August 31, 2027 - 从 8.0 起每条大版本的周期都是发布后 5 年的 10 月 31 日收尾。
- 网上(含第三方 EOL 站)常把 MongoDB 的大版本叫成 LTS,这是借用说法;官方文档里没有这个标签,写工单/评估报告时最好直接引用 EOL 日期而不是「LTS」。
- 还有一条容易被忽略的官方条款:
MongoDB Drivers released more than 3 years after a MongoDB Server version's end-of-life date will not be compatible with that server version.——选 server 版本要连带看驱动的支持矩阵,比如 pymongo 4.19.0 支持 9.0,而某个在 2034 年后发布的驱动就不会再兼容 9.0。
- 一个必须先说清的事:老的
mongoshell 在 MongoDB 6.0 起就被彻底移除了,现在只有mongosh。网上大量教程(包括本篇原文)里那些db.coll.find().count()、db.coll.remove()、db.coll.save()、_id: ObjectId(...)手写插入的写法,在 mongosh 里要么报警告要么直接报错。本文按 mongosh 的真实行为重写。
核心特点
- 文档导向:基本单位是文档(BSON),可以嵌套数组和子文档,读写直观,一次读就能拿到整个对象,不用多表 JOIN 拼回来。
- 动态模式:集合不预设字段约束。数据结构变化频繁的场景(爬虫、用户行为、商品属性)特别省事;但「没有约束」也意味着脏数据能进来,必要时用
$jsonSchema校验规则补上。 - 可扩展:水平扩展靠分片(sharding),把集合按分片键拆到多个
mongod上;高可用靠副本集(replica set)。 - 丰富的查询:等值、范围、正则、数组成员、嵌套字段、地理空间,还有聚合管道做多级加工。
- 索引:单字段、复合、多键(数组)、文本、地理、哈希、通配符投影索引,
explain()能看是否命中。 - 聚合管道:
$match→$group→$project→$sort→$lookup这种流水线,等价于 SQL 的WHERE + GROUP BY + JOIN组合。 - 驱动支持广:官方与社区驱动覆盖主流语言。
- 服务端 JavaScript:
mongosh本身就是 JS 环境,还能存服务端函数(db.createSystemJsFunctions那套已废弃,现在用应用端逻辑或聚合表达式)。原文说的「支持 JavaScript 编写的存储函数」在 4.4 起已被移除,别再依赖db.eval()。 - 多种部署形态:本地、Docker、MongoDB Atlas(官方云)。
- 社区与生态:文档、博客、工具(Compass、mongodump 家族)都很成熟。
- 多文档 ACID 事务:4.0 起支持,但有前提——必须跑在副本集或分片集群上。本机是单机
mongod,实测启动事务直接失败:1
OperationFailure: Transaction numbers are only allowed on a replica set member or mongos
- 所以「MongoDB 支持事务」这句话面试时要补上条件:单机实例没有事务,只有单文档的原子性(对单个文档的多字段更新天生原子)。
- 另有能力:时序集合(
timeseries,实测可建可写)、全文搜索与向量检索($vectorSearch、Atlas Search,需要相应索引类型)、Change Streams(变更流,也要求副本集)。
- 关于本机这套环境的一个真实状态:
/etc/mongod.conf里没有security段,admin用户虽然建了,但鉴权并未开启,实测不带账号也能连、且拿到的是 root 权限。学习用没问题,一旦要放开bindIp,必须先补:再1
2security:
authorization: enabledsystemctl restart mongod,否则等于把库裸奔到网络上。
- 到 官方下载中心 选 Windows、选版本(ZIP 或 MSI)。
- 运行 MSI 安装程序,按提示走;选「Complete」会顺带装 MongoDB Compass。
- 安装时可以选择「Run as Windows Service」,它会注册名为
MongoDB的服务并自动启动;数据默认在C:\Program Files\MongoDB\Server\9.0\data\db,日志在同目录的log下。 - 如果装的是 ZIP:解压后要自己建数据目录(默认路径是
C:\data\db),然后手动启动:1
mongod --dbpath C:\data\db
- 把
bin目录加进系统Path,之后在任意目录能敲mongod、mongosh。 - 另开一个终端验证:
1
mongosh
1
2Test it with 'db.version()'
MongoDB 9.0.4 ...- 别再敲
mongo,那个命令早就没了。
- 别再敲
- Windows 上表名/库名不区分大小写这一套在 MongoDB 里同样成立(MongoDB 的集合名本来就区分大小写,与平台无关,这点和 MySQL 不同)。
- 用 Homebrew 官方的 tap(不要装 Homebrew 核心仓库里的
mongodb-community,那个 formula 已被移除):1
2
3brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community - 验证:
1
mongosh --eval "db.runCommand({ping: 1})"
- 配置文件在
/opt/homebrew/etc/mongod.conf(Intel 机器是/usr/local/etc/mongod.conf),数据目录在/opt/homebrew/var/mongodb。 - 停止服务:
brew services stop mongodb-community。
- ① 导入公钥。
https://pgp.mongodb.com/server-9.0.asc是 404,实测:1
curl -sI https://pgp.mongodb.com/server-9.0.asc | head -1
1
HTTP/2 404
- 照原文写会导致后面
apt update报NO_PUBKEY F366D55B602E502D。可用做法是从密钥服务器取,并且校验指纹再用:1
2
3
4gpg --keyserver hkps://keyserver.ubuntu.com \
--recv-keys B3B42B6C39E5CDDEC0A27E3CF366D55B602E502D
gpg --export B3B42B6C39E5CDDEC0A27E3CF366D55B602E502D \
| sudo tee /usr/share/keyrings/mongodb-server-9.0.gpg > /dev/null- 一定要比对完整指纹(40 位十六进制),只匹配后 16 位等于没校验。
- 照原文写会导致后面
- ② 写源文件。
$releasever用 Ubuntu 的代号,本机 26.04 是resolute,不是jammy:1
2
3echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-9.0.gpg] \
https://repo.mongodb.org/apt/ubuntu resolute/mongodb-org/9.0 multiverse" \
| sudo tee /etc/apt/sources.list.d/mongodb-org-9.0.list- 顺带一个坑:Ubuntu 26.04 上 8.0 那条线没有 server 包(
resolute/mongodb-org/8.0里只有 atlas/tools),要 9.0 才有mongodb-org-server。
- 顺带一个坑:Ubuntu 26.04 上 8.0 那条线没有 server 包(
- ③ 安装:
1
2sudo apt-get update
sudo apt-get install -y mongodb-orgmongodb-org是元包,会带上mongodb-org-server(mongod)、mongodb-mongosh、mongodb-org-mongos、mongodb-database-tools(mongodump那一族)。装完实测版本:1
2
3mongod --version | head -1
mongosh --version
mongodump --version1
2
3db version v9.0.4
2.14.1
mongodump version: 100.20.1- 注意工具的版本号体系和 server 不一样(工具是 100.x,这是 MongoDB Database Tools 自己的版本线)。
- ④ 启动与自启:
1
2
3sudo systemctl start mongod
sudo systemctl enable mongod
systemctl is-active mongod1
active
- ⑤ 连接:
1
mongosh
1
2
3
4Current MongoDB version: 9.0.4
...
test> db.runCommand({ping: 1})
{ ok: 1 }- 配置在
/etc/mongod.conf,实测关键项:1
2
3storage.dbPath: /var/lib/mongodb
net.bindIp: 127.0.0.1
net.port: 27017 - 默认只绑回环。要远程访问就改
bindIp: 0.0.0.0,但必须先开security.authorization: enabled,否则任何人都能读写你的库。 - 日志在
/var/log/mongodb/mongod.log;systemctl status mongod看不到日志内容,要journalctl -u mongod或看那个文件。
- 配置在
MySQL与 MongoDB 对比
| 概念 | MySQL(关系型) | MongoDB(文档型) |
|---|---|---|
| 数据库 | DATABASE |
DATABASE |
| 表 | TABLE(固定列) |
COLLECTION(无固定字段) |
| 一行记录 | ROW |
DOCUMENT(BSON) |
| 列 | COLUMN |
FIELD |
| 主键 | PRIMARY KEY |
_id(默认 ObjectId,唯一且不可变) |
| 表结构 | CREATE TABLE 定义 |
无模式;可选 $jsonSchema 校验器 |
| 关联 | JOIN |
$lookup(聚合管道里)或冗余嵌入 |
| 事务 | 完整 ACID | 多文档事务需副本集/分片;单机只有单文档原子性 |
| 索引 | B+ 树为主 | B 树、多键、文本、地理、哈希、通配符投影 |
| 大小写 | Linux 下表名区分(lower_case_table_names=0) |
集合名一律区分大小写 |
| 单条上限 | 行大小 65535 字节 | 文档 16 MB(实测 maxBsonObjectSize = 16777216) |
| 连接串 | mysql://user:pw@host:3306/db |
mongodb://user:pw@host:27017/db?authSource=admin |
- 选型上的经验法则:
- 数据结构稳定、强关联、要多表事务和复杂报表 → MySQL。
- 字段经常变、读多写多、内容天然成对象(商品、文章、事件、IoT) → MongoDB。
- 「既要关联又要灵活」的场景很常见,多数系统是两个都用,别为了纯粹而硬套一个。
- 一句常被误解的话:MongoDB 不是「不能做 JOIN」,而是
$lookup性能特征和关系库不同——它本质是子查询拼接,大表关联照样慢。设计时优先考虑嵌入(把一/多关系塞进同一文档),只有「一对多且会无限增长」才拆集合引用。
MongoDB 之 db 操作
-
创建数据库
- MongoDB 不需要显式建库:第一次向某个集合插入文档时才真正创建这个库。实测输出:
1
2db = db.getSiblingDB("myNewDatabase"); // 切换(不存在也切,只是还没落盘)
db.myNewCollection.insertOne({a: 1}); // 这一步才真的把库和集合建出来1
2
3
4
5{
acknowledged: true,
insertedId: ObjectId('6aea00b975b57147bab77331')
}
当前库 -> mongo_note use的坑:在mongosh --eval或--file里,use是 REPL 专用命令,它会吞掉后面的语句。实测:1
mongosh --eval 'use mongo_note; print("db=" + db.getName())'
1
switched to db mongo_note;
- 只打印了切换提示,
print那句根本没执行。脚本里请改用db.getSiblingDB("库名")(实测输出db=mongo_note)。
- 只打印了切换提示,
- 只
use不插入的话,show dbs里看不到这个库(空库不列出),这也是新手以为「建库失败」的原因。
- MongoDB 不需要显式建库:第一次向某个集合插入文档时才真正创建这个库。
-
选择数据库
1
use myDatabase
- 交互模式下这就是标准写法;
--eval/--file场景用db = db.getSiblingDB("myDatabase")。
- 交互模式下这就是标准写法;
-
查看当前数据库
1
db.getName()
1
mongo_note
- 直接敲
db会打印连接对象(mongosh 里显示成[object Object],实测如此),想知道库名请用db.getName();交互提示符本身也带库名(mongo_note>)。
- 直接敲
-
列出所有数据库
1
show dbs
- 等价的代码写法(脚本里更可靠):本机实测输出:
1
2
3db.adminCommand({ listDatabases: 1 }).databases.forEach(function (d) {
print(d.name + " " + d.sizeOnDisk + " bytes");
});1
2
3admin 102400 bytes
config 110592 bytes
local 40960 bytes show dbs不列空库;刚use完还没插数据的库不会出现。admin、config、local是系统库:admin存用户和权限,config是分片元数据(单机也有),local只在本节点有效、不会被复制。
- 等价的代码写法(脚本里更可靠):
-
删除数据库
1
db.dropDatabase()
实测返回:
1
{ ok: 1, dropped: 'mongo_note_restored' }
- 删的是当前
db指向的库,删之前务必db.getName()确认一遍。 - 这个操作不可逆,且不会走回收站;生产上先
mongodump备份。
- 删的是当前
-
其他数据库级别的操作
- 查看数据库状态:实测返回的键是:
1
db.stats()
1
["db","collections","views","objects","avgObjSize","dataSize","storageSize","indexes","indexSize","totalSize","scaleFactor","fsUsedSize","fsTotalSize","ok"]
collections是集合数、objects是文档数、avgObjSize是平均文档字节数、dataSize是文档逻辑总大小。- 索引数量在
indexes这个键上(不是indexCount,实测indexCount是undefined)。 storageSize是实际磁盘占用,fsUsedSize/fsTotalSize是文件系统层面的用量,scaleFactor是单位换算系数。
- 创建用户:
1
2
3
4
5db.createUser({
user: "appUser",
pwd: "一个足够强的密码",
roles: [{ role: "readWrite", db: "mydb" }]
})- 必须在开了
security.authorization之后才有意义;本机没开鉴权,所以建了用户也拦不住匿名连接(实测不带账号ping就返回 1)。 - 第一次建管理员要在
admin库上给root或userAdminAnyDatabase角色,别把自己锁在门外。
- 必须在开了
- 查看集合:
show collections或db.getCollectionNames()(后者返回数组,实测["myNewCollection"])。 - 备份:
mongodump。实测:1
mongodump --db mongo_note --out /tmp/mongodump_test
1
2writing `mongo_note.collectionName` to `/tmp/mongodump_test/mongo_note/collectionName.bson`
done dumping `mongo_note.collectionName` (3 documents)- 每个集合产出两个文件:
.bson(数据)和.metadata.json(索引等元信息,新版还多了prelude.json)。 - 备份工具版本与 server 不同源,本机是
mongodump version: 100.20.1。
- 每个集合产出两个文件:
- 恢复:
mongorestore,还能顺手改库名:1
mongorestore --nsFrom 'mongo_note.*' --nsTo 'mongo_note_restored.*' /tmp/mongodump_test
1
6 document(s) restored successfully. 0 document(s) failed to restore.
- 恢复完实测集合列表:
["ts_demo","collectionName","py_collection","myNewCollection2","myCappedCollection"],索引也一并重建(日志里有restoring indexes for collection ... from metadata)。 - 加
--drop会先删同名集合再恢复。
- 恢复完实测集合列表:
- 导入导出:
1
mongoexport --db mongo_note --collection exp --type=csv --fields n,t
1
2
3n,t
1,a
2,b--type支持json(默认)与csv;csv必须显式给--fields,否则只会导出_id。- 反向用
mongoimport(--jsonArray一次读整个数组,--drop先清集合)。
- 查看数据库状态:
MongoDB 之 collection 操作
-
查看所有集合
1
show collections
- 代码写法:实测返回
1
db.getCollectionNames()
["myNewCollection"]。 - 想连集合统计一起看用
db.getCollectionInfos(),返回每个集合的name、type、options。
- 代码写法:
-
创建集合
-
平时不用显式创建(插入文档时自动建)。需要指定选项时才用
createCollection:1
db.createCollection("myNewCollection")
1
{ ok: 1 }
-
固定大小集合(capped collection):
1
db.createCollection("myCappedCollection", { capped: true, size: 100000 })
1
2{ ok: 1 }
capped 选项 -> {"capped":true,"size":100000}size单位是字节,100000 就是约 97 KB,不是 10 万条文档。- capped 集合按插入顺序存、写满后自动覆盖最老的文档,适合日志环形缓冲;它的自然顺序(
$natural)就是插入顺序,读起来很省。 - 老资料常说「capped 集合不能删文档」,那是针对已废弃的
remove()。实测deleteOne()是能用的:只是删除不会回收空间(集合大小固定),真要清空只能1
deleteOne 允许吗 -> 是
drop()重建。
-
重复创建同名集合不报错,实测第二次也是
{ ok: 1 }(它只是确认已存在)。 -
带校验规则的集合(防止脏字段进来):
1
2
3
4
5
6
7
8
9
10
11
12db.createCollection("validated", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "age"],
properties: {
name: { bsonType: "string", description: "必须是字符串" },
age: { bsonType: "int", minimum: 0 }
}
}
}
})- 实测拦截效果(三种非法写法都拒):
1
2
3
4
5合法插入 -> true
缺字段 -> Document failed validation
类型错 -> Document failed validation
minimum -> Document failed validation
更新成非法值 -> Plan executor error during update :: caused by :: Document failed validation- 注意
bsonType: "int"只认 32 位整数,JS 里要写NumberInt(20);{age: 20}在 mongosh 里是double,会被判成类型不符。这是最容易误报的一条。 - 校验默认只作用于插入和更新,已有脏数据不会被追溯;想放宽成「只告警不拒绝」,加
validationAction: "warn"。
- 注意
- 实测拦截效果(三种非法写法都拒):
-
-
删除集合
1
db.myOldCollection.drop()
- 原笔记这里说「删不存在的集合返回
false」,在 mongosh 里不成立。实测:1
2删存在的 -> true
删不存在的 -> true- 驱动层把「集合不存在」也当成删除成功(服务端返回
ok: 1),所以别拿返回值判断集合是否存在。要判断就用:1
db.getCollectionNames().indexOf("myOldCollection") !== -1
- 驱动层把「集合不存在」也当成删除成功(服务端返回
- 删除不可逆,集合里的所有文档和索引一起消失。
- 只想清空数据、保留集合和索引:
db.coll.deleteMany({})。
- 原笔记这里说「删不存在的集合返回
MongoDB 之 document 操作
下面所有示例都在集合 mongo_note.collectionName 上跑,初始数据是这 5 条(另外还有一条手工指定整数 _id 的,见插入一节):
1 | db.collectionName.insertOne({ name: "John Doe", age: 30, email: "johndoe@example.com" }); |
-
插入文档
-
插入单个文档
1
2
3
4
5db.collectionName.insertOne({
name: "John Doe",
age: 30,
email: "johndoe@example.com"
})实测返回:
1
2
3
4{
acknowledged: true,
insertedId: ObjectId('6aea00b975b57147bab77332')
}- 集合不存在时会自动创建,库也一样。
insertedId就是这条文档的_id,可以直接拿去后续更新/删除。
-
插入多个文档
1
2
3
4db.collectionName.insertMany([
{ name: "Jane Doe", age: 25, email: "janedoe@example.com" },
{ name: "Jim Beam", age: 35, email: "jimbeam@example.com" }
])实测返回(4 条的完整版):
1
2
3
4
5
6
7
8
9{
acknowledged: true,
insertedIds: {
'0': ObjectId('6aea00b975b57147bab77333'),
'1': ObjectId('6aea00b975b57147bab77334'),
'2': ObjectId('6aea00b975b57147bab77335'),
'3': ObjectId('6aea00b975b57147bab77336')
}
}- 注意
insertedIds是以数组下标为键的对象,不是数组——原笔记说「得到_id的列表」,形状上要说清楚。 - 默认是「有序」插入:中途某条失败,前面已成功的不会回滚(单机没有事务)。想让剩余文档继续尝试,传
{ordered: false}。
- 注意
-
_id的规则- 每个文档都有
_id,不提供就自动生成ObjectId:1
自动 _id 类型 -> object // typeof 是 object,构造函数名是 ObjectId
- 也可以自己指定,类型不限(整数、字符串都行):
1
db.collectionName.insertOne({ _id: 1, name: "IntId" })
1
{ acknowledged: true, insertedId: 1 }
- 主键冲突的报错原文:
1
E11000 duplicate key error collection: mongo_note.collection index: _id_ ...
_id是字符串还是 ObjectId 是两种不同的值,实测用字符串去查一个 ObjectId 主键查不到:1
2按 ObjectId 查 -> true
字符串查 ObjectId -> false- 这是从别的系统迁数据、或前端传 id 过来时最常见的「明明存在却查不到」。要么
new ObjectId(str)转一下,要么一开始就把_id存成字符串。
- 这是从别的系统迁数据、或前端传 id 过来时最常见的「明明存在却查不到」。要么
ObjectId自带时间信息,前 4 字节是创建时刻:1
new ObjectId().getTimestamp() -> Tue Nov 03 2026 23:45:56 GMT+0800
- 所以「按
_id排序」约等于「按插入时间排序」,很多简单场景不用再单独存创建时间。
- 所以「按
- 每个文档都有
-
-
查询文档
-
查询所有文档
1
db.collectionName.find()
- 返回的是游标(
db.coll.find().constructor.name实测是Cursor),不是数组。交互模式下 mongosh 会自动帮你展开,默认一次显示 20 条、超出提示Type "it" for more(这是 REPL 的显示行为,脚本里看不到,脚本要自己.toArray()或forEach,实测 25 条数据forEach计数正好 25)。 - 在
--file/--eval脚本里要自己收:db.collectionName.find().toArray(),或者用.forEach(...)。 - 直接把游标塞进
JSON.stringify()会报错,实测原文:1
MongoshInvalidInputError: [COMMON-10001] Cannot serialize a cursor to JSON. Did you mean to call .toArray() first?
- 原笔记里那些
JSON.stringify(db.coll.find(...))的写法都会踩这句,改成.toArray()。
- 原笔记里那些
- 返回的是游标(
-
查询特定文档
1
db.collectionName.find({ age: { $gt: 25 } })
实测命中:
1
["John Doe","Jim Beam","Leo Sun"]
-
查询并返回特定字段(投影)
1
db.collectionName.find({}, { name: 1, age: 1, _id: 0 })
1
[{"name":"John Doe","age":30},{"name":"Jane Doe","age":25}]
1是包含、0是排除;_id默认总是返回,要它得显式写_id: 0。- 除
_id之外不能混用包含和排除({name: 1, age: 0}会报错)。
-
查询单个文档
1
db.collectionName.findOne({ name: "John Doe" })
1
{"_id":"6aea00b975b57147bab77332","name":"John Doe","age":30,"email":"johndoe@example.com"}
- 没匹配到返回
null,不是空对象。判空要=== null。
- 没匹配到返回
-
使用查询操作符
1
db.collectionName.find({ age: { $lte: 30 } })
1
["John Doe","Jane Doe","Kate Bush"]
- 更多操作符见「查询条件操作符」一节,那里每个都有实测结果。
-
多条件查询
-
$and1
db.collectionName.find({ $and: [{ age: { $gte: 20 } }, { name: "John Doe" }] })
1
["John Doe"]
- 同一文档上写多个等值条件时,直接并列就隐含 AND:
{name: "John Doe", age: 30},不必动用$and。$and主要用于同一个字段两个范围(如>=20且<=30)或子条件本身是$or。
- 同一文档上写多个等值条件时,直接并列就隐含 AND:
-
$or1
db.collectionName.find({ $or: [{ name: "Jane Doe" }, { age: { $lt: 20 } }] })
1
["Jane Doe","Kate Bush"]
-
$nor1
db.collectionName.find({ $nor: [{ age: { $gt: 20 } }, { name: "John Doe" }] })
1
["Kate Bush","IntId"]
- 注意
IntId也在结果里:它根本没有age字段,所以age > 20对它不成立,于是「两个条件都不满足」成立。这是「缺字段」在三值逻辑里的表现。
- 注意
-
$not1
db.collectionName.find({ name: { $not: { $regex: "^J" } } })
1
["Kate Bush","Leo Sun","IntId"]
$not同样会把「没有这个字段」的文档算进来。要排除它们得再加{name: {$exists: true}}。
-
组合
$and与$or1
2
3
4
5
6db.collectionName.find({
$and: [
{ $or: [{ name: "John Doe" }, { age: { $lt: 30 } }] },
{ age: { $gt: 20 } }
]
})- 优先级靠结构表达,别指望「
$and写在外面就一定先算」——把它想成括号最稳。
- 优先级靠结构表达,别指望「
-
-
嵌套字段与数组查询
- 用「点号路径」查子文档字段,实测:
1
db.collectionName.find({ "address.city": "Shanghai" })
1
["Leo Sun"]
- 想整体匹配一个子文档(字段顺序和数量都要一致)就写
{address: {city: "Shanghai", zip: "200000"}},实际很少这么用。
- 想整体匹配一个子文档(字段顺序和数量都要一致)就写
- 数组字段可以直接按元素查,MongoDB 会自动匹配「数组里有没有这个值」:
1
db.collectionName.find({ tags: "music" })
1
["Kate Bush"]
- 要按数组条件组合(同一元素满足多个条件)用
$elemMatch。
- 要按数组条件组合(同一元素满足多个条件)用
- 字段是否存在:
1
db.collectionName.find({ tags: { $exists: true } })
1
["Kate Bush"]
- 用「点号路径」查子文档字段,实测:
-
正则查询
1
db.collectionName.find({ name: /^J/ })
1
["John Doe","Jane Doe","Jim Beam"]
1
db.collectionName.find({ name: { $regex: "doe", $options: "i" } })
1
["John Doe","Jane Doe"]
- 前缀锚定
^J能走索引;/doe/这种中间匹配要全扫,代价高。
- 前缀锚定
-
排序查询结果
1
db.collectionName.find().sort({ age: -1 })
1
[{"name":"Leo Sun"},{"name":"Jim Beam"},{"name":"John Doe"},{"name":"Jane Doe"},{"name":"Kate Bush"},{"name":"IntId"}]
1升序、-1降序。没有age字段的文档按「缺失最小」排在升序最前、降序最后(上面IntId落在降序末尾就是这个原因)。- 多字段排序:
sort({ age: -1, name: 1 })。
-
限制查询结果数量
1
db.collectionName.find().limit(5)
- 实测
limit(3):1
[{"name":"John Doe"},{"name":"Jane Doe"},{"name":"Jim Beam"}]
limit(0)或不写表示不限制。
- 实测
-
跳过指定数量的文档
1
db.collectionName.find().skip(5)
- 实测
skip(3):1
[{"name":"Kate Bush"},{"name":"Leo Sun"},{"name":"IntId"}]
skip是先扫过再丢掉,不是免费跳转,深分页照样慢。
- 实测
-
链式使用查询方法
1
db.collectionName.find().sort({ age: 1 }).limit(5).skip(10)
- 原笔记的解释是「跳过前 10 个,然后按 age 排序,再取 5 个」,顺序说反了。真实执行顺序永远是 排序 → 跳过 → 限制,和链式调用谁写在前头无关。实测两条完全等价:
1
2sort+limit+skip -> [{"name":"Jane Doe","age":25},{"name":"John Doe","age":30},{"name":"Jim Beam","age":35}]
limit(3).sort().skip(2) -> [{"name":"Jane Doe","age":25},{"name":"John Doe","age":30},{"name":"Jim Beam","age":35}] - 所以「先
limit再sort」不会得到「未排序的前 N 条再排序」,别被代码书写顺序骗了。 - 分页的正确姿势:
sort({_id: 1}).skip(页码 * 每页).limit(每页),或者用「上一页最后一条的_id」做游标(find({_id: {$gt: last}}).sort({_id: 1}).limit(n)),后者在深翻页时快得多。
- 原笔记的解释是「跳过前 10 个,然后按 age 排序,再取 5 个」,顺序说反了。真实执行顺序永远是 排序 → 跳过 → 限制,和链式调用谁写在前头无关。实测两条完全等价:
-
-
更新文档
-
更新单个文档
1
2
3
4db.collectionName.updateOne(
{ name: "John Doe" },
{ $set: { email: "john.doe@newdomain.com" } }
)实测返回:
1
2
3
4
5
6
7{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}- 四个计数各有含义:
matchedCount命中几条、modifiedCount真正改动几条(值没变就不算)、upsertedCount插了几条、insertedId只在 upsert 触发插入时非 null。 $set这类操作符不能省:第二个参数直接写{email: "..."}会被当成「用这个文档整体替换」,实测报错:想整体替换请明确用1
Update document requires atomic operators
replaceOne()。
- 四个计数各有含义:
-
更新多个文档
1
db.collectionName.updateMany({ age: { $gt: 30 } }, { $set: { status: "senior" } })
1
2{ acknowledged: true, insertedId: null, matchedCount: 2, modifiedCount: 2, upsertedCount: 0 }
senior 人数 -> 2 -
常用更新操作符
1
2
3db.collectionName.updateOne({ name: "Jane Doe" }, { $inc: { age: 1 } });
db.collectionName.updateOne({ name: "Jane Doe" }, { $push: { hobby: "chess" } });
db.collectionName.updateOne({ name: "Jane Doe" }, { $unset: { hobby: "" } });实测:
1
2
3$inc -> Jane 年龄 26
$push -> hobby ["chess"]
$unset -> 字段消失(hobby === undefined 为 true)$unset的值随便给个占位(惯例是"")。- 其余常用:
$set(设值)、$setOnInsert(只在 upsert 插入时设)、$currentDate、$min/$max、$pull(从数组删匹配元素)、$addToSet(数组去重添加)、$pop、$rename。
-
upsert:没有就插一条
1
db.collectionName.updateOne({ name: "Nobody" }, { $set: { age: 1 } }, { upsert: true })
1
2
3{ acknowledged: true, insertedId: ObjectId('6aea00d209b6d4b9ec034b03'),
matchedCount: 0, modifiedCount: 0, upsertedCount: 1 }
Nobody 存在吗 -> true- upsert 出来的文档 = 查询条件里的等值字段 + 更新操作符的结果,所以条件里写了
name: "Nobody",新文档自然带name。
- upsert 出来的文档 = 查询条件里的等值字段 + 更新操作符的结果,所以条件里写了
-
replaceOne:整体替换
1
db.collectionName.replaceOne({ name: "Nobody" }, { name: "Nobody2", age: 2 })
1
{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
- 替换会丢掉没写进新文档的所有字段,只保留
_id。
- 替换会丢掉没写进新文档的所有字段,只保留
-
旧写法还能不能用
db.coll.update(...):mongosh 里已不提供,改用updateOne/updateMany。db.coll.save(doc):彻底没了,实测:1
db.collectionName.save is not a function
db.coll.remove(...):还能跑,但有告警:1
DeprecationWarning: Collection.remove() is deprecated. Use deleteOne, deleteMany, findOneAndDelete, or bulkWrite.
-
-
删除文档
1
db.collectionName.deleteOne({ name: "Nobody2" })
1
{ acknowledged: true, deletedCount: 1 }
1
db.collectionName.deleteMany({ status: "senior" })
1
{ acknowledged: true, deletedCount: 2 }
- 条件匹配不到时
deletedCount是 0,不报错。 deleteMany({})清空集合但保留索引;要连索引一起没就drop()。- 想「删掉并把删掉的那条返回」,用
findOneAndDelete(filter)。
- 条件匹配不到时
-
计数文档
1
2
3db.collectionName.countDocuments()
db.collectionName.countDocuments({ age: { $gt: 25 } })
db.collectionName.estimatedDocumentCount()实测:
1
2
3countDocuments() -> 6
countDocuments(age > 25) -> 3
estimatedDocumentCount() -> 6countDocuments()是精确计数(会真的按条件扫);estimatedDocumentCount()直接读集合元数据,不接受过滤条件,快但是估算。- 老的
count()在 mongosh 里还能用,但每次都会告警:1
DeprecationWarning: Collection.count() is deprecated. Use countDocuments or estimatedDocumentCount.
- 它返回的还是
6(上面那次实测),所以「能用」不等于「该用」。
- 它返回的还是
- PyMongo 4.x 里
Cursor.count()已经移除,实测:1
AttributeError: 'Cursor' object has no attribute 'count'
-
排序文档
见上面「排序查询结果」。补充两点:
- 排序字段最好有索引,否则大集合会走内存排序。实测无索引排序的
explain()里执行计划顶层就是SORT阶段({"stage":"SORT", ...}),超过 100 MB 会报Sort exceeded memory limit of 104857600 bytes;这时要么给排序字段建索引,要么在聚合里加allowDiskUse: true(实测接受该选项)。 - 字符串排序默认按 UTF-8 码点,中文默认不是拼音序。实测「八啊张爱」四条:
1
2默认 sort -> ["八","啊","张","爱"] // 按码点:516B 554A 5F20 7231
collation zh sort -> ["啊","爱","八","张"] // 拼音序:a ai ba zhang- 用法:
db.zh.find().sort({w: 1}).collation({locale: "zh", strength: 2})。 - 想让它走索引,建索引时也要带同样的 collation,实测
db.zh.createIndex({w: 1}, {collation: {locale: "zh", strength: 2}})成功;查询侧的 collation 必须和索引一致,否则索引不会被用于排序。
- 用法:
- 排序字段最好有索引,否则大集合会走内存排序。实测无索引排序的
-
限制查询结果
见上面「限制查询结果数量」。
limit+skip组合是分页的最小实现,但真正高并发场景要用「游标式分页」(记住上一页末尾的排序键)。
查询条件操作符
| 操作符 | 描述 | 示例与本机实测结果 |
|---|---|---|
$eq |
等于 | {age: {$eq: 30}} → 命中 John Doe |
$gt |
大于 | {age: {$gt: 25}} → John Doe, Jim Beam, Leo Sun |
$gte |
大于等于 | {age: {$gte: 20}} |
$lt |
小于 | {age: {$lt: 20}} → Kate Bush |
$lte |
小于等于 | {age: {$lte: 30}} → John Doe, Jane Doe, Kate Bush |
$ne |
不等于 | {age: {$ne: 30}};注意:字段缺失的文档也会被算进「不等于」 |
$in |
在数组里 | {age: {$in: [25, 30, 99]}} → John Doe, Jane Doe |
$nin |
不在数组里 | {age: {$nin: [25, 30]}} → Jim Beam, Kate Bush, Leo Sun, IntId(IntId 没有 age 字段,照样进来) |
$or |
或 | {$or: [{age: 20}, {name: "John Doe"}]} → 命中任一即可 |
$and |
与 | {$and: [{age: {$gt: 20}}, {name: "John Doe"}]} → John Doe |
$nor |
都不满足 | {$nor: [{age: {$gt: 20}}, {name: "John Doe"}]} → Kate Bush, IntId |
$not |
取反 | {age: {$not: {$lt: 30}}} → 不小于 30 的以及没有 age 字段的 |
$exists |
字段是否存在 | {tags: {$exists: true}} → Kate Bush |
$regex |
正则 | {name: {$regex: "^J"}} → John Doe, Jane Doe, Jim Beam;配 $options: "i" 忽略大小写 |
$type |
类型检查 | {age: {$type: "int"}} → 只有 age 是 32 位整数的文档 |
$size |
数组长度 | {tags: {$size: 2}} |
$elemMatch |
数组元素同时满足多条件 | {scores: {$elemMatch: {$gt: 80, $lt: 90}}} |
$all |
数组包含全部给定值 | {tags: {$all: ["music", "art"]}} |
$mod |
取模 | {age: {$mod: [2, 0]}} → 偶数年龄 |
$expr |
在查询里用聚合表达式 | {$expr: {$gt: ["$age", "$parentAge"]}} |
$jsonSchema |
校验(用于 validator,不是查询) | 见「创建集合」一节 |
- 三个必须记住的语义细节,全部有实测支撑:
- 「字段缺失」会同时满足
$ne、$nin、$not,还会满足{field: null}。上面IntId(只有_id和name)反复出现在这些结果里,就是证据。另一组更干净的实测(age分别写成NumberInt(30)、30、25.5、null,外加两条根本没有age的文档):1
2
3
4{age: null} -> ["d","e","f","g"] // d 是 null,e/f/g 是没有这个字段
$eq null -> ["d","e","f","g"] // 同上
$exists true -> ["a","b","c","d"] // 只要字段真的存在
$ne 30 -> ["c","d","e","f","g"]- 所以
{field: null}是「值为 null 或 字段不存在」,这是 MongoDB 查询里最反直觉的一条。要严格只匹配 null,得写{age: null, ...}之外再加{"age": {"$type": "null"}}。
- 所以
$type认的是 BSON 类型,而 mongosh 会把整数值的 JS number 存成 int32。实测同一批数据:1
2
3
4$type int -> ["a","b"] // a 是 NumberInt(30),b 是普通 30,都被存成 int32
$type double -> ["c"] // 只有 25.5 是 double
$type number -> ["a","b","c"] // number 是 int/double/decimal 的统称
{age: 30} 等值 -> ["a","b"] // 等值比较跨 int/double 都能匹配- 也就是说:mongosh 里写
{age: 30}存的就是 int32,不用刻意NumberInt;但带小数点的会存成 double,而等值查询不受类型限制({age: 30}能同时命中 int32 的 30 和 double 的 30.0)。 - PyMongo 侧同理:Python 的
int在 32 位范围内会被编码成 int32,超范围自动升 int64。
- 也就是说:mongosh 里写
$elemMatch与「同字段写两个条件」的区别只在数组上存在,实测:1
2
3文档 pair scores [85] 文档 split scores [70, 95]
{scores: {$gt: 80, $lt: 90}} -> ["pair","split"] // 85 满足、70 满足 <90,元素可以不同
{scores: {$elemMatch: {$gt: 80, $lt: 90}}} -> ["pair"] // 必须有同一个元素同时满足
- 「字段缺失」会同时满足
- 逻辑操作符只能在顶层用(
$and/$or/$nor的 value 必须是数组),写成{name: {$or: [...]}}会报语法错。 - 想确认查询到底怎么执行的,用
explain():实测关键片段:1
db.collectionName.find({ age: 30 }).explain()
1
{"stage":"FETCH","inputStage":{"stage":"IXSCAN","keyPattern":{"age":1}, ...}}
IXSCAN表示走了索引;看到COLLSCAN就是全集合扫描,该加索引了。
PyMongo 之 document 操作
-
本节把上面的 mongosh 操作逐条换成 PyMongo 写法,全部在
pymongo 4.19.0+MongoDB 9.0.4上实跑。 -
连接与准备:
1
2
3
4
5
6import pymongo
client = pymongo.MongoClient("mongodb://127.0.0.1:27017/")
db = client["mongo_note"]
collection = db["py_collection"]
collection.drop() # 演示用,先清干净 -
插入文档
1
2
3
4
5
6
7
8
9r = collection.insert_one({"name": "John Doe", "age": 30, "email": "j@e.com"})
print(r.inserted_id) # 6aea00fce0de4906812d23b6 (类型是 ObjectId)
r2 = collection.insert_many([
{"name": "Jane", "age": 25},
{"name": "Jim", "age": 35},
{"name": "Kate", "age": 19, "tags": ["a", "b"]},
])
print(len(r2.inserted_ids), r2.acknowledged) # 3 True实测输出:
1
2insert_one -> ObjectId 6aea00fce0de4906812d23b6
insert_many ids -> 3 acknowledged: Trueinsert_one返回InsertOneResult,只有inserted_id和acknowledged;想拿完整结果得用ordered=True的insert_many。- 字典的键就是字段名,值会被自动编码成对应 BSON 类型。
-
查询文档
1
2
3
4
5
6
7
8
9
10
11print(collection.find_one({"name": "John Doe"}))
# {'_id': ObjectId('...'), 'name': 'John Doe', 'age': 30, 'email': 'j@e.com'}
print([d["name"] for d in collection.find({"age": {"$gt": 25}})]) # ['John Doe', 'Jim']
print(list(collection.find({}, {"name": 1, "_id": 0})))
# [{'name': 'John Doe'}, {'name': 'Jane'}, {'name': 'Jim'}, {'name': 'Kate'}]
print(list(collection.find({}, {"name": 1, "age": 1, "_id": 0})
.sort("age", pymongo.ASCENDING).skip(1).limit(2)))
# [{'name': 'Jane', 'age': 25}, {'name': 'John Doe', 'age': 30}]find()返回Cursor,是惰性的:不迭代就不发查询(除了第一次取批次)。find_one()没命中返回None。- 排序方向用
pymongo.ASCENDING/DESCENDING(就是 1/-1),多字段用列表:.sort([("age", -1), ("name", 1)])。 - 计数:
1
2
3collection.count_documents({}) # 4
collection.count_documents({"age": {"$gt": 20}}) # 3
collection.estimated_document_count() # 4collection.count()与find().count()在 PyMongo 4.x 都已删除,实测:1
AttributeError: 'Cursor' object has no attribute 'count'
-
更新文档
1
2
3
4
5
6
7
8
9
10
11u = collection.update_one({"name": "John Doe"}, {"$set": {"email": "new@e.com"}})
print(u.matched_count, u.modified_count, u.upserted_id) # 1 1 None
um = collection.update_many({"age": {"$gte": 25}}, {"$set": {"status": "adult"}})
print(um.matched_count, um.modified_count) # 3 3
print(collection.replace_one({"name": "Kate"}, {"name": "Kate2", "age": 20}).modified_count) # 1
print(collection.find_one_and_update(
{"name": "Jane"}, {"$inc": {"age": 1}},
return_document=pymongo.ReturnDocument.AFTER))实测最后一条返回:
1
{'_id': ObjectId('...'), 'name': 'Jane', 'age': 26, 'status': 'adult'}
find_one_and_update默认返回更新前的文档,要更新后的得显式传return_document=ReturnDocument.AFTER。- 同样必须带操作符,
update_one(filter, {"a": 1})会报和 shell 一样的错。
-
删除文档
1
2print(collection.delete_one({"name": "Kate2"}).deleted_count) # 1
print(collection.delete_many({"status": "adult"}).deleted_count) # 3 -
排序文档 / 计数文档
1
2
3
4for doc in collection.find().sort("age", pymongo.DESCENDING):
print(doc)
n = collection.count_documents({"age": {"$gt": 25}}) -
索引
1
2
3
4print(collection.create_index("age")) # age_1
print(collection.create_index([("email", pymongo.ASCENDING)], unique=True)) # email_1
print([i["name"] for i in collection.list_indexes()]) # ['_id_', 'age_1', 'email_1']
print(collection.drop_index("age_1")) # Nonecreate_index返回的是索引名(自动按「字段_方向」生成)。drop_index返回None,不要拿返回值判断成败。- 复合索引同样遵守最左前缀:
create_index([("a", 1), ("b", -1)])能服务a和a,b,服务不了单独的b。
-
废弃 API 在 PyMongo 4.x 的下场(实测)
老写法 现在的结果 collection.insert(...)TypeError: 'Collection' object is not callable. If you meant to call the 'insert' method...collection.remove(...)同上( 'remove' method)collection.save(...)同上( 'save' method)collection.update(...)同上( 'update' method)collection.find_and_modify(...)同上,改用 find_one_and_updatecollection.find().count()AttributeError: 'Cursor' object has no attribute 'count'- 那串
TypeError的完整提示其实很有用:它会告诉你「PyMongo 4 移除了这些方法,请用insert_one/insert_many」。老项目升级时按这个报错逐个替换即可。
- 那串
-
BSON 类型对照(实测)
Python 值 能否直接存 取回类型 备注 bson.ObjectId()能 ObjectId就是 _id的类型datetime.datetime(...)(naive)能 datetime按 UTC 存,取回是 naive datetime.now(datetime.UTC)(aware)能 datetime取回时 tzinfo 丢失,仍是 UTC 数值 decimal.Decimal("1.25")不能 — InvalidDocument: cannot encode object: Decimal('1.25')bson.Decimal128("1.25")能 Decimal128用 .to_decimal()转回Decimalb"\x00\x01"能 bytes二进制 re.compile("^a")能 Regex存成 BSON 正则,实测回读是 Regex('^a', 32)float("inf")能 float值仍是 inf,别指望 JSON 序列化它bson.Int64(2)能 Int64需要 64 位整数时用 dict/list能 dict/list嵌套文档与数组 - 所以「金额用 Decimal」这条在 MongoDB 里要显式换成
Decimal128,否则一插入就炸。
- 所以「金额用 Decimal」这条在 MongoDB 里要显式换成
-
聚合管道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17list(collection.aggregate([
{"$group": {"_id": "$c", "total": {"$sum": "$n"}}},
{"$sort": {"_id": 1}},
]))
# [{'_id': 'a', 'total': 3}, {'_id': 'b', 'total': 3}]
list(collection.aggregate([{"$bucket": {"groupBy": "$n",
"boundaries": [0, 2, 4],
"default": "other"}}]))
# [{'_id': 0, 'count': 1}, {'_id': 2, 'count': 2}, {'_id': 'other', 'count': 1}]
list(collection.aggregate([
{"$lookup": {"from": "bson_test", "localField": "c",
"foreignField": "c", "as": "join"}},
{"$project": {"_id": 0, "c": 1, "joinN": {"$size": "$join"}}},
]))
# [{'c': 'a', 'joinN': 2}, {'c': 'a', 'joinN': 2}, {'c': 'b', 'joinN': 1}, {'c': 'd', 'joinN': 1}]aggregate()返回CommandCursor,不能下标取(实测TypeError: 'CommandCursor' object is not subscriptable),要list(...)或next(...)。- 超过 100 MB 的中间结果默认报错,加
allowDiskUse=True:collection.aggregate(pipeline, allowDiskUse=True)。
-
时序集合(5.0+,实测可用)
1
2
3
4
5db.create_collection("ts_demo", timeseries={
"timeField": "t", "metaField": "m", "granularity": "seconds"})
db["ts_demo"].insert_many([{"t": now, "m": i, "v": i * 2} for i in range(3)])
print(db["ts_demo"].count_documents({})) # 3
db.drop_collection("ts_demo")- 适合监控指标、传感器数据:MongoDB 自动按
metaField+ 时间桶打包,压缩率和按时间范围查询都明显好于普通集合。
- 适合监控指标、传感器数据:MongoDB 自动按
-
事务(本机不可用的实测证据)
1
2
3with client.start_session() as s:
with s.start_transaction():
db["x"].insert_one({"a": 1}, session=s)1
OperationFailure: Transaction numbers are only allowed on a replica set member or mongos
- 单机
mongod没有事务。要用事务必须起副本集(本地开发可以单节点副本集:--replSet rs0+rs.initiate())。 - 同理,Change Streams 也要求副本集。
- 单机
-
上一节是「按操作逐条对照」,这一节给一份能整段复制就跑的入门脚本,并把连接、选库、建索引、异常、关闭这些工程细节补齐。
-
安装 PyMongo
1
pip install pymongo
- 本机实测版本
pymongo 4.19.0。 - 需要 DNS 种子列表(
mongodb+srv://)时再加:pip install "pymongo[srv]"(会带上dnspython)。 - 网络慢时换镜像:
pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple pymongo。
- 本机实测版本
-
连接到 MongoDB
1
2
3
4
5
6
7from pymongo import MongoClient
# 本地默认端口
client = MongoClient('mongodb://localhost:27017/')
# 带账号密码(用户名与认证库都要写对)
client = MongoClient('mongodb://admin:密码@127.0.0.1:27017/?authSource=admin')authSource是最容易漏的一项:用户建在哪个库,就要在连接串里指回哪个库,否则报Authentication failed。本机admin用户建在admin库,所以必须带?authSource=admin。- 也可以分开传参:
1
2client = MongoClient(host='127.0.0.1', port=27017,
username='admin', password='密码', authSource='admin') - 连接是惰性的:
MongoClient(...)不会真的去连,第一条命令才建立连接。所以「构造成功」不代表「服务活着」,验证要用一条实打实的命令:1
print(client.server_info()["version"]) # 9.0.4
- 服务没起来时抛
pymongo.serverSelection.ServerSelectionTimeoutError,默认要等 30 秒才失败,脚本里建议显式缩短:1
client = MongoClient("mongodb://127.0.0.1:27017/", serverSelectionTimeoutMS=3000)
- 服务没起来时抛
- 常用参数(实测
MongoClient接受):host、port、document_class、tz_aware、uuidRepresentation、maxPoolSize、username、password、authSource、authMechanism、replicaSet、readPreference、w、serverSelectionTimeoutMS、connectTimeoutMS、tls。 - 本机环境的一个真实状态:MongoDB 服务端没有开启鉴权(
/etc/mongod.conf里没有security段),所以不带账号也能连。实测这样判断:1
print(client.admin.command("getCmdLineOpts").get("parsed", {}).get("security")) # None
- 返回
None就说明authorization没开——此时requirepass形同虚设,任何能连上 27017 的人都是 root。
- 返回
-
选择数据库和集合
1
2db = client['mydatabase'] # 或 client.mydatabase
collection = db['users'] # 或 db.users- 两种方式等价;字段名里有
-、.或和Database属性重名时用方括号形式。 - 库和集合都不需要提前创建,第一次写入时才落地:
1
2print(client.list_database_names()) # 空库不会出现在这里
print(db.list_collection_names())
- 两种方式等价;字段名里有
-
插入文档
1
2
3
4
5user1 = {'name': 'Alice', 'age': 25}
collection.insert_one(user1) # 返回 InsertOneResult
users = [{'name': 'Bob', 'age': 30}, {'name': 'Charlie', 'age': 35}]
collection.insert_many(users) # 返回 InsertManyResultinsert_one(...).inserted_id拿到新文档的_id。- 传字典会被原地补上
_id键(如果不想被改,传之前dict(user1)拷一份)。 - 批量导入建议一次多传几条,别在循环里一条条
insert_one。
-
查询文档
1
2
3
4
5
6
7print(collection.find_one({'name': 'Alice'})) # 没命中返回 None
for user in collection.find(): # 游标,惰性
print(user)
for user in collection.find({'age': {'$gt': 30}}, {'name': 1, '_id': 0}):
print(user) # {'name': 'Charlie'}- 第二个参数是投影(projection)。
- 游标支持下标和切片(实测
collection.find({})[0]直接返回第一条、[0:2]返回两条),但不支持负数下标:1
IndexError: Cursor instances do not support negative indices
- 只要一条时优先用
find_one;取「可能不存在的一条」也可以用next(collection.find(...), None)(实测返回None)。
-
更新文档
1
2
3collection.update_one({'name': 'Alice'}, {'$set': {'age': 26}})
collection.update_many({'age': {'$gte': 30}}, {'$set': {'senior': True}})
collection.replace_one({'name': 'Bob'}, {'name': 'Bob', 'age': 31})- 第二个参数必须带操作符(
$set等)。PyMongo 在客户端就拦下来了,实测:1
ValueError: update only works with $ operators
- 这和 mongosh 里那句
Update document requires atomic operators是同一个限制、不同来源:一个来自驱动,一个来自服务端。
- 这和 mongosh 里那句
- 第二个参数必须带操作符(
-
删除文档
1
2collection.delete_one({'name': 'Alice'})
collection.delete_many({'age': {'$lt': 30}}) -
索引
1
2
3
4
5
6import pymongo
name = collection.create_index([('name', pymongo.ASCENDING)]) # 返回索引名 'name_1'
collection.create_index([('age', pymongo.DESCENDING)], unique=True)
print([i['name'] for i in collection.list_indexes()])
collection.drop_index('name_1') -
关闭连接
1
client.close()
close()会释放连接池里所有连接。短脚本里不调用也不会泄漏进程资源(进程退出就回收),但长驻服务、脚本收尾、以及测试用例里都应该显式关闭。- 更稳的写法是用上下文管理器,异常时也会关:
1
2with MongoClient("mongodb://127.0.0.1:27017/") as client:
client["mongo_note"]["ping"].insert_one({"ok": 1})
-
综合示例
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
40from pymongo import MongoClient, ASCENDING
# 连接本地 MongoDB(默认端口 27017)
client = MongoClient('mongodb://localhost:27017/', serverSelectionTimeoutMS=3000)
# 选择数据库 mydatabase(不存在时会在第一次写入时自动创建)
db = client['mydatabase']
# 选择集合 users(相当于关系库的表)
collection = db['users']
# 插入一个文档
collection.insert_one({'name': 'Alice', 'age': 25})
# 插入多个文档
collection.insert_many([
{'name': 'Bob', 'age': 30},
{'name': 'Charlie', 'age': 35},
])
# 查询单个文档
print(collection.find_one({'name': 'Alice'}))
# 查询所有文档
for user in collection.find({}, {'_id': 0}):
print(user)
# 更新文档
collection.update_one({'name': 'Alice'}, {'$set': {'age': 26}})
# 删除文档
collection.delete_one({'name': 'Bob'})
# 建索引,提高按 name 查询的性能
collection.create_index([('name', ASCENDING)])
# 收尾
print('剩余条数:', collection.count_documents({}))
collection.drop()
client.close()本机实跑输出:
1
2
3
4
5{'_id': ObjectId('...'), 'name': 'Alice', 'age': 25}
{'name': 'Alice', 'age': 25}
{'name': 'Bob', 'age': 30}
{'name': 'Charlie', 'age': 35}
剩余条数: 2这个脚本演示了:
- 连接本地实例(并显式缩短服务发现超时)。
- 选择/自动创建数据库
mydatabase。 - 选择/自动创建集合
users。 - 插入单个文档、插入多个文档。
- 按条件查一条、遍历全部(并用投影去掉
_id)。 - 更新一条、删除一条。
- 建索引。
- 计数并清理、关闭连接。
-
异常处理的最小骨架
1
2
3
4
5
6
7
8
9
10
11from pymongo.errors import (ConnectionFailure, OperationFailure,
DuplicateKeyError, ServerSelectionTimeoutError)
try:
collection.insert_one({'name': 'Alice'})
except DuplicateKeyError:
print('唯一键冲突')
except (ConnectionFailure, ServerSelectionTimeoutError):
print('连不上:服务没起 / 网络不通 / 地址端口写错')
except OperationFailure as e:
print('服务端拒绝:', e.code, e.details.get('errmsg') if e.details else e)- 层级关系:
PyMongoError派生出ConnectionFailure(网络类)和OperationFailure(服务端返回错误,AuthenticationError、DuplicateKeyError、WriteConflict都在它下面)。兜底捕PyMongoError最省事。 OperationFailure上带code和details,实测唯一键冲突时:1
2DuplicateKeyError | code 11000 | details keys ['index', 'code', 'errmsg', 'keyPattern', 'keyValue']
E11000 duplicate key error collection: mydatabase.u4 index: name_1 dup key: { name: "A" }- 想精确判断撞了哪个键,读
e.details['keyPattern']/keyValue,别去匹配错误字符串。
- 想精确判断撞了哪个键,读
- 另外两个实测到的原文:
- 单机实例开事务:
OperationFailure: Transaction numbers are only allowed on a replica set member or mongos client.close()之后再发命令:InvalidOperation: Cannot use MongoClient after close(close()本身可以重复调用,不报错)
- 单机实例开事务:
- 层级关系:
-
工程实践上的几条建议
- 一个进程里复用同一个
MongoClient,它内部自带连接池(默认maxPoolSize=100),不要每次请求都新建。 - 写操作关心可靠性时用
write_concern:w='majority'表示多数节点确认;单机上这个选项没意义但也不会报错。 - 读扩展用
read_preference(secondaryPreferred等),同样只在副本集里生效。 - 大集合分页避免深
skip,改成「按上一页末尾的排序键继续查」:1
2
3last_id = ... # 上一页最后一条的 _id
for doc in collection.find({'_id': {'$gt': last_id}}).sort('_id', 1).limit(50):
... - 索引不是越多越好:每个索引都会拖慢写入并占空间,上线前用
explain()确认真的被用上。 - 别把密码硬写进代码。用环境变量或配置文件,并且把配置文件排除在版本库之外。
- 一个进程里复用同一个
参考
- MongoDB Manual — Install MongoDB Community Edition
- MongoDB Manual — mongosh
- MongoDB Manual — Query Operators
- MongoDB Manual — Aggregation Pipeline
- MongoDB Manual — Schema Validation ($jsonSchema)
- MongoDB Manual — Transactions
- PyMongo 官方教程
- MongoDB Database Tools(mongodump / mongoimport 等)
- MongoDB Compass
- Navicat 系列产品激活教程