按日期查询DynamoDB

172

我来自关系型数据库背景,现在尝试使用亚马逊的DynamoDB。

我有一个表格,其中包含哈希键“DataID”和范围“CreatedAt”以及一堆数据项。

我正在尝试获取所有在特定日期之后创建并按日期排序的项目,这在关系型数据库中非常简单。

在DynamoDB中,我能找到的最接近的东西是使用范围键大于过滤器的查询。唯一的问题是要执行查询,我需要一个哈希键,这违背了初衷。

那么我做错了什么?我的表模式有误吗,哈希键不应该是唯一的吗?还是有其他方法可以查询?

8个回答

92

根据您当前的表结构,这在DynamoDB中目前是不可能的。巨大的挑战是要理解表的哈希键(分区)应被视为创建单独的表。在某些方面,这真的很强大(将分区键视为为每个用户或客户等创建一个新表)。

查询只能在单个分区中执行。这就是故事的结束。这意味着如果您想按日期查询(您将希望使用自纪元以来的毫秒数),则您想要在单个查询中检索的所有项目必须具有相同的哈希(分区键)。

我应该说明一下。您绝对可以通过您要查找的标准来扫描,这没有问题,但这意味着您将查看表中的每一行,然后检查该行是否具有与您的参数匹配的日期。这非常昂贵,特别是如果您首先要存储事件按日期存储(即您有很多行)。

您可能会想把所有数据放在单个分区中来解决问题,您当然可以这样做,但是考虑到每个分区仅接收总设置量的一小部分,您的吞吐量将非常低。

最好的方法是确定更有用的分区以保存数据:

  • 您是否真的需要查看所有行,还是仅特定用户的行?

  • 首先按月份缩小列表并执行多个查询(每个月一个)是否可以?或按年份?

  • 如果您正在进行时间序列分析,则有几个选项:更改分区键以计算PUT上的内容以使query更容易,或使用另一个AWS产品,如Kinesis,它适用于只追加日志。


8
我想强调你在最后一段提出的“按年考虑”的选项。创建一个名为 yyyy 的属性,并以此进行哈希,同时创建一个 created 日期作为范围键。这样,每年您可以获得10GB的数据(每天27MB),这对于大多数情况可能足够了。但这意味着当日期查询跨越年份时,您必须针对每年创建一个查询,但至少它能工作,并且比创建虚拟哈希键更安全。 - Ryan Shillington
1
另一个选项:https://dev59.com/TlsV5IYBdhLWcg3wpgRD?noredirect=1&lq=1 - Ryan Shillington
1
正如上面的链接所解释的那样,严格基于时间的分区键可能会导致热点问题。如果您必须使用基于时间的分区键,则最好在分区键中添加其他元素,以将时间段分散到多个分区中。我曾看到建议只是在0-n之间使用前缀,其中n是每个时间桶应该分布在的分区数。 - dres
1
@RyanShillington 全局二级索引没有10GB的限制。该限制仅适用于本地二级索引。 - Simon Forsberg
2
每个分区只接收总集合数量的一部分,这已经不再是真实情况了,因为由于自适应容量,它已经得到改变。您可以在表中拥有一个始终具有相同值的虚拟属性。然后使用虚拟属性作为分区键和 CreatedAt 作为排序键创建全局二级索引。然后,您就可以跨所有项目按日期进行查询。这似乎有些取巧,但是否有更好的方法呢? - Bennett McElwee
显示剩余2条评论

47

更新的回答:

DynamoDB允许指定辅助索引来帮助进行此类查询。辅助索引可以是全局的,这意味着索引跨越哈希键扩展整个表,也可以是本地的,这意味着索引将存在于每个哈希键分区中,因此在进行查询时还需要指定哈希键。

对于这个问题中的用例,您需要在“CreatedAt”字段上使用全局辅助索引。

有关DynamoDB辅助索引的更多信息,请参见辅助索引文档

原始回答:

DynamoDB不允许仅基于范围键进行索引查找。哈希键是必需的,以便服务知道要查找数据的哪个分区。

当然,您可以执行扫描操作以按日期值进行过滤,但是这将需要完整的表扫描,因此并不理想。

如果您需要跨多个主键按时间执行索引查找记录,则DynamoDB可能不是您使用的理想服务,或者您可能需要使用单独的表(无论是在DynamoDB中还是关系存储中)来存储可以执行索引查找的项目元数据。


