范围分片适合按时间、ID区间、地域编码等连续值的范围查询场景,如后台报表查某月订单、用户按省统计、日志按天归档;但需避免纯递增字段作片键,推荐用{region:1,createdAt:1}复合片键。

范围分片适合什么查询场景
当你频繁按时间、ID区间、地域编码这类连续值做 find({ createdAt: { $gte: "2025-01-01", $lt: "2025-02-01" } }) 或 find({ userId: { $in: [1001, 1002, 1003] } })(且 ID 是有序递增)时,范围分片能直接路由到 1–2 个分片,避免广播查询。
但前提是片键本身有足够“宽度”:比如用 createdAt 做片键,如果写入集中在最近 5 分钟,所有新文档都会落到同一个 chunk,再被迁移到同一分片——瞬间变热点。实际中更稳妥的是用 { region: 1, createdAt: 1 } 这类复合片键,把地域打散在前,再按时间切分。
- ✅ 适用:后台报表查某月订单、用户按省分组统计、日志按天归档
- ❌ 避免:纯递增字段如
_id(ObjectId 默认递增)、自增int主键 - ⚠️ 注意:
sh.shardCollection()后不能改片键类型(范围→哈希),只能加后缀字段;MongoDB 5.0+ 支持重新分片,但会触发全量 chunk 迁移,慎用
哈希分片怎么避免写入热点
哈希分片本质是把片键值做 hash(如 { userId: "hashed" }),让原本连续的 userId 散列到不同分片。它不解决“查某段 ID”的效率,但能扛住突发写入——比如秒杀活动涌进 10 万用户,数据自动摊到所有分片上。
关键点在于:哈希只作用于片键本身,不是整个文档。所以如果你用 { email: "hashed" },那 email 必须存在且非空,否则插入失败;而且 email 一旦写入就不能改,否则 hash 值变,文档就“找不到了”。
- ✅ 适用:用户注册、设备上报、高并发写入且无强范围查询需求
- ❌ 避免:需要
$gt/$lt/$in范围扫描的业务逻辑(如“查所有年龄 > 30 的用户”会扫全集群) - ⚠️ 注意:哈希后无法建立唯一索引跨分片生效;
sh.shardCollection("db.users", { email: "hashed" })会自动建{ email: "hashed" }索引,别手动重复建
复合片键里字段顺序为什么不能乱
片键顺序 = 查询过滤顺序。MongoDB 只能高效路由那些「前缀匹配」的查询。比如片键是 { status: 1, createdAt: -1 },那么 find({ status: "done" }) 和 find({ status: "done", createdAt: { $gt: ... } }) 都能精准定位分片;但 find({ createdAt: { $gt: ... } }) 就得扫全集群。
这和普通复合索引原理一致,但后果更重:错序不仅慢,还可能让负载均衡器完全失效——因为 chunk 切分只看第一个字段的取值范围,后续字段只是“范围内的排序”,不参与分片决策。
- ✅ 推荐:高频等值查询字段放前面(如
tenantId),范围/排序字段放后面(如updatedAt) - ❌ 错误示例:
{ createdAt: 1, tenantId: 1 }→ 秒杀时所有租户数据都挤在最新 chunk - ⚠️ 注意:片键字段必须有索引,且该索引必须是集合已有索引;若集合非空,得先
createIndex()再shardCollection()
重新分片(reshard)前必须确认的三件事
MongoDB 5.0+ 允许用 sh.updateShardingKey() 或 sh.reshardCollection() 换片键,但不是“在线平滑切换”。它本质是新建分片结构、迁移数据、删旧结构三步,期间写入可能延迟、查询可能短暂不准。
真正要动之前,得盯紧三件事:集合是否启用了 balancer(关掉再操作)、是否有未完成的 chunk 迁移(sh.status() 查 movingChunks)、目标片键是否已在所有分片上建好索引(否则 reshard 会卡住)。
- ✅ 必做:
sh.stopBalancer()+sh.waitForBalancerOff()再开始 - ❌ 别跳过:
db.runCommand({ listShards: 1 })确认各分片状态正常 - ⚠️ 关键限制:不能从哈希片键改成范围片键,也不能删掉原片键字段(哪怕加了后缀)


















