上一节课我们介绍了 MongoDB 文档的基本操作,比如如何插入、更新、删除数据。接下来,我们要更进一步,学习如何通过强大的查询能力,从大量数据中高效、准确地筛选出我们想要的信息。掌握各种查询方式,是实现灵活数据检索和应对复杂业务需求的基础。

MongoDB 中的 find 方法是执行数据查询的主要接口。该方法基于提供的查询条件(以文档形式结构化表达)检索集合,并返回所有符合条件的文档结果。
在实际操作中,查询条件通常以键值对方式描述目标数据。例如,查找年龄为 25 岁的用户时,条件即为 { "age": 25 }。
|# 查找所有用户 > db.users.find() # 查找年龄为 25 岁的用户 > db.users.find({"age": 25}) # 查找姓名为「张三」的用户 > db.users.find({"name": "张三"})
当我们需要同时满足多个条件时,可以在查询文档中添加更多的键值对。这相当于在条件之间加上「并且」的逻辑关系。例如,查找年龄为 25 岁且姓名为「张三」的用户时,条件即为 { "name": "张三", "age": 25 }:
|# 查找年龄为 25 岁且姓名为「张三」的用户 > db.users.find({"name": "张三", "age": 25})
使用空查询文档(即 {})时,MongoDB 会返回集合中的全部文档,相当于执行全表扫描操作,常用于需要获取所有数据或调试场景。
在实际业务开发中,通常无需获取文档的全部字段。例如,仅需用户的姓名和邮箱用于邮件通知时,无需检索其他冗余信息。此时,可以通过 MongoDB 的 find 方法的第二个参数精确指定所需返回的字段集合,实现字段级的数据投影与精简。
|# 只返回用户名和邮箱字段 > db.users.find({}, {"username": 1, "email": 1}) # 排除特定字段,返回其他所有字段 > db.users.find({}, {"password": 0, "secret_key": 0}) # 只返回用户名,不返回默认的 _id 字段 > db.users.find({}, {"username": 1, "_id": 0})
这种字段选择不仅能减少网络传输的数据量,还能提高查询性能,特别是在处理包含大量字段的文档时。
在实际业务场景中,查询需求往往远超精确匹配的简单层级。例如,可能需要检索「年龄大于 18 岁的用户」或「价格介于 100 至 500 元之间的商品」。针对这类复杂的数据筛选,MongoDB 提供了丰富的条件与比较操作符,以支持多样化和高效的数据检索。

MongoDB 中的比较操作符可实现灵活的数值区间及条件筛选。这类操作符通常以美元符号($)为前缀,作为其操作符标识,能够覆盖大于、小于、区间、不等于等多种数据比较需求。
|# 查找年龄大于等于 18 岁的用户 > db.users.find({"age": {"$gte": 18}}) # 查找年龄在 18 到 65 岁之间的用户 > db.users.find({"age": {"$gte": 18, "$lte": 65}}) # 查找价格小于 100 元的商品 > db.products.find({"price": {"$lt": 100}})
范围查询在处理日期和时间类型数据时尤为重要。例如,当需要检索某一特定时间区间内注册的用户时:
|# 查找 2023 年 1 月 1 日之前注册的用户 > startDate = new Date("2023-01-01") > db.users.find({"registeredAt": {"$lt": startDate}})
不等于的查询可以用 $ne 操作符来实现,这样可以方便地筛除某些指定的值:
|# 查找状态不是「已删除」的用户 > db.users.find({"status": {"$ne": "deleted"}})
在需要检索字段值属于多个备选值之一的文档时,可利用 $in 操作符实现集合式匹配。这一操作符通常用于高效筛选满足任一指定取值条件的数据记录。
|# 查找类别为「电子产品」、「图书」或「服装」的商品 > db.products.find({"category": {"$in": ["电子产品", "图书", "服装"]}}) # 查找中奖号码为 100、200 或 300 的彩票 > db.lottery.find({"number": {"$in": [100, 200, 300]}})
与 $in 操作符相对,$nin 用于排除指定集合中的值,实现对不在指定列表内数据的筛选:
|# 查找类别不是「过期商品」和「下架商品」的商品 > db.products.find({"category": {"$nin": ["过期商品", "下架商品"]}})
对于需要跨多个字段或多组复杂条件进行逻辑“或”筛选的场景,可采用 $or 操作符实现跨字段的布尔运算,增强查询表达与业务规则适配能力:
|# 查找VIP用户或者消费金额大于1000元的用户 > db.users.find({"$or": [{"isVIP": true}, {"totalSpent": {"$gt": 1000}}]})
在设计OR查询时,尽量将匹配文档较多的条件放在前面,这样可以提高查询效率。
$not 是 MongoDB 中一个高级元操作符,用于对任何查询条件进行逻辑否定。例如,若需筛选出用户ID不满足某一特定条件(如不能被5整除)的用户,可以采用如下方式:
|# 查找用户ID除以5余数不为1的用户 > db.users.find({"userId": {"$not": {"$mod": [5, 1]}}})
这种否定查询在配合正则表达式使用时特别有用,可以找到不匹配某个模式的所有文档。
MongoDB 原生支持丰富的数据类型,不同类型在查询语义和操作层面均存在特有规则。

