MongoDB 脚本速查(PyMongo 4 + mongosh 实测版)
来源:
Django+Redis+MySQL背诵笔记.md(PyMongo 4.19.0 · MongoDB 8.0.32 实测 49/49) +MongoDB高级知识与分片实验手册.md(副本集 / 分片实测) 口径:Python 3.x + pymongo 4;mongosh 用db.xxx形式。✅=实测通过,⚠️=实测踩过的坑。
一、连接
python
from pymongo import MongoClient
client = MongoClient(
'mongodb://<user>:<pwd>@127.0.0.1:27017/<db>?authSource=admin',
serverSelectionTimeoutMS=3000, # 选不出节点多久抛异常(默认 30s,测试调小)
)
db = client['djtest']
users = db['users'] # 集合不存在时首次写入自动创建
bash
# mongosh 连接
mongosh "mongodb://<user>:<pwd>@127.0.0.1:27017/<db>?authSource=admin"
# SSH 隧道(远程容器/内网)
ssh -N -L 27018:127.0.0.1:27017 myserver
⚠️ authSource 必须写认证库 :账号建在 admin 就写 ?authSource=admin,否则 AuthenticationFailed。
- 无模式:同一集合文档字段可以完全不同,不需要迁移。
MongoClient自带拓扑感知 + 自动重连,断线恢复后无需重建客户端 ✅。
二、增删改查
python
# ── 增 ──
users.insert_one({'name': 'tom', 'tags': ['a', 'b'], 'profile': {'score': 90}})
users.insert_many([{...}, {...}], ordered=False) # ordered=False:跳过错误继续插
r = users.insert_one(doc); r.inserted_id # ObjectId 回填
# ── 查 ──
users.find_one({'name': 'tom'})
users.find({'age': {'$gte': 15}}) # 游标,list() 才取回
users.count_documents({}) # 精确计数
users.estimated_document_count() # 元数据估算(快)
users.distinct('city') # 去重值列表
users.find().sort('age', -1).skip(1).limit(3) # 分页:-1 降序
users.find({'name': 'tom'}, {'_id': 0, 'name': 1}) # 投影:1 保留、0 排除
users.find({'city': 'bj'}).hint('city_1').max_time_ms(500) # 强制索引 + 超时
# ── 改 ──
users.update_one({'name': 'tom'}, {'$set': {'age': 21}}) # res.modified_count
users.update_many({'city': 'bj'}, {'$inc': {'age': 1}})
users.replace_one({'name': 'u2'}, {'name': 'u2', 'age': 99}) # 整体替换
users.find_one_and_update({'name': 'u3'}, {'$set': {'age': 77}}) # 默认返回更新前
users.find_one_and_update(
{'name': 'u3'}, {'$set': {'age': 77}},
return_document=ReturnDocument.AFTER, # 返回更新后
)
users.update_one({'name': 'u4'}, {'$set': {'age': 1}}, upsert=True) # 不存在则插入
# ── 删 ──
users.delete_one({'name': 'arr'}) # res.deleted_count
users.delete_many({'name': {'$in': ['u6']}})
js
// mongosh 等价写法
db.users.insertOne({ name: "tom", tags: ["a","b"] })
db.users.find({ age: { $gte: 15 } }).sort({ age: -1 }).skip(1).limit(3)
db.users.updateOne({ name: "tom" }, { $set: { age: 21 } })
db.users.deleteMany({ name: { $in: ["u6"] } })
嵌套与数组查询(面试常问)
python
users.find({'profile.score': 90}) # 点号路径直达深层
users.find({'tags': 'a'}) # 数组包含 a
users.find({'scores': {'$gt': 75}}) # 数组里有任一元素 >75
users.find({'tags': {'$all': ['a', 'b']}}) # 同时包含 a 和 b
users.find({'items': {'$elemMatch': {'qty': {'$gte': 2}}}}) # 同一元素满足多条件
ObjectId 转换
python
from bson import ObjectId
ObjectId('66a100000000000000000001') # 非法字符串抛 bson.errors.InvalidId ✅
client['djtest']['users'].find_one({'_id': ObjectId(s)}) # URL 传来的 id 必须先转换 ✅
三、索引与执行计划
python
from pymongo import ASCENDING, DESCENDING
users.create_index([('name', ASCENDING)], unique=True, name='uniq_name')
users.create_index([('city', ASCENDING), ('age', DESCENDING)]) # 复合索引
users.create_index([('createdAt', ASCENDING)], expireAfterSeconds=3600) # TTL 索引
users.create_index(
[('status', ASCENDING), ('createdAt', DESCENDING)],
partialFilterExpression={'status': {'$in': ['已发货', '已完成']}}, # 部分索引
)
users.list_indexes()
users.drop_index('uniq_name')
plan = users.find({'name': 'n5'}).explain()
plan['queryPlanner']['winningPlan'] # COLLSCAN / IXSCAN / FETCH
plan['executionStats']['totalKeysExamined']
plan['executionStats']['totalDocsExamined'] # 走索引=1,全表=1000 ✅
plan['executionStats']['nReturned']
ESR 规则:复合索引字段顺序 = Equality(等值)→ Sort(排序)→ Range(范围)。
覆盖查询 :查询字段 + 投影字段全部在索引里,执行计划出现 PROJECTION_COVERED,不 FETCH。
四、聚合管道
python
users.aggregate([
{'$match': {'city': 'bj'}}, # WHERE
{'$group': {'_id': '$city', 'n': {'$sum': 1}, # GROUP BY + 聚合
'avg_age': {'$avg': '$age'}, 'max_age': {'$max': '$age'}}},
{'$sort': {'n': -1}}, {'$limit': 5}, # ORDER BY + LIMIT
])
js
// 商品销量 TOP5:先 unwind 再 group
db.orders.aggregate([
{ $unwind: "$items" },
{ $group: { _id: "$items.name", qty: { $sum: "$items.qty" } } },
{ $sort: { qty: -1 } },
{ $limit: 5 }
]);
// $lookup = LEFT JOIN,再 $unwind 拆数组
db.orders.aggregate([
{ $match: { status: "已完成" } },
{ $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } },
{ $unwind: "$user" },
{ $project: { orderNo: 1, payAmount: 1, "user.nickname": 1 } }
]);
// $graphLookup:分类树 / 关系链路
db.categories.aggregate([
{ $graphLookup: {
from: "categories", startWith: "$parentId",
connectFromField: "parentId", connectToField: "_id",
as: "ancestors" } }
]);
// $facet:一次返回列表 + 总数 + 统计
db.orders.aggregate([
{ $facet: {
list: [{ $sort: { createdAt: -1 } }, { $limit: 10 }],
total: [{ $count: "n" }],
byStatus: [{ $group: { _id: "$status", n: { $sum: 1 } } }] } }
]);
⚠️ $lookup 的 localField(本表字段)与 foreignField(对方字段)必须是真实存在的键 ,拼错不报错、只是关联为空。 ⚠️ 管道优化:$match / $limit 尽量前置以利用索引;$project 提前裁剪字段。
五、事务、原子性与并发
单文档原子(够用就不上事务)
python
users.update_one({'name': 'u1'}, {'$inc': {'age': 1}}) # 单文档天然原子 ✅
条件更新防超卖
python
r = products.update_one(
{'_id': pid, 'stock': {'$gte': 2}},
{'$inc': {'stock': -2}},
)
if r.matched_count == 0:
raise RuntimeError('库存不足') # 没匹配上 = 条件不满足
乐观并发(版本号)
python
r = users.update_one(
{'_id': uid, 'version': 3},
{'$set': {'name': 'new'}, '$inc': {'version': 1}},
)
if r.matched_count == 0:
raise RuntimeError('并发冲突,重试')
多文档事务(必须副本集)
python
with client.start_session() as session:
session.start_transaction()
try:
orders.insert_one(order_doc, session=session)
r = products.update_one(
{'_id': pid, 'stock': {'$gte': 1}},
{'$inc': {'stock': -1}},
session=session,
)
if r.matched_count == 0:
raise RuntimeError('库存不足')
session.commit_transaction()
except Exception:
session.abort_transaction()
raise
⚠️ 单节点跑 start_transaction 报 OperationFailure: Transaction numbers are only allowed on a replica set member or mongos------单机版没有事务,需副本集。 ⚠️ 事务默认 60 秒超时、受 16MB oplog 条目限制,不能在里面建索引 / 建集合。
六、写关注 / 读关注 / 读偏好
python
from pymongo import MongoClient, ReadPreference
from pymongo.write_concern import WriteConcern
from pymongo.read_concern import ReadConcern
client = MongoClient(uri, w='majority', wtimeoutms=5000, journal=True)
# w: 0 不等待 / 1 主库确认 / majority 多数派确认
# journal=True 写 journal 才算成功;wtimeoutms 超时报 WriteConcernError
coll = users.with_options(write_concern=WriteConcern(w=1)) # 单集合降级写关注 ✅
coll.insert_one({'name': 'x'})
db = client.get_database('djtest', read_concern=ReadConcern('majority'))
coll = db.get_collection('users', read_preference=ReadPreference.SECONDARY_PREFERRED)
⚠️ pymongo 4 把 wtimeout 改名 wtimeoutms:旧写法直接 ConfigurationError: Unknown option: wtimeout。 ⚠️ pymongo 4 移除了 insert_one(write_concern=...):TypeError: unexpected keyword argument,要用 with_options()。
七、Schema 校验与 TTL
python
db.create_collection('orders_v2', validator={
'$jsonSchema': {
'bsonType': 'object',
'required': ['orderNo', 'userId', 'payAmount', 'status'],
'properties': {
'orderNo': {'bsonType': 'string'},
'payAmount': {'bsonType': 'number', 'minimum': 0},
'status': {'enum': ['待付款', '已支付', '已发货', '已完成', '已取消']},
},
},
}, validationLevel='strict', validationAction='error')
# validationLevel: off / strict / moderate
# validationAction: error / warn
db.sessions.create_index([('createdAt', 1)], expireAfterSeconds=3600) # 到期自动删
TTL 后台线程每 60 秒扫一次,删除不保证精确到秒。
八、批量与运维脚本
python
from pymongo import UpdateOne, InsertOne, DeleteOne
users.bulk_write([
UpdateOne({'name': 'u1'}, {'$set': {'age': 1}}),
UpdateOne({'name': 'u2'}, {'$set': {'age': 2}}),
], ordered=False) # 一次 RTT 批量写 ✅
last_id = None
while True: # 游标分页,替代深 skip
q = {'_id': {'$gt': last_id}} if last_id else {}
batch = list(users.find(q).sort('_id', 1).limit(100))
if not batch:
break
last_id = batch[-1]['_id']
...
js
// mongosh 运维
db.stats()
db.users.stats()
db.users.totalIndexSize()
db.setProfilingLevel(1, { slowms: 100 }) // 慢查询 profiler:0 关 / 1 慢 / 2 全部
db.system.profile.find().sort({ ts: -1 }).limit(5)
db.currentOp()
db.serverStatus()
bash
# 备份 / 导入导出
mongodump --uri="mongodb://..." --out=./dump
mongorestore --uri="mongodb://..." ./dump
mongoexport --uri="..." --collection=users --out=users.json
mongoimport --uri="..." --collection=users --file=users.json
九、副本集脚本
js
rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "127.0.0.1:27017" }] })
rs.status() // 节点角色、健康状态
rs.conf() // 配置:priority / hidden / votes
rs.add("127.0.0.1:27018") // 加从节点
rs.stepDown(60) // 主动让出主节点 60 秒,演练切换
rs.freeze(30) // 冻结本节点 30 秒不参选
- 单节点副本集也能开事务,便于本地测试。
- Change Streams 依赖副本集 / 分片集群,可监听 insert/update/delete。
十、分片脚本(实测版)
搭建(Docker 组件)
bash
sudo docker network create shardnet
# Config Server(必须副本集)
sudo docker run -d --name mongo-cfg1 --network shardnet \
-v mongo-cfg1-data:/data/db mongo:8.0 \
mongod --configsvr --replSet rs-config --port 27019 \
--dbpath /data/db --bind_ip_all --wiredTigerCacheSizeGB 0.25
# Shard(每个 shard 也是副本集)
sudo docker run -d --name mongo-shard1 --network shardnet \
-v mongo-shard1-data:/data/db mongo:8.0 \
mongod --shardsvr --replSet rs-shard1 --port 27018 \
--dbpath /data/db --bind_ip_all --wiredTigerCacheSizeGB 0.25
# mongos(只绑回环,安全)
sudo docker run -d --name mongo-mongos --network shardnet \
-p 127.0.0.1:27020:27017 mongo:8.0 \
mongos --configdb rs-config/mongo-cfg1:27019 --bind_ip_all --port 27017
# 初始化副本集 + 加 shard
sudo docker exec mongo-cfg1 mongosh --port 27019 --quiet \
--eval 'rs.initiate({_id:"rs-config",members:[{_id:0,host:"mongo-cfg1:27019"}]})'
sudo docker exec mongo-mongos mongosh --port 27017 --quiet \
--eval 'sh.addShard("rs-shard1/mongo-shard1:27018")'
开启分片并写入
js
db.getSiblingDB("config").settings.updateOne(
{ _id: "chunksize" }, { $set: { value: 1 } }, { upsert: true } // 调 1MB 便于观察
);
sh.enableSharding("shardlab");
sh.shardCollection("shardlab.orders", { userId: "hashed" }); // 哈希分片
const m = db.getSiblingDB("shardlab");
let batch = [];
for (let i = 0; i < 50000; i++) {
batch.push({ orderNo: "SO" + (100000 + i),
userId: Math.floor(Math.random() * 5000),
amount: Math.round(Math.random() * 500000) / 100,
status: "已完成",
createdAt: new Date() });
if (batch.length === 1000) { m.orders.insertMany(batch, { ordered: false }); batch = []; }
}
if (batch.length) m.orders.insertMany(batch, { ordered: false });
验证与手动操作
js
sh.status() // 集群全貌
sh.getBalancerState() // balancer 开关
db.orders.getShardDistribution() // 每个 shard 数据量
db.orders.find({ userId: 42 }).explain("executionStats").queryPlanner.winningPlan
// 带 shard key -> stage: SINGLE_SHARD;不带 -> SHARD_MERGE(广播)
sh.splitFind("shardlab.orders", { userId: 888 }) // 分裂 chunk
sh.moveChunk("shardlab.orders", { userId: 42 }, "rs-shard2") // 手动迁移
sh.stopBalancer() / sh.startBalancer()
db.getSiblingDB("config").chunks.countDocuments({ ns: "shardlab.orders" })
db.getSiblingDB("config").collections.findOne({ _id: "shardlab.orders" })
⚠️ 哈希 shard key 不能手动 sh.splitAt(..., {userId: 1250}):报 MongoServerError: split failed :: Value of type 'int' is not allowed for hashed fields,split point 必须是哈希后的 64 位 Long。 ⚠️ 分片集合上的唯一索引必须包含 shard key (_id 除外),否则 Index build failed; 正确写法:db.orders.createIndex({ userId: 1, orderNo: 1 }, { unique: true })。 ⚠️ 数据均衡看的是数据量不是 chunk 数:实测 chunk 拆成 4:1 后,数据仍是 50:50。
十一、异常对照表
| 异常 | 触发场景 | 处理 |
|---|---|---|
ServerSelectionTimeoutError |
连不上 / 节点不可选 | 检查网络、serverSelectionTimeoutMS、副本集状态 |
DuplicateKeyError |
唯一索引冲突 | 对应 MySQL 的 1062,捕获后走幂等逻辑 ✅ |
InvalidId |
ObjectId('badid') 非法 |
先校验 24 位十六进制再转换 ✅ |
OperationFailure (code 13) |
权限不足 | 检查 RBAC 角色、authSource ✅ |
OperationFailure: Transaction numbers... |
单节点开事务 | 改用副本集 ✅ |
ConfigurationError: Unknown option: wtimeout |
pymongo 4 旧写法 | 改 wtimeoutms ✅ |
TypeError: unexpected keyword argument 'write_concern' |
pymongo 4 移除参数 | 改 with_options(write_concern=...) ✅ |
AuthenticationFailed |
authSource 填错 | 填账号所在库(常见 admin)✅ |
十二、清理与重建
bash
sudo docker stop mongo-mongos mongo-shard1 mongo-shard2 mongo-cfg1 # 保留数据卷
sudo docker start mongo-cfg1 mongo-shard1 mongo-shard2 # 等 PRIMARY 后再起 mongos
sudo docker start mongo-mongos
sudo docker rm -f mongo-mongos mongo-shard1 mongo-shard2 mongo-cfg1 # 彻底删除(谨慎)
sudo docker volume rm mongo-cfg1-data mongo-shard1-data mongo-shard2-data
sudo docker network rm shardnet
十三、速记
- 单文档原子;多文档事务要副本集(4.0+)/ 分片(4.2+)。
- 写关注:
w:1主库确认、w:'majority'多数派、j:true落 journal。 - 读偏好:
primary/primaryPreferred/secondary/secondaryPreferred/nearest。 - 索引顺序按 ESR:等值 → 排序 → 范围。
- 分片 key 选高基数 + 写分散 + 覆盖主要查询,且值不可变。
- 深分页别用大
skip,用_id > last_id游标。