最优分片键需匹配写入与查询模式:低基数字段(如tenant_id仅5值)不可单独使用,基数应>分片数×3;单调递增字段(如created_at)须打散,避免写入热点;范围查询多则禁用哈希片键,否则引发全分片广播扫描。

选错分片键,集群很快会退化成“单点写入瓶颈”——不是某个分片磁盘爆满,就是全集群扫表查不出数据。最优分片键不靠直觉,而取决于你实际的 write pattern 和 query pattern 是否匹配字段的基数与增长趋势。
看字段基数:低基数字段不能单独当片键
基数(cardinality)指字段去重后的值数量。若 tenant_id 只有 5 个取值,status 只有 "active"/"inactive",这类低基数字段单独做片键会导致 chunk 极少、无法有效拆分,所有写入挤在少数几个 chunk 上,最终落到一两个分片。
- 可用命令快速估算:
db.collection.aggregate([{$group: {_id: "$tenant_id"}}, {$count: "distinct_count"}]) - 安全下限建议:单字段基数应 > 分片数 × 3,否则必须组合高基数字段
-
_id看似唯一,但若应用层用 UUID v4(随机)或自定义字符串,实际分布可能不均;ObjectId虽高基数,但时间戳前缀导致写入热点,不能直接单用
看增长趋势:单调递增字段必须打散
任何带时间语义的字段(created_at、timestamp、自增 seq_id)都天然倾向写入末尾 chunk,造成单一分片持续接收写请求,IO 和内存压力集中。
- 验证方法:
db.collection.find().sort({created_at: -1}).limit(1)和.sort({created_at: 1}).limit(1)查时间跨度,再结合sh.status()观察各分片 chunk 数是否严重失衡 - 补救不是加索引,而是改策略:要么用
{created_at: "hashed"}(牺牲范围查询),要么组合如{region: 1, created_at: "hashed"}(region必须高基数且稳定) - 切记:
sh.shardCollection()前必须已建对应索引,否则报错cannot shard collection without an index on the shard key
看查询模式:范围查询多就别用哈希,写入热点高就别用范围
片键同时是默认查询索引。如果你 70% 的查询带 $gt/$lt 条件(如查某天内日志、某用户 ID 段订单),那哈希片键会让每次查询发往所有分片,变成广播式扫描。
- 典型误用:
{user_id: "hashed"}后执行find({user_id: {$gt: 1000, $lt: 2000}})→ 路由到全部分片 - 折中方案:
{user_type: 1, user_id: "hashed"},前提是user_type在绝大多数查询中都作为等值条件出现(如{user_type: "premium"}) - 字段顺序不可逆:
{user_id: "hashed", user_type: 1}无法支持{user_type: "premium"}单独路由,必须把等值字段放前面
真正难的不是算出哪个字段基数高,而是把写入节奏、查询条件、字段稳定性三者对齐——比如 region 高基数但业务正在合并区域,user_id 均匀但 80% 查询都不带它,这些细节一漏,分片键就废了一半。

















