DynamoDB表设计 #
一、设计原则概述 #
1.1 DynamoDB设计理念 #
text
核心原则:
├── 以访问模式为中心
├── 一个表解决多种访问模式
├── 利用排序键组织数据
└── 合理使用索引补充查询
1.2 与关系型数据库设计差异 #
| 关系型数据库 | DynamoDB |
|---|---|
| 以数据模型为中心 | 以访问模式为中心 |
| 多表关联 | 单表设计 |
| JOIN查询 | 索引查询 |
| 规范化 | 反规范化 |
| 灵活查询 | 预定义访问模式 |
1.3 设计流程 #
text
设计流程:
├── 1. 分析访问模式
│ ├── 识别所有查询需求
│ ├── 确定查询频率
│ └── 分析数据关系
├── 2. 设计主键
│ ├── 选择分区键
│ ├── 选择排序键
│ └── 确保均匀分布
├── 3. 设计索引
│ ├── 识别额外查询模式
│ ├── 选择投影类型
│ └── 评估成本
└── 4. 优化调整
├── 测试性能
├── 调整设计
└── 监控优化
二、访问模式分析 #
2.1 识别访问模式 #
示例:电商订单系统
text
访问模式列表:
├── 1. 根据用户ID获取用户信息
├── 2. 根据用户ID获取所有订单
├── 3. 根据订单ID获取订单详情
├── 4. 根据日期范围查询订单
├── 5. 根据状态查询订单
├── 6. 根据产品ID查询订单
└── 7. 统计用户订单数量
2.2 访问模式分类 #
text
访问模式类型:
├── 单项目访问
│ └── 通过主键获取单个项目
├── 范围查询
│ └── 通过分区键+排序键范围查询
├── 过滤查询
│ └── 需要扫描过滤
└── 聚合查询
└── 需要计算统计
2.3 访问模式优先级 #
text
优先级排序:
├── 高频访问模式 → 主键设计
├── 中频访问模式 → 二级索引
└── 低频访问模式 → Scan或导出分析
三、主键设计 #
3.1 分区键设计原则 #
text
分区键设计原则:
├── 高基数
│ └── 大量不同的值
├── 均匀分布
│ └── 访问均匀分散
├── 稳定性
│ └── 值不频繁变化
└── 查询友好
└── 支持主要查询模式
3.2 分区键选择示例 #
好的分区键:
json
{
"PK": "USER#12345", // 用户ID,高基数
"PK": "ORDER#2024-001234", // 订单ID,高基数
"PK": "DEVICE#SENSOR001" // 设备ID,高基数
}
不好的分区键:
json
{
"PK": "STATUS#ACTIVE", // 状态值,低基数
"PK": "DATE#2024-01-01", // 日期,可能热点
"PK": "REGION#BEIJING" // 区域,可能热点
}
3.3 排序键设计原则 #
text
排序键设计原则:
├── 支持范围查询
├── 支持排序需求
├── 组织相关数据
└── 支持层级关系
3.4 排序键设计模式 #
时间序列模式:
json
[
{"PK": "DEVICE#SENSOR001", "SK": "2024-01-01T10:00:00"},
{"PK": "DEVICE#SENSOR001", "SK": "2024-01-01T10:01:00"},
{"PK": "DEVICE#SENSOR001", "SK": "2024-01-01T10:02:00"}
]
层级关系模式:
json
[
{"PK": "USER#12345", "SK": "PROFILE"},
{"PK": "USER#12345", "SK": "ORDER#2024-001"},
{"PK": "USER#12345", "SK": "ORDER#2024-002"},
{"PK": "USER#12345", "SK": "ADDRESS#HOME"}
]
版本控制模式:
json
[
{"PK": "DOC#README", "SK": "v1.0"},
{"PK": "DOC#README", "SK": "v1.1"},
{"PK": "DOC#README", "SK": "v2.0"}
]
四、单表设计模式 #
4.1 单表设计优势 #
text
优势:
├── 简化架构
├── 降低成本
├── 提高性能
├── 简化运维
└── 支持事务
4.2 复合主键模式 #
设计示例:
json
[
{
"PK": "USER#12345",
"SK": "PROFILE",
"Type": "User",
"Name": "John Doe",
"Email": "john@example.com"
},
{
"PK": "USER#12345",
"SK": "ORDER#2024-001",
"Type": "Order",
"OrderId": "2024-001",
"Total": 99.99,
"Status": "COMPLETED"
},
{
"PK": "USER#12345",
"SK": "ORDER#2024-002",
"Type": "Order",
"OrderId": "2024-002",
"Total": 149.99,
"Status": "PENDING"
}
]
查询示例:
javascript
// 获取用户所有数据
await docClient.query({
TableName: 'AppTable',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': 'USER#12345' }
});
// 获取用户所有订单
await docClient.query({
TableName: 'AppTable',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':pk': 'USER#12345',
':prefix': 'ORDER#'
}
});
4.3 访问模式映射 #
示例:电商系统
text
访问模式 → 主键/索引映射:
1. 获取用户信息
PK = USER#userId, SK = PROFILE
2. 获取用户所有订单
PK = USER#userId, SK begins_with ORDER#
3. 获取订单详情
PK = USER#userId, SK = ORDER#orderId
或 GSI: PK = ORDER#orderId
4. 按日期查询订单
GSI: PK = ORDER_DATE#2024-01-01
5. 按状态查询订单
GSI: PK = ORDER_STATUS#PENDING
4.4 GSI过载模式 #
json
[
{
"PK": "USER#12345",
"SK": "PROFILE",
"GSI1PK": "EMAIL#john@example.com",
"GSI1SK": "USER#12345"
},
{
"PK": "USER#12345",
"SK": "ORDER#2024-001",
"GSI1PK": "ORDER_STATUS#PENDING",
"GSI1SK": "2024-01-01T10:00:00"
}
]
一个GSI支持多种查询:
javascript
// 通过Email查询用户
await docClient.query({
TableName: 'AppTable',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk',
ExpressionAttributeValues: { ':pk': 'EMAIL#john@example.com' }
});
// 查询待处理订单
await docClient.query({
TableName: 'AppTable',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk',
ExpressionAttributeValues: { ':pk': 'ORDER_STATUS#PENDING' }
});
五、常见设计模式 #
5.1 时间序列数据 #
设计:
json
{
"PK": "DEVICE#SENSOR001",
"SK": "2024-01#2024-01-01T10:00:00",
"Temperature": 25.5,
"Humidity": 60
}
查询最近一小时数据:
javascript
const oneHourAgo = new Date(Date.now() - 3600000).toISOString();
await docClient.query({
TableName: 'TimeSeries',
KeyConditionExpression: 'PK = :pk AND SK >= :startTime',
ExpressionAttributeValues: {
':pk': 'DEVICE#SENSOR001',
':startTime': `2024-01#${oneHourAgo}`
}
});
5.2 购物车设计 #
设计:
json
{
"PK": "USER#12345",
"SK": "CART",
"Items": [
{
"ProductId": "PROD001",
"Name": "iPhone 15",
"Price": 999,
"Quantity": 1
}
],
"TotalPrice": 999,
"LastUpdated": "2024-01-01T10:00:00Z"
}
操作:
javascript
// 获取购物车
await docClient.get({
TableName: 'AppTable',
Key: { PK: 'USER#12345', SK: 'CART' }
});
// 更新购物车
await docClient.update({
TableName: 'AppTable',
Key: { PK: 'USER#12345', SK: 'CART' },
UpdateExpression: 'SET Items = :items, TotalPrice = :total, LastUpdated = :now',
ExpressionAttributeValues: {
':items': updatedItems,
':total': calculateTotal(updatedItems),
':now': new Date().toISOString()
}
});
5.3 排行榜设计 #
设计:
json
{
"PK": "LEADERBOARD#GAME001",
"SK": "SCORE#99999#USER#12345",
"UserId": "12345",
"Score": 99999,
"Rank": 1,
"Timestamp": "2024-01-01T10:00:00Z"
}
获取Top 10:
javascript
await docClient.query({
TableName: 'Leaderboard',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': 'LEADERBOARD#GAME001' },
ScanIndexForward: false, // 降序
Limit: 10
});
5.4 社交关系设计 #
关注关系:
json
[
{
"PK": "USER#12345",
"SK": "FOLLOWING#USER#67890",
"FollowedAt": "2024-01-01T10:00:00Z"
},
{
"PK": "USER#67890",
"SK": "FOLLOWER#USER#12345",
"FollowedAt": "2024-01-01T10:00:00Z"
}
]
查询关注列表:
javascript
// 获取用户关注的人
await docClient.query({
TableName: 'Social',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':pk': 'USER#12345',
':prefix': 'FOLLOWING#'
}
});
// 获取用户的粉丝
await docClient.query({
TableName: 'Social',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':pk': 'USER#12345',
':prefix': 'FOLLOWER#'
}
});
5.5 评论系统设计 #
设计:
json
[
{
"PK": "POST#POST001",
"SK": "COMMENT#2024-01-01T10:00:00#COMMENT001",
"UserId": "USER#12345",
"Content": "Great post!",
"CreatedAt": "2024-01-01T10:00:00Z"
},
{
"PK": "POST#POST001",
"SK": "COMMENT#2024-01-01T11:00:00#COMMENT002",
"UserId": "USER#67890",
"Content": "Thanks for sharing!",
"CreatedAt": "2024-01-01T11:00:00Z"
}
]
查询评论:
javascript
// 获取文章所有评论(按时间排序)
await docClient.query({
TableName: 'Comments',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': 'POST#POST001' },
ScanIndexForward: true // 升序(时间从早到晚)
});
六、索引设计策略 #
6.1 GSI设计原则 #
text
GSI设计原则:
├── 仅创建必要的索引
├── 选择合适的投影类型
├── 考虑稀疏索引
├── 避免热点分区键
└── 监控索引使用
6.2 稀疏索引 #
概念:
text
稀疏索引特点:
├── 仅包含有索引键属性的项目
├── 适合可选属性查询
└── 减少索引存储和成本
示例:
json
[
{
"PK": "USER#12345",
"SK": "PROFILE",
"Email": "john@example.com"
// 无Phone属性
},
{
"PK": "USER#67890",
"SK": "PROFILE",
"Email": "jane@example.com",
"Phone": "+86-138-xxxx-xxxx" // 有Phone属性
}
]
GSI仅包含有Phone的用户:
javascript
// PhoneIndex只包含有Phone属性的用户
await docClient.query({
TableName: 'Users',
IndexName: 'PhoneIndex',
KeyConditionExpression: 'Phone = :phone',
ExpressionAttributeValues: { ':phone': '+86-138-xxxx-xxxx' }
});
6.3 投影类型选择 #
| 投影类型 | 适用场景 | 成本 |
|---|---|---|
| KEYS_ONLY | 仅需主键回表查询 | 最低 |
| INCLUDE | 需要部分属性 | 中等 |
| ALL | 需要所有属性 | 最高 |
七、热点问题处理 #
7.1 热点识别 #
text
热点迹象:
├── 特定分区键请求量异常高
├── 延迟增加
├── ThrottledRequestCount增加
└── CloudWatch指标异常
7.2 热点解决方案 #
随机后缀法:
javascript
// 原始设计(可能热点)
const partitionKey = `DATE#${date}`;
// 优化设计(分散写入)
const suffix = Math.floor(Math.random() * 10);
const partitionKey = `DATE#${date}#${suffix}`;
// 读取时需要查询所有后缀
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(docClient.query({
TableName: 'Table',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': `DATE#${date}#${i}` }
}));
}
const results = await Promise.all(promises);
时间窗口法:
javascript
// 按小时分片
const hour = new Date().getHours();
const partitionKey = `DATE#${date}#HOUR#${hour}`;
八、设计最佳实践 #
8.1 设计检查清单 #
text
设计检查清单:
├── 访问模式分析
│ ├── 所有查询模式已识别
│ ├── 高频查询已优化
│ └── 低频查询有方案
├── 主键设计
│ ├── 分区键高基数
│ ├── 分区键均匀分布
│ └── 排序键支持范围查询
├── 索引设计
│ ├── 索引数量合理
│ ├── 投影类型合适
│ └── 无热点问题
└── 成本优化
├── 项目大小合理
├── 避免过度索引
└── 选择合适容量模式
8.2 常见反模式 #
text
避免以下设计:
├── 低基数分区键
│ └── 如:STATUS#ACTIVE
├── 过度使用Scan
│ └── Scan消耗大量RCU
├── 过多索引
│ └── 增加成本和写入延迟
├── 大项目
│ └── 接近400KB限制
└── 深度嵌套
└── 超过32层嵌套
九、总结 #
表设计要点:
| 设计方面 | 要点 |
|---|---|
| 访问模式 | 以查询需求为中心 |
| 分区键 | 高基数、均匀分布 |
| 排序键 | 支持范围查询和排序 |
| 索引 | 按需创建、合理投影 |
| 单表设计 | 简化架构、降低成本 |
下一步,让我们学习数据操作!
最后更新:2026-03-27