入门
连接到 MongoDB
mongosh 是现代的 MongoDB Shell(取代了旧版 mongo shell)。连接字符串遵循 URI 格式 mongodb://[user:pass@]host[:port]/[db][?options]。/admin 数据库是默认的认证数据库。你可以通过在 URI 末尾追加数据库名直接连接到特定数据库。
# connect to a local server on default port
mongosh
# connect to a specific host and port
mongosh "mongodb://localhost:27017"
# connect with authentication
mongosh "mongodb://user:pass@localhost:27017/admin"
# connect to a specific database directly
mongosh "mongodb://localhost:27017/mydb"
# show connection info
db.getMongo().getDBNames()数据库基础
MongoDB 惰性创建数据库和集合——只有在插入第一个文档后它们才会出现。'use mydb' 切换上下文,但在写入数据前不会创建数据库。show dbs 不会列出空数据库。dropDatabase 会永久删除所有集合及其数据。
# show all databases
show dbs
# switch to (or create) a database
use mydb
# show the current database
db
# create is implicit: inserting into a
# non-existent collection creates both the
# collection and the database
# drop the current database
db.dropDatabase()集合
集合类似于 SQL 表,用于存放文档。它们在首次插入时自动创建,但 createCollection 允许你设置选项,如 capped(固定大小集合,会覆盖旧文档——适用于日志)。stats() 显示存储大小、文档数量和索引详情。
# list collections in current database
show collections
db.getCollectionNames()
# create a collection explicitly with options
db.createCollection("users", {
capped: true,
size: 5242880,
max: 5000
})
# rename a collection
db.users.renameCollection("accounts")
# drop a collection
db.users.drop()
# get collection stats
db.users.stats()BSON 数据类型
BSON 扩展了 JSON,增加了应用程序必需的额外类型:ObjectId(12 字节唯一 ID)、ISODate、NumberDecimal(货币精确小数——避免浮点舍入)、NumberLong(64 位整数)、BinData(二进制)等。对货币使用 NumberDecimal 可避免经典的浮点误差。
# MongoDB stores BSON (binary JSON) with rich types
{
_id: ObjectId("65a1b2c3d4e5f6a7b8c9d0e1"),
name: "Alice", // String
age: 30, // Int32
score: NumberLong(9007199254740992), // Int64
price: NumberDecimal("19.99"), // Decimal128 (exact money)
active: true, // Boolean
birthday: ISODate("1995-08-15"), // Date
tags: ["red", "blue"], // Array
meta: { views: 100 }, // Embedded document
data: BinData(0, "aGVsbG8="), // Binary
id: UUID("...") // UUID
}Shell 辅助命令与帮助
mongosh 支持完整的 JavaScript,因此你可以在 shell 中编写循环、函数和变量。.pretty() 格式化输出以提高可读性(mongosh 默认启用)。load() 在 shell 上下文中运行 .js 文件——适用于重复的管理脚本。db.collection.help() 列出所有可用方法。
# get help on database methods
db.help()
# get help on collection methods
db.users.help()
# list common shell commands
help
# pretty-print query results
db.users.find().pretty()
# print JSON in a compact form
db.users.find().toArray()
# execute a JavaScript file
mongosh --file script.js
load("script.js")CRUD 操作
插入文档
如果你不提供 _id,MongoDB 会自动生成一个(ObjectId)。_id 在集合内必须唯一——重复的 _id 会引发写入错误。ordered:false 让 insertMany 在出错后继续插入剩余文档,提高批量加载的吞吐量。
# insert a single document
db.users.insertOne({
name: "Alice",
email: "[email protected]",
age: 30
})
# insert multiple documents
db.users.insertMany([
{ name: "Bob", age: 25 },
{ name: "Carol", age: 28 }
])
# insert with a custom _id
db.users.insertOne({ _id: 1, name: "Dave" })
# ordered insert (stops on error) vs unordered
db.users.insertMany(docs, { ordered: false })查询文档
find() 返回一个游标(惰性求值);findOne() 返回单个文档或 null。投影控制返回哪些字段——不能混用包含和排除(_id 除外)。用 1 表示包含,0 表示排除。countDocuments 是精确的;旧版 count() 对带过滤的计数已弃用。
# find all documents
db.users.find()
# find with a filter
db.users.find({ age: 30 })
db.users.find({ "address.city": "NYC" })
# find one matching document
db.users.findOne({ name: "Alice" })
# projection: include/exclude fields
db.users.find({}, { name: 1, age: 1, _id: 0 })
db.users.find({}, { email: 0 })
# count matching documents
db.users.countDocuments({ age: { $gte: 18 } })更新文档
updateOne/updateMany 使用更新操作符($set、$inc 等)修改特定字段。replaceOne 替换整个文档(保留 _id)。upsert:true 在没有匹配时创建新文档——适用于'创建或更新'模式。始终使用 $set 更新字段;裸对象会替换文档(旧行为)。
# update one document
db.users.updateOne(
{ name: "Alice" },
{ $set: { age: 31, status: "active" } }
)
# update many documents
db.users.updateMany(
{ status: "pending" },
{ $set: { status: "active" } }
)
# replace an entire document (keeps _id)
db.users.replaceOne(
{ name: "Alice" },
{ name: "Alice", age: 32, city: "NYC" }
)
# upsert: insert if no match found
db.users.updateOne(
{ name: "Eve" },
{ $set: { age: 22 } },
{ upsert: true }
)更新操作符
更新操作符在文档级别原子地修改字段。$inc 是并发安全的(没有读-改-写竞争)。$min/$max 仅在新值更小/更大时更新。$currentDate 适用于'最后修改'时间戳。多个操作符可以在一次更新中组合使用。
# $set and $unset
db.users.updateOne({n:"A"}, { $set: {age:30}, $unset: {temp:""} })
# $inc increments a numeric field
db.users.updateOne({n:"A"}, { $inc: {views: 1} })
db.users.updateOne({n:"A"}, { $inc: {balance: -50} })
# $rename a field
db.users.updateOne({n:"A"}, { $rename: {n: "name"} })
# $min / $max keep the smaller/larger value
db.users.updateOne({n:"A"}, { $min: {lowScore: 80} })
# $mul multiply the field value
db.users.updateOne({n:"A"}, { $mul: {price: 1.1} })
# $currentDate sets to current date
db.users.updateOne({n:"A"}, { $currentDate: {updatedAt: true} })删除文档
deleteOne 删除第一个匹配项;deleteMany 删除所有匹配项。deleteMany({}) 清空集合但保留索引(如果你想保留索引定义,比 drop+recreate 更快)。findOneAndDelete 原子地返回被删除的文档。删除是不可逆的——务必仔细过滤。
# delete one matching document
db.users.deleteOne({ name: "Alice" })
# delete all matching documents
db.users.deleteMany({ status: "inactive" })
# delete all documents in a collection
db.users.deleteMany({})
# findAndDelete: return the deleted document
db.users.findOneAndDelete({ name: "Bob" })
# remove is deprecated, prefer deleteOne/deleteMany
db.users.remove({ name: "Bob" }, { justOne: true })数组更新操作符
$push 追加(允许重复);$addToSet 去重。$pull 删除匹配条件的元素。位置运算符 ($) 引用查询匹配的第一个数组元素——对于在不知道索引的情况下更新特定数组 元素至关重要。$[] 更新所有数组元素。
# $push append to an array
db.users.updateOne({n:"A"}, { $push: {tags: "new"} })
# $push with $each for multiple values
db.users.updateOne({n:"A"}, { $push: {tags: { $each: ["x","y"] } } })
# $addToSet adds only if not already present
db.users.updateOne({n:"A"}, { $addToSet: {tags: "x"} })
# $pull remove all matching array elements
db.users.updateOne({n:"A"}, { $pull: {tags: "x"} })
# $pop remove first (-1) or last (1) element
db.users.updateOne({n:"A"}, { $pop: {tags: 1} })
# update an array element by position
db.users.updateOne({}, { $set: {"tags.0": "first"} })
db.users.updateOne({"tags":"old"}, { $set: {"tags.$":"new"} })查询文档
比较操作符
比较操作符是查询的基础。$in 比同一字段上多个 $or 条件高效得多。你可以在一个字段上组合多个操作符(例如 $gte 和 $lte 表示范围)。$exists:true 查找包含该字段的文档,$exists:false 查找不包含的文档。
# $eq equal (same as direct value)
db.products.find({ price: { $eq: 100 } })
# $ne not equal
db.products.find({ price: { $ne: 100 } })
# $gt, $gte, $lt, $lte
db.products.find({ price: { $gte: 50, $lte: 200 } })
# $in matches any value in an array
db.products.find({ category: { $in: ["books", "music"] } })
# $nin not in array
db.products.find({ category: { $nin: ["books"] } })
# $exists check field presence
db.products.find({ discount: { $exists: true } })逻辑操作符
隐式 AND(过滤器中的逗号)是最常见且高效的。只有当需要同一字段上的多个条件时才使用显式 $and(同一字段上的隐式 AND 会覆盖前面的条件)。$or 使用独立的索引扫描并合并结果——确保 $or 中的字段已索引以提高性能。
# $and (explicit, useful for same-field conditions)
db.users.find({ $and: [
{ age: { $gte: 18 } },
{ age: { $lte: 65 } }
]})
# $or matches if ANY condition is true
db.users.find({ $or: [
{ status: "active" },
{ vip: true }
]})
# $nor matches if NO condition is true
db.users.find({ $nor: [{ status: "active" }]})
# $not inverts a condition
db.users.find({ age: { $not: { $gt: 18 } } })
# implicit AND (comma-separated)
db.users.find({ age: { $gte: 18 }, status: "active" })元素与求值操作符
$type 按 BSON 类型过滤(适用于混合类型字段)。$regex 支持 'i'(不区分大小写)等选项——但没有前缀锚点的正则表达式无法高效使用索引。$expr 支持文档字段之间的比较,普通操作符做不到。 谨慎使用 $expr,因为它可能绕过索引。
# $type filter by BSON data type
db.users.find({ age: { $type: "int" } })
db.users.find({ age: { $type: ["int", "double"] } })
# $regex regular expression match
db.users.find({ name: { $regex: "^Al", $options: "i" } })
# $mod modulo operation
db.users.find({ age: { $mod: [2, 0] } }) # even ages
# $expr compare fields within a document
db.orders.find({ $expr: { $gt: ["$total", "$budget"] } })
# $jsonSchema validate against a schema
db.users.find({ $jsonSchema: {
required: ["name", "email"]
}})数组查询操作符
$all 查找包含所有指定值的数组(顺序无关)。当一个数组元素必须同时满足多个条件时,$elemMatch 至关重要(普通查询会分别匹配不同元素)。$size 仅匹配精确长度——没有带范围的 $size;改为预计算并将长度存储为单独字段。
# $all: array contains ALL specified values
db.posts.find({ tags: { $all: ["mongo", "db"] } })
# $elemMatch: at least one element matches all conditions
db.students.find({ scores: { $elemMatch: {
subject: "math", score: { $gte: 90 }
}}})
# $size: array of exact length
db.users.find({ tags: { $size: 3 } })
# match if array contains the value
db.posts.find({ tags: "mongo" })
# match a specific array index
db.users.find({ "scores.0": 95 }) # first score is 95游标方法与排序
skip+limit 实现分页,但对于大偏移量 skip 会变慢(它会扫描所有跳过的文档)。对于深度分页,使用键集分页:用排序字段大于最后看到的值进行查询过滤。sort() 可以使用索引来避免内存排序——对于大集合务必索引排序字段。
# limit and skip (pagination)
db.users.find().limit(10)
db.users.find().skip(20).limit(10) # page 3
# sort: 1 ascending, -1 descending
db.users.find().sort({ age: 1, name: -1 })
# count results
db.users.find({active:true}).count()
# iterate the cursor in the shell
db.users.find().forEach(doc => print(doc.name))
# get a specific field's distinct values
db.users.distinct("city")索引
创建索引
索引极大地加速查询,但会减慢写入并消耗磁盘。单字段索引支持该字段在任一排序方向上的查询。复合索引遵循 ESR(相等、排序、范围)规则以获得最佳顺序。TTL 索引会在一段时间后自动删除文档——非常适合会话和日志。
# create a single-field index
db.users.createIndex({ email: 1 }) # ascending
db.users.createIndex({ age: -1 }) # descending
# create a unique index
db.users.createIndex({ email: 1 }, { unique: true })
# create a compound index
db.users.createIndex({ lastName: 1, firstName: 1 })
# create a text index
db.posts.createIndex({ title: "text", body: "text" })
# create a TTL (time-to-live) index
db.sessions.createIndex({ createdAt: 1 },
{ expireAfterSeconds: 3600 })
# name an index explicitly
db.users.createIndex({ email: 1 }, { name: "email_idx" })查看与管理索引
getIndexes 列出所有索引及其键和选项。hideIndex/unhideIndex 让你测试删除索引的影响而无需实际删除——查询规划器会忽略隐藏的索引。如果性能保持不变,就可以安全删除。在索引变更前后始终运行 explain()。
# list all indexes on a collection
db.users.getIndexes()
# total index size in bytes
db.users.totalIndexSize()
# drop an index by name
db.users.dropIndex("email_1")
# drop all indexes except _id
db.users.dropIndexes()
# hide an index (test impact before dropping)
db.users.hideIndex("email_1")
db.users.unhideIndex("email_1")
# check if queries use indexes
db.users.find({email:"[email protected]"}).explain("executionStats")特殊索引类型
2dsphere 索引支持地理查询($near、$geoWithin)。哈希索引支持基于哈希的分片以实现均匀数据分布。通配符索引($**)覆盖文档中不可预测/可变的字段名——适用于多态数据。部分索引仅索引匹配的文档,在你只查询子集时节省空间。
# geospatial 2dsphere index (GeoJSON points)
db.places.createIndex({ location: "2dsphere" })
db.places.find({
location: { $near: {
$geometry: { type: "Point", coordinates: [-73.99, 40.73] },
$maxDistance: 1000
}}
})
# hashed index (for sharding)
db.users.createIndex({ user_id: "hashed" })
# wildcard index (index arbitrary field names)
db.products.createIndex({ "$**": 1 })
# partial index (only matching documents)
db.users.createIndex({ email: 1 },
{ partialFilterExpression: { active: true } })执行计划与查询计划
explain() 是主要的性能工具。COLLSCAN(集合扫描)表示未使用索引——对大集合来说是危险信号。IXSCAN 表示使用了索引。totalDocsExamined >> nReturned 表示索引不佳(检查很多文档却返回很少)。hint() 强制使用特定索引以测试规划器的选择。
# explain a query (basic)
db.users.find({age:30}).explain()
# explain with execution stats (timing)
db.users.find({age:30}).explain("executionStats")
# key fields in the output:
# winningPlan.stage -> COLLSCAN (bad) or IXSCAN (good)
# totalDocsExamined -> should be close to nReturned
# executionTimeMillis -> query duration
# indexUsed -> which index was chosen
# force a specific index (testing)
db.users.find({age:30}).hint("age_1")
db.users.find({age:30}).hint({age:1})索引最佳实践
ESR(相等、排序、范围)规则是最重要的复合索引设计原则:先放相等过滤字段,再放排序字段,最后放范围字段。覆盖查询(所有字段都在索引中)从不获取文档——最快的查询方式。避免使用未使用的索引;每个索引都会增加写入开销。
# ESR rule: Equality, Sort, Range for compound indexes
# equality fields first, then sort, then range
db.orders.createIndex({
status: 1, # Equality (filter)
date: 1, # Sort (order)
amount: 1 # Range (comparison)
})
# avoid over-indexing: each index slows writes
db.orders.getIndexes().length
# covered query: index satisfies query without fetching doc
db.users.createIndex({ name: 1, email: 1 })
db.users.find({ name: "A" }, { _id: 0, name: 1, email: 1 })
# background index build (deprecated in 4.2+, builds don't block)
db.users.createIndex({ name: 1 }) # non-blocking by default聚合框架
聚合管道基础
聚合管道按顺序通过阶段处理文档。管道早期的 $match 减少工作集(并可使用索引)。$group 类似 SQL GROUP BY。$count 是 $group + $project 的快捷方式。顺序很重要:先过滤,再分组,再排序,最后限制。
# a pipeline is an array of stages
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
{ $limit: 10 }
])
# each stage transforms documents and passes them
# to the next stage
# count documents
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $count: "shippedCount" }
])$match、$project 与 $limit
始终尽早放置 $match——它减少后续所有阶段的文档数量,并可以使用索引。$project 重塑文档并使用表达式计算新字段。在 $group 之前在 $project 中重命名或计算字段很常见。$unset 是排除字段的简写。
# $match filters documents (like find)
{ $match: { status: "active", age: { $gte: 18 } } }
# $project reshapes documents (1 include, 0 exclude)
{ $project: { name: 1, _id: 0, "address.city": 1 } }
# $project with computed fields
{ $project: {
fullName: { $concat: ["$first", " ", "$last"] },
ageNextYear: { $add: ["$age", 1] }
}}
# $limit and $skip
{ $limit: 10 }
{ $skip: 20 }
# $unset removes fields (inverse of $project)
{ $unset: "tempField" }$group 与累加器
$group 是核心聚合阶段。_id 指定分组键(使用 null 聚合所有文档)。$push 为每组构建值数组;$addToSet 去重。$first/$last 引用组内的文档顺序——如果顺序重要,请在 $group 之前使用 $sort。每阶段的内存限制为 100MB(大数据集使用 allowDiskUse)。
# $group: group by a field and aggregate
{ $group: {
_id: "$category",
count: { $sum: 1 },
totalRevenue: { $sum: "$amount" },
avgPrice: { $avg: "$price" },
minPrice: { $min: "$price" },
maxPrice: { $max: "$price" },
products: { $push: "$name" }, # array of values
firstOrder: { $first: "$date" },
lastOrder: { $last: "$date" }
}}
# group by multiple fields
{ $group: { _id: { category: "$cat", status: "$status" } } }
# group ALL documents together (_id: null)
{ $group: { _id: null, total: { $sum: "$amount" } } }$lookup(连接)
$lookup 执行左外连接——类似 SQL LEFT JOIN。连接的数据放在由 'as' 命名的数组字段中。管道形式允许你过滤和转换连接的文档。$unwind 解构数组字段(每个数组元素一个输出文档)——常在 $lookup 之后用于展平结果。在 MongoDB 中,连接比内嵌数据效率低。
# $lookup: join another collection (left outer join)
{ $lookup: {
from: "orders",
localField: "_id",
foreignField: "customerId",
as: "orders"
}}
# with pipeline (filtered/transformed join)
{ $lookup: {
from: "orders",
let: { custId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$customerId", "$$custId"] } } },
{ $project: { amount: 1, _id: 0 } }
],
as: "recentOrders"
}}
# $unwind: flatten an array field into multiple docs
{ $unwind: "$orders" }$unwind、$sort 与 $facet
$facet 在同一输入上并行运行多个子管道——非常适合需要多种数据聚合(Top N、计数、细分)的仪表板。$bucket 自动将值分类到范围中。带 preserveNullAndEmptyArrays 的 $unwind 保留数组为空或缺失的文档——有助于避免管道中的数据丢失。
# $unwind with preserveNullAndEmptyArrays
{ $unwind: {
path: "$tags",
preserveNullAndEmptyArrays: true
}}
# $sort within pipeline
{ $sort: { total: -1 } }
# $facet: run multiple pipelines in parallel
{ $facet: {
"topCustomers": [{ $sort: {total:-1} }, { $limit: 5 }],
"byCategory": [{ $group: { _id: "$cat" } }],
"totalCount": [{ $count: "n" }]
}}
# $bucket: group into ranges automatically
{ $bucket: {
groupBy: "$price",
boundaries: [0, 50, 100, 500],
default: "other",
output: { count: { $sum: 1 } }
}}全文搜索与地理空间
全文搜索
文本索引通过 $text 启用全文搜索。单词被分词和词干化。搜索默认是 OR;短语需要转义引号;减号前缀排除。$meta:'textScore' 返回相关性;按其排序可获得最佳匹配。每个集合只能有一个文本索引。对于高级搜索,使用 Atlas Search(基于 Lucene)。
# create a text index on multiple fields
db.posts.createIndex({
title: "text",
body: "text",
tags: "text"
})
# search for words (OR by default)
db.posts.find({ $text: { $search: "mongodb tutorial" } })
# exact phrase (escape quotes)
db.posts.find({ $text: { $search: "\"mongo db\"" } })
# exclude a word (minus)
db.posts.find({ $text: { $search: "mongodb -sql" } })
# sort by relevance score
db.posts.find(
{ $text: { $search: "mongodb" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })地理空间 2dsphere
GeoJSON 坐标是 [经度, 纬度](x, y 顺序——常见陷阱)。2dsphere 索引支持 $near(按距离排序)、$geoWithin(在形状内)和 $geoIntersects。$maxDistance 以米为单位。距离计算使用球体模型。对于 2D 平面数据(罕见),改用 '2d' 索引。
# store points as GeoJSON
db.places.insertOne({
name: "Park",
location: {
type: "Point",
coordinates: [-73.99, 40.73] # [lng, lat]
}
})
# create a 2dsphere index
db.places.createIndex({ location: "2dsphere" })
# find nearby places within a distance (meters)
db.places.find({
location: { $near: {
$geometry: { type: "Point", coordinates: [-73.99, 40.73] },
$maxDistance: 1000
}}
})
# find places within a polygon
db.places.find({
location: { $geoWithin: {
$geometry: { type: "Polygon", coordinates: [[[-73,40],[-74,40],[-74,41],[-73,41],[-73,40]]] }
}}
})地理空间距离与操作符
$nearSphere 使用精确的球面几何(更适合大距离)。$geoNear 聚合阶段是唯一能在结果中返回计算距离的方式——distanceField 以米(球面)或弧度存储。$centerSphere 对半径使用弧度(弧度 = 距离_公里 / 6371)。
# $nearSphere: accurate spherical distance
db.places.find({
location: { $nearSphere: {
$geometry: { type: "Point", coordinates: [-73.99, 40.73] },
$minDistance: 100,
$maxDistance: 5000
}}
})
# $geoNear aggregation stage (includes distance)
db.places.aggregate([{
$geoNear: {
near: { type: "Point", coordinates: [-73.99, 40.73] },
distanceField: "dist",
maxDistance: 2000,
spherical: true
}
}])
# $center / $centerSphere for simple radius
db.places.find({
location: { $geoWithin: { $centerSphere: [[-73.99,40.73], 0.01] } }
})正则表达式与模式匹配
前缀锚定正则表达式(/^Ali/)可以高效使用索引;未锚定的模式(/ali/)不能使用索引并扫描整个集合。对于大集合上的不区分大小写搜索,优先使用文本索引或 Atlas Search 而非正则表达式。$regexMatch(聚合)允许你在计算字段和条件中使用正则表达式结果。
# case-insensitive regex
db.users.find({ name: { $regex: /alice/i } })
# prefix-anchored regex CAN use an index
db.users.find({ name: { $regex: /^Ali/ } })
# wildcard match
db.users.find({ email: { $regex: /@example\.com$/ } })
# $regex with options string
db.users.find({ name: { $regex: "ali", $options: "ix" } })
# use $expr with $regexMatch for conditional logic
db.users.aggregate([{
$project: {
name: 1,
isAlice: { $regexMatch: { input: "$name", regex: /^ali/i } }
}
}])通配符与灵活查询
$where 对每个文档运行 JavaScript——非常慢,应避免;$expr 通常可以替代它。$elemMatch 确保一个数组元素匹配所有条件(普通逗号查询会匹配不同元素)。$expr 支持普通操作符无法做到的字段间比较——但它可能无法高效使用索引。
# $jsonSchema validation as a query filter
db.users.find({ $jsonSchema: {
bsonType: "object",
required: ["name"],
properties: { age: { bsonType: "int", minimum: 0 } }
}})
# $where (JavaScript) — slow, avoids indexes
db.users.find({ $where: "this.a + this.b > 10" })
# $mod modulo operator
db.users.find({ age: { $mod: [10, 0] } })
# $expr: compare two fields
db.orders.find({ $expr: { $lt: ["$paid", "$total"] } })
# $elemMatch with multiple conditions on array elements
db.scores.find({ results: {
$elemMatch: { $gte: 80, $lt: 90 }
}})事务
ACID 事务
多文档事务提供跨集合的 ACID 保证(需要副本集或分片集群)。将 session 传递给事务中的每个操作。commitTransaction 使更改永久且可见;abortTransaction 回滚所有更改。保持事务简短——它们持有锁并可能影响性能。
# multi-document transaction (replica set required)
const session = db.getMongo().startSession()
session.startTransaction()
try {
db.accounts.updateOne(
{ name: "Alice" }, { $inc: { balance: -100 } }, { session }
)
db.accounts.updateOne(
{ name: "Bob" }, { $inc: { balance: 100 } }, { session }
)
session.commitTransaction()
} catch (e) {
session.abortTransaction()
print("Transaction aborted: " + e)
} finally {
session.endSession()
}事务读写关注
读关注控制读取的'新鲜度'和一致性。'snapshot' 提供跨分片的事务一致性视图——适用于报表。写关注控制持久性:w:majority 确保写入在确认前已复制(能在主节点故障转移中存活);j:true 等待日志刷新。更高的关注级别更安全但更慢。
# set transaction-level read/write concern
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority", j: true, wtimeout: 5000 }
})
# read concern levels:
# "local" - sees latest local data (default)
# "majority" - committed, replicated to majority
# "snapshot" - consistent snapshot across shards
# write concern levels:
# { w: 1 } - ack from primary (default)
# { w: "majority" } - ack from majority of members
# { j: true } - wait for journal write重试逻辑与错误
MongoDB 用 errorLabels 标记可重试错误:TransientTransactionError(重试整个事务)和 UnknownCommitResult(仅重试提交)。生产驱动程序提供内置重试。尽可能设计幂等事务。避免运行时间长、持有 锁并阻塞其他操作的事务。
# transactions can fail with TransientTransactionError
# retry the whole transaction on these errors
function runTransaction(txnFunc) {
while (true) {
const session = db.getMongo().startSession()
try {
session.startTransaction()
txnFunc(session)
session.commitTransaction()
return
} catch (e) {
session.abortTransaction()
if (e.errorLabels &&
e.errorLabels.includes("TransientTransactionError")) {
continue // retry
}
throw e // non-retryable error
} finally {
session.endSession()
}
}
}原子单文档操作
单文档操作默认是原子的——不需要事务。findOneAndUpdate/findOneAndReplace/findOneAndDelete 原子地返回文档(非常适合生成顺序 ID)。对于许多工作负载,将相关数据内嵌在一个文档中并使用原子操作符($inc、$push)完全避免了多文档事务的需要。
# single-document operations are ALWAYS atomic
db.counters.updateOne(
{ _id: "pageViews" },
{ $inc: { count: 1 } }
)
# findAndModify: atomically find & update, return the doc
db.sequences.findOneAndUpdate(
{ _id: "orderId" },
{ $inc: { seq: 1 } },
{ returnNewDocument: true }
)
# atomic upsert (create-or-increment)
db.counters.updateOne(
{ _id: "visits", date: "2025-01-01" },
{ $inc: { count: 1 } },
{ upsert: true }
)
# use $expr for conditional atomic updates
db.products.updateMany(
{ $expr: { $lt: ["$stock", "$reorderLevel"] } },
{ $set: { needsRestock: true } }
)两阶段提交(模式)
两阶段提交模式在没有原生多文档事务的情况下模拟分布式事务。它跟踪'pending'状态,以便恢复进程在传输中途崩溃时可以恢复或回滚。当有原生事务可用时请改用原生事务——此模式复杂且容易出错,主要出于教育和遗留目的保留。
# pattern for atomic transfers without transactions
# (pre-4.0 compatibility, or cross-system transfers)
# 1. create a pending transaction document
db.transactions.insertOne({
_id: "t1", from: "Alice", to: "Bob", amount: 100, state: "pending"
})
# 2. debit source
db.accounts.updateOne(
{ name: "Alice", pendingTransactions: { $ne: "t1" } },
{ $inc: { balance: -100 }, $push: { pendingTransactions: "t1" } }
)
# 3. credit destination
db.accounts.updateOne(
{ name: "Bob", pendingTransactions: { $ne: "t1" } },
{ $inc: { balance: 100 }, $push: { pendingTransactions: "t1" } }
)
# 4. mark transaction done
db.transactions.updateOne({ _id: "t1" }, { $set: { state: "done" } })
# 5. clean up pending references
db.accounts.updateMany({},
{ $pull: { pendingTransactions: "t1" } })数据建模
内嵌与引用
MongoDB 建模的关键决策:对于一起访问且很少更改的数据内嵌;对于大型、共享或独立更新的数据引用。当'has-a'关系是独占的(用户的地址)时内嵌。当数据被重用(多个订单中的产品)或无界增长(用户数百万条日志条目)时引用。
# EMBED: store related data in one document
db.users.insertOne({
name: "Alice",
addresses: [
{ city: "NYC", zip: "10001" },
{ city: "LA", zip: "90001" }
]
})
# Pros: single read, atomic update
# Cons: document size limit (16MB), data duplication
# REFERENCE: store IDs, join with $lookup
db.users.insertOne({ name: "Alice" })
db.orders.insertOne({
customerId: ObjectId("..."),
amount: 100
})
# Pros: no duplication, independent updates
# Cons: needs joins, more queries一对多关系
对于'少'的关系,内嵌。对于'多',从子文档引用(每个子文档上的父 ID)。对于'非常多'(数百万),使用桶模式:将子文档分组到父级拥有的桶中(例如按月),以平衡文档大小和查询速度。这避免了无界数组和过多的子文档。
# FEW: embed (e.g., a few addresses per user)
db.users.insertOne({
name: "Alice",
addresses: [{ city: "NYC" }, { city: "LA" }]
})
# MANY: reference with parent pointer
db.posts.insertOne({ title: "Hello", authorId: ObjectId("...") })
# VERY MANY: reference from parent (bucket pattern)
db.users.updateOne(
{ _id: userId },
{ $push: { postIds: postId } } # beware 16MB limit
)
# BUCKET pattern for time series (thousands of items)
db.buckets.insertOne({
ownerId: userId,
date: "2025-01",
measurements: [ {t: 1, v: 30}, {t: 2, v: 31} ]
})多对多关系
对于多对多,根据查询 模式和数据大小选择方法。双向数组速度快但需要双重维护。连接集合(映射表)最灵活且扩展性最好——它可以存储关系元数据(注册日期、成绩)并避免膨胀父文档。
# option 1: arrays of ObjectIds on both sides
db.students.insertOne({ name: "Alice", courseIds: [c1, c2] })
db.courses.insertOne({ name: "Math", studentIds: [s1, s2] })
# option 2: one side references (smaller side)
db.students.insertOne({ name: "Alice", courseIds: [c1, c2] })
# option 3: mapping/junction collection
db.enrollments.insertOne({
studentId: s1,
courseId: c1,
enrolledAt: ISODate(),
grade: "A"
})
# query: find a student's courses
db.enrollments.find({ studentId: s1 })模式设计模式
属性模式处理具有可变属性的实体(不同规格的产品)——避免稀疏字段并支持动态属性。多态模式使用类型鉴别器在一个集合中存储相关但不同的形状。异常模式通过引用而非内嵌来处理特殊文档(拥有数百万粉丝的名人)。
# ATTRIBUTE PATTERN: flexible attributes (e.g., products)
db.products.insertOne({
name: "Laptop",
attributes: [
{ k: "cpu", v: "i7" },
{ k: "ram", v: 16 }
]
})
# query: db.products.find({ "attributes.k": "cpu", "attributes.v": "i7" })
# POLYMORPHIC PATTERN: one collection, type discriminator
db.events.insertMany([
{ type: "click", url: "/home", userId: 1 },
{ type: "purchase", orderId: 99, total: 50 }
])
# OUTLIER PATTERN: cap large lists
db.users.updateOne({n:"A"}, { $set: { reviews: "see reviews collection" } })模式验证
模式验证在写入时强制执行文档结构——MongoDB 对固定模式的回应。validationLevel 'strict' 验证所有写入;'moderate' 仅验证插入和完全更新。validationAction 'error' 拒绝无效文档;'warn' 记录但接受。验证补充(而非取代)应用层验证。
# add a JSON schema validator to a collection
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email"],
properties: {
name: { bsonType: "string", maxLength: 100 },
email: { bsonType: "string", pattern: "^\S+@\S+$" },
age: { bsonType: "int", minimum: 0, maximum: 150 },
role: { enum: ["admin", "user", "guest"] }
}
}
},
validationLevel: "strict",
validationAction: "error"
})
# add validation to an existing collection
db.runCommand({ collMod: "users", validator: { ... } })
# check existing validation rules
db.getCollectionInfos({ name: "users" })副本集
副本集基础
副本集提供高可用性和读取扩展。只有一个成员是主节点(接受写入);其他成员异步复制。如果主节点故障,选举会自动提升一个从节点。奇数投票成员(最少 3 个)可避免选举平局。rs.status() 显示每个成员的健康状况、状态和复制延迟。
# a replica set = 1 primary + N secondaries
# all writes go to the primary; secondaries replicate
# initiate a replica set
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "host1:27017" },
{ _id: 1, host: "host2:27017" },
{ _id: 2, host: "host3:27017" }
]
})
# check replica set status
rs.status()
rs.conf()
# step down the primary (force an election)
rs.stepDown(60)
# add a new member
rs.add("host4:27017")
rs.remove("host4:27017")读偏好
读偏好平衡一致性和负载。primary 保证你总是读取最新写入。secondary/secondaryPreferred 从主节点卸载读取,但数据可能过时(复制延迟,通常为毫秒级)。nearest 最小化延迟——适合地理分布的应用。分析/报表工作负载应使用从节点读取。
# control where reads are routed
# primary (default) - always read from primary (strongest consistency)
db.users.find().readPref("primary")
# primaryPreferred - primary if available, else secondary
db.users.find().readPref("primaryPreferred")
# secondary - only secondaries (may be stale)
db.users.find().readPref("secondary")
# secondaryPreferred - prefer secondaries to offload primary
db.users.find().readPref("secondaryPreferred")
# nearest - lowest latency member
db.users.find().readPref("nearest")
# set in connection string
mongodb://host1:27017/mydb?replicaSet=rs0&readPreference=secondary写关注
写关注用速度换取安全性。w:1 在主节 点写入后立即确认(快但如果主节点在复制前崩溃则有数据丢失风险)。w:majority 确保写入已复制——能在单节点故障中存活(推荐用于重要数据)。j:true 等待磁盘日志刷新。在集群级别设置合理的默认值。
# control how durable a write must be before ack
# w: 1 - primary acks (default, fastest, least safe)
db.users.insertOne(doc, { writeConcern: { w: 1 } })
# w: "majority" - majority of members confirmed
db.users.insertOne(doc, { writeConcern: { w: "majority" } })
# j: true - wait for journal flush (survives crash)
db.users.insertOne(doc, { writeConcern: { w: 1, j: true } })
# wtimeout - max wait time in ms
db.users.insertOne(doc, {
writeConcern: { w: "majority", wtimeout: 5000 }
})
# set default write concern for the cluster
db.adminCommand({
setDefaultRWConcern: { defaultWriteConcern: { w: "majority" } }
})仲裁者与选举
仲裁者提供一票以达到多数但不存储数据——适用于双节点部署,但第三个数据节点更可取。Priority 0 成员永远不会成为主节点(适合专用分析或灾难恢复节点)。隐藏成员被排除在读偏好 'nearest' 之外,适合备份。延迟成员滞后,可从意外删除中恢复。
# an arbiter votes in elections but holds no data
rs.addArb("host5:27017")
# useful to break ties in even-member sets (e.g., 2 data + 1 arbiter)
# but prefer 3 data members for durability
# force a member to never become primary (priority 0)
rs.conf().members[1].priority = 0
rs.reconfig(rs.conf())
# hidden member: not visible to clients, used for analytics/backup
rs.conf().members[2].hidden = true
rs.conf().members[2].priority = 0
rs.reconfig(rs.conf())
# delayed member: lags behind (insurance against human errors)
rs.conf().members[3].secondaryDelaySecs = 3600
rs.conf().members[3].priority = 0Oplog 与复制内部机制
oplog(操作日志)是一个记录每次写入的固定集合——从节点按顺序应用这些条目来复制。如果从节点落后超过 oplog 窗口,它需要完全重新同步。printReplicationInfo 显示 oplog 的时间跨度;printSecondaryReplicationInfo 显示每个从节点的延迟。更大的 oplog 可适应更长的停机时间。
# the oplog is a capped collection of all writes
use local
db.oplog.rs.stats()
db.oplog.rs.find().sort({ $natural: -1 }).limit(5)
# each oplog entry is an idempotent operation
# { ts: ..., op: "i", ns: "mydb.users", o: { ... } }
# check replication lag
rs.printReplicationInfo() # primary's oplog window
rs.printSecondaryReplicationInfo() # each secondary's lag
# resync a stale secondary (full re-sync)
rs.syncFrom("host2:27017")
# change the oplog size (MongoDB 4.4+)
db.adminCommand({ replSetResizeOplog: 1, size: 16384 }) # MB安全与认证
认证与用户
认证验证身份。在启用认证之前创建管理员用户,否则会被锁在外面。内置角色从只读到完全管理员。在 shell 中使用 passwordPrompt() 以避免历史记录中的明文密码。每个用户限定于一个数据库,但可以在其他数据库上有角色。在生产环境中启用 TLS(net.tls.mode)。
# enable auth: start mongod with --auth
# or set security.authorization: "enabled" in config
# create the first admin user (before enabling auth)
use admin
db.createUser({
user: "admin",
pwd: passwordPrompt(), // prompts securely
roles: [ { role: "userAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" } ]
})
# create a user for a specific database
use mydb
db.createUser({
user: "appUser",
pwd: passwordPrompt(),
roles: [ { role: "readWrite", db: "mydb" } ]
})
# authenticate as a user
db.auth("appUser", "password")内置角色与自定义角色
内置角色涵盖常见情况;自定义角色实现最小权限。权限将资源(数据库/集合)与允许的操作(find、insert、update 等)配对。授予所需的最低权限——报表用户只需要特定集合的 'find'。自定义角色存储在 admin 数据库中,可跨用户重用。
# built-in roles:
# read, readWrite, dbAdmin, dbOwner, userAdmin (per database)
# readAnyDatabase, readWriteAnyDatabase, (all databases)
# userAdminAnyDatabase, dbAdminAnyDatabase
# root (superuser), clusterAdmin, backup, restore
# create a custom role with fine-grained privileges
db.createRole({
role: "analyst",
privileges: [
{ resource: { db: "mydb", collection: "reports" },
actions: ["find"] },
{ resource: { db: "mydb", collection: "" },
actions: ["listCollections"] }
],
roles: []
})
# grant a role to a user
db.grantRolesToUser("appUser", [{ role: "analyst", db: "mydb" }])
db.revokeRolesFromUser("appUser", [{ role: "analyst", db: "mydb" }])TLS/SSL 加密
TLS 加密传输中的数据——生产环境必需。requireTLS 拒绝非 TLS 连接。对于内部 PKI,使用 x.509 证书认证(无密码——证书即凭证)。$external 数据库存储 x.509 用户。在生产中始终使用真实 CA(或内部 CA);自签名证书仅用于测试。
# enable TLS in mongod.conf
net:
tls:
mode: requireTLS
certificateKeyFile: /etc/mongodb/server.pem
CAFile: /etc/mongodb/ca.pem
# connect with TLS and verify the server cert
mongosh "mongodb://host:27017/mydb" \
--tls --tlsCAFile /etc/mongodb/ca.pem
# connect with a client certificate (x.509 auth)
mongosh "mongodb://host:27017/mydb" \
--tls --tlsCertificateKeyFile client.pem \
--tlsCAFile ca.pem \
--authenticationMechanism MONGODB-X509 \
--authenticationDatabase '$external'
# generate a self-signed cert for testing
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout server.key -out server.crt -days 365网络与审计
bindIp 限制 MongoDB 监听的网络接口——永远不要将 MongoDB 直接暴露到互联网(bindIp: 0.0.0.0 + 无认证已导致无数次数据泄露)。审计(仅企业版)记录与安全相关的操作以符合合规要求。社区版可以使用操作系统级工具(auditd)或应用级日志作为替代。
# restrict network interface in mongod.conf
net:
bindIp: 127.0.0.1,10.0.0.5 # never 0.0.0.0 in production
port: 27017
# enable auditing (enterprise feature)
auditLog:
destination: file
format: JSON
path: /var/log/mongodb/audit.log
filter: '{ atype: { $in: ["authenticate","createCollection","dropDatabase"] } }'
# view current authentication mechanisms
db.runCommand({ getCmdLineOpts: 1 })
# enable FIPS mode (government compliance)
setParameter:
fipsMode: true字段级加密 (CSFLE)
CSFLE 在客户端加密敏感字段,使服务器、备份和日志永远不会包含明文——最强的数据保护。确定性加密支持等值查询;随机加密更安全但不可查询。自动加密(通过驱动程序模式)对应用代码透明。加密密钥在 KMS(AWS KMS、本地密钥等)中管理 。
# Client-Side Field Level Encryption encrypts fields
# before sending to the server — server never sees plaintext
# define a data key in the key vault
const clientEncryption = db.getMongo().getClientEncryption()
const keyId = clientEncryption.createDataKey(
"local", { keyAltNames: ["mainKey"] }
)
# encrypt a field explicitly
const ssn = clientEncryption.encrypt(
keyId, "123-45-6789", "deterministic"
)
# store encrypted, query with encrypted value
db.patients.insertOne({ name: "Alice", ssn: ssn })
db.patients.find({
ssn: clientEncryption.encrypt(keyId, "123-45-6789", "deterministic")
})
# automatic CSFLE uses a schema to encrypt per a JSON schema
# { encryptMetadata: { keyId: [keyId], algorithm: "AEAD_AES_256_CBC..." } }备份与恢复
mongodump 与 mongorestore
mongodump 创建逻辑备份(BSON 文件)——可移植但对大数据集很慢,且没有 --oplog 时跨集合不是时间点一致的。--drop 在恢复前删除现有集合。--archive + --gzip 生成单个压缩文件。对于大型生产备份,优先使用文件系统快照或 Percona Backup 而非 mongodump。
# dump an entire database
mongodump --db=mydb --out=/backup/$(date +%F)
# dump a single collection
mongodump --db=mydb --collection=users --out=/backup
# dump with a query filter
mongodump --db=mydb --collection=logs \
--query='{"date":{"$gte":{"$date":"2025-01-01T00:00:00Z"}}}'
# compress the output archive
mongodump --archive=/backup/full.gz --gzip
# restore from a dump
mongorestore --db=mydb --drop /backup/2025-01-01/mydb
mongorestore --archive=/backup/full.gz --gzipmongoexport 与 mongoimport
mongoexport/mongoimport 处理 JSON/CSV——用于与其他系统的数据交换,不用于备份(它们会丢失 ObjectId 和 Date 精度等 BSON 类型)。备份请使用 mongodump。CSV 导出适合电子表格和 BI 工具。始终指定 --fields 以获得可预测的 CSV 列顺序。默认情况下 JSON 导出限制为每行一个文档(--jsonArray 用于单个数组)。
# export to JSON (human-readable)
mongoexport --db=mydb --collection=users \
--out=users.json
# export to CSV
mongoexport --db=mydb --collection=users \
--type=csv --fields=name,email,age --out=users.csv
# export with a query filter
mongoexport --db=mydb --collection=users \
--query='{"status":"active"}' --out=active.json
# import from JSON
mongoimport --db=mydb --collection=users --file=users.json
# import from CSV with header row
mongoimport --db=mydb --collection=users \
--type=csv --headerline --file=users.csv