声明:本文部分内容使用AI辅助生成,经人工编辑、审核和补充个人经验。
更新说明:技术栈版本信息基于 MongoDB 6.x。
接手了一个MongoDB项目,数据量涨到了900多万条,索引优化成了必修课。这里记录一些实际操作中踩过的坑。
索引创建方式
前台索引
默认情况下MongoDB用前台方式创建索引,这会把整个集合锁死:
1 2
| db.collection.createIndex({ name: 1 })
|
特点:
- 创建期间其他操作全部卡住
- 速度相对快一些
- 只适合维护窗口期或小数据量
后台索引
生产环境数据量大的时候,后台索引是救命稻草:
1 2 3 4 5
| db.collection.createIndex({ zipcode: 1 }, { background: true })
db.collection.createIndex({ city: 1 }, { background: true, sparse: true })
|
| 对比项 |
前台索引 |
后台索引 |
| 是否阻塞 |
是 |
否 |
| 创建速度 |
快 |
慢约40% |
| 资源占用 |
高 |
较低 |
| 使用场景 |
维护期 |
生产环境 |
注意:后台索引虽然不打断业务,但创建时间会更长,要做好时间规划。
千万级数据的实战经验
创建时间预估
亲身经历:927万条数据建索引,花了好几个小时。如果预估不足,会影响后续计划:
1 2 3 4 5
| db.collection.stats()
db.collection.getIndexes()
|
查看索引进度
使用db.currentOp()可以监控索引创建的实时进度:
1 2 3 4 5 6
| db.currentOp({ $or: [ { op: "command", "query.createIndexes": { $exists: true } }, { op: "insert", ns: /\.system\.indexes\b/ } ] })
|
输出示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| { "inprog": [ { "desc": "conn1", "threadId": "139911670933248", "client": "127.0.0.1:37524", "active": true, "opid": 5014925, "secs_running": 21, "microsecs_running": 21800738, "op": "command", "ns": "test.$cmd", "query": { "createIndexes": "inventory", "indexes": [ { "ns": "test.inventory", "key": { "item": 1 }, "unique": true, "name": "item_1_unique_true" } ] }, "msg": "Index Build Index Build: 3103284/5000000 62%", "progress": { "done": 3103722, "total": 5000000 }, "numYields": 0, "locks": { "Global": "w", "Database": "W", "Collection": "w" }, "waitingForLock": false } ], "ok": 1 }
|
关键字段:
| 字段 |
说明 |
secs_running |
已运行秒数 |
msg |
进度百分比 |
progress.done |
已处理文档数 |
progress.total |
总文档数 |
终止索引创建
建索引过程中如果发现影响太大,可以干掉它:
提示:
- 没建完的索引会被清理掉
- 已完成的不会受影响
- 重启后未完成的可能会变成前台模式继续
索引创建期间的限制
连接问题
建索引时:
- 执行创建的那个连接会被占用
- 要干别的需要开新连接
- 索引没建完之前不生效
- 建完立刻能用
这期间不能做的操作
| 操作 |
后果 |
repairDatabase |
失败 |
db.collection.drop() |
失败 |
compact |
失败 |
异常处理
mongod挂了怎么办:
- 重启后没建完的索引会变成前台模式继续
- 如果失败(比如重复键错误),mongod会报错退出
跳过失败的索引:
1 2 3 4 5 6
| mongod --noIndexBuildRetry
storage: indexBuildRetry: false
|
性能优化建议
资源规划
内存方面:
- 建索引需要足够内存
- 内存不够的话速度会显著下降
- 最好在业务低峰期操作
磁盘I/O:
- 后台索引对性能影响小一些
- 但还是会拖慢相关集合的操作
- 避开高峰期是明智之选
索引创建策略
分批创建:
1 2 3 4 5 6 7 8 9
|
db.orders.createIndex({ userId: 1, createTime: -1 }, { background: true })
db.orders.createIndex({ status: 1 }, { background: true })
db.orders.createIndex({ category: 1 }, { background: true })
|
覆盖索引:
1 2 3 4 5 6 7 8 9 10
|
db.users.createIndex( { userId: 1, name: 1, email: 1 }, { background: true } )
|
批量更新技巧
给数据加随机后缀
1 2 3 4 5 6 7 8 9 10 11
| db.robots.find({ name: "Guest" }).forEach(function(item) { db.robots.update( { "_id": item._id }, { $set: { name: "Guest" + Math.floor(Math.random() * 10000 + 10000) % 10000 } } ) })
|
批量更新优化版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var bulk = db.robots.initializeUnorderedBulkOp(); var count = 0;
db.robots.find({ name: "Guest" }).forEach(function(item) { bulk.find({ "_id": item._id }).updateOne({ $set: { name: "Guest" + Math.floor(Math.random() * 10000 + 10000) % 10000 } }); count++;
if (count % 1000 === 0) { bulk.execute(); bulk = db.robots.initializeUnorderedBulkOp(); print("已处理: " + count); } });
if (count % 1000 !== 0) { bulk.execute(); }
|
索引设计与监控
设计原则
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| db.collection.createIndex({ status: 1, createTime: -1 })
db.collection.createIndex({ category: 1, price: 1 })
db.collection.createIndex({ userId: 1, name: 1, email: 1 })
db.collection.createIndex( { price: 1 }, { partialFilterExpression: { price: { $gt: 0 } } } )
|
监控命令
1 2 3 4 5 6 7 8 9 10 11
| db.collection.find({ userId: 1 }).explain("executionStats")
db.collection.stats().indexSizes
db.system.profile.find().sort({ ts: -1 }).limit(10)
db.setProfilingLevel(1, { slowms: 100 })
|
explain分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| { "queryPlanner": { "plannerVersion": 1, "namespace": "test.inventory", "indexFilterSet": false, "parsedQuery": { "item": { "$eq": "canvas" } }, "winningPlan": { "stage": "FETCH", "inputStage": { "stage": "IXSCAN", "keyPattern": { "item": 1 }, "indexName": "item_1", "isMultiKey": false, "direction": "forward", "indexBounds": { "item": ["[\"canvas\", \"canvas\"]"] } } } }, "executionStats": { "executionSuccess": true, "nReturned": 1, "executionTimeMillis": 0, "totalKeysExamined": 1, "totalDocsExamined": 1, "executionStages": { "stage": "FETCH", "nReturned": 1, "executionTimeMillisEstimate": 0, "works": 2, "advanced": 1, "docsExamined": 1 } } }
|
七、最佳实践总结
7.1 索引创建Checklist
经验总结
| 场景 |
建议 |
| 数据量大 |
后台索引 |
| 并发高 |
分批创建,避开高峰 |
| 内存小 |
加内存或减少索引字段 |
| 写入多 |
控制索引数量 |
| 查询复杂 |
用复合索引 |
常用脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function showIndexSizes() { db.getCollectionNames().forEach(function(collection) { var stats = db[collection].stats(); print(collection + ":"); printjson(stats.indexSizes); }); }
function findUnusedIndexes() { return db.system.profile.aggregate([ { $match: { op: { $in: ["query", "command"] } } }, { $group: { _id: { ns: "$ns", index: "$planSummary" }, count: { $sum: 1 } } } ]); }
db.collection.aggregate([ { $indexStats: {} } ])
|
写在最后
MongoDB索引管理是个需要经验的活儿。后台索引、进度监控、批量更新这些技巧,在实际运维中帮了我不少忙。
几点心得:
- 生产环境一定要后台索引,别堵了业务
- 盯着进度,心里有底
- 复合索引设计好,减少冗余
- 批量更新用Bulk,效率高
- 定期看看慢查询,及时调整
参考: