1. 项目背景
业务场景:运营团队看着后台的 5000 个商品犯了愁——"我只需要数码类目下、100 到 500 元之间、带'蓝牙'标签的商品,目前导出全量数据在 Excel 里筛选,每次要花半小时。"产品经理同样急:"用户画像功能上线后,需要给'买了数码产品、也买过家居、标记为高价值用户'的人群推优惠券,这个查询能一次搞定吗?"更致命的是,运营上传了一个 CSV 批量更新了部分商品标签,结果因为 $set 会覆盖整个数组,导致旧标签全部丢失。这些问题都源于对 MongoDB 查询条件和数组操作的掌握不够。
痛点:查询写不好不仅慢,还可能错:$or 不加索引导致全表扫描,生产 CPU 飙满;$elemMatch 和普通的数组字段匹配结果不同,谁用谁懵;正则查询 /keyword/ 默认大小写敏感,中文模糊搜索用正则基本等于自杀;$in 配合大量值查询时,索引失效后变成超时杀手;不理解 $and 和直接写多个条件的区别,导致查询条件互相覆盖。
2. 项目设计
小胖(抱着一袋零食):大师,我现在能插入数据也能查了,但运营说他们要在 5000 个商品里找出"价格在 100 到 500 之间、属于数码或家居类目、带有'蓝牙'标签"的商品。这 MongoDB 能一条命令搞定吗?
大师:当然可以,而且比 SQL 直观。在 MongoDB 里,查询条件本身就是 JSON,嵌套组合不需要拼接字符串——你直接在代码里构建一个查询对象就能跑。比如你说的这个需求:
db.products.find({
price: { $gte: 100, $lte: 500 },
category: { $in: ["数码影音", "家居生活"] },
tags: "蓝牙"
})
小胖:等等,tags: "蓝牙" 这么简单?默认就是在数组里找?
大师:对,MongoDB 的设计非常人性化——当你对一个数组字段写 tags: "蓝牙" 时,MongoDB 自动理解为"数组里存在值为'蓝牙'的元素"。如果写成 tags: ["蓝牙", "降噪"],那就是"数组完全等于 ["蓝牙","降噪"],顺序也必须一致"。
技术映射:MongoDB 对数组字段的查询做了"简写处理"——标量值匹配(如 "蓝牙")等价于"数组中包含该元素";数组值匹配(如 ["蓝牙","降噪"])等价于精确匹配。
小白(抬起头):那如果我要查包含"蓝牙"和"降噪"两个标签的商品呢?用 tags: ["蓝牙", "降噪"] 是不行的,因为顺序和元素数量必须完全一致。
大师:好问题,这就要用到 $all 操作符了:
db.products.find({ tags: { $all: ["蓝牙", "降噪"] } })
$all 的含义是"数组中必须包含指定列表的全部元素",元素顺序无所谓,数组中可以有更多元素。
小胖:那 $elemMatch 又是什么?我在同事的代码里见过,感觉跟普通查询差不多?
大师:这就涉及到数组查询最容易搞混的地方。当数组元素是嵌套对象时,普通的条件写法可能出现"跨元素匹配"的陷阱。举个例子:
小白:跨元素匹配?什么意思?
大师:假设你有一个订单集合,每个文档的 items 数组如下:
{ items: [
{ name: "键盘", price: 199, qty: 2 },
{ name: "鼠标", price: 29, qty: 1 }
]}
如果你想找出"单价小于 50 且数量大于 1"的商品所在的订单,这样写:
db.orders.find({ "items.price": { $lt: 50 }, "items.qty": { $gt: 1 } })
这个查询会匹配到上面的订单——因为"价格 < 50"有鼠标(29)满足,"数量 > 1"有键盘(2)满足,但并不是同一行!这就是"跨元素匹配"——条件在数组的不同元素上分别满足,就被匹配了。
小胖:我去,那正确写法是什么?
大师:用 $elemMatch:
db.orders.find({
items: {
$elemMatch: { price: { $lt: 50 }, qty: { $gt: 1 } }
}
})
$elemMatch 要求同一个数组元素同时满足所有条件,而不是各条件在数组中分别找满足的元素。
技术映射:. 点记法查询字段时不关心条件是否来自同一元素;$elemMatch 强制条件必须在同一个子文档内满足。这是 MongoDB 数组查询中最重要的概念之一。
大师(继续深入):除了条件和数组操作,逻辑操作符 $and、$or、$not、$nor 在实际业务中也非常重要。
小胖:$and 感觉很多余啊,直接写多个条件不就是 AND 吗?
大师:大部分情况下确实可以省略 $and,但有两个场景必须显式使用:
-
同一字段多个条件:比如"年龄大于 18 且小于 60"用
age: { $gt: 18, $lt: 60 }就够了,但如果是"年龄大于 18 或小于 18"这种自相矛盾的不算。真正需要$and的情况是——"年龄既大于 18 也大于 30",这其实等价于$gt: 30,但如果条件涉及不同操作符的复杂组合,$and让意图更清晰。 -
多个条件针对同一字段但语法冲突:比如同时用
$or和$gt在同一个字段上,必须用$and包裹:
db.users.find({
$and: [
{ $or: [{ tags: "高价值" }, { balance: { $gt: 500 } }] },
{ status: "活跃" }
]
})
// 解释:(有"高价值"标签 或 余额>500) 且 状态为活跃
小白:那 $not 和 $nor 呢?我看它们不是简单取反,跟直接用 $ne 有什么区别?
大师:$ne 是"字段 ≠ 某值",$not 是"取反整个条件表达式"。比如 $not: { $gt: 100 } 等价于 $lte: 100,但有时候 $not 配合正则用更直观。
$nor 是"所有条件都不满足",即 !(A 或 B 或 C):
// 找出既不是新用户也不是VIP的用户
db.users.find({
$nor: [
{ tags: "新用户" },
{ tags: "VIP" }
]
})
技术映射:$ne = 字段级不等于;$not = 表达式级取反;$nor = 多条件全不满足。
大师:最后说下模糊查询。很多人上来就 name: /keyword/,这在百万级数据上是灾难——正则无法走索引(除非用了前缀固定的 ^keyword 模式)。中文模糊搜索更应该交给 Elasticsearch,MongoDB 的正则只适合少量数据的简单匹配。
3. 项目实战
3.1 环境准备
沿用之前的 Docker MongoDB 环境:
docker compose -f mongodb-lab/docker-compose.yml ps
3.2 分步实现
步骤一:初始化用户画像数据
目标:为本地生活电商创建带有丰富标签的用户数据,模拟用户画像场景。
use local_life
// 清理旧数据
db.user_profiles.drop()
// 插入 30 条用户画像数据(模拟真实用户特征差异)
db.user_profiles.insertMany([
{ name: "赵一", age: 22, city: "深圳", balance: NumberDecimal("120.50"),
tags: ["新用户", "学生", "数码爱好者"],
purchases: [{ name: "手机壳", price: 29, qty: 1 }, { name: "耳机", price: 199, qty: 1 }],
status: "活跃" },
{ name: "钱二", age: 35, city: "广州", balance: NumberDecimal("2800.00"),
tags: ["VIP", "高价值", "家居达人"],
purchases: [{ name: "吸尘器", price: 1299, qty: 1 }, { name: "毛巾", price: 19, qty: 10 }],
status: "活跃" },
{ name: "孙三", age: 28, city: "深圳", balance: NumberDecimal("300.00"),
tags: ["数码爱好者", "游戏玩家"],
purchases: [{ name: "键盘", price: 299, qty: 1 }, { name: "鼠标", price: 99, qty: 2 }],
status: "活跃" },
{ name: "李四", age: 40, city: "北京", balance: NumberDecimal("50.00"),
tags: ["新用户"],
purchases: [{ name: "数据线", price: 9.9, qty: 1 }],
status: "沉默" },
{ name: "周五", age: 25, city: "深圳", balance: NumberDecimal("5800.00"),
tags: ["VIP", "高价值", "数码爱好者", "游戏玩家"],
purchases: [{ name: "游戏主机", price: 3499, qty: 1 }, { name: "手柄", price: 399, qty: 2 }],
status: "活跃" },
{ name: "吴六", age: 33, city: "广州", balance: NumberDecimal("1500.00"),
tags: ["家居达人", "美妆用户"],
purchases: [{ name: "台灯", price: 199, qty: 1 }, { name: "香水", price: 699, qty: 1 }],
status: "活跃" },
{ name: "郑七", age: 19, city: "深圳", balance: NumberDecimal("15.00"),
tags: ["新用户", "学生"],
purchases: [{ name: "笔记本", price: 6, qty: 2 }],
status: "活跃" },
{ name: "王八", age: 45, city: "上海", balance: NumberDecimal("12000.00"),
tags: ["VIP", "高价值", "家居达人", "美食家"],
purchases: [{ name: "冰箱", price: 8999, qty: 1 }, { name: "烤箱", price: 1999, qty: 1 }],
status: "活跃" }
])
// 创建索引
db.user_profiles.createIndex({ age: 1 })
db.user_profiles.createIndex({ city: 1 })
db.user_profiles.createIndex({ tags: 1 })
db.user_profiles.createIndex({ "purchases.price": 1 })
步骤二:条件操作符实战
目标:掌握 $eq、$ne、$gt、$gte、$lt、$lte、$in、$nin 的组合使用。
// ---- 1. 基础比较 ----
// 年龄在 20 到 35 之间(含边界)
const youngAdults = db.user_profiles.find({
age: { $gte: 20, $lte: 35 }
}).count()
print("20-35岁用户:", youngAdults)
// 余额大于 1000 的高价值用户
const richUsers = db.user_profiles.find({
balance: { $gt: NumberDecimal("1000") }
}).project({ name: 1, balance: 1 }).toArray()
print("高余额用户:")
richUsers.forEach(u => print(` ${u.name}: ${u.balance}`))
// ---- 2. $in / $nin ----
// 深圳或广州的用户
const gzsz = db.user_profiles.find({
city: { $in: ["深圳", "广州"] }
}).project({ name: 1, city: 1 }).toArray()
print("\n深圳/广州用户:")
gzsz.forEach(u => print(` ${u.name} - ${u.city}`))
// 排除新用户和学生标签($nin 同样支持数组字段)
const noStudent = db.user_profiles.find({
tags: { $nin: ["学生", "新用户"] }
}).project({ name: 1, tags: 1 }).toArray()
print("\n非学生/非新用户:")
noStudent.forEach(u => print(` ${u.name}: [${u.tags}]`))
// ---- 3. 组合条件(隐含 AND) ----
// 深圳 + 活跃 + 年龄 > 25
const combo = db.user_profiles.find({
city: "深圳",
status: "活跃",
age: { $gt: 25 }
}).project({ name: 1, age: 1, city: 1 }).toArray()
print("\n深圳活跃且>25岁:")
combo.forEach(u => print(` ${u.name}, ${u.age}岁`))
步骤三:逻辑操作符实战
目标:掌握 $or、$nor、$not 和显式 $and 的用法。
// ---- 1. $or:或条件 ----
// 找"VIP用户"或"余额>1000"的用户(用于定向发优惠券)
const targetUsers = db.user_profiles.find({
$or: [
{ tags: "VIP" },
{ balance: { $gt: NumberDecimal("1000") } }
]
}).project({ name: 1, tags: 1, balance: 1 }).toArray()
print("=== 营销目标用户 (VIP 或 余额>1000) ===")
targetUsers.forEach(u => print(` ${u.name} | tags=[${u.tags}] | 余额=${u.balance}`))
// ---- 2. $nor:所有条件都不满足 ----
// 既不是VIP也不是高价值也不是家居达人
const ordinaryUsers = db.user_profiles.find({
$nor: [
{ tags: "VIP" },
{ tags: "高价值" },
{ tags: "家居达人" }
]
}).project({ name: 1, tags: 1 }).toArray()
print("\n=== 普通用户 (非VIP/非高价值/非家居达人) ===")
ordinaryUsers.forEach(u => print(` ${u.name}: [${u.tags}]`))
// ---- 3. 复杂嵌套逻辑 ----
// (有数码爱好者标签 且 在深圳) 或 (余额>1000 且 在北京)
const complex = db.user_profiles.find({
$or: [
{ $and: [{ tags: "数码爱好者" }, { city: "深圳" }] },
{ $and: [{ balance: { $gt: NumberDecimal("1000") } }, { city: "北京" }] }
]
}).project({ name: 1, city: 1, tags: 1, balance: 1 }).toArray()
print("\n=== 复杂逻辑查询结果 ===")
complex.forEach(u => print(` ${u.name} | ${u.city} | tags=[${u.tags}] | ${u.balance}`))
// ---- 4. $not:取反 ----
// 年龄不在 25-35 之间的用户
const notMiddle = db.user_profiles.find({
age: { $not: { $gte: 25, $lte: 35 } }
}).project({ name: 1, age: 1 }).toArray()
print("\n=== 年龄不在25-35 ===")
notMiddle.forEach(u => print(` ${u.name}: ${u.age}岁`))
// 等价写法:{ $or: [{ age: { $lt: 25 } }, { age: { $gt: 35 } }] }
可能的坑:
$or中的每个条件相当于独立查询,都会匹配索引,所以最好每个$or的条件字段都有索引。$not无法走索引——因为取反后选择性极差,几乎退化为全表扫描,尽量避免在线上大数据量用$not。$nor同样不走索引,应当作为最后手段。
步骤四:数组操作实战——$all、$elemMatch、$size
目标:掌握 MongoDB 最强大也是最多坑的数组查询能力。
// ---- 1. $all:必须包含全部指定元素 ----
const digitalPlusGamer = db.user_profiles.find({
tags: { $all: ["数码爱好者", "游戏玩家"] } // 同时有这两个标签
}).project({ name: 1, tags: 1 }).toArray()
print("=== 数码爱好者 + 游戏玩家 ===")
digitalPlusGamer.forEach(u => print(` ${u.name}: [${u.tags}]`))
// ---- 2. 标签精确匹配 vs 包含匹配 ----
// 包含"学生"标签
const hasStudentTag = db.user_profiles.find({ tags: "学生" }).count()
print("\n有'学生'标签的用户:", hasStudentTag)
// 标签恰好等于 ["新用户", "学生"]
const exactTags = db.user_profiles.find({
tags: ["新用户", "学生"] // 严格匹配:两个元素,顺序也必须一致
}).count()
// 注意:这要求 tags 数组的元素和顺序完全匹配,极少使用
print("标签严格等于['新用户','学生']:", exactTags)
// ---- 3. $elemMatch:元素级条件(杜绝跨元素匹配) ----
// 查找"至少有一个购买记录,单价>500 且 数量>1"的用户
const bigPurchase = db.user_profiles.find({
purchases: {
$elemMatch: { price: { $gt: 500 }, qty: { $gt: 1 } }
}
}).project({ name: 1, "purchases.name": 1, "purchases.price": 1 }).toArray()
print("\n=== 有大额购买记录的用户($elemMatch) ===")
bigPurchase.forEach(u => print(` ${u.name}: ${JSON.stringify(u.purchases)}`))
// 对比:不用 $elemMatch 的跨元素匹配陷阱
// 下面这个查询会命中"有商品<50元 且 有商品>1件"的订单,但不要求是同一个商品
const falseMatch = db.user_profiles.find({
"purchases.price": { $lt: 30 },
"purchases.qty": { $gt: 5 }
}).project({ name: 1 }).toArray()
print("\n无$elemMatch的陷阱结果:")
falseMatch.forEach(u => print(` ${u.name}`))
// ---- 4. $size:数组长度匹配 ----
// purchases 数组长度为 2 的用户
const twoItems = db.user_profiles.find({
purchases: { $size: 2 }
}).project({ name: 1 }).toArray()
print("\n=== 恰好有2条购买记录的用户 ===")
twoItems.forEach(u => print(` ${u.name}`))
// ⚠️ $size 只能匹配精确长度,不支持 $gt / $lt
// 如果需要"长度大于 N",可以用索引字段存储数组长度,如 purchaseCount
// ---- 5. 数组字段投影控制 ----
// 只返回 purchases 数组中价格 > 100 的元素($elemMatch 投影)
const projection = db.user_profiles.find(
{ tags: "数码爱好者" },
{
name: 1,
purchases: {
$elemMatch: { price: { $gt: 100 } } // 投影级别过滤
}
}
).toArray()
print("\n=== 数码爱好者的大额购买记录(投影过滤) ===")
projection.forEach(u => {
print(` ${u.name}: ${JSON.stringify(u.purchases)}`)
})
// 注意:$elemMatch 在投影中只返回第一个匹配的元素,而非所有匹配的
步骤五:正则查询与模糊搜索
目标:理解 MongoDB 正则查询的能力边界和性能风险。
// ---- 正则查询 ----
// 以"周"开头的用户名
const zhouUsers = db.user_profiles.find({
name: /^周/
}).project({ name: 1 }).toArray()
print("=== 姓'周'的用户 ===")
zhouUsers.forEach(u => print(` ${u.name}`))
// 忽略大小写匹配
const caseInsensitive = db.user_profiles.find({
name: /^zhou/i // i = case-insensitive
}).project({ name: 1 }).toArray()
print("\n=== 忽略大小写 'zhou' ===")
caseInsensitive.forEach(u => print(` ${u.name}`))
// 包含"数码"的标签(对数组字段的正则)
const digitalTag = db.user_profiles.find({
tags: /数码/ // 正则应用于每个数组元素
}).project({ name: 1, tags: 1 }).toArray()
print("\n=== 包含'数码'标签 ===")
digitalTag.forEach(u => print(` ${u.name}: [${u.tags}]`))
// ---- 性能陷阱 ----
// 正则如果不以 ^ 开头(前缀匹配),无法使用索引
// 下面这个查询即使在 name 上有索引,也不会用到:
const explainRegex = db.user_profiles.find({
name: /三/
}).explain("executionStats")
print("\n正则不以前缀开头的执行计划:")
print(" stage:", explainRegex.executionStats.executionStages.stage)
// 期望输出:COLLSCAN(全表扫描!)
步骤六:综合筛选——运营商品搜索接口
目标:实现一个电商运营后台常用的综合商品筛选功能。
// 准备商品数据
db.products_filter.drop()
const categoriesList = ["数码影音", "手机配件", "家居生活", "美妆个护", "食品饮料"]
const brands = ["华为", "小米", "苹果", "三星", "无品牌"]
const filterDocs = []
for (let i = 1; i <= 100; i++) {
filterDocs.push({
name: `${brands[i % 5]}_商品_${i}`,
category: categoriesList[i % 5],
price: NumberDecimal((Math.random() * 2000 + 10).toFixed(2)),
stock: Math.floor(Math.random() * 500),
rating: (Math.random() * 5).toFixed(1),
tags: [
i % 2 === 0 ? "包邮" : null,
i % 3 === 0 ? "热销" : null,
i % 5 === 0 ? "新品" : null,
i % 7 === 0 ? "限时" : null
].filter(Boolean), // 过滤掉 null
sales: Math.floor(Math.random() * 10000),
createdAt: new Date(2026, 0, i % 30 + 1),
status: i % 8 === 0 ? "下架" : "在售"
})
}
db.products_filter.insertMany(filterDocs)
// 创建索引
db.products_filter.createIndex({ category: 1, price: 1 })
db.products_filter.createIndex({ tags: 1 })
db.products_filter.createIndex({ rating: -1 })
db.products_filter.createIndex({ status: 1, sales: -1 })
// ---- 综合筛选函数(模拟后端接口) ----
function searchProducts({
categories = [],
priceMin = 0,
priceMax = 10000,
tags = [],
ratingMin = 0,
keyword = "",
status = "在售",
sortBy = "sales",
page = 1,
pageSize = 20
}) {
const filter = {}
// 状态过滤
if (status) filter.status = status
// 类目多选
if (categories.length > 0) filter.category = { $in: categories }
// 价格区间
filter.price = {
$gte: NumberDecimal(String(priceMin)),
$lte: NumberDecimal(String(priceMax))
}
// 标签:必须同时包含所有指定标签($all)
if (tags.length > 0) filter.tags = { $all: tags }
// 评分下限
if (ratingMin > 0) filter.rating = { $gte: ratingMin }
// 关键词模糊搜索(在 name 字段,用正则前缀匹配才能走索引)
if (keyword) filter.name = { $regex: `^.*${keyword}.*`, $options: "i" }
// 排序
const sort = {}
sort[sortBy] = -1 // 默认降序
const total = db.products_filter.countDocuments(filter)
const list = db.products_filter.find(filter)
.sort(sort)
.skip((page - 1) * pageSize)
.limit(pageSize)
.project({ name: 1, category: 1, price: 1, rating: 1, sales: 1, tags: 1 })
.toArray()
return { total, page, pageSize, list }
}
// ---- 测试筛选 ----
const result1 = searchProducts({
categories: ["数码影音", "手机配件"],
priceMin: 100,
priceMax: 1000,
tags: ["包邮"],
ratingMin: 3,
page: 1,
pageSize: 10
})
print(`=== 数码类 + 100-1000元 + 包邮 + 3分以上 ===`)
print(`共 ${result1.total} 个商品,当前页 ${result1.list.length} 个`)
result1.list.forEach(p => {
print(` ${p.name} | ${p.category} | ¥${p.price} | ${p.rating}分 | 销量${p.sales}`)
})
// 只查"新品"标签
const result2 = searchProducts({ tags: ["新品"], page: 1, pageSize: 10 })
print(`\n=== 新品标签商品 ===`)
print(`共 ${result2.total} 个商品`)
result2.list.forEach(p => print(` ${p.name} | 标签:[${p.tags}]`))
3.3 完整代码清单
| 文件 | 用途 |
|---|---|
mongodb-lab/scripts/ch05-query-conditions.js |
条件与逻辑操作符演练 |
mongodb-lab/scripts/ch05-array-operations.js |
数组操作与 $elemMatch 演示 |
mongodb-lab/scripts/ch05-product-search.js |
综合筛选函数实现 |
3.4 测试验证
use local_life
// 1. 验证条件操作符
assert = (condition, msg) => { if (!condition) throw new Error("FAIL: " + msg); else print("PASS:", msg) }
const ageCount = db.user_profiles.countDocuments({ age: { $gte: 20, $lte: 30 } })
assert(ageCount > 0, "年龄范围查询返回了结果")
// 2. 验证 $elemMatch 的正确性
const elemMatchUsers = db.user_profiles.find({
purchases: { $elemMatch: { price: { $gt: 100 }, qty: { $gt: 1 } } }
}).toArray()
for (const u of elemMatchUsers) {
const hasValidItem = u.purchases.some(item => item.price > 100 && item.qty > 1)
assert(hasValidItem, `用户 ${u.name} 的 $elemMatch 结果与手动检查一致`)
}
// 3. 验证 $all
const allTagUsers = db.user_profiles.find({ tags: { $all: ["VIP", "高价值"] } }).toArray()
for (const u of allTagUsers) {
assert(u.tags.includes("VIP") && u.tags.includes("高价值"),
`用户 ${u.name} 同时包含 VIP 和高价值标签`)
}
// 4. 验证正则查询
const regexUsers = db.user_profiles.find({ name: /^周/ }).toArray()
for (const u of regexUsers) {
assert(u.name.startsWith("周"), `正则 ^周 匹配了 ${u.name}`)
}
// 5. 验证综合筛选
const filterResult = searchProducts({
categories: ["数码影音", "手机配件"],
priceMin: 100,
priceMax: 1000,
tags: ["包邮"]
})
assert(filterResult.total > 0, "综合筛选返回了结果")
assert(filterResult.list.every(p => p.price >= 100 && p.price <= 1000),
"价格筛选生效")
assert(filterResult.list.every(p => ["数码影音", "手机配件"].includes(p.category)),
"类目筛选生效")
print("\n=== 全部验证通过 ===")
4. 项目总结
4.1 SQL vs MongoDB 查询对比
| 需求 | SQL 写法 | MongoDB 写法 |
|---|---|---|
| 等值匹配 | WHERE category = '数码' |
{ category: "数码" } |
| 范围查询 | WHERE price BETWEEN 100 AND 500 |
{ price: { $gte: 100, $lte: 500 } } |
| 多值匹配 | WHERE category IN ('A','B') |
{ category: { $in: ["A","B"] } } |
| 或条件 | WHERE age < 18 OR balance > 1000 |
{ $or: [{age:{$lt:18}}, {balance:{$gt:1000}}] } |
| 数组包含 | WHERE ARRAY_CONTAINS(tags, 'VIP') |
{ tags: "VIP" } |
| 嵌套对象条件 | 需要 JOIN 或 JSON 函数 | { items: { $elemMatch: { price: {$gt:100} } } } |
| 模糊搜索 | WHERE name LIKE '%keyword%' |
{ name: /keyword/ } |
4.2 适用场景
MongoDB 查询条件特别适合:
- 电商商品筛选——类目、价格区间、标签、评分多维度组合筛选。
- 用户画像圈选——按标签、余额、城市等组合条件精准定位营销人群。
- 订单查询——按状态、时间范围、金额区间查找订单。
- 实时日志分析——按时间范围、日志级别、关键字快速过滤。
- IoT 数据查询——按设备 ID、传感器类型、数值范围查询。
不适用场景:
- 中文全文搜索——MongoDB 正则性能差,Text Index 中文分词弱,应使用 Elasticsearch。
- 超复杂的嵌套关联查询——超过 3 层
$elemMatch嵌套时,查询条件和代码可读性都爆炸。
4.3 注意事项
| 注意事项 | 说明 |
|---|---|
$or 中每个条件最好各自有索引 |
否则退化为全表扫描,$or 会尝试为每个子句独立使用索引 |
$not 和 $nor 不使用索引 |
取反操作选择性极差,生产环境慎用 |
正则查询不以 ^ 开头 |
无法使用索引,大数据量下慎用 |
| 数组字段的隐式包含匹配 | { tags: "A" } 和 { tags: { $all: ["A"] } } 等价,但 { tags: ["A"] } 完全不同 |
$elemMatch 投影只返回第一个匹配 |
如果需要所有匹配的元素,用聚合管道的 $filter |
$size 不接受范围 |
需要数组长度范围查询时,应冗余一个 itemCount 字段 |
4.4 常见踩坑经验
故障案例一:$or 导致索引失效
某电商后台搜索功能,代码写为 { $or: [{name: /keyword/}, {category: /keyword/}, {tags: /keyword/}] }。数据量 50 万时,页面打开要 30 秒,CPU 飙满。根因:三个正则查询没有前缀锚定,都无法走索引,$or 的三个子句各自全表扫描。解决:name 用前缀正则 ^keyword 走索引;category 改为精确匹配;tags 用精确值匹配;真正需要模糊搜索的场景引入 Elasticsearch。
故障案例二:数组字段混淆
开发用 { tags: ["包邮", "新品"] } 想查询"有包邮或新品标签"的商品,结果一条都没查到。根因:这个写法是精确匹配——要求 tags 数组恰好只有"包邮"和"新品"两个元素且按此顺序排列。解决:应使用 { tags: { $in: ["包邮", "新品"] } }(包含任一标签)或 { tags: { $all: ["包邮", "新品"] } }(同时包含两者)。
故障案例三:更新数组时覆盖掉旧数据
运营上传 CSV 批量更新商品标签,代码用 updateOne({}, {$set: {tags: ["促销"]}}) 覆盖了原有标签。根因:$set 替换整个字段值,不是追加。解决:如果要追加标签,用 $addToSet;如果要批量替换但保留某些旧标签,在应用层做好合并逻辑。
4.5 思考题
- 如何高效实现"查找标签数组中包含且仅包含 'A' 和 'B' 两个标签"的查询?(提示:不能只用
$all,还需配合$size) - 假设 users 集合中每个文档有
loginHistory数组(元素为{ip, time}),如何查询"过去 7 天内从同一 IP 登录超过 5 次的用户"?纯 MongoDB 查询能做吗?如果不能,架构上如何处理?
(答案将在第 6 章末尾揭晓)
上一章思考题答案:
MongoDB 的乐观锁实现:在文档中增加一个
version字段,更新时 filter 中加入当前版本号,并通过$inc: { version: 1 }更新版本号。如果modifiedCount = 0,说明被其他请求先改了,需要重试。示例:db.collection.updateOne({ _id: id, version: 3 }, { $set: { data: newData }, $inc: { version: 1 } })。MongoDB 不提供原生乐观锁,但通过version字段 +updateOne的 filter 可以轻松实现。
findOneAndDelete返回被删除的文档(null 表示没找到),适合需要把被删数据展示给用户或记录审计日志的场景(如撤回消息、删除评论后展示"已删除的内容")。deleteOne只返回{ acknowledged: true, deletedCount: N },不返回文档内容。如果确认不需要读取被删内容,用deleteOne更高效(不需要在删除前做一次读取)。
延伸阅读与资源
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号