MongoDB 脚本速查

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 游标。
相关推荐
随性而行3601 小时前
企业微信 API 如何实现业务消息自动通知?接口应用场景解析
java·人工智能·后端·微信·机器人·企业微信
Mav1 小时前
[个人学习记录]从零构建高性能 LLM 推理网关:长连接优雅停机(Graceful Drain)、K8s 探针联动与 Context 底层哲学
后端
团子哒哒1 小时前
变量、常量和数据类型
后端
zz在掘金337081 小时前
Redis 脚本速查
后端
初见月1 小时前
适配器模式详解+对象透传治理
后端
怕浪猫1 小时前
3 小时搭一个能用的聊天机器人后端,我用了这套技术栈
后端·面试·github
xiaoqiMikko2 小时前
Redis 实战(二):宕机会丢多少数据 —— kill -9 与真断电下 RDB / AOF 逐个实测(6.2.4)
redis·后端
李哈哈8522 小时前
微服务依赖冲突
后端
努力的小雨2 小时前
每天盯着屏幕写代码,我终于不想再凑合了
后端