22
请查看下面答案的评论;目前没有处理这个问题的方法,至少不是针对OP所问的。GSIs仍然需要您指定一个哈希键,因此您无法查询所有记录中CreatedAt大于某个时间点的记录。 - pkaeding
5
@pkaeding 是正确的。你可以使用 scan 获取比某个特定 日期 更早的记录,但是无法按排序顺序获取它们。在这种情况下,GSI 也无法帮助你。无法对 分区键 进行排序,也无法仅查询 _范围键_。 - gkiko
24
对于那些感到困惑的人。 这个答案是错误的。 他原来的回答是正确的,但他更新后的回答是错误的。请阅读下面的Warren Parad的答案。它是正确的。 - Ryan Shillington
1
@MikeBrant 我想要查询(而不是扫描,因为它会查看表中的每个项目,使其非常低效和昂贵)一个表格上的表格GSI哈希键(CreatedAt),使用大于号。据我所知,这是不可能的。 - azizj
5
在使用日期作为主分区时,你可能会遇到一个问题,即由于在大多数数据存储中,新数据的查询频率通常比旧数据高,因此你可能会在某些或某个节点上创建热点。请注意,这里的“热点”是指某个节点处理请求的速度慢于其他节点,从而影响整个系统性能的情况。 - DrDirk
显示剩余3条评论

30

我用以下方法解决了这个问题,即创建了一个全局二级索引。不确定这是否是最佳方法,但希望对某些人有用。

Hash Key                 | Range Key
------------------------------------
Date value of CreatedAt  | CreatedAt

HTTP API用户对检索数据的时间范围有限制,缺省值为24小时。

这样,我总是可以将HashKey指定为当前日期的天数,并在检索时使用>和<操作符作为RangeKey。这样数据也会分布在多个shard中。


2
这是最好的方案,除非您可以创建更小的日期槽以跨越更多分片,具体取决于您的用例。如果您只需要能够查找已经过去的时间,并且知道您处理项目的速度足够快,例如,您可以让哈希键成为日期+时间的小时部分,例如将日期2021-04-17T16:22:07.000Z划分为哈希键2021-04-17T16和范围键22:07.000Z,这将使您能够使用查询dateHour = "2021-04-17T16" AND minutesSeconds <= 22:07来查找该日期之前的所有项目。 - JHH
1
如果“处理过去的项目”意味着设置一些标志,那么该标志可以是哈希键的前缀,例如<flag>_<date>T<hour>。然后搜索“NOTDONE_2021-04-17T16”将不包括“DONE_2021-04-17T16”项目。 - JHH
就其价值而言,这基本上是亚马逊在处理时间序列数据的示例中展示的内容。它仅具有时间(而不是日期时间)范围,但这只是次要问题。 - Captain Man

13

你的哈希键(主键和排序键)必须是唯一的(除非你有一个像其他人所说的范围那样的键)。

在你的情况下,为了查询你的表,你应该有一个辅助索引。

|  ID  | DataID | Created | Data |
|------+--------+---------+------|
| hash | xxxxx  | 1234567 | blah |

您的哈希键是ID。 您的二级索引定义为:DataID-Created-index(这是DynamoDB将使用的名称)。

然后,您可以进行以下查询:

var params = {
    TableName: "Table",
    IndexName: "DataID-Created-index",
    KeyConditionExpression: "DataID = :v_ID AND Created > :v_created",
    ExpressionAttributeValues: {":v_ID": {S: "some_id"},
                                ":v_created": {N: "timestamp"}
    },
    ProjectionExpression: "ID, DataID, Created, Data"
};

ddb.query(params, function(err, data) {
    if (err) 
        console.log(err);
    else {
        data.Items.sort(function(a, b) {
            return parseFloat(a.Created.N) - parseFloat(b.Created.N);
        });
        // More code here
    }
});

基本上,您的查询看起来像:

SELECT * FROM TABLE WHERE DataID = "some_id" AND Created > timestamp;

二级索引将增加所需的读写容量单位,因此您需要考虑这一点。但它仍然比执行扫描要好得多,扫描将在读取和时间上产生成本(并且我认为仅限于100个项目)。

这可能不是最佳方法,但对于习惯于 RD(我也习惯于 SQL)的人来说,这是实现高效率的最快方式。由于没有关于架构的约束条件,您可以制作出可行的东西,一旦有带宽来处理效率最高的方式,就可以改变事物的方式。


1
你说没有限制,但你应该知道这种方法意味着你最多只能保存10GB的数据(单个分区的最大值)。 - Ryan Shillington
1
如果我们知道DataID,那么这将是我们的方法。但是在这里,我们需要获取每一行,其创建日期大于某个日期。 - Yasith Prabuddhaka

4
您可以将哈希键设置为“产品类别” ID,然后将范围键设置为时间戳与附加唯一 ID 的组合。这样,您就知道哈希键,并且仍然可以查询大于某个日期的数据。

