分片键(Shard Keys)
分片键是用于确定集合文档在集群分片中分布得索引字段或复合索引字段。
具体来说,MongoDB将分片键值(或散列分片键值)得范围划分为不重叠得分片键值范围(或散列分片键值)。每一个范围都与一个块相关联,MongoDB尝试在集群中得分片之间均匀分布块。
分片键与块分配得有效性有直接关系。
分片键规范(Shard Key Specification)
在对集合进行分片时必须指定分片键,通过 mongo shell方法 sh.shardCollection() 对集合进行分片:
shell
$ sh.shardCollection(<namespace>, <key>) // Optional parameters omitted
namespace : 指定待分片集合完整得命名空间,例如 <database.collection>
key :指定一个文档 {<shard key field> : <1|"hashed">, ...} ,其中:
- 1 表示基于范围得分片(range-based sharding);
- hashed 表示散列分片(hashed sharding)
分片键字段和值(Shard Key Fields and Values)
存在(Exiatence)
自V4.4版本,已分片集合中的文档可以允许缺失分片键。缺少分片键和空值分片键属于统一范围;在V4.2及之前的版本中,已分片集合中的文档必须都有分片键。
更新分片键的值(Update Field's Value)
自V4.2,你可以更新文档的分片键值,除非分片键字段是不可变的_id字段。在V4.2之前,一个文档的分片键值是不可变的。
优化分片键(Refine a Shard Key)
自MongoDB4.4,你可以使用 refineCollectionShardKey 来定义集合的分片键。refineCollectionShardKey 添加一个或多个后缀字段以创建新的分片键。
例如,假设你已存在一个名为 orders 的集合,该集合拥有分片键 { customer_id : 1} 。你可以通过向分片键添加后缀order_id字段来更改分片键,以便 {customer_id : 1 , order_id : 1} 成为新的分片键。
细化集合的分片键可以实现更细粒度的数据分布,并且可以解决由于基数不足使得现有键导致了巨型块(即不可分割的块)。
在4.2及之前版本的MongoDB中,在分片之后分片键的选择是不能更改的。
分片键索引(Shard Key Indexes)
所有已分片的集合都必定拥有支持分片键的索引,索引既可以是在分片键上建立的索引,也可以是分片键是其前缀的复合索引:
- 如果集合是空集合且分片键上没有索引存在,
sh.shardCollection()将在分片键上创建索引; - 如果集合非空,则你必须在使用
sh.shardCollection()之前在分片键上面创建索引。
如果你删除了分片键的最后一个有效索引,请通过仅在分片键上创建索引来恢复。
唯一索引(Unique Indexes)
MongoDB可以对范围分片键索引强制执行唯一性约束。通过在分片键上使用唯一约束,MongoDB对整个键组合而不是片键的单个组件强制唯一性。
对于范围分片的集合,仅有以下的索引可以是唯一的:
- 分片键上的索引;
- 分片键是前缀的复合索引;
- 默认的
_id索引;然而,如果_id字段不是分片键或分片键的前缀,则_id索引仅对每个分片强制执行唯一性约束。
唯一性和_id索引
如果 _id 不是分片键或分片键的前缀,则 _id 索引仅对每一个分片强制执行唯一性约束而不是跨分片执行。
例如,考虑存在某个跨越两个分片且拥有分片键 { x : 1} 的集合,由于 _id 不是分片键的一部分,集合可以在分片A存在一个文档的 _id 值为1,在分片B也存在一个 _id 值为1的文档。
如果id 既不是分片键也不是分片键的前缀,则MongoDB期望应用程序强制跨分片的id值具有唯一性。
唯一性索引限制意味着:
- 对于一个待分片集合,如果该集合有其他唯一索引,则不能对该集合进行分片;
- 对于一个已经分片的集合,你不能在其他字段上面创建唯一性索引;
- 唯一索引为缺少索引字段的文档存储空值(null value);即缺少索引字段被视为是
null索引键值的另一个实例。
为了在分片键值上强制执行唯一性约束,在 sh.shardCollection() 方法中传递参数 unique 并设为 true:
- 如果集合是空集合且分片键上没有索引,
sh.shardCollection()将在分片键上创建索引; - 如果集合是非空集合,在使用
sh.shardCollection()之前,你必须创建索引。
尽管你可以创建以分片键作为前缀的唯一复合索引,如果使用 unique 参数,集合必须拥有分片键上的唯一性索引。
你不能对哈希索引(hashed index)指定唯一性限制。
选择分片键(Choosing a Shard Key)
分片键的选择可能会影响块在可用分片上的创建和分布。这回影响分片集群内操作的整体效率和性能。
理想的分片键允许MongoDB在整个集群均匀地分布文档。
至少需要考虑一个潜在分片键的基数(cardinality)、频率(frequency)和单调性(monotonicity)的后果。
分片键基数(Shard Key Cardinality)
分片键的基数决定了平衡器(balancer)能创建的块的最大数量。这会降低或消除集群中水平扩展的有效性。
在任何给定时间,唯一的分片键值最多只能存在于一个块上面。如果 分片键拥有基数 4 ,则在分片集群中不能有超过4个块,每个块存储一个唯一的分片键值。这也将集群中的有效分片数量限制为4个------添加额外的分片并不会带来好处。
下图说明了分片集群使用字段 X 作为分片键。如果 X 拥有较低的基数,则插入数据的分布可能类似图中所示:

此示例中的集群不会水平扩展,因为传入的写入只会路由到分片的子集。
选择具有高基数的分片键本身并不能保证数据在分片集群中的均匀分布。分片键的频率和单调性也对数据的分布有影响,在选择分片键的时候将每一个因素都纳入考虑。
如果你的数据模型要求在较低基数的键上面进行分片,考虑使用有较高基数的字段组成复合索引。
分片键频率(Shard Key Frequence)
考虑一个表示分片键值范围的集合------分片键值的频率(frequency)表示了给定值在数据中出现的频率。如果文档中的大多数都包含了这些值的子集,然后存储这些文档的块成为集群内的瓶颈。此外,随着这些块的增长,它们可能变为不可分割的块,因为它们不能再进一步分割。这会降低或消除集群内水平扩展的有效性。
下图表示了一个以字段 x 作为分片键的集群。如果X值得一个子集在集群内高频率出现,则插入数据得分布类似下图:

选择低频率得分片键本身并不能保证数据在分片集群中均匀分布。基数和单调性也对数据的分布有影响,在选择分片键的时候将每一个因素都纳入考虑。
如果你的数据模型要求在较高频率的键上面进行分片,考虑使用具有唯一值或低频值的复合索引。
分片键单调性(Shard Key Monotonicity)
单调增加或减少的值的分片键更有可能将插入分配到集群内的单个分片。
发生这种情况是因为每个集群都有一个块,该块捕获具有 maxKey 上限的范围。maxKey 总是比较高于其他值。相似地,有一个块,该块捕获具有 minKey 下限的范围。minKey 总是比较低于其他值。
如果分片键的值总是增加,所有的插入操作都被路由至以 maxKey 作为上限的块。如果分片键的值总是减少,所有的插入操作都被路由至以 minKey 作为下限的块。包含该块的分片将是写入操作的瓶颈。
下图表明了一个以字段 x 作为分片键的分片集群。如果字段x的值单挑增加,则插入文档的分布类似下图:

如果分片键的值单调减少,则所有的插入数据被路由至 Chunk A。
选择一个不单调改变的分片键本身并不保证数据在分片键均匀分布。基数和频率也对数据的分布有影响,在选择分片键时每一个因素都应当纳入考虑。
如果你的模型要求在一个单调改变的键上分片,考虑使用哈希分片(Hashed Sharding)。
更改文档的分片键值(Change a Document's Shard Key Value)
备注
在更改分片键值时:
- 你必须在mongos上执行操作而不是直接对分片进行操作;
- 你必须在事务中运行或作为可重试写入运行;
- 你必须在查询过滤器的完整分片键上包含相等的条件。例如,集合消息显示使用
{ activityid : 1 , userid : 1}作为分片键,为了更新一个文档的分片键,你必须在查询过滤器中包含activityid : <value> , userid : <value>。你可以根据需要在过滤器中包含其他字段。
自V4.2,如果分片键不是不可变的 _id 字段,你可以更改文档的分片键值。使用下述操作来更新文档的分片键值:
| 命令行 | 方法 |
|---|---|
multi : false 的 update |
db.collection.replaceOne() db.collection.updateOne() multi : false 的db.collection.update() |
| findAndModify | db.collection.replaceOne() |
注意
自v4.4,分片集合中的文档可以缺失分片键值。在更改文档的分片键时采取预防措施,避免意外删除分片键。
缺失分片键(Missing Shard Key)
自v4.4,分片集合中的文档可以缺失分片键值。
块范围和缺失的分片键字段(Chunk Range and Missing Shard Key Fields)
缺失分片键和具有空值的分片键属于相同的范围。示例,如果分片键处于 { x : 1 , y : 1},则:
| 缺失分片键文档 | 落入相同范围(的键值为空的文档) |
|---|---|
| { x : 'hello' } | { x : 'hello' , y : null} |
| { y : 'goodbye' } | { x : null , y : 'goodbye'} |
| { z : 'oops' } | { x : null , y : null } |
读写操作和缺失分片键值(Read/Write Operations and Missing Shard Key Fields)
为了定位缺失分片键的文档,可以通过在分片键字段上使用 { $exists : false }过滤条件。例如,如果分片键在字段 {x:1, y:1}上,为了赵傲缺失分片键的文档:
shell
db.shardedcollection.find( { $or: [ { x: { $exists: false } }, { y: { $exists: false } } ] } )
如果你指定 空相等 查询条件(例如:{ x : null} ),则过滤器将既匹配到缺失分片键的文档,也会匹配到分片键值设为 null 的文档。
一些写操作,例如带有 upsert 规范的写入(即带有更新插入的写操作,upsert是操作的可选参数,其含义是如果存在记录则更新,否则插入,默认为false,不插入,当值为true时,插入。)。在这些情况下,为了定位缺失分片键的文档,除了以 null 相等性匹配外,还包括另一个过滤条件,:
{ _id: <value>, <shardkeyfield>: null } // _id of the document missing shard key
设置缺失的分片键字段(Set the Missing Shard Key Fields)
为了设置缺失的分片键(这不同于本文前面部分提到的 更改文档的分片键值 ,即前文提到的是针对一个存在的分片键字段进行值得修改),你可以在mongos上使用下面的操作来实现:
| 命令(Command) | 方法(Method) | 描述 |
|---|---|---|
带有 { multi : true } 的 update |
带有 { multi : true} 的 db.collection.update() |
1. 仅可用于将缺少的键值设置为 null ;2. 可在事务内或者事务外进行执行; 3. 可以当作可重试写入执行,也可以不以可重试执行的方式 |
带有 { multi : false} 的 update |
带有 { multi : false} 的 db.collection.update();db.collection.updateOne() ; db.collection.replaceOne() |
1. 可用于将缺少的键值设置为 null 或其他任意数值; 2. 要设置为非空值,必须在事务内执行或作为可重试写入执行; |
findAndModify |
db.collection.findOneAndReplace() ; db.collection.findOneAndUpdate() ; db.collection.findAndModify() | 1. 可用于将缺少的键值设置为 null 或其他任意数值; 2. 要设置为非空值,必须在事务内执行或作为可重试写入执行; 3. 必须在查询过滤器里面包含分片键上的相等条件。由于缺少的键值作为空相等匹配的一部分返回,为避免更新空值键,请酌情包括其他查询条件。 |
| db.collection.bulkWrite() ; Bulk.find.replaceOne() ; Bulk.find.updateOne() ; Bulk.find.update() | 1. 要设置为空值,可以在批量操作(the bulk operation)中指定多个分片键修改 ; 2. 要设置为非空值,可以在批量操作中指定单个 shard key 修改;即批量大小为1 ; 3. 要设置为非空值,必须在事务内执行或作为可重试写入执行; |
一旦设置了分片键的值,可参考 更改文档的分片键值 部分进行分片键值的修改。