DynamoDB分页结果不符合预期,如何实现指定条数分页?
问题描述
使用@aws-sdk/client-dynamodb": "3.188.0"实现DynamoDB分页功能,总用户数98,设置每页大小为20,预期得到5页结果(每页数据量分别为20、20、20、20、18条),但实际分页超过5页,且每页数据量不固定(如10、12、11等)。以下是实现分页的pagedList方法代码:
public async pagedList(usersPerPage: number, lastEvaluatedKey?: string): Promise<PagedUser> { const params = { TableName: tableName, Limit: usersPerPage, FilterExpression: '#type = :type', ExpressionAttributeValues: { ':type': { S: type }, }, ExpressionAttributeNames: { '#type': 'type', }, } as ScanCommandInput; if (lastEvaluatedKey) { params.ExclusiveStartKey = { 'oid': { S: lastEvaluatedKey } }; } const command = new ScanCommand(params); const data = await client.send(command); const users: User[] = []; if (data.Items !== undefined) { data.Items.forEach((item) => { if (item !== undefined) { users.push(this.makeUser(item)); } }); } let lastKey; if (data.LastEvaluatedKey !== undefined) { lastKey = data.LastEvaluatedKey.oid.S?.valueOf(); } return { users: users, lastEvaluatedKey: lastKey }; }
原因与修复方案
核心问题
DynamoDB的Scan操作里,Limit参数指的是扫描的原始条目数,不是过滤后返回的条目数。比如你设Limit=20,DynamoDB会先扫20条数据,再用FilterExpression过滤掉不符合type条件的条目,最后返回的可能只有10条;而下一页的ExclusiveStartKey是基于扫描到的第20条数据的位置,导致下一页从该位置继续扫描,最终分页数量超标、每页数据量不稳定。
修复步骤
1. 优先用全局二级索引(GSI)+ Query替代Scan
如果你的查询条件是固定的type字段,最合理的方案是创建以type为分区键的GSI,然后用Query操作代替Scan。Query的Limit是针对匹配GSI分区键的条目数,能保证每页返回指定数量的结果,性能也比Scan好得多。
创建GSI后,修改代码如下:
public async pagedList(usersPerPage: number, lastEvaluatedKey?: string): Promise<PagedUser> { const params = { TableName: tableName, IndexName: 'TypeIndex', // 替换成你的GSI名称 KeyConditionExpression: '#type = :type', ExpressionAttributeValues: { ':type': { S: type }, }, ExpressionAttributeNames: { '#type': 'type', }, Limit: usersPerPage, ExclusiveStartKey: lastEvaluatedKey ? { 'oid': { S: lastEvaluatedKey } } : undefined } as QueryCommandInput; const command = new QueryCommand(params); const data = await client.send(command); const users: User[] = data.Items?.map(item => this.makeUser(item)) || []; const lastKey = data.LastEvaluatedKey?.oid.S?.valueOf(); return { users, lastEvaluatedKey: lastKey }; }
2. 必须用Scan时,客户端循环凑够一页数据
如果无法创建GSI,只能在客户端循环执行Scan,直到收集到足够数量的过滤后数据:
public async pagedList(usersPerPage: number, lastEvaluatedKey?: string): Promise<PagedUser> { const users: User[] = []; let currentLastKey = lastEvaluatedKey; let needMore = usersPerPage; while (needMore > 0 && currentLastKey !== undefined) { const params = { TableName: tableName, Limit: needMore, // 每次请求尽可能多的原始数据,减少请求次数 FilterExpression: '#type = :type', ExpressionAttributeValues: { ':type': { S: type } }, ExpressionAttributeNames: { '#type': 'type' }, ExclusiveStartKey: currentLastKey ? { 'oid': { S: currentLastKey } } : undefined } as ScanCommandInput; const command = new ScanCommand(params); const data = await client.send(command); if (data.Items) { const filtered = data.Items.map(item => this.makeUser(item)); users.push(...filtered); needMore -= filtered.length; } currentLastKey = data.LastEvaluatedKey?.oid.S?.valueOf(); // 没有更多数据了,直接退出 if (!currentLastKey) break; } // 确保返回的数量不超过每页大小 return { users: users.slice(0, usersPerPage), lastEvaluatedKey: currentLastKey }; }
注意:这种方式会增加请求次数和RCU消耗,仅作为临时方案使用。
3. 检查ExclusiveStartKey的完整性
如果你的表主键包含分区键+排序键,原代码只传oid(分区键)会导致ExclusiveStartKey不完整,必须同时传入排序键的值。如果只有分区键,当前处理没问题。
内容的提问来源于stack exchange,提问作者era
相关产品推荐
相关产品推荐

