Python(boto3)中的 DynamoDB Scan
boto3 里的一次 scan 是两个决定:请求本身,以及你怎么给它分页。下面的代码用的是内置分页器,它不是某人围着你的循环写的一层封装。它是五行 botocore 配置,而正是这五行决定了你的扫描是否正确、以及它花多少钱。(至于你到底该不该扫描,那是另一个问题。)
代码
import boto3
client = boto3.client("dynamodb")
paginator = client.get_paginator("scan")
items = []
for page in paginator.paginate(
TableName="Music",
FilterExpression="#filter0 >= :filterValue0",
ExpressionAttributeNames={"#filter0": "Year"},
ExpressionAttributeValues={":filterValue0": {"N": "2010"}},
):
items.extend(page["Items"])
print(f"Matched {len(items)} items")说明
- 分页器是数据,不是代码。botocore 在
paginators-1.json里给每个操作放了一条;Scan那条是{"input_token": "ExclusiveStartKey", "output_token": "LastEvaluatedKey", "limit_key": "Limit", "result_key": ["Items", "Count", "ScannedCount"], "non_aggregate_keys": ["ConsumedCapacity"]}。下面的一切都是从这些键里推出来的。 PaginationConfig={"PageSize": n}设置的是Limit,因为Limit就是那个limit_key。Limit限制的是 读取 的项数,从来不是返回的项数,所以配上FilterExpression时,一页可以是空的,却照样有成本。MaxItems数的是result_key里的项,并交给你一个NextToken,你可以在之后的进程里把它作为StartingToken传回去。它并不会阻止请求读过你设的那条线。build_full_result()只聚合result_key字段。Items、Count和ScannedCount会被累加;ConsumedCapacity是non_aggregate_key,所以合并后的结果会把一页的容量当成整次扫描的容量报出来。请自己逐页累加,否则你会少报整整一个页数的倍数。FilterExpression是在读取之后于服务端运行的,所以你是按ScannedCount计费的,不是Count。#filter0给Year做别名,是因为它是保留字;没有这个别名,请求在读到任何东西之前就失败了。- 错误全都以
botocore.exceptions.ClientError的形式到达。按e.response["Error"]["Code"]分支;每个错误对应的类只作为客户端上生成的属性存在(client.exceptions.ProvisionedThroughputExceededException),从来不是可导入的符号。 - 资源 API 是另一套手感。
Table.scan接收原生 Python 类型,把数字作为decimal.Decimal返回,并用Attr("Year").gte(2010)而不是占位符映射来构建过滤条件。
一个被过滤的页面实际花多少钱
60 个项,每个大约 2 KB,Year = 2024 命中其中两个,PageSize=10,针对 DynamoDB Local 运行:
page 1: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 2: Count=1 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 3: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 4: Count=1 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 5: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 6: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 7: Count=0 ScannedCount=0 CU=0.0 LastEvaluatedKey=no
total CU across pages: 15.0六个真实页面里有四个什么都没返回,而且是全价。这正是分页器要防的那种 bug 的形状:一个在 Items 为空时就跳出的手写循环会在第 1 页退出,把两首命中的歌报告成零。
第 7 页是另一半。第 6 页在表里最后一个项上撞到了它的 Limit,所以 DynamoDB 照样返回了一个 LastEvaluatedKey,分页器又花了一次往返才知道后面已经没东西了。一个 LastEvaluatedKey 的意思是"我停下了",不是"还有更多"。
对同一次扫描调用 build_full_result() 报告的是 CapacityUnits: 2.5。那六页实际消耗了 15.0。
不用自己写循环也能分页
DynamoDB 查询构建器会把过滤条件、别名映射和分页循环拼成一个可运行的程序,于是上面那个 Limit 与 Count 的陷阱在你粘贴之前就已经处理好了。要以交互方式而不是从脚本里翻一张真实的表,请下载 DynoTable。
相关指南
- Query 与 Scan 的取舍——什么时候(很少)一次
scan是合理的。 - 为什么我的 DynamoDB Scan 又慢又贵?——成本模型,以及怎么避开它。
- 并行扫描——用
Segment/TotalSegments拆分表,每个分段一个分页器。 - DynamoDB ProvisionedThroughputExceededException——一次全表扫描会把一张预置表的容量弄成什么样。
参考资料
- Scan — Amazon DynamoDB API Reference
- scan — Boto3 DynamoDB.Client Reference
- Scan paginator — Boto3 DynamoDB Reference
- Scanning tables — Amazon DynamoDB Developer Guide
最后核实于 2026-07-28,依据上方链接的 AWS 官方文档。