在 MongoDB 中,针对 null 值的查询具有特殊的语义。当对某字段以 null 作为查询条件时,系统既会匹配字段值确为 null 的文档,也会匹配完全未包含该字段的文档。因此,null 查询本质上是“字段不存在或其值为 null”的复合判断。
|# 这个查询会同时匹配 description 为 null 和不存在 description 字段的文档 > db.products.find({"description": null})
如果我们只想查找字段值确实为 null 的文档,需要结合 $eq 和 $exists 操作符:
|# 只查找 description 字段存在且值为 null 的文档 > db.products.find({"description": {"$eq": null, "$exists": true}})
正则表达式为 MongoDB 的文本查询提供了灵活且高效的模式匹配能力,广泛应用于模糊检索、复杂格式验证及大小写不敏感等高级文本过滤场景。
|# 查找姓名包含「张」字的用户(不区分大小写) > db.users.find({"name": {"$regex": /张/i}}) # 查找以「138」开头的手机号码 > db.users.find({"phone": /^138/}) # 查找邮箱地址以「.com」结尾的用户 > db.users.find({"email": /\.com$/})
使用以 ^ 开头的正则表达式可以利用索引提高查询性能,而包含大小写不敏感标志的正则表达式则无法使用索引。
MongoDB 针对数组类型提供了非常直观且强大的查询能力,使我们可以灵活高效地实现各种需求。查询数组字段的原理:只要数组中包含了你查询的目标值,这份文档就能被匹配出来。无需使用特别的语法,查询数组的方式与普通字段基本一致。

当我们对数组字段进行等值查询时,MongoDB 会自动检查数组内的每一项。如果任意一项与查询条件相等,则视为匹配。例如,查询所有爱好数组 hobbies 中包含「读书」的用户时,MongoDB 内部会遍历 hobbies 数组的每个元素,只要有一个元素是「读书」,该用户就会被作为结果返回。 这种查询无需指定具体位置,完全由 MongoDB 帮你处理。
|# 查找爱好包含「读书」的用户 > db.users.find({"hobbies": "读书"}) # 查找技能包含「JavaScript」的程序员 > db.programmers.find({"skills": "JavaScript"})
$all 操作符用于判断数组字段是否同时包含所有指定的值。只有当指定的每一个元素都出现在数组中时,查询才会匹配该文档。无论数组中的顺序如何,只要所有条件元素都存在即可。例如:
$all 也可以同时和多个值配合,限制数组必须包含这些值,但不要求它们在数组中的排列顺序或位置。这种方式适合于「集合包含关系」的查询场景,是处理数组多值包含需求的重要工具。
|# 查找同时具备「Python」和「Java」技能的程序员 > db.programmers.find({"skills": {"$all": ["Python", "Java"]}}) # 查找同时爱好「读书」和「运动」的用户 > db.users.find({"hobbies": {"$all": ["读书", "运动"]}})
要注意的是,$all 不关心元素在数组中的顺序,只要都存在即可。
当需要对数组字段进行严格的全等匹配(即数组元素及顺序完全一致)时,可直接以完整数组作为查询条件:
|# 只匹配爱好完全为 ["读书", "运动", "旅行"] 的用户 > db.users.find({"hobbies": ["读书", "运动", "旅行"]})
这种查询要求数组的内容和顺序完全一致,缺少或多出任何元素都不会匹配。
可以针对数组字段执行更为精准的条件,如指定数组的长度,或定位数组中特定索引位置的元素:
|# 查找恰好有3个爱好的用户 > db.users.find({"hobbies": {"$size": 3}}) # 查找第二个技能是「React」的程序员(数组索引从0开始) > db.programmers.find({"skills.1": "React"})
$slice 操作符可以让我们只返回数组的一部分:
|# 只返回前3个评论 > db.articles.find({}, {"comments": {"$slice": 3}}) # 返回最后5个评论 > db.articles.find({}, {"comments": {"$slice": -5}}) # 跳过前10个评论,返回接下来的5个 > db.articles.find({}, {"comments": {"$slice": [10, 5]}})
在对数组进行范围查询时需要特别小心。考虑这样一个查询:
|# 意图:查找分数在80-90之间的学生 > db.students.find({"scores": {"$gte": 80, "$lte": 90}})
如果某个学生的分数是 [75, 95],这个查询也会匹配,因为 95 大于 80,75 小于 90。要避免这个问题,可以使用 $elemMatch:
|# 确保同一个数组元素同时满足两个条件 > db.students.find({"scores": {"$elemMatch": {"$gte": 80, "$lte": 90}}})
在 MongoDB 中,文档可以嵌套子文档,实现复杂的数据结构和灵活的数据建模。这种嵌套关系在实际应用中非常常见,但针对嵌入文档的查询需采用针对性的技术手段。

