我正在尝试建模一个需要非常高的写入吞吐量和合理的读取吞吐量的数据库。我有一组分布式系统,它们将“事件”数据添加到数据库中。
目前,事件记录的ID是Guid。我一直在阅读guids不倾向于创建好的索引,因为它们的随机分布意味着最近的数据将分散在磁盘中,这可能导致分页问题。
所以这是我想要验证的第一个假设: 我假设我不想选择一个创建一个正确平衡树的_id,比如像autonumber这样的东西。这将是有益的,因为最近的两个事件基本上就在磁盘上彼此相邻。这是正确的假设吗?
假设(1)是正确的,那么我正在尝试找出生成这样一个id的最佳方法。我知道Mongo本身支持ObjectId,这对于将数据绑定到Mongo的应用程序很方便,但我的应用程序不是这样。由于有多个系统生成数据,因此模拟“自动编号”字段有点问题,因为mongo不支持服务器端的自动编号,因此生产者必须分配id,如果他们不喜欢则很难我不知道其他系统在做什么。
为了解决这个问题,我正在考虑的是将_id字段作为{localId,producerId}上的复合键,其中本地id是生产者可以生成的自动编号,因为producerId将使其唯一。 ProducerId是我可以在制作人之间协商的东西,以便他们能够提出独特的ID。
所以这是我的下一个问题: 如果我的目标是从所有生产者那里获取最新的数据,那么{localId,producerId}应该是首选的密钥排序,因为localId将是right-ist而producerId将是一个小集群,我宁愿最近的2事件彼此保持在一起。如果我颠倒了那个顺序,那么我对树最终看起来如何的推理就像下面这样:
root
/ | \
p0 p1 p2
/ | \
e0..n e0..n e0..n
其中p#是生产者ID,e#是一个事件。这似乎会将我的索引分成p#数据集群,而新事件不一定是彼此相邻的。我对首选排序的假设应该(请验证)看起来像这样:
root
/ | \
e0 e1 e2
/ | \
p0..n p0..n p0..n
这似乎可以使最近的事件保持在彼此附近。 (我知道Mongo使用B树作为索引,但我只是想在这里简化视觉效果)。
我可以看到{localId,producerId}的唯一警告是用户的常见查询是列出生产者的最新事件,{producerId,localId}实际上会处理得更好。为了使这个查询与{localId,producerId}一起使用,我想我还需要将producerId作为字段添加到文档中,并将其编入索引。
要明确我的问题是什么,我想知道我是否正确地思考这个问题,或者是否有更明智的方法来解决这个问题。
由于
答案 0 :(得分:1)
要回答你的问题:像这样的化合物:{a,b}将以散点查询结束,如果你只是用b查询然后按a排序。但它会使用索引进行排序。
如果您使用Document而不是ObjectId,_id将被编入索引,但不会被使用,但它不是复合索引!
示例:
鉴于此集合中的文档'a'并且没有其他索引:
{ "_id" : { "e" : 1, "p" : 1 } }
{ "_id" : { "e" : 1, "p" : 2 } }
{ "_id" : { "e" : 2, "p" : 1 } }
{ "_id" : { "e" : 1, "p" : 3 } }
{ "_id" : { "e" : 2, "p" : 3 } }
{ "_id" : { "e" : 2, "p" : 2 } }
{ "_id" : { "e" : 3, "p" : 1 } }
{ "_id" : { "e" : 3, "p" : 2 } }
{ "_id" : { "e" : 3, "p" : 3 } }
这样的查询:
db.a.find({'_id.p' : 2}).sort({'_id.e' : 1}).explain()
不会使用索引:
{
"cursor" : "BasicCursor",
"nscanned" : 9,
"nscannedObjects" : 9,
"n" : 3,
"scanAndOrder" : true,
"millis" : 0,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
}
}
仅仅因为文档被编入索引。
如果您创建这样的索引:
db.a.ensureIndex({'_id.e' : 1, '_id.p' : 1})
然后再次查询:
db.a.find({'_id.p' : 2}).sort({'_id.e' : 1}).explain()
{
"cursor" : "BtreeCursor _id.e_1__id.p_1",
"nscanned" : 9,
"nscannedObjects" : 3,
"n" : 3,
"millis" : 0,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
"_id.e" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
],
"_id.p" : [
[
2,
2
]
]
}
}
它会查询索引(nscanned:9)因为排序然后提取对象:3,这比_id排序更好(nscanned和nscannedObjects将是9)。
因此,对于高写入吞吐量(每秒写入超过15k),您可能需要进行分片。如果选项设置,两个索引都将保证唯一性。但只有复合分片键才能帮助您进行直接查询而不会分散聚集。
使用({'_ id.e':1,'_ id.p':1})作为分片键将直接路由所有“_id.e”查询但不“_id.p”(不带“e”)查询,所以这些查询将发送到每个主机并在那里以索引查找结束,但也可能是快速的(取决于网络等)。如果你想用“p”聚类这些查询,你必须把'_id.p'作为复合键的第一部分,如下所示:
{'_id.p' : 1, '_id.e' : 1}
因此所有“p”查询都是直接查询。但是,是的,这将分散整个群集中的最近事件。因此,使用基于时间的密钥的单独索引可能会加快这些分散查询的速度。
我会生成一些示例数据,并在开发系统中使用两个分片进行设置并使用.explain()来选择分片键+索引。