哈希索引必须建在单字段分片键上,且需先建索引再分片;仅支持int、long、string、ObjectId等类型;不支持范围查询,但可避免数据倾斜;分片键一旦设定不可更改。

哈希索引必须建在分片键上,且只能是单字段
MongoDB 的哈希分片(shard key)要求索引类型为哈希,但这个哈希索引不能随便建——它必须直接作用于你选定的分片键字段,且该字段必须是单字段(不支持复合哈希索引)。如果你试图对 {a: 1, b: 1} 创建哈希索引,db.collection.createIndex({a: "hashed", b: "hashed"}) 会报错:cannot create a compound index with more than one hashed field。
常见错误是先建普通索引再分片,结果发现数据倾斜严重。正确顺序是:先确认业务中该字段写入均匀(如 user_id、UUID 字符串),再建哈希索引,最后启用分片。
- 哈希索引只支持
int、long、string、ObjectId等可确定哈希值的类型;Date或嵌套文档不行 - 字段值不能频繁更新,否则会触发 chunk 迁移和哈希值重算,增加集群压力
- 建索引前确保集合为空或数据量小,否则
createIndex会阻塞写入(尤其在副本集主节点上)
执行分片前必须先对数据库启用分片,再对集合指定分片键
很多人卡在 sh.shardCollection() 报错 collection does not have a unique index on the shard key,其实是因为漏掉了前置步骤:哈希索引本身不是唯一索引,但 MongoDB 要求分片键上必须存在对应类型的索引(哈希索引即可),且该索引必须已构建完成。
实操顺序不能颠倒:
- 运行
sh.enableSharding("mydb") - 在
mydb.mycollection上创建哈希索引:db.mycollection.createIndex({user_id: "hashed"}) - 再执行分片:
sh.shardCollection("mydb.mycollection", {user_id: "hashed"})
注意:shardCollection 的第二个参数必须与索引字段名和类型完全一致,连空格都不能多——写成 {user_id: " hashed"} 就会失败,错误信息是 shard key pattern does not match existing index。
哈希分片无法支持范围查询,但能避免单调递增键导致的数据倾斜
用 ObjectId 或时间戳做分片键时,新数据总写入同一个 chunk,形成“热点 shard”,而哈希分片把相似值打散到不同 chunk,写入天然均匀。代价是:所有基于该字段的范围查询(如 {user_id: {$gt: "abc", $lt: "xyz"}})都会变成广播查询(scatter-gather),性能下降明显。
所以选字段要权衡:
- 如果查询主要是等值匹配(
find({user_id: "xxx"}))、高并发写入,哈希分片很合适 - 如果大量按时间范围查日志、按区间查用户等级,别用哈希,改用升序索引 + zone 分片或组合键
- 哈希值是 64 位整数,但 MongoDB 不暴露原始哈希结果,也无法用应用层预计算哈希来路由——路由完全由 mongos 内部完成
分片后无法修改分片键,哈希索引也不能转为升序索引
一旦 shardCollection 执行成功,分片键就固化了。你不能删掉哈希索引、重建升序索引、再重新分片——sh.removeShard() 也不行,因为数据已经分布,且没有全局锁支持原子重分片。
补救成本极高,通常只能导出数据、新建集合、重新分片、再导入。所以建之前务必验证:
- 用
db.mycollection.aggregate([{$group: {_id: {$mod: ["$user_id", 100]}, count: {$sum: 1}}}, {$sort: {count: -1}}])模拟哈希桶分布(取模代替哈希),看是否均匀 - 检查
user_id是否真无规律:比如前端传的"U1001"、"U1002"是伪随机,实际哈希后仍可能聚集 - 测试写入吞吐:用
mongostat观察各 shard 的netIn和conn是否均衡,别只看 chunk 数量
哈希分片看着简单,但字段选择、索引时机、查询模式这三点没对齐,后期基本没法调优。


















