用 Claude Code 设计 NoSQL/MongoDB:Schema、索引与聚合实战
从访问模式出发,用 Claude Code 完成 MongoDB 建模、校验、索引、聚合、测试和上线检查。
先理解 MongoDB 的设计起点
NoSQL/MongoDB 不是把数据强行放进固定的行和列,而是按接近 JSON 的文档保存数据的数据库。因此,MongoDB 的重点不是先画出很多表,而是先确认应用会怎样读取、写入、过滤和汇总这些文档。
使用 Claude Code 时,不要只说“帮我建 users、products、orders 三个 collection”。更有效的做法是把页面、API 响应、更新频率、数据规模、报表需求和权限边界说清楚。这样 Claude Code 才能判断什么时候 embedding,什么时候 reference,哪些索引真正对应查询,聚合管道是否会变重,以及上线前应该怎样测试。
三个以上真实场景
| 场景 | 常见读取方式 | 推荐模型 | 主要风险 |
|---|---|---|---|
| 电商订单 | 用户订单列表、订单详情、月度销售额 | 在订单里嵌入购买时的商品名、价格、分类,同时保留 productId | 商品改价后错误改写历史订单 |
| SaaS 审计日志 | 按组织、用户、日期和事件查询 | 追加型文档,配合复合索引和必要的 TTL | 大集合全表扫描 |
| 内容管理系统 | 通过 slug 取文章,按状态和分类列出 | 明确 slug、status、发布时间索引 | 草稿或内部备注泄露 |
| 客服工单 | 客户队列、负责人队列、最近评论 | 有上限的评论可嵌入,附件和长文本分离 | 数组无限增长 |
如果你还在比较 MongoDB、SQL 和 ORM 的边界,可以一起参考 Claude Code 数据库设计、API 开发、Prisma ORM 和 SQL 优化。
推荐给 Claude Code 的提示词
你是 MongoDB 设计评审。
请先从访问模式出发,而不是先从 collection 名称出发。
需求:
- 用户按时间倒序查看自己的订单列表
- 订单详情显示购买时的商品名、价格和分类
- 商品主数据改价不能影响历史订单
- 管理后台需要按月份、状态、商品分类统计收入
- 只有在部分成功会破坏业务一致性时才使用 transaction
- 输出 validation schema、索引、seed 数据、aggregation、explain 检查和上线 checklist
回答里必须写清楚哪些字段是嵌入,哪些字段是引用。订单里的商品名、购买价格、购买时分类属于订单事实,通常应该嵌入。商品库存、当前描述、商品页展示信息则属于商品主数据,用 productId 引用更合适。
可直接运行的最小示例
先启动 MongoDB:
docker run --name mongo-claude-demo -p 27017:27017 -d mongo:8
安装官方 Node.js Driver:
npm init -y
npm install mongodb
npm install -D tsx typescript
mkdir -p src
创建 src/mongodb-workflow.ts:
import { MongoClient, ObjectId } from "mongodb";
const client = new MongoClient(process.env.MONGODB_URI ?? "mongodb://localhost:27017");
async function main() {
await client.connect();
const db = client.db("claude_code_shop_zh");
await db.dropDatabase();
await db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["userId", "status", "items", "totalAmount", "createdAt", "updatedAt"],
properties: {
userId: { bsonType: "objectId" },
status: { enum: ["pending", "paid", "shipped", "cancelled"] },
totalAmount: { bsonType: ["int", "long", "double", "decimal"], minimum: 0 },
createdAt: { bsonType: "date" },
updatedAt: { bsonType: "date" },
items: {
bsonType: "array",
minItems: 1,
items: {
bsonType: "object",
required: ["productId", "name", "category", "price", "quantity"],
properties: {
productId: { bsonType: "objectId" },
name: { bsonType: "string" },
category: { bsonType: "string" },
price: { bsonType: ["int", "long", "double", "decimal"], minimum: 0 },
quantity: { bsonType: "int", minimum: 1 }
}
}
}
}
}
}
});
const products = db.collection("products");
const orders = db.collection("orders");
await Promise.all([
orders.createIndex({ userId: 1, createdAt: -1 }, { name: "orders_by_user_recent" }),
orders.createIndex({ status: 1, createdAt: -1 }, { name: "orders_by_status_recent" }),
orders.createIndex({ "items.category": 1, createdAt: -1 }, { name: "orders_by_category_month" })
]);
const inserted = await products.insertMany([
{ name: "Claude Code Workshop", category: "training", currentPrice: 48000 },
{ name: "MongoDB Review Template", category: "template", currentPrice: 9800 }
]);
const userId = new ObjectId();
const now = new Date("2026-06-01T09:00:00.000Z");
await orders.insertOne({
userId,
status: "paid",
items: [
{ productId: inserted.insertedIds[0], name: "Claude Code Workshop", category: "training", price: 48000, quantity: 1 },
{ productId: inserted.insertedIds[1], name: "MongoDB Review Template", category: "template", price: 9800, quantity: 2 }
],
totalAmount: 67600,
createdAt: now,
updatedAt: now
});
const report = await orders.aggregate([
{ $match: { status: { $in: ["paid", "shipped"] } } },
{ $unwind: "$items" },
{
$group: {
_id: {
month: { $dateToString: { format: "%Y-%m", date: "$createdAt" } },
category: "$items.category"
},
revenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] } },
quantity: { $sum: "$items.quantity" }
}
},
{ $sort: { "_id.month": 1, revenue: -1 } }
]).toArray();
const explain = await orders.find({ userId }).sort({ createdAt: -1 }).limit(10).explain("executionStats");
if (report.length !== 2) throw new Error("aggregation failed");
if ((explain.executionStats?.totalDocsExamined ?? 0) > 1) throw new Error("index check failed");
console.log(JSON.stringify({ report, examined: explain.executionStats.totalDocsExamined }, null, 2));
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await client.close();
});
执行:
npx tsx src/mongodb-workflow.ts
这个示例同时覆盖 validation schema、索引、seed、aggregation pipeline 和 explain 检查。把输出交给 Claude Code 复盘,可以让它判断索引顺序是否匹配查询和排序。
embedding 与 reference 的判断
如果数据经常一起读取,并且代表某个时间点的事实,就适合 embedding。订单里的购买时价格就是典型例子。如果数据会独立更新、会无限增长,或者必须只有一个真实来源,就更适合 reference。不要把审计日志塞进账户文档,也不要把无限评论数组一直写进同一个工单。
这种重复不是偷懒,而是为了读性能做的有意非规范化。关键是把理由写进设计文档。
索引、聚合与 transaction
索引要从查询倒推。find({ userId }).sort({ createdAt: -1 }) 对应 { userId: 1, createdAt: -1 }。聚合管道应尽量先 $match 缩小范围,再 $unwind,最后 $group 和 $sort。高频后台报表可以考虑缓存或预聚合集合,不一定每次页面访问都实时计算。
transaction 只放在业务上不能部分成功的边界,例如“订单改为 paid”和“写入支付记录”必须一起成功。通知、搜索索引同步、浏览量统计通常可以异步重试。
官方资料建议直接看 Data Modeling、Indexes、Aggregation、Transactions 和 MongoDB Node.js Driver。
常见坑
第一个坑是把关系型数据库的范式照搬过来,导致每个订单详情都要在应用层做多次查询。第二个坑是过度嵌入,让评论、日志、通知这种会增长的数据撑大父文档。第三个坑是只依赖 TypeScript 类型,不在数据库层做校验。第四个坑是创建了索引却不看 explain("executionStats")。第五个坑是把重型 aggregation 放在每个 API 请求里。
上线前请确认:核心查询都有索引,validation schema 已应用,seed 覆盖列表、详情、报表和异常数据,explain 没有意外全扫描,transaction 边界已写清楚,回滚和备份方案可执行。
如果团队需要把这些步骤变成可复用流程,ClaudeCodeLab 可以提供 Claude Code 培训、CLAUDE.md 模板和 MongoDB/API 设计咨询。这个工作流实际使用后,最有价值的不是多加索引,而是让 Claude Code 基于 explain 结果指出哪些查询形状应该先调整。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。