我正在创建一个Azure Cosmos数据库,以在多租户系统中存储多个客户的电话记录。用户需要按电话号码,姓名,分机号和日期范围搜索这些电话记录。每个电话记录的结构都非常牢固(我认为),如下所示:
{
"id": "M7o_ddRB7VRpjUA",
"ownerTime": "2458671015_202008",
"ownerId": "2458671015",
"callTime": "2020-08-30T18:47:44.424+00:00",
"direction": "Outbound",
"action": "VoIP Call",
"result": "Call connected",
"duration": 57,
"hasR": true,
"hasV": false,
"xNums": [
"2605"
],
"xIds": [
"2204328014"
],
"names": [
"sally wayfield"
],
"phoneNums": [
"2098368307",
"2097449211"
],
"emails": [
"sally.wayfield@company.com"
],
"xNums_s": "2605",
"xIds_s": "2204328014",
"phoneNums_s": "2098368307 2097449211",
"names_s": "sally wayfield",
"emails_s": "sally.wayfield@company.com",
"_rid": "QB1nAK5kxPMBAAAAAAAAAA==",
"_self": "dbs/QB1nAA==/colls/QB1nAK5kxPM=/docs/QB1nAK5kxPMBAAAAAAAAAA==/",
"_etag": "\"d2013319-0000-0500-0000-5f4c63dd0000\"",
"_attachments": "attachments/",
"_ts": 1598841821
}
但是我一直在努力提出一个好的分区密钥策略。我最初的想法是保持简单,并指定每个租户的帐户ID作为分区键。在上方的文档中,该帐户ID为ownerId
。我对旧数据库中的一些导入数据进行了基准测试,效果很好。但是,每个租户的通话记录量差异很大。我遇到的一个特殊问题是,每天有几位租户接收大约10,000个通话记录。而在6个月的时间内,这大约相当于4GB。由于我们每天继续为这些帐户提取10,000个通话记录,因此我们将在3年内达到每个分区20GB的限制。因此,该策略将不起作用。
我的下一个想法是将时态数据作为分区键的一部分。这似乎很有意义,因为我们每天都在批量导入大量带时间戳的数据。并且,针对该数据库的所有查询中有99%将包含日期范围。我想出了一个派生的抽象值,在上面的文档中标记为ownerTime
。此值将帐户ID与时间因素(每个电话的年和月)组合在一起。使用此方案,每个租户每年将获得12个分区,而不是整个帐户的单个分区。每个租户的数量仍在变化,因此我们仍将有一些大分区和一些小分区。但是差异不会像我们每个租户有一个分区那样极端。
使用“每个租户一个分区”策略,典型的查询如下所示:
partitionKey = / ownerId
select c.id,c.callTime,c.direction,c.action,c.result,c.duration,c.hasR,c.hasV,c.callers
from c
where
ownerId='2458671015'
and c.callTime>='2020-01-01T00:00:00 +000'
and c.callTime<='2020-08-31T23:59:59 +000'
and (CONTAINS(c.phoneNums_s, 'rus')
or CONTAINS(c.names_s, 'rus')
or CONTAINS(c.xNums_s, 'rus'))
order by c.callTime desc
使用ownerTime
分区策略,相同的查询如下所示:
partitionKey = / ownerTime
select c.id,c.callTime,c.direction,c.action,c.result,c.duration,c.hasR,c.hasV,c.callers
from c
where array_contains([
'2458671015_202001',
'2458671015_202002',
'2458671015_202003',
'2458671015_202004',
'2458671015_202005',
'2458671015_202006',
'2458671015_202007',
'2458671015_202008'], c.ownerTime)
and c.callTime>='2020-01-01T00:00:00 +000'
and c.callTime<='2020-08-31T23:59:59 +000'
and (CONTAINS(c.phoneNums_s, 'rus')
or CONTAINS(c.names_s, 'rus')
or CONTAINS(c.xNums_s, 'rus'))
order by c.callTime desc
您可以在ownerTime
策略中看到,我正在明确计算需要搜索的分区(基于搜索日期范围)。我正在查询中定义这些分区。
在我最初的基准测试中,我尝试通过这些查询针对我的一个大容量帐户(有110万条电话记录)检索25条记录。 “每个租户单个分区”查询使用了大约2200个RU,以带回一条记录。我不得不继续使用延续令牌来获得更多结果,每个请求使用大约2000 RU,直到最终获得25条记录。经过多次迭代后,我失去了RU的数量。 ownerTime
分区查询使用了大约2500个RU来检索25条记录,而不必使用延续令牌。
似乎ownerTime
策略是更好的选择,只要所有查询都包含日期范围。但是我想获得有关此策略的一些反馈。它不是完美的,但似乎效果很好。我来自SQL背景,这是我第一次实现文档数据库。我这样做是为了利用改进的文本搜索和可伸缩性。将来使用ownerTime
分区策略时是否会遇到“陷阱”?根据我提供的信息,有没有一种分区策略可能会更好?
答案 0 :(得分:0)
有界跨分区查询绝对比无界查询好得多,因此基于您在此处的问题,我同意将时间元素包括在内以创建合成分区键是一个很好的分区策略。
这里没有解释什么,是否还有其他大量查询在过滤谓词中不包含时间(或ownerId)?如果是这样,那么您将需要使用Change Feed将第一个容器中的数据复制并同步到另一个容器中,并使用对这些查询也有意义的分区键。
另一种选择也是,特别是如果您希望对此数据进行分析,则是启用分析存储并使用Synapse Link,然后对数据编写Spark(以及不久的SQL Serverless)查询。