范围分片键必须是升序索引,不能用哈希;分片键不能为数组或嵌套文档;单调递增键易导致写入热点,需预分片缓解;范围查询必须包含分片键及合适操作符才能精准路由。

范围分片键必须是升序索引,不能用哈希
MongoDB 的范围分片(range-based sharding)依赖分片键值的自然顺序来划分块(chunk),所以 _id、timestamp、user_id 这类单调递增字段天然适合——但前提是它已被建为升序索引(1),而非降序(-1)或哈希("hashed")。哈希分片会打乱顺序,直接导致范围查询无法路由到目标分片,mongos 只能广播。
- 建分片键前,先确认索引:执行
db.collection.getIndexes(),确保分片键字段存在{"field": 1}类型索引 - 若只有
{"field": -1},需先删掉再重建:db.collection.dropIndex({"field": -1}),再db.collection.createIndex({"field": 1}) - 切勿在已有数据的集合上直接
sh.shardCollection("db.coll", {"field": "hashed"})后改范围分片——不支持动态切换分片策略,必须重建集合
分片键字段不能是数组或嵌套对象
范围分片要求每个文档的分片键值可线性比较,而数组和嵌套文档(如 {"addr": {"city": "bj"}})无法定义全局大小关系。一旦指定这类字段为分片键,sh.shardCollection() 会直接报错:cannot shard collection with array or embedded document as shard key。
- 常见踩坑:误用
tags: ["a", "b"]或metadata: {version: 1, env: "prod"}作为分片键 - 替代方案:提取确定性标量值,比如用
tags[0](需保证每条文档都有且仅有一个首标签)、或预计算tag_hash字段(但这就变成哈希分片了) - 时间戳字段推荐用
created_at而非log.time(嵌套路径),后者需先用聚合管道展开或 $addFields 提取为顶层字段
写入热点问题:单调递增键会导致单分片写入集中
像 ObjectId 或毫秒级 timestamp 这类键,新文档总落在最大块(maxKey)之后,所有写请求都打向同一个分片,其他分片空转。这不是配置错误,而是范围分片的固有行为。
- 缓解方法不是换分片键,而是拆分预分片(pre-splitting):在启用分片前,用
sh.splitAt()手动把初始块按预期范围切开,例如按小时/天切分created_at - 配合
sh.status()观察各分片 chunk 数量,若某分片 chunk 明显偏多,说明 balancer 还没完成迁移——检查sh.isBalancerRunning()和配置服务器日志 - 真正高写入场景下,如果业务允许,优先考虑哈希分片;若必须范围分片,就接受“冷热分离”设计:热数据(最新)集中在少数分片,冷数据(历史)均匀分布
范围查询性能取决于是否带分片键条件
mongos 路由逻辑完全依赖查询中是否包含分片键及操作符类型。哪怕只差一个 $gt 写成 $gte,结果可能从精准路由变成全分片广播。
- 高效查询示例:
db.orders.find({"order_time": {"$gte": ISODate("2026-07-01"), "$lt": ISODate("2026-07-02")}})→ mongos 计算出对应 chunk 范围,只发给 2–3 个分片 - 低效查询示例:
db.orders.find({"status": "pending"})(无分片键)→ scatter/gather,所有分片参与 - 陷阱:
db.orders.find({"order_time": {"$in": [ISODate("2026-07-01"), ISODate("2026-07-02")]}})看似含分片键,但$in对范围分片不友好,mongos 仍可能广播——应改用多个$or+ 精确等值,或确保$in元素数量极少且离散
分片键选型没有银弹,范围分片的价值在于支持高效的时间范围扫描和归档,代价是写入倾斜与查询条件强约束。配置时别只盯着 sh.shardCollection() 是否成功,重点看后续 chunk 分布、balancer 活动日志和真实查询的 explain("executionStats") 中的 nShards 字段。