可以通过完整指定嵌入文档的全部键值对进行查询,但此方法要求查询条件与目标文档中的嵌入子文档字段和值完全一致,并且字段顺序也必须一致:
|# 查找地址完全匹配的用户 > db.users.find({"address": {"city": "北京", "district": "朝阳区"}})
更为常用和灵活的方法是使用「点号(.)」表示法,通过指定嵌入文档中的具体字段名进行查询。例如,如果一个用户的地址信息被嵌入为 address 字段,并且其中包含 city 和 postcode 等子字段,那么可以通过 address.city 或 address.postcode 这样的点号路径来直接访问和查询嵌入文档内部的具体属性。
使用点号表示法查询时,不需要关心嵌入文档中字段的顺序,也可以仅匹配部分字段:
|# 查找居住在北京的用户 > db.users.find({"address.city": "北京"}) # 查找地址邮编为100000的用户 > db.users.find({"address.postcode": "100000"}) 卸载 # 同时查询多个嵌入字段 > db.users.find({"address.city": "北京", "address.district": "朝阳区"})
当嵌入文档以数组形式存在时,针对这些嵌入数组元素的查询会比单纯的嵌入文档更加复杂。因为数组里的每个元素本身又是一个文档,查询时需要考虑条件是作用于同一个数组元素,还是分别作用于不同的元素。
例如,设想有一个博客文章集合,其中每个文档包含一个 comments 字段,该字段是一个数组,数组内的每个元素都是一个评论对象,每个评论对象包含作者(author)和评分(score)等属性。
如果我们需要查找评论作者为「张三」且评分不低于8分的博客文章,必须确保这些条件是针对同一个评论元素进行匹配的,而不是跨多个评论元素分别匹配作者和评分。否则,可能错误地返回那些并不真正符合要求的文档:
|# 错误的查询方式:可能匹配不同评论的不同字段 > db.blog.find({"comments.author": "张三", "comments.score": {"$gte": 8}})
因此,在查询嵌入文档数组时,通常需要使用特定的操作符(如 $elemMatch)来确保查询条件严格应用于同一个数组元素:
|# 确保同一个评论同时满足作者和分数条件 > db.blog.find({"comments": {"$elemMatch": {"author": "张三", "score": {"$gte": 8}}}})
在查询嵌入数组时,始终要考虑是否需要使用 $elemMatch 来确保条件应用于同一个数组元素。
在面对复杂、标准查询操作符难以覆盖的检索需求时,MongoDB 提供了 $where 操作符,可使用 JavaScript 进行自定义查询逻辑,实现更高层次的灵活数据筛选。
$where 运算符常用于实现复杂的自定义逻辑,尤其是在需要比较同一文档内不同字段的值时。例如,若需检索库存数量与已售数量相等的商品:
|> db.products.find({"$where": function() { return this.stock === this.sold; }})
如果需要检索那些任意两个字段值相等的文档,可以这样实现:
|> db.collection.find({"$where": function() { for (var field1 in this) { for (var field2 in this) { if (field1 !== field2 && this[field1] === this[field2]) { return true; } } } return false; }})
尽管 $where 操作符为查询提供了高度的灵活性,但其使用会带来显著的性能开销:
$where 查询无法使用索引,且需要将每个文档转换为JavaScript对象,因此性能较差。应该尽量避免使用,或者与其他查询条件结合使用以减少需要处理的文档数量。
在执行查询操作时,MongoDB 并不会立即返回所有结果集,而是返回一个游标(Cursor)对象。游标为遍历和处理查询结果提供了高度的灵活性,支持高效的数据分页、排序与结果集限制等操作。

