MongoDB 练习题(12 题)
学了那么多 mongosh 语法,得动手写才算真的会。这一页给 12 道精选练习题,按难度分三级——基础 4 题(insertOne / find / delete / 投影)、操作符 4 题(比较 / 逻辑 / 字段更新 / 数组更新)、进阶 4 题(分页 / 聚合 / 关联 / 索引)。每题含题目描述、提示、可折叠的参考答案。
练习环境:本地 mongosh 或 MongoDB Atlas(免费云沙盒)。所有题目围绕一个 posts 集合展开,文档形如 { title, views, tags, author: { name, age }, createdAt }。先 insertMany 灌一批数据,再按题练习。
使用建议:先别急着展开答案。读题后自己写,卡住再看提示,实在写不出再对照参考答案。MongoDB 查询写法灵活,你的答案可能比参考更好——能跑出正确结果、可读性强就是好查询。
基础(4 题):CRUD 入门
这一组覆盖 MongoDB 的最基本操作——插入、查询、删除、字段筛选。目标是建立"文档数据库"的直觉。
第 1 题:insertOne / insertMany
题目:(1) 插入一条文档,包含 title、views、tags(数组)、author(嵌套对象)、createdAt;(2) 批量插入 3 条。
提示:insertOne({...}) 插一条;insertMany([...]) 批量插;_id 自动生成;schema 灵活,字段不必所有文档一致。
参考答案
// 插入一条文档(MongoDB 的"行"叫文档,就是 JSON 对象)
db.posts.insertOne({
title: "Hello MongoDB",
views: 0,
tags: ["入门", "笔记"],
author: { name: "小明", age: 30 },
createdAt: new Date()
});
// 批量插入
db.posts.insertMany([
{ title: "第二篇", views: 10, tags: ["进阶"] },
{ title: "第三篇", views: 20, tags: ["进阶", "笔记"] }
]);
// 文档自动生成 _id(ObjectId 类型);schema 灵活,字段不必统一
// insertOne 返回 { acknowledged: true, insertedId: ... }第 2 题:find 基础查询
题目:写出四条查询——(1) 查全部;(2) 按 title 等值查;(3) 按嵌套字段 author.name 查;(4) 按 tags 数组是否包含某元素查。再写一个计数。
提示:find({...}) 查询;嵌套字段用点号路径 "author.name";数组只要含某元素即命中;countDocuments 计数。
参考答案
// 查全部文档
db.posts.find();
// 等值查询
db.posts.find({ title: "Hello MongoDB" });
// 嵌套文档字段查询(用点号路径)
db.posts.find({ "author.name": "小明" });
// 数组字段:只要 tags 包含 "入门" 就命中
db.posts.find({ tags: "入门" });
// 计数
db.posts.countDocuments({ tags: "入门" });
// find 返回游标,pretty() 让输出更易读
db.posts.find({ views: 0 }).pretty();第 3 题:删除 deleteOne / deleteMany
题目:(1) 删除单条;(2) 删除阅读量小于 5 的全部;(3) 清空集合所有文档(保留集合);(4) 解释 deleteMany({}) vs drop() 的区别。
提示:deleteOne 删第一条匹配;deleteMany({}) 清所有文档;drop() 删整个集合(含索引,更快)。安全习惯:先 find 确认再删。
参考答案
// 删除单条(匹配到多条也只删第一条)
db.posts.deleteOne({ title: "第三篇" });
// 删除多条:views 小于 5 的全部删
db.posts.deleteMany({ views: { $lt: 5 } });
// 清空集合所有文档(保留集合与索引)
db.posts.deleteMany({});
// ⚠️ 删除整个集合(包括索引,比 deleteMany 快很多)
db.posts.drop();
// 安全习惯:先 find 确认要删的内容,再改成 deleteOne/deleteMany
db.posts.find({ views: { $lt: 5 } });第 4 题:投影(只取需要的字段)
题目:(1) 只返回 title 和 views;(2) 只返回 title 和 views,且排除 _id;(3) 排除 author 字段,返回其余;(4) findOne 取一条。
提示:find(查询条件, { 字段: 1/0 });1 包含、0 排除;_id 默认返回,要显式 _id: 0。投影能减少网络传输和内存。
参考答案
// 第二个参数控制返回哪些字段:1 包含,0 排除
// _id 默认会返回,要显式排除
db.posts.find({}, { title: 1, views: 1, _id: 0 });
// 排除某字段(返回其余全部)
db.posts.find({}, { author: 0 });
// 投影的核心价值:减少网络传输 + 内存占用
// 对应 SQL 的 SELECT col1, col2 FROM ...
// findOne 只返回一条(没匹配返回 null)
db.posts.findOne({ title: "Hello MongoDB" }, { title: 1, views: 1 });操作符(4 题):查询 + 更新
这一组是 MongoDB 的核心——$ 开头的各种操作符,让你的查询和更新表达力爆棚。
第 5 题:比较操作符 $gt / $lt / $gte / $lte
题目:(1) 阅读量大于 10;(2) 10 ≤ views ≤ 50;(3) 不等于 0;(4) tags 是"入门"或"进阶";(5) 多字段且条件。
提示:$gt / $gte / $lt / $lte / $ne / $in / $nin;同一对象里逗号分隔即隐式 AND。
参考答案
// 阅读量大于 10
db.posts.find({ views: { $gt: 10 } });
// 10 <= views <= 50
db.posts.find({ views: { $gte: 10, $lte: 50 } });
// 不等于 $ne,在数组里 $in,不在 $nin
db.posts.find({ views: { $ne: 0 } });
db.posts.find({ tags: { $in: ["入门", "进阶"] } });
db.posts.find({ tags: { $nin: ["笔记"] } });
// 多字段"且"(同一对象里逗号分隔,隐式 AND)
db.posts.find({ views: { $gte: 10 }, tags: "笔记" });第 6 题:逻辑操作符 $or / $and / $nor
题目:(1) views > 50 或 title 是某值;(2) 同一字段 views ≥ 10 且 ≤ 50(用 $and 演示);(3) 所有条件都不满足;(4) 取反某条件。
提示:$or / $and / $nor 接收数组,每个元素是条件对象;$not 接收对象。多字段 AND 通常直接逗号分隔,无需 $and。
参考答案
// $or:满足任一条件(注意是数组,每个元素是一个条件对象)
db.posts.find({
$or: [
{ views: { $gt: 50 } },
{ title: "Hello MongoDB" }
]
});
// $and:通常直接逗号分隔即可;但同一字段多条件时必须用 $and
// 例:views >= 10 且 views <= 50(可简写,这里演示语法)
db.posts.find({
$and: [
{ views: { $gte: 10 } },
{ views: { $lte: 50 } }
]
});
// $nor:所有条件都不满足
db.posts.find({ $nor: [ { views: { $gt: 0 } }, { tags: "笔记" } ] });
// $not:取反(注意是对象)
db.posts.find({ views: { $not: { $gt: 100 } } });第 7 题:$set / $unset / $rename 字段操作
题目:(1) 把某文档的 views 改成 100,嵌套字段 author.age 改成 31;(2) 删除某字段;(3) 把 createdAt 重命名为 publishedAt;(4) 给所有低阅读量文章加 promoted 字段。
提示:$set 设值(嵌套字段用点号);$unset 删字段(值随便填);$rename 重命名;updateMany 批量改。
参考答案
// $set 修改字段值(或新增字段)
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $set: { views: 100, "author.age": 31 } } // 点号改嵌套字段
);
// $unset 删除字段(值随便给,看的是 key)
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $unset: { promoted: "" } }
);
// $rename 重命名字段
db.posts.updateMany({}, { $rename: { createdAt: "publishedAt" } });
// updateMany 批量更新(所有匹配文档)
db.posts.updateMany(
{ views: { $lt: 10 } },
{ $set: { promoted: true } }
);
// updateOne 只改第一条匹配;updateMany 改全部匹配第 8 题:$inc / $push / $pull 数组与计数
题目:(1) views 自增 1(原子计数);(2) tags 数组追加一个元素;(3) 一次追加多个元素;(4) 从 tags 删除某元素;(5) 去重追加(已有则不动)。
提示:$inc 自增(原子,适合计数器);$push 追加($each 批量);$pull 删元素;$addToSet 去重追加。
参考答案
// $inc 自增(原子操作,适合计数器、点赞、库存)
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $inc: { views: 1 } } // 加 1;负数则减
);
// $push 数组追加
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $push: { tags: "MongoDB" } }
);
// $push 多个用 $each
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $push: { tags: { $each: ["数据库", "NoSQL"] } } }
);
// $pull 删除数组中所有匹配元素
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $pull: { tags: "入门" } }
);
// $addToSet:不存在才加(去重追加)
db.posts.updateOne(
{ title: "Hello MongoDB" },
{ $addToSet: { tags: "MongoDB" } } // 已有则不动
);进阶(4 题):分页 / 聚合 / 关联 / 索引
这一组把 MongoDB 推向生产级——大数据量分页、多文档统计、跨集合关联、性能优化。
第 9 题:sort + limit + skip 分页
题目:(1) 按阅读量降序取 Top 3;(2) 实现分页——每页 5 条,取第 2 页;(3) 解释大数据量下 skip 分页的性能问题,给出更好的方案。
提示:sort({ col: ±1 }) / limit(n) / skip(n);大数据量 skip 慢,改用"游标翻页"——记住上一页末尾值,查询时 $lt 它。
参考答案
// 按阅读量降序,取 Top 3
db.posts.find().sort({ views: -1 }).limit(3);
// 分页:每页 5 条,取第 2 页(跳过前 5 条)
db.posts.find().sort({ views: -1 }).skip(5).limit(5);
// sort: 1 升序,-1 降序;可多字段排序
db.posts.find().sort({ tags: 1, views: -1 });
// ⚠️ 大数据量分页 skip 会很慢(要扫过前 N 条)
// 生产环境改用"游标翻页":记住上一页末尾的排序值,查询时 $gt 它
// 第一页:db.posts.find().sort({_id:-1}).limit(5)
// 下一页:db.posts.find({_id: {$lt: lastId}}).sort({_id:-1}).limit(5)
db.posts.find().sort({ createdAt: -1 }).limit(5);第 10 题:聚合 $match + $group
题目:(1) 按标签统计文章数和平均阅读量(tips: tags 是数组,要 $unwind 展开);(2) 按作者统计阅读量 ≥ 10 的文章总阅读量,降序输出。
提示:$unwind 展开数组;$group 分组(_id 是分组键,$sum/$avg 聚合);$match 放最前能用索引提速;管道像 Unix 管道。
参考答案
// 按标签统计文章数、平均阅读量
// $unwind 把数组展开成多条(每个 tag 一条)
db.posts.aggregate([
{ $unwind: "$tags" },
{ $group: {
_id: "$tags", // 按哪个字段分组
count: { $sum: 1 }, // 计数
avgViews: { $avg: "$views" }, // 平均
totalViews: { $sum: "$views" } // 求和
}},
{ $sort: { count: -1 } }
]);
// $match 过滤(放最前面能用索引,性能更好!)
db.posts.aggregate([
{ $match: { views: { $gte: 10 } } },
{ $group: { _id: "$author.name", totalViews: { $sum: "$views" } } },
{ $sort: { totalViews: -1 } }
]);
// 管道像 Unix 管道:每步输入上一步的输出,顺序很关键第 11 题:$lookup 关联查询
题目:orders 集合(有 customerId 字段)关联 customers 集合,查出金额大于 100 的订单及其客户名。再讨论"嵌入 vs 引用"的设计抉择。
提示:$lookup { from, localField, foreignField, as } 类似 SQL 的 LEFT JOIN;输出 as 字段是数组,通常配 $unwind 展开;一对一常嵌入,一对多/多变用引用。
参考答案
// orders 集合关联 customers(类似 SQL 的 LEFT JOIN)
db.orders.aggregate([
{
$lookup: {
from: "customers", // 被关联的集合
localField: "customerId", // 当前集合的字段
foreignField: "_id", // 目标集合的字段
as: "customer" // 输出字段名(数组)
}
},
{ $unwind: "$customer" }, // 把数组展开成对象
{ $match: { amount: { $gt: 100 } } },
{ $project: { "customer.name": 1, amount: 1, _id: 0 } },
{ $sort: { amount: -1 } }
]);
// 简写(MongoDB 3.4+)
db.orders.aggregate([
{ $lookup: { from: "customers", localField: "customerId",
foreignField: "_id", as: "cust" } }
]);
// $lookup 输出永远是数组,通常配合 $unwind 展开
// 嵌入 vs 引用:一对一常嵌入;一对多、多变用引用——这是 schema 设计的核心抉择第 12 题:索引 createIndex + explain
题目:(1) 给 views 建单字段索引;(2) 建 tags + views 复合索引(说明 ESR 规则);(3) 建 title 唯一索引;(4) 建 title + content 文本索引;(5) 用 explain 看查询是否走索引。
提示:createIndex({ col: ±1 });复合索引遵循 ESR(Equality → Sort → Range);unique: true 唯一;"text" 全文;explain("executionStats") 看 IXSCAN vs COLLSCAN。
参考答案
// 单字段索引(1 升序,-1 降序)
db.posts.createIndex({ views: 1 });
// 复合索引:遵循 ESR 规则(Equality 等值 → Sort 排序 → Range 范围)
db.posts.createIndex({ tags: 1, views: -1 });
// 唯一索引(强制值不重复)
db.posts.createIndex({ title: 1 }, { unique: true });
// 文本索引(支持全文搜索)
db.posts.createIndex({ title: "text", content: "text" });
// 查看集合的所有索引
db.posts.getIndexes();
// explain 看是否命中索引(性能优化核心工具)
db.posts.find({ views: { $gt: 50 } }).explain("executionStats");
// 关键看:
// winningPlan.stage = "COLLSCAN" → 全表扫描(慢,需加索引)
// winningPlan.stage = "IXSCAN" → 走索引(快)
// totalDocsExamined → 实际扫描的文档数,越接近 n 越好练习建议
灌数据:把答案里的命令复制到 mongosh 或 Compass,先 insertMany 灌一批文档,再练习查询。光看答案不动手,MongoDB 永远学不会。
试着扩展:做完第 10 题,能不能算"每篇文章的标签数"(用 $size 或 $unwind + $group)?做完第 11 题,能不能做"统计每个客户的订单数 Top 5"?做完第 12 题,能不能对比加索引前后的 totalDocsExamined?每个扩展都是一次实战。
性能意识:写完查询用 .explain("executionStats") 看执行计划——是 COLLSCAN 全表扫描还是 IXSCAN 走索引?totalDocsExamined 是否接近返回行数?这是从"会用 MongoDB"到"精通 MongoDB"的分水岭。
← 上一篇 MongoDB 副本集与事务
返回 MongoDB 教程目录