Python(boto3)中查询 DynamoDB GSI

一次 GSI 查询就是普通的 query 加上 IndexName,而 AlbumTitle-index 让我们能按专辑取歌曲——这是 Artist + SongTitle 这个表键无法服务的访问模式。在 Python 里真正不同的是错误处理:两个最常见的索引错误分别在 boto3 的不同层里失败,而其中只有一个能按异常类捕获。

代码

import boto3

client = boto3.client("dynamodb")

paginator = client.get_paginator("query")

items = []
for page in paginator.paginate(
    TableName="Music",
    IndexName="AlbumTitle-index",
    KeyConditionExpression="#hashKey = :hashKeyValue",
    ExpressionAttributeNames={"#hashKey": "AlbumTitle"},
    ExpressionAttributeValues={":hashKeyValue": {"S": "Danzon"}},
):
    items.extend(page["Items"])

print(f"Found {len(items)} songs on the album")

except ValidationException 别说捕获了,连编译都过不去

给上面的查询加上 ConsistentRead=True,boto3 就会抛出下面这个,client API 和 resource API 都一样:

botocore.exceptions.ClientError: An error occurred (ValidationException) when
calling the Query operation: Consistent reads are not supported on global
secondary indexes

最顺手的处理方式是 except client.exceptions.ValidationException。可它并不存在:

AttributeError: <botocore.errorfactory.DynamoDBExceptions object> has no
attribute ValidationException. Valid exceptions are: BackupInUseException,
... IndexNotFoundException, ... ProvisionedThroughputExceededException, ...

botocore 是从服务模型生成异常类的,而 DynamoDB 只建模了其中 33 个。ValidationException 是协议层面的错误,不在其列,所以唯一可靠的分支是按错误码判断:

except ClientError as exc:
    if exc.response["Error"]["Code"] == "ValidationException":
        ...

这种不对称是真实存在的。把索引名打错,你会得到 IndexNotFoundException,它确实被建模了,可以按类捕获。把一致性标志用错,你得到的却是一次字符串比较。两者都是索引错误,只有一个有类型。

游标里也带着表的键

分页器把 LastEvaluatedKey 藏了起来,但值得知道它在索引上装的是什么。以同一张专辑上的 300 首歌为例:

page 1: Count 271  capacity 128.5  LastEvaluatedKey ['AlbumTitle', 'Artist', 'SongTitle']
page 2: Count  29  capacity  14.0  LastEvaluatedKey []

GSI 的键不是唯一的,所以光靠索引键无法恢复这次读取;DynamoDB 会把索引键和基表键一起返回。手写的翻页逻辑如果只保存索引键,就会重复或漏掉项目。

2026-07-28 针对 DynamoDB Local(amazon/dynamodb-local)用 CPython 3.14.6 上的 boto3 1.43.58 复现。错误文本和键列表均为该库自己的输出。

说明

  • IndexName 不会取代 TableName。两者进入同一次调用,而 KeyConditionExpression 写的是索引的分区键(AlbumTitle),运算符集合与表查询相同。
  • 你只能拿到投影里的内容,别的没有。索引返回它所投影的内容(ALLKEYS_ONLYINCLUDE 列表);按 API 参考所说,「全局二级索引查询无法从父表获取属性」。缺少某个属性就意味着要按基表键再做一次 get_item,或者新建一个投影更宽的索引。
  • 缺少索引键的项目根本不会出现——这就是稀疏索引模式。它能让一个 status = "OPEN" 上的索引保持很小,同时也解释了为什么 GSI 查询可能返回得比你预期的少,却不报任何错。
  • resource API 接受同样的 IndexNametable.query(IndexName="AlbumTitle-index", KeyConditionExpression=Key("AlbumTitle").eq("Danzon")),传入原生 Python 值,返回 Decimal
  • GSI 的写入落地晚于表的写入。复制是异步的,所以针对索引的「写后读」路径偶尔会读不到。在紧凑循环里重试只会烧容量,并不会让复制更快。

用可视化的方式来做

DynamoDB 表达式构建器会把索引的键条件和带类型的值映射写成 boto3 可直接用的 Python,包括 client API 坚持要有、而 resource API 又禁止的 {"S": ...} 包装。

要从一个表单把同样的索引查询指向你自己的表,并在带分页的网格里读取结果,请下载 DynoTable

相关示例

参考资料

可视化构建此请求

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

打开 DynamoDB 查询构建器

无需控制台即可使用 DynamoDB

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

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