

# 在 DynamoDB 中使用全局二级索引
<a name="GSI"></a>

一些应用程序可能需要使用很多不同的属性作为查询条件，来执行许多类型的查询。要支持这些要求，您可以创建一个或多个*全局二级索引*，在 Amazon DynamoDB 中针对这些索引发出 `Query` 请求。

**Topics**
+ [场景：使用全局二级索引](#GSI.scenario)
+ [属性投影](#GSI.Projections)
+ [多属性键架构](#GSI.MultiAttributeKeys)
+ [从全局二级索引读取数据](#GSI.Reading)
+ [表与全局二级索引之间的数据同步](#GSI.Writes)
+ [具有全局二级索引的表类别](#GSI.tableclasses)
+ [全局二级索引的预调配吞吐量注意事项](#GSI.ThroughputConsiderations)
+ [全局二级索引的存储注意事项](#GSI.StorageConsiderations)
+ [设计模式](GSI.DesignPatterns.md)
+ [在 DynamoDB 中管理全局二级索引](GSI.OnlineOps.md)
+ [在 DynamoDB 中检测和纠正索引键违规](GSI.OnlineOps.ViolationDetection.md)
+ [处理全局二级索引：Java](GSIJavaDocumentAPI.md)
+ [处理全局二级索引：.NET](GSILowLevelDotNet.md)
+ [借助 AWS CLI 在 DynamoDB 中使用全局二级索引](GCICli.md)

## 场景：使用全局二级索引
<a name="GSI.scenario"></a>

为进行说明，考虑使用一个名为 `GameScores` 的表跟踪一个移动游戏应用程序的用户和分数。`GameScores` 中的每一项使用一个分区键 (`UserId`) 和一个排序键 (`GameTitle`) 标识。下表显示了此表中项目的组织方式，（并未显示所有属性。）

![\[包含用户 ID、头衔、得分、日期和输赢次数的 GameScores 表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/GSI_01.png)


现在假设您要编写一个排行榜应用程序以显示每个游戏的最高分数。指定键属性 (`UserId` 和 `GameTitle`) 的查询将会非常高效。但是，如果应用程序仅需要基于 `GameScores` 从 `GameTitle` 检索数据，则需要使用 `Scan` 操作。随着更多项目添加到表中，所有数据的扫描会变得缓慢且低效。这会使得难于回答以下问题：
+ 对游戏 Meteor Blasters 记录的最高分数是多少？
+ 哪个用户拥有 Galaxy Invaders 的最高分数？
+ 最高赢输比是多少？

要加快对非键属性的查询，您可以创建一个全局二级索引。全局二级索引包含从基表中选择的一组属性，但是这些属性按与表主键不同的主键进行排列。索引键不必具有来自表的任何键属性。它甚至不必具有与表相同的键架构。

例如，您可以创建名为 `GameTitleIndex` 的全局二级索引，其分区键为 `GameTitle`，排序键为 `TopScore`。基表的主键属性始终投影到某个索引，因此 `UserId` 属性也存在。`GameTitleIndex` 索引如下图所示。

![\[包含头衔、得分和用户 ID 的 GameTitleIndex 表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/GSI_02.png)


现在，您可以查询 `GameTitleIndex` 并方便地获取 Meteor Blasters 的分数。结果按排序键值 `TopScore` 进行排序。如果您将 `ScanIndexForward` 参数设置为 false，则结果按降序返回，因此最高分数最先返回。

每个全局二级索引都必须有分区键，另外可以有可选的排序键。索引键架构可以不同于基表架构。您可以拥有带有简单主键（分区键）的表，然后使用复合主键（分区键和排序键）创建全局二级索引，反之亦然。索引键属性可以包含来自基表的任意顶级 `String`、`Number` 或 `Binary` 属性。不允许使用其他标量类型、文档类型和集合类型。

您可以在需要时将其他基表属性投影到索引。当您查询索引时，DynamoDB 便可高效地检索这些已投影的属性。但是，全局二级索引查询无法从基表提取属性。例如，如果您如上图所示查询 `GameTitleIndex`，则查询无法访问除 `TopScore`（虽然键属性 `GameTitle` 和 `UserId` 将自动投影）之外的任何非键属性。

在 DynamoDB 表中，每个键值都必须唯一。但是，全局二级索引中的键值无需唯一。为进行说明，假设一个名为 Comet Quest 的游戏难度特别高，许多新用户进行尝试，但是无法获得零以上的分数。以下是可以表示这种情况的一些数据。


****  

| UserId | GameTitle | TopScore | 
| --- | --- | --- | 
| 123 | Comet Quest | 0 | 
| 201 | Comet Quest | 0 | 
| 301 | Comet Quest | 0 | 

当此数据添加到 `GameScores` 表中时，DynamoDB 将其传播到 `GameTitleIndex`。如果我们随后以 Comet Quest 作为 `GameTitle` 并以 0 作为 `TopScore` 来查询索引，则将返回以下数据。

![\[包含头衔、最高得分和用户 ID 的表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/GSI_05.png)


响应中仅显示具有指定键值的项。在该组数据中，项没有特定顺序。

全局二级索引仅跟踪其键属性实际存在的数据项。例如，假设您向 `GameScores` 表添加了另一个新项目，但是仅提供了必需的主键属性。


****  

| UserId | GameTitle | 
| --- | --- | 
| 400 | Comet Quest | 

因为您未指定 `TopScore` 属性，DynamoDB 不会将此项目传播到 `GameTitleIndex`。因此，如果您针对所有 Comet Quest 项目查询 `GameScores`，则会获得以下四个项目。

![\[包含 4 个头衔列表、最高得分和用户 ID 的表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/GSI_04.png)


对 `GameTitleIndex` 执行的相似查询仍会返回三项，而不是四个。这是因为，不存在 `TopScore` 的项不会传播到索引。

![\[包含 3 个头衔列表、最高得分和用户 ID 的表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/GSI_05.png)


## 属性投影
<a name="GSI.Projections"></a>

*投影*是从表复制到二级索引的属性集。表的分区键和排序键始终投影到索引中；您可以投影其他属性以支持应用程序的查询要求。当您查询索引时，Amazon DynamoDB 可以访问投影中的任何属性，就像这些属性位于自己的表中一样。

创建二级索引时，需要指定将投影到索引中的属性。DynamoDB 为此提供了三种不同的选项：
+ *KEYS\$1ONLY* – 索引中的每个项目仅包含表的分区键、排序键值以及索引键值。`KEYS_ONLY` 选项会导致最小二级索引。
+ *INCLUDE* – 除 `KEYS_ONLY` 中描述的属性外，二级索引还包括您指定的其他非键属性。
+ *ALL* – 二级索引包括源表中的所有属性。由于所有表数据都在索引中复制，因此 `ALL` 投影会产生最大二级索引。

在上图中，`GameTitleIndex` 只有一个投影属性：`UserId`。因此，尽管应用程序通过在查询中使用 `UserId` 和 `GameTitle` 能够高效确定每个游戏中得分榜选手的 `TopScore`，但不能高效确定得分榜选手的最高输赢比。为此，应用程序必须对基表执行额外查询，以获取每个得分榜选手的输赢数据。要支持对此数据进行查询，更高效的方法是将这些属性从基表投影到全局二级索引，如下图所示。

![\[将非键属性投影到 GSI 中以支持高效查询的描述。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/GSI_06.png)


因为非键属性 `Wins` 和 `Losses` 投影到索引，所以应用程序可以确定任何游戏或是任何游戏和用户 ID 组合的赢输比。

您在选择要投影到全局二级索引的属性时，必须在预置吞吐量成本和存储成本之间做出权衡：
+ 如果只需要访问少量属性，同时尽可能降低延迟，就应考虑仅将键属性投影到全局二级索引。索引越小，存储索引所需的成本越少，并且写入成本也会越少。
+ 如果您的应用程序频繁访问某些非键属性，就应考虑将这些属性投影到全局二级索引。全局二级索引的额外存储成本会抵消频繁执行表扫描的成本。
+ 如果需要频繁访问大多数非键属性，则可以将这些属性（甚至整个基表）投影到全局二级索引中。这为您带来了最大限度的灵活性。但是，您的存储成本将增长，甚至翻倍。
+ 如果您的应用程序并不会频繁查询表，但必须要对表中的数据执行大量写入或更新操作，就应考虑投影 `KEYS_ONLY`。这是最小的全局二级索引，但仍可用于查询活动。

## 多属性键架构
<a name="GSI.MultiAttributeKeys"></a>

全局二级索引支持多属性键，让您可以从多个属性组成分区键和排序键。使用多属性键，您可以从最多四个属性创建分区键，从最多四个属性创建排序键，这样每个键架构最多总共可以有八个属性。

多属性键无需手动将属性连接成合成键，这样可以简化数据模型。您可以直接使用域模型中的自然属性，而不是创建像 `TOURNAMENT#WINTER2024#REGION#NA-EAST` 这样的复合字符串。DynamoDB 自动处理复合键逻辑，对多个分区键属性一起进行哈希处理用于数据分布，并维护多个排序键属性的分层排序顺序。

例如，假设有一个游戏锦标赛系统，您想按锦标赛和地区组织比赛。使用多属性键，您可以将分区键定义为两个单独的属性：`tournamentId` 和 `region`。同样，您可以使用 `round`、`bracket` 和 `matchId` 等多个属性来定义排序键，用于创建自然的层次结构。这种方法可以保持数据类型化和代码整洁，无需对字符串进行操作或解析。

使用多属性键查询全局二级索引时，必须使用相等条件指定所有分区键属性。对于排序键属性，您可以按照它们在键架构中定义的顺序从左到右进行查询。这意味着您可以单独查询第一个排序键属性，同时查询前两个属性，或者同时查询所有属性，但您不能跳过中间的属性。不相等条件（例如 `>`、`<`、`BETWEEN` 或 `begins_with()`）必须是查询中的最后一个条件。

在现有表上创建全局二级索引时，多属性键特别有效。您可以使用表中已经存在的属性，而无需在数据中回填合成键。这样就可以创建索引，使用不同的属性组合来识别数据，从而直接向应用程序添加新的查询模式。

多属性键中的每个属性都可以有自己的数据类型：`String`（S）、`Number`（N）或 `Binary`（B）。选择数据类型时，请考虑 `Number` 属性按数字排序而无需补零，而 `String` 属性则按字典顺序排序。例如，如果您为分数属性使用 `Number` 类型，则值 5、50、500 和 1000 将按自然数字顺序排序。为相同的值使用 `String` 类型时，则排序的结果为“1000”、“5”、“50”、“500”，除非您用前导零填充它们。

在设计多属性键时，请按从最一般到最具体的顺序对属性进行排序。对于分区键，请将始终会一起查询且能够提供良好数据分布的属性组合在一起。对于排序键，请将经常查询的属性放在层次结构的前面，以最大限度地提高查询灵活性。这种排序让您可以按照与访问模式匹配的任何粒度级别进行查询。

有关实施示例，请参阅[多属性键](GSI.DesignPattern.MultiAttributeKeys.md)。

## 从全局二级索引读取数据
<a name="GSI.Reading"></a>

您可以使用 `Query` 和 `Scan` 操作从全局二级索引检索项目。`GetItem` 和 `BatchGetItem` 操作不能用于全局二级索引。

### 查询全局二级索引
<a name="GSI.Querying"></a>

您可以使用 `Query` 操作来访问全局二级索引中的一个或多个项目。查询必须指定要使用的基表名称和索引名称、查询结果中要返回的属性以及要应用的任何查询条件。DynamoDB 可以按升序或降序返回结果。

考虑为排行榜应用程序请求游戏数据的 `Query` 返回的以下数据。

```
{
    "TableName": "GameScores",
    "IndexName": "GameTitleIndex",
    "KeyConditionExpression": "GameTitle = :v_title",
    "ExpressionAttributeValues": {
        ":v_title": {"S": "Meteor Blasters"}
    },
    "ProjectionExpression": "UserId, TopScore",
    "ScanIndexForward": false
}
```

在此查询中：
+ DynamoDB 使用 *GameTitle* 分区键访问 *GameTitleIndex*，查找 Meteor Blasters 的索引项目。具有此键的所有索引项目都彼此相邻存储，以实现快速检索。
+ 在此游戏中，DynamoDB 使用索引访问此游戏的所有用户 ID 和最高分数。
+ 因为 `ScanIndexForward` 参数设置为 false，所以结果按降序返回。

### 扫描全局二级索引
<a name="GSI.Scanning"></a>

您可以使用 `Scan` 操作从全局二级索引检索全部数据。您必须在请求中提供基表名称和索引名称。通过 `Scan`，DynamoDB 可读取索引中的全部数据并将其返回到应用程序。您还可以请求仅返回部分数据并放弃其余数据。为此，请使用 `FilterExpression` 操作的 `Scan` 参数。有关更多信息，请参阅 [扫描的筛选表达式](Scan.md#Scan.FilterExpression)。

## 表与全局二级索引之间的数据同步
<a name="GSI.Writes"></a>

DynamoDB 自动将每个全局二级索引与其基表同步。当应用程序对某个表写入或删除项目时，该表的所有全局二级索引都会使用最终一致性模型异步更新。应用程序绝不会直接向索引中写入内容。但是，您有必要了解 DynamoDB 如何维护这些索引。

 全局二级索引继承基表的读/写入容量模式。有关更多信息，请参阅 [在 DynamoDB 中切换容量模式时的注意事项](bp-switching-capacity-modes.md)。

在创建全局二级索引之后，您可以指定一个或多个索引键属性及其数据类型。这就意味着，无论您何时向基表中写入项目，这些属性的数据类型必须与索引键架构的数据类型匹配。在 `GameTitleIndex` 的情况下，索引中的 `GameTitle` 分区键定义为 `String` 数据类型。索引中的 `TopScore` 排序键为 `Number` 类型。如果您尝试向 `GameScores` 表添加项目并为 `GameTitle` 或 `TopScore` 指定其他数据类型，DynamoDB 会因数据类型不匹配而返回 `ValidationException`。

在表中放置或删除项目时，表的全局二级索引会以最终一致性方式进行更新。在正常情况下，对表数据进行的更改会瞬间传播到全局二级索引。但是，在某些不常发生的故障情况下，可能出现较长时间的传播延迟。因此，应用程序需要预计和处理对全局二级索引进行的查询返回不是最新结果的情况。

如果向表中写入项目，无需指定全局二级索引任何排序键的属性。以 `GameTitleIndex` 为例，您无需指定 `TopScore` 属性的值就可以向 `GameScores` 表写入新项目。在本示例中，DynamoDB 不会向此特定项目的索引写入任何数据。

相较于索引数量较少的表，拥有较多全局二级索引的表会产生较高的写入活动成本。有关更多信息，请参阅 [全局二级索引的预调配吞吐量注意事项](#GSI.ThroughputConsiderations)。

## 具有全局二级索引的表类别
<a name="GSI.tableclasses"></a>

全局二级索引将始终使用与其基表相同的表类别。为表添加新的全局二级索引时，新索引将使用与其基表相同的表类别。更新表的表类别时，所有关联的全局二级索引也会更新。

## 全局二级索引的预调配吞吐量注意事项
<a name="GSI.ThroughputConsiderations"></a>

在预置模式表创建全局二级索引时，必须根据该索引的预期工作负载指定读取和写入容量单位。全局二级索引的预置吞吐量设置独立于其基表的相应设置。对全局二级索引执行的 `Query` 操作占用索引（而非基表）的读取容量单位。在表中放置、更新或删除项目时，还会更新表的全局二级索引。这些索引更新占用索引（而非基表）的写入容量单位。

例如，如果您对全局二级索引执行 `Query` 操作并超过其预配置读取容量，则您的请求会受到阻止。如果您对表执行大量写入活动，但是该表的全局二级索引没有足够写入容量，则对该表进行的写入活动会受到限制。

**重要**  
 为了避免触发可能的限制，全局二级索引的预配置写入容量应等于或大于基表的写入容量，因为新更新将同时写入基表和全局二级索引。

要查看全局二级索引的预配置吞吐量设置，请使用 `DescribeTable` 操作。这将返回表的所有全局二级索引的详细信息。

### 读取容量单位
<a name="GSI.ThroughputConsiderations.Reads"></a>

全局二级索引支持最终一致性读取，每个读取占用一半的读取容量单位。这意味着，单个全局二级索引查询对于每个读取容量单位，可以检索最多 2 × 4 KB = 8 KB。

对于全局二级索引查询，DynamoDB 计算预配置读取活动的方式与对表查询使用的方式相同。唯一不同的是，本次计算基于索引条目的大小，而不是基表中项目的大小。读取容量单位的数量就是返回的所有项目的所有投影属性大小之和。然后，该结果会向上取整到 4 KB 边界。有关 DynamoDB 如何计算预配置吞吐量使用情况的更多信息，请参阅 [DynamoDB 预置容量模式](provisioned-capacity-mode.md)。

`Query` 操作返回的结果大小上限为 1 MB。这包括所有属性名称的大小和所返回的所有项目的值。

例如，请考虑使用每项均包含 2000 字节数据的 全局二级索引。现在假设您对此索引执行 `Query` 操作，并且该查询的 `KeyConditionExpression` 匹配八个项目。匹配项目的总大小为 2000 字节 x 8 个项目 = 16000 字节。然后，该结果会向上取整到最近的 4 KB 边界。由于全局二级索引查询具有最终一致性，因此总成本是 0.5 × (16 KB / 4 KB))，即 2 个读取容量单位。

### 写入容量单位
<a name="GSI.ThroughputConsiderations.Writes"></a>

在添加、更新或删除表中的项目，并且全局二级索引受此影响时，全局二级索引将占用为此操作预配置的写入容量单位。一次写入操作的预配置吞吐量总成本是对基表执行的写入操作以及更新全局二级索引所占用的写入容量单位之和。如果对表执行的写入操作不需要全局二级索引更新，则不会占用索引的写入容量。

要成功写入表，表及其所有全局二级索引的预配置吞吐量设置必须具有足够的写入容量来允许写入。否则，对表的写入将受到限制。

**重要**  
创建全局二级索引（GSI）时，如果写入基表所产生的 GSI 活动超过了 GSI 的预置写入容量，则对基表的写入操作会受限。这种节流会影响所有写入操作，其影响小至干扰索引编制流程，大至可能中断您的生产工作负载。有关更多信息，请参阅 [Amazon DynamoDB 中节流故障排除](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TroubleshootingThrottling.html)。

向全局二级索引写入项目的成本取决于多个因素：
+ 如果您向定义了索引属性的表中写入新项目，或更新现有的项目来定义之前未定义的索引属性，只需一个写入操作即可将项目放置到索引中。
+ 如果对表执行的更新操作更改了索引键属性的值（从 A 更改为 B），就需要执行两次写入操作，一次用于删除索引中之前的项目，另一次用于将新项目放置到索引中。  
+ 如果索引中已有某一项目，而对表执行的写入操作删除了索引属性，就需要执行一次写入操作删除索引中旧的项目投影。
+ 如果更新项目前后索引中没有此项目，此索引就不会额外产生写入成本。
+ 如果对表的更新仅更改了索引键架构中投影属性的值，但不更改任何索引键属性的值，则需要执行一次写入以将投影属性的值更新到索引中。

所有这些因素都假定索引中每个项目的大小小于或等于 1 KB 这一项目大小（用于计算写入容量单位）。如果索引条目大于这一大小，就会占用额外的写入容量单位。您可以考虑查询需要返回的属性类型并仅将这些属性投影到索引中，从而最大程度地减少写入成本。

## 全局二级索引的存储注意事项
<a name="GSI.StorageConsiderations"></a>

当应用程序向表中写入项目时，DynamoDB 会自动将适当的属性子集复制到应包含这些属性的所有全局二级索引。您的 AWS 账户需要支付在基表中存储项目以及在表的任何全局二级索引中存储属性的费用。

索引项目所占用的空间大小就是以下内容之和：
+ 基表的主键 (分区键和排序键) 的大小 (按字节计算)
+ 索引键属性的大小（按字节计算）
+ 投影的属性（如果有）的大小（按字节计算）
+ 每个索引项目 100 字节的开销

要估算全局二级索引的存储要求，您可以估算索引中项目的平均大小，然后乘以基表中具有全局二级索引键属性的项目数。

如果表包含的某个项目未定义特定属性，但是该属性定义为索引分区键或排序键，则 DynamoDB 不会将该项目的任何数据写入到索引中。