通过游标,可以逐条高效地处理大规模查询结果,适用于需要分批检索和后续处理场景:
|# 创建游标 > var cursor = db.users.find({"age": {"$gte": 18}}) # 逐个获取结果 > while (cursor.hasNext()) { var user = cursor.next(); print("用户: " + user.name); } # 使用forEach方法 > cursor.forEach(function(user) { print("邮箱: " + user.email); });
在实际开发中,分页查询是非常常见的需求。为了实现分页,我们通常会用到 limit 和 skip 这两个方法:limit 用于指定每页最多返回多少条结果,skip 用于跳过前面多少条结果。 例如,如果我们希望一次只获取 10 条数据作为一页的内容,可以使用 limit(10);如果我们要获取第 3 页的数据,并且每页 10 条,就要 skip(20) 跳过前面两页的 20 条记录,然后再 limit(10) 获取第 3 页的内容。通过这种方式,可以灵活地实现数据的浏览和翻页:
|# 获取前10个用户 > db.users.find().limit(10) # 跳过前20个用户,获取接下来的10个 > db.users.find().skip(20).limit(10) # 这些方法可以链式调用,顺序不影响结果 > db.users.find().limit(10).skip(20).sort({"age": 1})
排序是数据查询中的常用且重要的功能,能够帮助我们快速定位、组织和展示结果数据。MongoDB 提供了灵活的排序能力,不仅可以按照单个字段进行升序或降序排列,还支持对多个字段进行联合排序。 例如,我们可以同时指定先按“年龄”升序,再按“姓名”降序。
|# 按年龄升序排序 > db.users.find().sort({"age": 1}) # 按年龄降序排序 > db.users.find().sort({"age": -1}) # 多字段排序:先按年龄升序,再按姓名降序 > db.users.find().sort({"age": 1, "name": -1})
传统的 skip/limit 分页方式虽然实现简单,但在面对大量数据时性能会急剧下降。其原因在于,MongoDB 在执行 skip 操作时需要跳过前面指定数量的数据行,即使这些数据不会被返回,也必须遍历扫描。这会导致随着数据量的增长,分页位置越靠后,查询的响应速度越慢,消耗的资源也越多。
|# 效率低下的分页方式 > var page1 = db.articles.find().limit(20) > var page2 = db.articles.find().skip(20).limit(20) > var page3 = db.articles.find().skip(40).limit(20)
相比传统的 skip/limit 分页方式,更高效且常用的做法是利用排序字段的具体值(比如自增 ID 或时间戳)来实现分页。这种方式通常被称为“基于游标的分页”或“基于字段值的翻页”。它 的核心思想是:每次查询时按照某一确定的字段(如创建时间、唯一ID等)排序,每页查询结束后,记录下该页最后一条数据该字段的值。 下次请求下一页数据时,利用该值作为查询条件,查询“比上一次最后一条数据更大/更小”的新一页数据。这样可以避免遍历和跳过大量无关数据,使分页在大数据量场景下依然保持高性能和良好响应速度。
|# 第一页 > var page1 = db.articles.find().sort({"publishDate": -1}).limit(20) # 获取最后一篇文章的日期 > var lastDate = null; > page1.forEach(function(article) { lastDate = article.publishDate; // 显示文章 }); # 第二页:基于上一页的最后日期 > var page2 = db.articles.find({"publishDate": {"$lt": lastDate}}) .sort({"publishDate": -1}).limit(20)
这种方式避免了大量的跳跃操作,在处理大数据集时性能更好。
在实际应用场景中,我们经常会遇到“随机推荐一条数据”或“抽取若干条随机样本”等需求。比如:电商首页推荐随机商品、运营活动抽奖、抽取问卷样本等。 此时就需要从集合中随机获取文档。最直观、传统的方法通常是先统计集合总数,然后用 skip 随机跳过指定数量的数据,再取出一条文档。 然而,这种方式在集合较大时非常低效,因为 MongoDB 在执行 skip 操作时仍需扫描前面的所有文档,不仅慢,还极其消耗资源:
|# 效率低下的随机获取方式 > var total = db.products.count() > var randomSkip = Math.floor(Math.random() * total) > db.products.find().skip(randomSkip).limit(1)
更高效且实际可用的做法,是在每次插入文档时同时为其生成并保存一个随机数作为额外字段(比如 random),这样做可以让我们直接利用索引进行高效的随机筛选, 无需在海量数据上用 skip 扫描消耗资源。具体做法如下:每插入一条文档即生成一个 0~1 之间的随机浮点数作为 random 字段。后续需要随机获取文档时,只要在 random 字段上做大于或小于某个随机值的高效查询即可,大大提升性能和可扩展性。
|# 插入时添加随机字段 > db.products.insertOne({ "name": "智能手机", "price": 2999, "random": Math.random() }) # 随机查询 > var randomValue = Math.random() > var randomProduct = db.products.findOne({"random": {"$gte": randomValue}}) # 如果没找到,反向查找 > if (!randomProduct) { randomProduct = db.products.findOne({"random": {"$lt": randomValue}}) }
每当我们在客户端执行 find、aggregate 等查询时,MongoDB 都会在服务端为结果集分配一个游标对象。 游标在服务器上占用内存和句柄等资源,如果管理不当,大量未关闭或长时间未清理的游标可能导致服务器资源耗尽,影响数据库性能和稳定性。因此,合理管理游标的生命周期非常重要。
MongoDB 提供了多种机制来自动清理游标资源,主要包括以下几方面:
toArray() 或循环 hasNext/next 直到结束),MongoDB 会立即自动关闭该游标并释放其占用的所有服务器资源。这是最常见且推荐的游标关闭方式。close() 方法立即释放游标对应的服务器端资源,通常用于需要更精细资源控制的场景。合理利用这些机制,确保游标不会长时间闲置在服务器上,是高性能和高可用 MongoDB 应用设计的重要一环。
对于需要长时间处理的游标,可以禁用超时机制:
|# 创建不会超时的游标(需要谨慎使用) > var cursor = db.largeCollection.find().addOption(DBQuery.Option.noTimeout) # 确保手动关闭游标 > try { while (cursor.hasNext()) { // 处理文档 var doc = cursor.next(); // ... 长时间处理逻辑 } } finally { cursor.close(); // 重要:手动关闭游标 }
使用不超时的游标时,必须确保在完成处理后手动关闭游标,否则会导致服务器资源泄漏。
MongoDB 的查询系统为我们提供了强大而灵活的数据检索能力。从简单的精确匹配到复杂的聚合查询,从基础的字段查询到高级的JavaScript逻辑,掌握这些查询技巧能够帮助我们构建高效的数据库应用程序。 在实际开发中,合理选择查询方式、优化查询性能,以及正确管理游标资源,都是构建可扩展MongoDB应用的关键要素。
首先,让我们创建一些测试数据来进行练习:
|# 创建学生集合并插入数据 > db.students.insertMany([ {"name": "张三", "age": 20, "grade": "大二", "major": "计算机科学"}, {"name": "李四", "age": 22, "grade": "大三", "major": "信息工程"}, {"name": "王五", "age": 21, "grade":
问题1:查询所有计算机科学专业学生的姓名和年龄
|> db.students.find({"major": "计算机科学"}, {"name": 1, "age": 1, "_id": 0})
问题2:查询年龄大于等于20岁的学生信息
|> db.students.find({"age": {"$gte": 20}})
创建包含数组字段的商品数据:
|# 创建商品集合并插入数据 > db.products.insertMany([ {"name": "iPhone 15", "tags": ["电子产品", "手机", "苹果"], "price": 5999}, {"name": "MacBook Pro", "tags": ["电子产品", "笔记本", "苹果"], "price": 12999}, {"name": "JavaScript教程",
问题3:查询所有包含"编程"标签的商品
|> db.products.find({"tags": "编程"})
问题4:查询同时包含"电子产品"和"苹果"标签的商品
|> db.products.find({"tags": {"$all": ["电子产品", "苹果"]}})
创建包含嵌入文档的用户数据:
|# 创建用户信息集合并插入数据 > db.users.insertMany([ { "username": "zhangsan", "profile": { "realName": "张三", "city": "北京", "hobbies": ["读书", "跑步"] } }, { "username": "lisi", "profile": { "realName": "李四",
问题5:查询居住在北京的用户信息
|> db.users.find({"profile.city": "北京"})
问题6:查询爱好包含"读书"的用户信息
|> db.users.find({"profile.hobbies": "读书"})
使用之前创建的 students 集合:
问题7:查询计算机科学专业且年龄小于22岁的学生,按年龄升序排列
|> db.students.find({"major": "计算机科学", "age": {"$lt": 22}}).sort({"age": 1})
问题8:查询价格在100-10000元之间的商品,只显示名称和价格,按价格降序排列
|> db.products.find({"price": {"$gte": 100, "$lte": 10000}}, {"name": 1, "price": 1, "_id": 0}).sort({"price": -1})
使用 products 集合进行分页练习:
问题9:获取前3个最贵的商品
|> db.products.find().sort({"price": -1}).limit(3)
问题10:跳过前2个商品,获取接下来的3个商品(按价格升序)
|> db.products.find().sort({"price": 1}).skip(2).limit(3)