1
你可以拥有多个相同的哈希键;但前提是你有一个可变的范围键。这就像文件格式一样;你可以在同一个文件夹中拥有2个相同名称的文件,只要它们的格式不同即可。如果它们的格式相同,则它们的名称必须不同。相同的概念适用于DynamoDB的哈希/范围键;只需将哈希视为名称,将范围视为格式即可。
此外,我不记得他们在OP时是否有这些,但现在他们提供本地二级索引。
我的理解是,现在应该可以执行所需的查询而无需进行全面扫描。缺点是这些索引必须在表创建时指定,并且在创建项目时也不能留空(我认为)。此外,它们需要额外的吞吐量(尽管通常不像扫描那么多)和存储,因此它不是一个完美的解决方案,但对于某些人来说,它是一个可行的替代方案。
我仍然建议使用Mike Brant的答案作为使用DynamoDB的首选方法,并且我自己也使用这种方法。在我的情况下,我只有一个中央表,其中仅有一个散列键作为我的ID,然后是具有可查询的哈希和范围的二级表,然后该项将代码直接指向中央表的“感兴趣的项目”。
有关辅助索引的其他数据可以在亚马逊的DynamoDB文档此处中找到。
无论如何,希望这对于遇到这个线程的任何其他人都有所帮助。

我尝试创建一个DynamoDB表,其中有类型为哈希的AWSDynamoDBKeySchemaElement 'createdAt',以及类型为范围的AWSDynamoDBKeySchemaElement 'createdAt'。但是我收到了一个错误,错误信息为Error Domain=com.amazonaws.AWSDynamoDBErrorDomain Code=0 "(null)" UserInfo={__type=com.amazon.coral.validate#ValidationException, message=Both the Hash Key and the Range Key element in the KeySchema have the same name}。所以我认为你说的不正确。 - user1709076
我相信你误解了(虽然我想我的描述也不是很清楚)。在表中,您不能有两个具有相同名称的不同属性(列),但是当您创建具有范围键的哈希键时,只要它们的范围不同,您可以拥有多个使用相同哈希的项目,反之亦然。例如:您的哈希是“ID”,您的范围是“日期”,只要它们的日期不同,您就可以有2个ID为“1234”的实例。 - DGolberg
阿,DGoldberg!我现在明白你的意思了。太好了。对于我的情况,因为我只想查询文本消息“日期大于x”的情况,看起来我可以将所有文本消息设置为相同的“fake_hash = 1”。然后执行查询操作。keyConditionExpression = @"fake_hash = 1 and #Date > :val"。非常感谢。如果您有其他建议,我很乐意听取,因为似乎总是使用相同值的哈希值有些奇怪? - user1709076
我需要再次确认,但我相当确定您可以对仅哈希表进行查询...尽管如果您将日期/时间戳用作哈希,则建议记录到最短的单位,如毫秒或纳秒/微秒(代码可以记录的最小时间单位),以减少日期/时间重叠的可能性。此外,您可以添加乐观锁定以进一步减少重叠的可能性:http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/JavaVersionSupportHLAPI.html 如果存在冲突,请简单地重试另一个时间。 - DGolberg

-1

有效查询 1. aws dynamodb scan --table-name tableName --region us-east-1 --filter-expression "begins_with(createdTm,:gen)" --expression-attribute-values "{":gen":{"S":"2021-04-15"}}" --select "COUNT"

2. aws dynamodb scan --table-name tableName --region us-east-1 --filter-expression "createdTm BETWEEN :v1 AND :v2" --expression-attribute-values '{":v1":{"S":"2021-04-13"}, ":v2":{"S":"2021-04-14"}}' --select "COUNT"


请编辑以添加解释,说明您的建议如何/为什么解决了OP的问题,并修复格式问题。您可以突出显示代码,然后使用滚动条,或使用Markdown格式化内联代码或代码块,具体取决于情况。更多信息可在StackOverflow.com/help上获得。 - SherylHohman

-10

更新答案 使用可预测吞吐量的Dynamo DB查询没有方便的方法来完成此操作。一种(次优)选择是使用带有人造HashKey&CreatedAt的GSI。然后仅按HashKey查询并提到ScanIndexForward以排序结果。如果您可以想出自然的HashKey(例如物品类别等),则此方法是赢家。另一方面,如果对所有项目保持相同的HashKey,则当数据集增长超过10GB(一个分区)时,它将在很大程度上影响吞吐量。

原始答案: 现在你可以通过使用GSI在DynamoDB中完成这个任务。将“CreatedAt”字段设置为GSI,并发出诸如(GT some_date)之类的查询。将日期存储为数字(自纪元以来的毫秒数)以进行此类查询。

详细信息在此处: Global Secondary Indexes - Amazon DynamoDB:http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html#GSI.Using

这是一个非常强大的功能。请注意,查询仅限于(EQ | LE | LT | GE | GT | BEGINS_WITH | BETWEEN)条件 - 亚马逊 DynamoDB:http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html


32
我Downvote了,因为据我所知,你的回答是不正确的。就像表的主键一样,你只能使用EQ运算符查询GSI的哈希键。如果你暗示CreatedAt应该是GSI的范围键,那么你需要选择一个哈希键 - 然后你又回到了起点,因为你只能针对哈希键的特定值查询CreatedAt上的GT。 - PaF
同意PaF的观点。在GSI中使用哈希键作为创建时间并不能解决OP中提出的问题。 - 4-8-15-16-23-42

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接