DynamoDB创建GSI查询布尔类型未读通知的问题咨询
解决方案
问题核心原因
DynamoDB 全局二级索引(GSI)的分区键、排序键仅支持字符串、数字、二进制三类数据类型,原生不支持布尔类型,因此无法直接将布尔字段Read设为索引排序键。而使用Scan接口会执行全表扫描,即使加了过滤条件也是先扫完全表再做结果过滤,性能和成本都很差,不适合生产环境使用。
可落地的索引设计方案
根据通知场景的通用需求,推荐两种经过生产验证的设计:
- 方案1:复合排序键GSI(通用性最强,支持按发送时间排序)
写入通知数据时,新增一个字符串类型字段作为GSI排序键,格式为{读状态}#{发送时间}:- 未读通知:排序键值为
UNREAD#{sentAt},例如UNREAD#2024-05-20T14:30:00Z - 已读通知:排序键值为
READ#{sentAt}
GSI规则设置: - 分区键:和主表一致,使用存储用户ID的
PK字段 - 排序键:上述新增的字符串类型复合字段
查询时直接调用Query接口,指定分区键为目标用户ID,排序键用begins_with匹配UNREAD#前缀,即可拿到该用户所有未读通知,还可以通过ScanIndexForward: false参数实现按发送时间倒序排列,查询延迟稳定在毫秒级。
- 未读通知:排序键值为
- 方案2:稀疏GSI(存储成本最低,适合未读消息占比低的场景)
利用DynamoDB稀疏索引的特性:只有当条目包含索引定义的所有键字段时,才会被同步到GSI中。
设计规则:- GSI分区键依然使用
PK(用户ID) - GSI排序键使用存储发送时间的
sentAt字段
写入逻辑调整:仅当通知状态为未读(Read: false)时,才给该条目赋值GSI需要的排序键字段;当用户把通知标记为已读时,直接删除该条目的GSI排序键字段,此时该条目会自动从GSI中移除。
查询时直接对GSI执行Query,指定分区键为目标用户ID即可拿到所有未读通知,不需要额外加状态过滤条件,索引体量小,查询和存储成本都更低。
- GSI分区键依然使用
代码调整示例
替换原来的Scan操作为Query操作,以方案1的复合排序键为例,代码如下:
const params = { TableName: await this.configService.get('NOTIFICATION_TABLE'), IndexName: '替换为你创建的GSI名称,例如UserUnreadNotificationIndex', KeyConditionExpression: '#PK = :userId AND begins_with(#GSISortKey, :unreadPrefix)', ExpressionAttributeNames: { '#PK': 'PK', '#GSISortKey': '替换为你定义的GSI排序键字段名' }, ExpressionAttributeValues: { ':userId': 'NOTIFICATION#a8a8e4c7-cab0-431e-8e08-1bcf962358b8', ':unreadPrefix': 'UNREAD#' }, ScanIndexForward: false // 按发送时间倒序,最新通知在前 }; const response = await this.dynamoDB.query(params).promise();
临时修复原有Scan代码布尔值报错的方法:如果你使用的是原生DynamoDB客户端而非DocumentClient,布尔值需要按指定类型传参,格式为
:Read: { BOOL: true },但该方案仅做临时调试用,生产环境必须替换为Query+GSI的实现,避免全表扫描带来的性能和成本问题。
内容的提问来源于stack exchange,提问作者Meridian Asllani
相关产品推荐
相关产品推荐

