结论:必须按业务读写模式重构文档模型,而非照搬关系表结构。因MongoDB的性能优势在于单次查询获取全部所需字段,需根据读写频率、数据大小决定嵌入或引用,并手动调整迁移工具规则、建立复合索引、启用文档验证、压测真实查询路径。

直接说结论:不能照搬表结构,必须按业务读写模式重构文档模型。照着 MySQL 的 users、orders、order_items 三张表,原样建三个 MongoDB 集合,等于把关系型数据库的枷锁套在 MongoDB 上——索引难建、查询慢、扩展受限,后期维护成本翻倍。
为什么“一对一映射表→集合”是最大误区
很多人第一反应是让 users 表变 users 集合、orders 表变 orders 集合,看似省事。但问题立刻暴露:
- 查一个用户+他最近 5 个订单,得发 2 次查询(甚至更多),应用层做 join;
- 订单项分散在
order_items集合里,每次查订单详情还得再查一次,延迟叠加; - MongoDB 不支持跨集合事务(即使 4.0+ 的多文档事务,也只限单次操作,不解决读扩散);
- 无法用单个
db.orders.findOne({ customer_id: "u123" })带出完整业务视图。
这不是迁移,是“换壳不换心”。真正高效的前提,是承认:MongoDB 的读性能优势,来自“一次查询拿到所有需要的字段”,而不是靠外键约束和关联查询保障一致性。
嵌入还是引用?看访问频率和更新粒度
关键判断不是“能不能嵌”,而是“值不值得嵌”。决定因素就两个:
-
读写比例:如果订单详情几乎只读、极少修改,
items数组嵌进orders文档完全合理; -
数据大小与增长:单个订单超 50 个商品项?或商品描述字段很长?那
items就该拆成独立集合,用order_id引用——否则文档可能突破 16MB 限制,且每次更新都重写整个大文档。
典型场景示例:
用户资料(name、email、profile_pic_url)稳定且体积小 → 直接嵌入 users 文档;
用户登录日志(高频写、按时间范围查)→ 单独 user_login_logs 集合,用 user_id 字段索引;
用户收货地址(中低频读写、数量有限)→ 嵌为 addresses 数组,每个元素含 is_default 字段标记主地址。
Relational Migrator 的默认规则要手动改
像 MongoDB 官方 Relational Migrator 工具,开箱即用时会为每张源表生成一个集合,并把外键转成 ObjectId 引用——这恰恰是最保守、最接近“反模式”的起点。
- 它默认选
将行迁移为新文档,但你大概率需要改成将行迁移为嵌入式数组或将行迁移为嵌入式文档; - 它自动推断
varchar→string、datetime→Date,没问题;但不会帮你把status_code(int)转成带语义的status: "shipped",这种清洗得自己加转换逻辑; - 若源库没定义外键(比如旧系统用字符串关联),工具无法自动识别关系,必须手动配置“合成外键”,否则嵌入逻辑直接失效。
别跳过这步编辑。跑一遍默认迁移再返工,比提前花 2 小时调映射规则更耗时。
迁移后最容易被忽略的三件事
数据进去了不等于能用好:
- 没建复合索引:比如
db.orders.createIndex({ customer_id: 1, created_at: -1 }),查用户最新订单就会全表扫; - 没设文档验证(schema validation):上线后任意字段乱塞,几个月后
status字段出现"pending"、"PENDING"、null、0四种值,聚合统计全错; - 没压测真实查询路径:用
explain()看执行计划,确认是否命中索引、是否触发COLLSCAN;光比记录数相等没用,慢查询才是线上事故源头。
文档模型设计不是迁移前的一步预处理,而是贯穿建模、迁移、验证、压测的持续决策。越早按读场景想清楚“这个查询要什么字段”,越不容易在上线前夜推倒重来。


















