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),运算符集合与表查询相同。- 你只能拿到投影里的内容,别的没有。索引返回它所投影的内容(
ALL、KEYS_ONLY或INCLUDE列表);按 API 参考所说,「全局二级索引查询无法从父表获取属性」。缺少某个属性就意味着要按基表键再做一次get_item,或者新建一个投影更宽的索引。 - 缺少索引键的项目根本不会出现——这就是稀疏索引模式。它能让一个
status = "OPEN"上的索引保持很小,同时也解释了为什么 GSI 查询可能返回得比你预期的少,却不报任何错。 - resource API 接受同样的
IndexName:table.query(IndexName="AlbumTitle-index", KeyConditionExpression=Key("AlbumTitle").eq("Danzon")),传入原生 Python 值,返回Decimal。 - GSI 的写入落地晚于表的写入。复制是异步的,所以针对索引的「写后读」路径偶尔会读不到。在紧凑循环里重试只会烧容量,并不会让复制更快。
用可视化的方式来做
DynamoDB 表达式构建器会把索引的键条件和带类型的值映射写成 boto3 可直接用的 Python,包括 client API 坚持要有、而 resource API 又禁止的 {"S": ...} 包装。
要从一个表单把同样的索引查询指向你自己的表,并在带分页的网格里读取结果,请下载 DynoTable。
相关示例
- Node.js 中查询 DynamoDB GSI——用 AWS SDK v3 完成同样的索引查询。
- 用 AWS CLI 查询 DynamoDB GSI——同样的索引查询,从命令行发出。
- Python 中的 DynamoDB Query——查询基表。
- GSI vs. LSI——哪种索引类型适合该访问模式。
- 为什么 GSI 是最终一致的——复制延迟详解。
- "The table does not have the specified index"——索引名不匹配(GSI 名称区分大小写)。
- "Consistent reads are not supported on global secondary indexes"——为什么强一致读标志在 GSI 上会失败。