Node.js(AWS SDK v3)中查询 DynamoDB GSI

一次 GSI 查询就是普通的 Query 加上 IndexName,然后有两件事不再像表查询那样行事:你习惯的那个一致性标志变成了错误,而分页游标会多出一个属性。这里 AlbumTitle-index 按专辑取歌曲,而基表(Artist + SongTitle)不做扫描是办不到的。

代码

import {DynamoDBClient, QueryCommand} from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({region: 'us-east-1'});

const items = [];
let lastEvaluatedKey;

do {
  const response = await client.send(
    new QueryCommand({
      TableName: 'Music',
      IndexName: 'AlbumTitle-index',
      KeyConditionExpression: '#hashKey = :hashKeyValue',
      ExpressionAttributeNames: {
        '#hashKey': 'AlbumTitle'
      },
      ExpressionAttributeValues: {
        ':hashKeyValue': {S: 'Danzon'}
      },
      ExclusiveStartKey: lastEvaluatedKey
    })
  );

  items.push(...(response.Items ?? []));
  lastEvaluatedKey = response.LastEvaluatedKey;
} while (lastEvaluatedKey);

console.log(`Found ${items.length} songs on the album`);

游标有三个属性宽,不是两个

拿一张专辑下的 300 首歌跑上面的循环,看看它交回来的 LastEvaluatedKey

table query  -> ['Artist', 'SongTitle']
GSI query    -> ['AlbumTitle', 'Artist', 'SongTitle']

GSI 的键不唯一,所以光靠索引键无法续读。DynamoDB 会把索引键基表键一起返回,两者都必须原封不动地放回 ExclusiveStartKey。这就是为什么一个只存"我看到的最后一个排序键"的手写游标在表上能用,在索引上却会悄悄丢项或重复项——也是为什么当表键是一个你并不想泄露的用户 id 时,把那个键持久化到客户端是个坏主意。

ConsistentRead: true 是 400,不是升级

直觉是:强一致读花更多容量,换来更新的数据。在 GSI 上,它花掉的是你的这次请求:

ValidationException: Consistent reads are not supported on global secondary indexes
HTTP 400

API 参考同样直白:"Strongly consistent reads are not supported on global secondary indexes. If you query a global secondary index with ConsistentRead set to true, you will receive a ValidationException." GSI 上的 Scan 会用同样的消息拒绝同一个标志。本地二级索引则是接受它的。

2026-07-28 针对 DynamoDB Local(amazon/dynamodb-local)、在 node v24.18.0 上用 @aws-sdk/client-dynamodb 3.1095.0 复现。错误文本和键的形状都是引擎自己的输出。

说明

  • IndexName 不会取代 TableName。两者放在同一条命令里,而 KeyConditionExpression 这时命名的是索引的分区键(AlbumTitle),不是表的分区键,可用的运算符集合和表查询一样。
  • 你拿到的只有投影里的东西。GSI 查询返回的是索引所投影的内容(ALLKEYS_ONLYINCLUDE 列表),按 API 参考的说法:"global secondary index queries cannot fetch attributes from the parent table"。缺属性意味着要在基表键上再做一次 GetItem,或者放宽投影并重建索引。
  • 缺少索引键的项永远不会出现。这就是稀疏索引模式,而且它是个特性:只给 status = "OPEN" 的行建索引,GSI 就能保持很小。这也是为什么 GSI 查询可能返回比你预期更少的项,却不报任何错。
  • 复制是异步的,所以刚落到表上的写入可能还没进索引。在"写后读"的路径上要为此留出余量,而不是在紧凑循环里重试。

用可视化的方式来做

事后再加 GSI 是学会这件事的昂贵方式。单表设计规划器会接过你的访问模式,算出其中哪些需要索引键、哪些基表本来就能满足。

要浏览一张表的索引、从表单里运行 GSI 查询、并用分页表格看结果,请下载 DynoTable

相关示例

参考资料

可视化构建此请求

在免费的 DynamoDB 查询构建器中组装此操作 —— 键条件、筛选、索引、Limit、排序方向和分页循环 —— 再把它作为可运行的 SDK v3、CLI 或 boto3 程序复制回来。

打开 DynamoDB 查询构建器

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。