You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 08:50:29