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。
  • 一个必须先说清的事:老的 mongo shell 在 MongoDB 6.0 起就被彻底移除了,现在只有 mongosh。网上大量教程(包括本篇原文)里那些 db.coll.find().count()、db.coll.remove()、db.coll.save()、_id: ObjectId(...) 手写插入的写法,在 mongosh 里要么报警告要么直接报错。本文按 mongosh 的真实行为重写。

核心特点

  1. 文档导向:基本单位是文档(BSON),可以嵌套数组和子文档,读写直观,一次读就能拿到整个对象,不用多表 JOIN 拼回来。
  2. 动态模式:集合不预设字段约束。数据结构变化频繁的场景(爬虫、用户行为、商品属性)特别省事;但「没有约束」也意味着脏数据能进来,必要时用 $jsonSchema 校验规则补上。
  3. 可扩展:水平扩展靠分片(sharding),把集合按分片键拆到多个 mongod 上;高可用靠副本集(replica set)。
  4. 丰富的查询:等值、范围、正则、数组成员、嵌套字段、地理空间,还有聚合管道做多级加工。
  5. 索引:单字段、复合、多键(数组)、文本、地理、哈希、通配符投影索引,explain() 能看是否命中。
  6. 聚合管道:$match → $group → $project → $sort → $lookup 这种流水线,等价于 SQL 的 WHERE + GROUP BY + JOIN 组合。
  7. 驱动支持广:官方与社区驱动覆盖主流语言。
  8. 服务端 JavaScript:mongosh 本身就是 JS 环境,还能存服务端函数(db.createSystemJsFunctions 那套已废弃,现在用应用端逻辑或聚合表达式)。原文说的「支持 JavaScript 编写的存储函数」在 4.4 起已被移除,别再依赖 db.eval()。
  9. 多种部署形态:本地、Docker、MongoDB Atlas(官方云)。
  10. 社区与生态:文档、博客、工具(Compass、mongodump 家族)都很成熟。
  11. 多文档 ACID 事务:4.0 起支持,但有前提——必须跑在副本集或分片集群上。本机是单机 mongod,实测启动事务直接失败:
    1
    OperationFailure: Transaction numbers are only allowed on a replica set member or mongos
    • 所以「MongoDB 支持事务」这句话面试时要补上条件:单机实例没有事务,只有单文档的原子性(对单个文档的多字段更新天生原子)。
  12. 另有能力:时序集合(timeseries,实测可建可写)、全文搜索与向量检索($vectorSearch、Atlas Search,需要相应索引类型)、Change Streams(变更流,也要求副本集)。
  • 关于本机这套环境的一个真实状态:/etc/mongod.conf 里没有 security 段,admin 用户虽然建了,但鉴权并未开启,实测不带账号也能连、且拿到的是 root 权限。学习用没问题,一旦要放开 bindIp,必须先补:
    1
    2
    security:
    authorization: enabled
    再 systemctl restart mongod,否则等于把库裸奔到网络上。
  1. 到 官方下载中心 选 Windows、选版本(ZIP 或 MSI)。
  2. 运行 MSI 安装程序,按提示走;选「Complete」会顺带装 MongoDB Compass。
  3. 安装时可以选择「Run as Windows Service」,它会注册名为 MongoDB 的服务并自动启动;数据默认在 C:\Program Files\MongoDB\Server\9.0\data\db,日志在同目录的 log 下。
  4. 如果装的是 ZIP:解压后要自己建数据目录(默认路径是 C:\data\db),然后手动启动:
    1
    mongod --dbpath C:\data\db
  5. 把 bin 目录加进系统 Path,之后在任意目录能敲 mongod、mongosh。
  6. 另开一个终端验证:
    1
    mongosh
    1
    2
    Test it with 'db.version()'
    MongoDB 9.0.4 ...
    • 别再敲 mongo,那个命令早就没了。
  7. Windows 上表名/库名不区分大小写这一套在 MongoDB 里同样成立(MongoDB 的集合名本来就区分大小写,与平台无关,这点和 MySQL 不同)。
  1. 用 Homebrew 官方的 tap(不要装 Homebrew 核心仓库里的 mongodb-community,那个 formula 已被移除):
    1
    2
    3
    brew tap mongodb/brew
    brew install mongodb-community
    brew services start mongodb-community
  2. 验证:
    1
    mongosh --eval "db.runCommand({ping: 1})"
  3. 配置文件在 /opt/homebrew/etc/mongod.conf(Intel 机器是 /usr/local/etc/mongod.conf),数据目录在 /opt/homebrew/var/mongodb。
  4. 停止服务: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
      4
      gpg --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
    3
    echo "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。
  • ③ 安装:
    1
    2
    sudo apt-get update
    sudo apt-get install -y mongodb-org
    • mongodb-org 是元包,会带上 mongodb-org-server(mongod)、mongodb-mongosh、mongodb-org-mongos、mongodb-database-tools(mongodump 那一族)。装完实测版本:
      1
      2
      3
      mongod --version | head -1
      mongosh --version
      mongodump --version
      1
      2
      3
      db version v9.0.4
      2.14.1
      mongodump version: 100.20.1
      • 注意工具的版本号体系和 server 不一样(工具是 100.x,这是 MongoDB Database Tools 自己的版本线)。
  • ④ 启动与自启:
    1
    2
    3
    sudo systemctl start mongod
    sudo systemctl enable mongod
    systemctl is-active mongod
    1
    active
  • ⑤ 连接:
    1
    mongosh
    1
    2
    3
    4
    Current MongoDB version: 9.0.4
    ...
    test> db.runCommand({ping: 1})
    { ok: 1 }
    • 配置在 /etc/mongod.conf,实测关键项:
      1
      2
      3
      storage.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
      2
      db = 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 里看不到这个库(空库不列出),这也是新手以为「建库失败」的原因。
  • 选择数据库

    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
      3
      db.adminCommand({ listDatabases: 1 }).databases.forEach(function (d) {
      print(d.name + " " + d.sizeOnDisk + " bytes");
      });
      本机实测输出:
      1
      2
      3
      admin  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
      5
      db.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
      2
      writing `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
      3
      n,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
      12
      db.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
2
3
4
5
6
7
db.collectionName.insertOne({ name: "John Doe", age: 30, email: "johndoe@example.com" });
db.collectionName.insertMany([
{ name: "Jane Doe", age: 25, email: "janedoe@example.com" },
{ name: "Jim Beam", age: 35, email: "jimbeam@example.com" },
{ name: "Kate Bush", age: 19, email: "kate@example.com", tags: ["music", "art"] },
{ name: "Leo Sun", age: 41, email: "leo@example.com", address: { city: "Shanghai", zip: "200000" } }
]);
  • 插入文档

    • 插入单个文档

      1
      2
      3
      4
      5
      db.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
      4
      db.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 存成字符串。
      • 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"]
      • 更多操作符见「查询条件操作符」一节,那里每个都有实测结果。
    • 多条件查询

      • $and
        1
        db.collectionName.find({ $and: [{ age: { $gte: 20 } }, { name: "John Doe" }] })
        1
        ["John Doe"]
        • 同一文档上写多个等值条件时,直接并列就隐含 AND:{name: "John Doe", age: 30},不必动用 $and。$and 主要用于同一个字段两个范围(如 >=20 且 <=30)或子条件本身是 $or。
      • $or
        1
        db.collectionName.find({ $or: [{ name: "Jane Doe" }, { age: { $lt: 20 } }] })
        1
        ["Jane Doe","Kate Bush"]
      • $nor
        1
        db.collectionName.find({ $nor: [{ age: { $gt: 20 } }, { name: "John Doe" }] })
        1
        ["Kate Bush","IntId"]
        • 注意 IntId 也在结果里:它根本没有 age 字段,所以 age > 20 对它不成立,于是「两个条件都不满足」成立。这是「缺字段」在三值逻辑里的表现。
      • $not
        1
        db.collectionName.find({ name: { $not: { $regex: "^J" } } })
        1
        ["Kate Bush","Leo Sun","IntId"]
        • $not 同样会把「没有这个字段」的文档算进来。要排除它们得再加 {name: {$exists: true}}。
      • 组合 $and 与 $or
        1
        2
        3
        4
        5
        6
        db.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
        2
        sort+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)),后者在深翻页时快得多。
  • 更新文档

    • 更新单个文档

      1
      2
      3
      4
      db.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
      3
      db.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。
    • 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
    3
    db.collectionName.countDocuments()
    db.collectionName.countDocuments({ age: { $gt: 25 } })
    db.collectionName.estimatedDocumentCount()

    实测:

    1
    2
    3
    countDocuments()            -> 6
    countDocuments(age > 25) -> 3
    estimatedDocumentCount() -> 6
    • countDocuments() 是精确计数(会真的按条件扫);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。
    • $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
    6
    import 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
    9
    r = 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
    2
    insert_one -> ObjectId 6aea00fce0de4906812d23b6
    insert_many ids -> 3 acknowledged: True
    • insert_one 返回 InsertOneResult,只有 inserted_id 和 acknowledged;想拿完整结果得用 ordered=True 的 insert_many。
    • 字典的键就是字段名,值会被自动编码成对应 BSON 类型。
  • 查询文档

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    print(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
      3
      collection.count_documents({})                 # 4
      collection.count_documents({"age": {"$gt": 20}}) # 3
      collection.estimated_document_count() # 4
      • collection.count() 与 find().count() 在 PyMongo 4.x 都已删除,实测:
        1
        AttributeError: 'Cursor' object has no attribute 'count'
  • 更新文档

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    u = 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
    2
    print(collection.delete_one({"name": "Kate2"}).deleted_count)      # 1
    print(collection.delete_many({"status": "adult"}).deleted_count) # 3
  • 排序文档 / 计数文档

    1
    2
    3
    4
    for doc in collection.find().sort("age", pymongo.DESCENDING):
    print(doc)

    n = collection.count_documents({"age": {"$gt": 25}})
  • 索引

    1
    2
    3
    4
    print(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")) # None
    • create_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_update
    collection.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() 转回 Decimal
    b"\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,否则一插入就炸。
  • 聚合管道

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    list(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
    5
    db.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 + 时间桶打包,压缩率和按时间范围查询都明显好于普通集合。
  • 事务(本机不可用的实测证据)

    1
    2
    3
    with 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
    7
    from 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
      2
      client = 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
    2
    db = client['mydatabase']          # 或 client.mydatabase
    collection = db['users'] # 或 db.users
    • 两种方式等价;字段名里有 -、. 或和 Database 属性重名时用方括号形式。
    • 库和集合都不需要提前创建,第一次写入时才落地:
      1
      2
      print(client.list_database_names())     # 空库不会出现在这里
      print(db.list_collection_names())
  • 插入文档

    1
    2
    3
    4
    5
    user1 = {'name': 'Alice', 'age': 25}
    collection.insert_one(user1) # 返回 InsertOneResult

    users = [{'name': 'Bob', 'age': 30}, {'name': 'Charlie', 'age': 35}]
    collection.insert_many(users) # 返回 InsertManyResult
    • insert_one(...).inserted_id 拿到新文档的 _id。
    • 传字典会被原地补上 _id 键(如果不想被改,传之前 dict(user1) 拷一份)。
    • 批量导入建议一次多传几条,别在循环里一条条 insert_one。
  • 查询文档

    1
    2
    3
    4
    5
    6
    7
    print(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
    3
    collection.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 是同一个限制、不同来源:一个来自驱动,一个来自服务端。
  • 删除文档

    1
    2
    collection.delete_one({'name': 'Alice'})
    collection.delete_many({'age': {'$lt': 30}})
  • 索引

    1
    2
    3
    4
    5
    6
    import 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
      2
      with 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
    40
    from 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

    这个脚本演示了:

    1. 连接本地实例(并显式缩短服务发现超时)。
    2. 选择/自动创建数据库 mydatabase。
    3. 选择/自动创建集合 users。
    4. 插入单个文档、插入多个文档。
    5. 按条件查一条、遍历全部(并用投影去掉 _id)。
    6. 更新一条、删除一条。
    7. 建索引。
    8. 计数并清理、关闭连接。
  • 异常处理的最小骨架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from 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
      2
      DuplicateKeyError | 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
      3
      last_id = ...   # 上一页最后一条的 _id
      for doc in collection.find({'_id': {'$gt': last_id}}).sort('_id', 1).limit(50):
      ...
    • 索引不是越多越好:每个索引都会拖慢写入并占空间,上线前用 explain() 确认真的被用上。
    • 别把密码硬写进代码。用环境变量或配置文件,并且把配置文件排除在版本库之外。

参考