大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
查询运算符
这一课你将能做到
- 熟练使用比较(
$gt/$lt/$in/$ne)、逻辑($and/$or/$not)、元素($exists/$type)三大类运算符组合出任意查询 - 正确使用
$elemMatch匹配数组元素,并说清楚它和「多个点号条件」的区别 - 用
$regex和$expr处理模糊匹配与「字段间比较」这类高级场景,知道它们的性能代价
为什么要学
find({userId: 1024}) 只能解决 10% 的查询。真实需求是:「查最近 7 天得分 60 到 100 之间的提交」「查 tags 里同时包含 mongo 和 index 的课程」「查 doneLessons 数组长度大于 5 的用户」。这些全靠运算符。
面试里「MongoDB 查询怎么写」几乎是必问,而 $elemMatch 是区分「用过」和「用明白」的分水岭。
先建立图像
运算符分类地图:
比较: $eq $ne $gt $gte $lt $lte $in $nin
逻辑: $and $or $not $nor
元素: $exists $type
数组: $all $elemMatch $size
字符串: $regex
评估: $expr(字段 vs 字段比较) $mod
隐式 AND vs 显式 $and 的区别场景:
// 这两个等价
db.submissions.find({ userId: 1024, status: "accepted" })
db.submissions.find({ $and: [ {userId: 1024}, {status: "accepted"} ] })
// 但「同一字段两个条件 + 另一个字段」时,对象 key 不能重复,必须用 $and
db.submissions.find({ $and: [ {score: {$gte: 60}}, {score: {$lt: 90}} ] })
// 其实同字段多条件写一个对象里就行:
db.submissions.find({ score: { $gte: 60, $lt: 90 } })
跟着做
准备数据:
use codeforge
db.submissions.drop()
db.submissions.insertMany([
{ userId: 1024, lessonSlug: "mdbrm-intro", status: "accepted", score: 100, attempts: 1, tags: ["easy","crud"], submittedAt: new Date("2026-10-01") },
{ userId: 1024, lessonSlug: "mdbrm-types", status: "failed", score: 40, attempts: 2, tags: ["hard"], submittedAt: new Date("2026-10-02") },
{ userId: 1024, lessonSlug: "mdbrm-crud", status: "accepted", score: 85, attempts: 3, tags: ["crud","write"], submittedAt: new Date("2026-10-04") },
{ userId: 2048, lessonSlug: "mdbrm-intro", status: "accepted", score: 60, attempts: 1, tags: ["easy"], submittedAt: new Date("2026-10-05") },
{ userId: 2048, lessonSlug: "mdbrm-types", status: "pending", score: 0, attempts: 0, submittedAt: new Date("2026-10-06") }
])
步骤 1:比较运算符
// 及格线以上
db.submissions.find({ score: { $gte: 60 } })
// 区间:60 ~ 90(含头不含尾)
db.submissions.find({ score: { $gte: 60, $lt: 90 } })
// 在集合内
db.submissions.find({ status: { $in: ["accepted", "pending"] } })
// 不等于(注意:$ne 也会匹配「字段不存在」的文档!)
db.submissions.find({ status: { $ne: "failed" } })
步骤 2:逻辑运算符
查「1024 的提交,或者所有失败的提交」:
db.submissions.find({
$or: [ { userId: 1024 }, { status: "failed" } ]
})
「及格但状态不是 accepted」(逻辑取反):
db.submissions.find({ score: { $gte: 60 }, status: { $not: { $eq: "accepted" } } })
步骤 3:元素运算符
// 有 tags 字段的文档
db.submissions.find({ tags: { $exists: true } })
// userId 被脏写成字符串的异常数据
db.submissions.find({ userId: { $type: "string" } })
步骤 4:数组运算符
// tags 包含 "crud"
db.submissions.find({ tags: "crud" })
// tags 同时包含 "easy" 和 "crud"(顺序不限)
db.submissions.find({ tags: { $all: ["easy", "crud"] } })
// tags 恰好 2 个元素
db.submissions.find({ tags: { $size: 2 } })
步骤 5:$elemMatch —— 本课重点
先造一个「数组元素是对象」的集合:
db.judgeResults.drop()
db.judgeResults.insertOne({
submissionId: 1,
cases: [
{ name: "case1", passed: true, costMs: 12 },
{ name: "case2", passed: false, costMs: 250 },
{ name: "case3", passed: true, costMs: 300 }
]
})
需求:找「存在一个用例 既通过 又耗时 > 200ms」的提交:
// ✅ 正确:条件落在同一个数组元素上
db.judgeResults.find({ cases: { $elemMatch: { passed: true, costMs: { $gt: 200 } } } })
// 命中(case3 同时满足)
对比错误写法:
// ❌ 错误:两个条件可以分别落在不同元素上
db.judgeResults.find({ "cases.passed": true, "cases.costMs": { $gt: 200 } })
// 也会命中——但语义不同!case1 passed=true,case3 costMs>200,分开满足也匹配
当前数据恰好都命中,体会不出差别;把 case3 的 passed 改成 false 再各跑一遍,$elemMatch 就不命中而错误写法仍命中。
步骤 6:$regex 与 $expr
// 模糊匹配课时 slug(前缀匹配可走索引,见索引课)
db.submissions.find({ lessonSlug: { $regex: /^mdbrm-/ } })
// 字段间比较:attempts > 2 但 score 不及格——直接用 $expr
db.submissions.find({ $expr: { $and: [ { $gt: ["$attempts", 2] }, { $lt: ["$score", 60] } ] } })
$expr 里字段名前要加 $(表示取字段值),不加 $ 的是字面量。
讲清楚要点
- 同字段多条件写在一个对象里:
{score: {$gte: 60, $lt: 90}},不需要$and。$and只在「同字段需要重复 key」或「组合$or结果」时使用。 $ne的坑:它匹配「值不等」也匹配「字段不存在」。只想排除值、不想混入缺失字段时,加$exists: true。- 数组字段的等值查询
{tags: "crud"}语义是「包含」:数组任一元素等于该值即命中,也能命中标量字段。 $elemMatch约束「同一元素」:数组内嵌对象的多字段条件,不用$elemMatch就会「跨元素满足」——这是查询结果偏多的经典原因。$size只能精确匹配长度:没有「长度大于 N」的运算符。变通:写入时维护tagCount字段,或用$expr: {$gt: [{$size: "$tags"}, 5]}。$regex性能:以^开头的前缀正则能用索引;含.*的任意位置匹配必然全表扫描,数据量大时上 Atlas Search / 分词方案。$expr让查询条件里能使用聚合表达式:字段间比较、$size、$eq拼条件都能写,但通常走不了索引(除非能等价改写为普通条件,7.x 会做部分优化)。
你可能会懵的地方
$and什么时候必须写? 只有两种情况:同一个运算符名需要在顶层重复(如两个$or的组合),或为了可读性显式分组。日常 90% 的多条件用隐式 AND 就够。$in里能放正则吗? 能:{status: {$in: [/^acc/, "pending"]}},每个元素独立匹配。$not和$ne有什么区别?$ne只做「不等」;$not包裹任意运算符做整体取反,如{score: {$not: {$gte: 60}}}。同样注意$not也匹配字段缺失的文档。- 嵌套字段条件写在
$elemMatch里还是外面? 想约束同一数组元素就写里面;条件本来就是针对不同元素/字段的(如「case1 通过且 case2 失败」)可以写外面或混合写。 $expr慢,为什么还用它? 有些语义普通查询表达不了(字段 vs 字段)。数据量小直接用;数据量大时考虑把比较结果在写入时预计算成字段(如isExpensive: true),查询走普通索引。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 数组对象多条件直接用点号 | 跨元素匹配,结果偏多 | $elemMatch |
{status: {$ne: "failed"}} 想排除失败 | 连没有 status 字段的也查出来了 | 加 status: {$exists: true, $ne: "failed"} |
全文搜索用 {$regex: /关键字/} | 全表扫描 | 前缀正则或 Atlas Search |
{$size: {$gt: 5}} | $size 不支持范围,直接报错 | 维护长度字段或 $expr + $size |
$expr 里写 { $gt: ["attempts", 2] } | 把字段名当字符串字面量,恒 false | 字段前加 $:"$attempts" |
卡点急救
- 查询结果比预期多:第一反应检查数组条件是否该用
$elemMatch;第二检查$ne/$not是否混入了字段缺失的文档。 - 查询结果比预期少:检查类型(
$type确认)、检查隐式 AND 是否把本该$or的条件写成了并列、检查日期时区。 - 正则查询超时:去掉前导
.*改前缀匹配;必须任意位置匹配就对该字段建 Atlas Search 索引,或把可搜索文本冗余成专用小集合。
面试常问
$elemMatch解决什么问题? 数组内嵌文档的多字段条件默认是「分别匹配任意元素」,可能由不同元素凑齐条件导致误匹配;$elemMatch强制所有条件落在同一个数组元素上。$or和索引的关系?$or的每个分支独立走索引再合并结果,任一分支无索引则该分支全表扫描。优化方向是每个分支都有可用索引,或改写成$in(同字段时)。$where为什么不该用? 它把 JavaScript 传到服务端逐文档执行,无法走索引、慢且有安全风险(7.x 已在很多上下文中禁用/不推荐)。字段间比较用$expr替代。
自测三问
{tags: "crud"}能匹配tags: ["crud","write"]吗?能匹配tags: "crud"(标量)吗?- 「cases 中存在通过且耗时大于 200ms 的用例」正确写法是什么?
- 为什么
{status: {$ne: "failed"}}可能把不该查的文档查出来?
小结一句话
运算符是查询的词汇表:比较管数值、逻辑管组合、$elemMatch 管数组元素边界、$expr 管字段互比——用错一个,结果就悄悄错了。
延伸
- 官方文档:Query and Projection Operators
- 下一课:投影与游标——查出来之后,怎么只取需要的字段、怎么分页遍历百万级数据。