# MongoDB vs MySQL 核心知识点总结
## 一、适用场景对比 ### ✅ MongoDB适合的数据类型 | 数据类型 | 特点 | 示例场景 | |---------|------|---------| | **Schema灵活/不固定** | 同集合文档结构可不同 | 电商商品、CMS、用户自定义字段 | | **深层嵌套文档** | 避免多表JOIN | 订单+用户+商品信息聚合 | | **高并发写入日志** | 写多读少 | IoT传感器、应用日志、事件追踪 | | **大数据量水平扩展** | 原生Sharding | 海量数据需要分片 | | **地理位置数据** | 内置地理索引 | LBS应用、附近的人 | ### ❌ MongoDB不适合的场景 | 场景 | 原因 | 应选择 | |------|------|--------| | 复杂事务(特别是跨分片) | 事务支持弱、2PC性能差 | MySQL/NewSQL | | 多表关联查询 | JOIN性能差 | MySQL | | 强一致性要求 | 默认最终一致性 | MySQL | | 财务/支付数据 | 需要严格ACID | MySQL | | 复杂BI报表 | 优化器不成熟 | MySQL + 数仓 | --- ## 二、MongoDB写入性能优势 ### 核心原因 ``` 写入流程对比: MongoDB (默认): 写入 → 内存 → Journal(100ms) → 返回客户端 ↓ 后台异步刷盘(60s checkpoint) MySQL (InnoDB): 写入 → redo log → binlog → 提交确认 → 返回客户端 (需fsync) (需fsync) ``` ### 技术差异详解 | 维度 | MongoDB | MySQL | MongoDB优势 | |------|---------|-------|------------| | **写入确认** | 可选:w:0/w:1/w:majority | 必须等待事务提交 | ✅ 灵活可配 | | **Schema验证** | 运行时可选 | 必须严格验证 | ✅ 无验证开销 | | **数据结构** | 嵌套文档一次写入 | 多表分别写入+外键 | ✅ 减少写次数 | | **索引维护** | B树索引更新轻量 | 主键索引+二级索引+外键 | ✅ 开销较小 | | **事务开销** | 默认单文档原子性 | MVCC+undo log | ✅ 轻量级 | | **并发控制** | 文档级锁(细粒度) | 行锁+gap锁 | ⚖️ 各有优势 | ### 性能代价 | 牺牲项 | 影响 | |--------|------| | 数据一致性 | 默认最终一致性(可配置) | | 事务支持 | 复杂事务能力弱 | | 存储空间 | 通常占用更多(冗余设计) | | 数据完整性 | 无外键约束 | --- ## 三、MongoDB持久化机制(Journal) ### Journal = MongoDB的Redo Log ``` 写入流程: 数据 → WiredTiger Cache → Journal → 数据文件 ↓ ↓ ↑ 立即返回 100ms刷盘 60s checkpoint ``` ### 对比表 | 特性 | MongoDB Journal | MySQL Redo Log | |------|----------------|----------------| | **作用** | 崩溃恢复 | 崩溃恢复 | | **机制** | WAL预写日志 | WAL预写日志 | | **刷盘频率** | 100ms (commitIntervalMs) | 1秒或每次提交 | | **文件管理** | checkpoint后删除 | 固定大小循环使用 | | **压缩** | 支持(snappy/zlib) | 不压缩 | ### WriteConcern持久化级别 ```javascript // w:0 - 不等待确认(最快,可能丢数据) { writeConcern: { w: 0 } } // w:1 - 等待主节点内存确认(默认,崩溃可能丢100ms) { writeConcern: { w: 1 } } // w:1, j:true - 等待Journal刷盘(类似MySQL默认) { writeConcern: { w: 1, j: true } } // w:"majority" - 等待多数节点确认(最安全) { writeConcern: { w: "majority" } } ``` ### 崩溃恢复流程 ``` 1. 检查最后一个checkpoint 2. 从checkpoint开始重放Journal 3. 恢复到崩溃前的一致性状态 ``` --- ## 四、MongoDB分片机制 ### 架构组成(三层) ``` 客户端 ↓ [Mongos路由层] ← 无状态,可多个 ↓ [Config Servers] ← 元数据(副本集,3节点) ↓ [Shard分片节点] ← 实际数据存储(每个Shard是副本集) ├─ Shard1: server-1a, 1b, 1c ├─ Shard2: server-2a, 2b, 2c └─ Shard3: server-3a, 3b, 3c ``` **关键:不同分片在不同物理节点上** ### Chunk机制 ```javascript // 数据按Shard Key分成Chunks(默认64MB) Chunk1: user_id [MinKey → 1000] → Shard1 Chunk2: user_id [1000 → 2000] → Shard2 Chunk3: user_id [2000 → MaxKey] → Shard3 // Chunk自动分裂和平衡 - Chunk > 64MB → 自动分裂 - 各Shard的Chunk数量差异大 → Balancer迁移 ``` ### 分片策略 | 策略 | 特点 | 适用场景 | |------|------|---------| | **范围分片** | `{ user_id: 1 }` | 范围查询多,但可能数据倾斜 | | **哈希分片** | `{ user_id: "hashed" }` | 数据均匀分布,但范围查询慢 | | **Zone分片** | 地理/业务隔离 | 多租户、合规要求 | ### 数据路由 ```javascript // 单分片查询(高效) db.users.find({ user_id: 1500 }) // Mongos查Config → 路由到Shard2 → 返回 // 多分片查询(Scatter-Gather) db.users.find({ age: 25 }) // 没有分片键 // Mongos并行查询所有Shard → 合并结果 → 返回 ``` ### 典型部署规模 **3-Shard集群需要:** - 3个Shard × 3副本 = 9台服务器 - 3个Config Server = 3台服务器 - N个Mongos(通常与应用服务器同机部署) - **总计:至少12台服务器** --- ## 五、MongoDB二级字段索引 ### ✅ 完全支持,这是核心优势 ```javascript // 1. 嵌套文档字段索引 { profile: { city: "北京", zipcode: "100000" } } db.users.createIndex({ "profile.city": 1 }); db.users.find({ "profile.city": "北京" }); // 使用索引 // 2. 数组内字段索引(多键索引) { specs: [ { k: "color", v: "black" }, { k: "ram", v: "16GB" } ] } db.products.createIndex({ "specs.v": 1 }); // 为每个数组元素都创建索引条目 // 3. 复合索引 db.users.createIndex({ "status": 1, "profile.city": 1 }); ``` ### 缺失字段的索引记录 | 情况 | 普通索引 | 稀疏索引 (sparse: true) | |------|---------|------------------------| | 字段存在且有值 | ✅ 索引 | ✅ 索引 | | 字段值为 null | ✅ 索引 | ❌ 不索引 | | 字段不存在 | ❌ 不索引 | ❌ 不索引 | **典型应用:可选唯一字段** ```javascript // 允许多个用户不填email,但填了必须唯一 db.users.createIndex( { email: 1 }, { unique: true, sparse: true } ); ``` --- ## 六、MongoDB vs MySQL 索引差异 ### 核心差异对比表 | 特性 | MySQL (InnoDB) B+Tree | MongoDB (WiredTiger) B-Tree | |------|----------------------|----------------------------| | **核心结构** | 聚簇索引(数据即索引) | 非聚簇索引(独立索引+文档) | | **主键查询** | 一步到位(数据在索引叶子) | 两步(索引→指针→文档) | | **二级索引** | 叶子存主键值(需回表) | 叶子存RecordID指针 | | **索引类型** | 基础索引为主 | 多键、稀疏、文本、地理等丰富类型 | | **NULL/缺失** | NULL会被索引 | 默认稀疏(缺失不索引) | | **更新代价** | 更新PK代价极高 | 文档移动需更新所有索引 | ### 详细解析 #### 1. 聚簇索引 vs _id索引(最大区别) **MySQL聚簇索引:** ``` 数据按主键组织成一棵大B+Tree 叶子节点 = 完整行数据 主键查询:一次树查找,数据就在旁边 优点:主键查询极快、范围扫描高效(顺序IO) 缺点:只能有一个、主键更新代价高、页分裂 ``` **MongoDB _id索引:** ``` 数据独立存储,_id索引是普通索引 叶子节点 = 文档位置指针(RecordID) 查询:索引查找 → 获取指针 → 读取文档 优点:所有索引平等、文档移动灵活 缺点:任何查询都是两步、范围查询随机IO ``` #### 2. 二级索引实现 **MySQL:** ```sql -- 二级索引存主键值 INDEX(age) → 叶子存储 primary_key_value 查询流程:age索引树 → 找到PK → 聚簇索引树 → 行数据 (回表,两次B+Tree查找) ``` **MongoDB:** ```javascript // 二级索引存文档指针 INDEX(age) → 叶子存储 RecordID 查询流程:age索引树 → RecordID → 文档数据 (一次树查找 + 指针访问) ``` #### 3. MongoDB特有索引类型 | 索引类型 | 解决问题 | MySQL对应方案 | |---------|---------|--------------| | **多键索引** | 数组元素索引 | 需要关联表+JOIN | | **稀疏索引** | 可选字段/节省空间 | 无法实现 | | **文本索引** | 内置全文搜索 | 需要外部ES | | **地理索引** | LBS查询 | 空间索引(功能较弱) | --- ## 七、范围查询性能对比 ### 性能对比总结表 | 场景 | MongoDB | MySQL | 谁更强 | 原因 | |------|---------|-------|--------|------| | **主键范围查询** | 随机IO | 顺序IO | ✅ **MySQL** | 聚簇索引 | | **覆盖索引范围查询** | 部分支持 | 完全支持 | ✅ **MySQL** | 不回表 | | **二级索引范围查询** | 指针访问 | 回表(2次树查找) | ⚖️ **相近** | - | | **嵌套字段范围查询** | 直接查询 | 需要JOIN | ✅ **MongoDB** | 文档模型 | | **分片环境范围查询** | 原生支持 | 应用层处理 | ✅ **MongoDB** | 架构优势 | ### 主键范围查询详解(MySQL优势明显) **MySQL(顺序IO):** ```sql SELECT * FROM users WHERE id BETWEEN 1000 AND 5000; 磁盘布局(物理连续): [1000] → [1001] → [1002] → ... → [5000] 一次顺序读取,磁盘磁头移动最少 性能:~10ms ``` **MongoDB(随机IO):** ```javascript db.users.find({ _id: { $gte: 1000, $lte: 5000 } }) 磁盘布局(可能分散): [其他] [1000] [其他] [1003] [1001] ... 多次随机IO,磁盘跳来跳去 性能:~100-500ms(无缓存) ``` ### 影响因素 | 因素 | 对MongoDB的影响 | |------|----------------| | **数据在内存** | 随机IO劣势消失,性能接近MySQL | | **返回数据量** | 小数据量差距不大,大数据量MySQL优势明显 | | **SSD vs HDD** | SSD大幅缩小差距,HDD差距巨大 | | **文档大小** | 小文档影响小,大文档IO开销大 | --- ## 八、MongoDB显著慢于MySQL的场景 ### 真正的固有劣势(2个) #### 1. 跨分片事务(架构决定,无法优化) ```javascript // 需要两阶段提交(2PC) session.startTransaction(); db.shard1.accounts.updateOne(...); // 账户A在分片1 db.shard2.accounts.updateOne(...); // 账户B在分片2 session.commitTransaction(); 性能对比: - MySQL单库事务:5ms - MongoDB单分片事务:20ms - MongoDB跨分片事务:200-500ms (10-100倍慢) ``` #### 2. 全表/复杂聚合统计(优化器不成熟) ```javascript // MySQL有:表统计信息、直方图、查询计划缓存、成熟优化器 // MongoDB:优化器相对简单 百万级数据复杂报表: - MySQL: ~100ms - MongoDB: ~1500ms (15倍慢) ``` ### 使用不当导致的"慢"(6个) #### 1. 多表JOIN(场景选择错误) ```javascript // ❌ 错误用法 db.users.aggregate([ { $lookup: { from: "orders" ... }}, { $lookup: { from: "order_items" ... }} ]); // 性能: ~3500ms // ✅ 正确建模(嵌套文档) { user: { name: "张三" }, order: { order_no: "..." }, items: [...] // 嵌入 } // 性能: ~5ms 结论:需要频繁JOIN,别用MongoDB ``` #### 2. 主键大批量范围查询(场景选择错误) ```javascript // 如果核心场景是主键范围扫描 // 应该用MySQL(聚簇索引) // MongoDB优化: // - 使用投影减少数据传输 // - 分批查询 // - 但本质上这不是MongoDB的强项 ``` #### 3. 频繁更新计数器(需要优化设计) **实测性能(公平配置下):** ``` 小文档 + 无多余索引 + w:1,j:false: - MongoDB: 8000 QPS - MySQL: 4000 QPS MongoDB甚至更快! 但以下情况MongoDB慢: - 大文档(100KB+): MongoDB 1500 QPS vs MySQL 4000 QPS - 多索引包含计数器: MongoDB 1000 QPS vs MySQL 3500 QPS - 高writeConcern(majority): MongoDB 800 QPS vs MySQL 4000 QPS ``` **最佳方案:Redis(100000+ QPS)** #### 4. 复杂WHERE条件(索引设计问题) ```javascript // ❌ 没有合理索引 db.products.find({ category: 'electronics', price: { $gte: 1000, $lte: 5000 }, stock: { $gt: 0 }, rating: { $gte: 4.0 } }); // ✅ 创建复合索引 db.products.createIndex({ category: 1, rating: 1, price: 1 }); ``` #### 5. 大范围排序(索引设计问题) ```javascript // ❌ 没有支持排序的索引 db.logs.find({ status: 'error' }).sort({ created_at: -1 }); // 内存排序,32MB限制,可能失败 // ✅ 创建支持排序的复合索引 db.logs.createIndex({ status: 1, created_at: -1 }); // 使用索引排序,性能完全OK ``` #### 6. 强一致性读(配置问题) ```javascript // ❌ 默认可能读到从节点旧数据 db.users.findOne({ _id: 123 }); // ✅ 配置强一致性 db.users.findOne({ _id: 123 }, { readPreference: 'primary', readConcern: { level: 'majority' } }); // 和MySQL一样的强一致性,但延迟略高 ``` --- ## 九、核心思维方式 ### ❌ 错误思维 ``` "用MySQL的方式使用MongoDB" "用MongoDB存关系数据,然后抱怨JOIN慢" "不理解文档模型,直接把表结构搬过来" ``` ### ✅ 正确思维 **场景选择:** ``` 数据高度关系化 + 复杂JOIN → MySQL 数据文档化 + Schema灵活 → MongoDB 高并发计数器 → Redis 全文搜索 → Elasticsearch 分布式事务 → MySQL/NewSQL ``` **设计原则:** ``` MongoDB: - 按查询模式设计Schema - 接受反范式设计(数据冗余) - 嵌套相关数据(避免JOIN) - 合理使用索引(避免过度索引) MySQL: - 范式设计(减少冗余) - 通过JOIN关联数据 - 主键选择至关重要(聚簇索引) ``` --- ## 十、最佳实践速查表 ### MongoDB索引优化 ```javascript // 1. 为嵌套字段创建索引 db.coll.createIndex({ "address.city": 1 }); // 2. 复合索引顺序:等值 → 排序 → 范围 db.coll.createIndex({ status: 1, created_at: -1, price: 1 }); // 3. 不要对计数器字段建索引 // ❌ db.articles.createIndex({ view_count: 1 }); // 4. 使用稀疏索引节省空间 db.users.createIndex({ email: 1 }, { unique: true, sparse: true }); // 5. 使用explain分析 db.coll.find(...).explain("executionStats"); // 6. 避免内存排序 // 创建支持排序的索引,或使用 allowDiskUse(true) ``` ### MongoDB写入优化 ```javascript // 1. 根据场景选择writeConcern // 日志类:{ w: 1, j: false } // 重要数据:{ w: "majority", j: true } // 2. 批量写入 db.coll.insertMany([...], { ordered: false }); // 3. 小文档设计 // 计数器独立存储,避免大文档 // 4. 合理分片键 // 高基数、分布均匀、符合查询模式 sh.shardCollection("db.coll", { user_id: "hashed" }); ``` ### 典型架构组合 ``` 客户端 ↓ ┌────────┬──────────┬──────────────┬──────────┐ │ │ │ │ │ MySQL MongoDB Redis Elasticsearch 主数据 文档数据 缓存/计数器 搜索/日志分析 事务保证 灵活Schema 高性能 全文检索 ``` --- ## 十一、性能数据速查 ### 写入性能(参考值) | 场景 | MongoDB | MySQL | |------|---------|-------| | 单条插入(默认配置) | ~8ms | ~12ms | | 批量插入(1000条) | ~2秒 | ~12秒 | | 高并发写入(日志) | 50K+ writes/s | 20K writes/s | ### 查询性能(参考值) | 场景 | MongoDB | MySQL | |------|---------|-------| | 主键查询 | ~1ms | ~0.5ms | | 主键范围(1万条) | ~200ms | ~20ms | | 二级索引查询 | ~2ms | ~2ms | | 3表JOIN | ~3500ms | ~10ms | | 嵌套字段查询 | ~2ms | ~50ms(JOIN) | ### 事务性能(参考值) | 场景 | MongoDB | MySQL | |------|---------|-------| | 单文档事务 | ~2ms | ~2ms | | 单分片多文档事务 | ~20ms | ~5ms | | 跨分片事务 | ~500ms | N/A | --- ## 十二、关键结论 1. **MongoDB写入快**的本质:用一致性和复杂查询能力换取写入性能 2. **MongoDB有Journal机制**,类似MySQL的redo log 3. **MongoDB分片**在不同物理节点,通过Chunk机制自动平衡 4. **MongoDB支持二级字段索引**,这是其文档模型的核心优势 5. **索引差异核心**:MySQL聚簇索引 vs MongoDB非聚簇索引 6. **范围查询**:主键查询MySQL强,嵌套字段查询MongoDB强 7. **MongoDB慢的场景**:主要是跨分片事务和复杂统计(固有),其他多是使用不当 8. **计数器更新**:合理配置下MongoDB与MySQL性能相当