dynamodb-single-table
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDynamoDB Single Table Design
DynamoDB 单表设计
This skill covers single table design patterns for DynamoDB, the AWS-recommended approach for modeling complex relationships in a single table.
本技能介绍DynamoDB的单表设计模式,这是AWS推荐的在单表中建模复杂关系的方法。
Core Philosophy
核心理念
Single table design principles:
- One table to rule them all: Model all entities in a single table
- Overload keys: Use generic pk/sk for flexibility
- Optimize for access patterns: Design around how you query data
- Denormalize when needed: Duplicate data to avoid joins
- Use GSIs strategically: Add secondary indexes for alternate access patterns
单表设计原则:
- 一表统管所有数据:在单个表中建模所有实体
- 键重载:使用通用pk/sk以提升灵活性
- 针对访问模式优化:围绕数据查询方式进行设计
- 按需反规范化:通过数据冗余避免关联查询
- 策略性使用GSI:添加二级索引以支持备选访问模式
Why Single Table Design?
为什么选择单表设计?
Benefits:
- ✅ Consistent performance across all queries
- ✅ Lower costs (fewer tables to provision)
- ✅ Atomic transactions across entity types
- ✅ Simpler infrastructure management
- ✅ Better suited for serverless architectures
Trade-offs:
- ❌ More complex to design initially
- ❌ Requires understanding access patterns upfront
- ❌ Less intuitive than relational databases
优势:
- ✅ 所有查询性能一致
- ✅ 成本更低(需配置的表更少)
- ✅ 跨实体类型的原子事务
- ✅ 基础设施管理更简单
- ✅ 更适配无服务器架构
权衡点:
- ❌ 初始设计复杂度更高
- ❌ 需要提前明确访问模式
- ❌ 比关系型数据库更不直观
Key Concepts
核心概念
Generic Key Names
通用键名
Use generic partition (pk) and sort (sk) keys instead of entity-specific names:
typescript
// ✅ Good - Generic and flexible
{
pk: "USER#123",
sk: "PROFILE",
// ... entity data
}
// ❌ Bad - Entity-specific
{
userId: "123",
// ... entity data
}使用通用分区键(pk)和排序键(sk)替代实体特定的键名:
typescript
// ✅ Good - Generic and flexible
{
pk: "USER#123",
sk: "PROFILE",
// ... entity data
}
// ❌ Bad - Entity-specific
{
userId: "123",
// ... entity data
}Composite Keys
复合键
Build keys from multiple attributes:
typescript
// User entity
pk: "USER#userId"
sk: "PROFILE"
// User's posts
pk: "USER#userId"
sk: "POST#postId"
// Post details
pk: "POST#postId"
sk: "METADATA"
// Post comments
pk: "POST#postId"
sk: "COMMENT#commentId"由多个属性构建键:
typescript
// User entity
pk: "USER#userId"
sk: "PROFILE"
// User's posts
pk: "USER#userId"
sk: "POST#postId"
// Post details
pk: "POST#postId"
sk: "METADATA"
// Post comments
pk: "POST#postId"
sk: "COMMENT#commentId"Item Collections
项集合
Group related items under the same partition key:
typescript
// All items with pk="USER#123" form an "item collection"
// Can retrieve in a single query
// User profile
{ pk: "USER#123", sk: "PROFILE", name: "John", email: "..." }
// User's posts
{ pk: "USER#123", sk: "POST#post1", title: "...", content: "..." }
{ pk: "USER#123", sk: "POST#post2", title: "...", content: "..." }
// User's subscriptions
{ pk: "USER#123", sk: "SUB#sub1", plan: "pro", ... }将相关项分组到同一分区键下:
typescript
// All items with pk="USER#123" form an "item collection"
// Can retrieve in a single query
// User profile
{ pk: "USER#123", sk: "PROFILE", name: "John", email: "..." }
// User's posts
{ pk: "USER#123", sk: "POST#post1", title: "...", content: "..." }
{ pk: "USER#123", sk: "POST#post2", title: "...", content: "..." }
// User's subscriptions
{ pk: "USER#123", sk: "SUB#sub1", plan: "pro", ... }Access Pattern Design
访问模式设计
Pattern 1: Get Single Item
模式1:获取单个项
typescript
// Get user profile
{
pk: "USER#123",
sk: "PROFILE"
}
// DynamoDB operation
await client.send(new GetCommand({
TableName: Resource.Database.name,
Key: {
pk: "USER#123",
sk: "PROFILE"
}
}));typescript
// Get user profile
{
pk: "USER#123",
sk: "PROFILE"
}
// DynamoDB operation
await client.send(new GetCommand({
TableName: Resource.Database.name,
Key: {
pk: "USER#123",
sk: "PROFILE"
}
}));Pattern 2: Query Item Collection
模式2:查询项集合
typescript
// Get all posts by user
await client.send(new QueryCommand({
TableName: Resource.Database.name,
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":pk": "USER#123",
":sk": "POST#"
}
}));typescript
// Get all posts by user
await client.send(new QueryCommand({
TableName: Resource.Database.name,
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":pk": "USER#123",
":sk": "POST#"
}
}));Pattern 3: Query with Sort
模式3:带排序的查询
typescript
// Get user's recent posts (sorted by timestamp)
{
pk: "USER#123",
sk: "POST#2025-01-02T10:30:00Z#post1" // ISO timestamp for sorting
}
await client.send(new QueryCommand({
TableName: Resource.Database.name,
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":pk": "USER#123",
":sk": "POST#"
},
ScanIndexForward: false // Descending order
}));typescript
// Get user's recent posts (sorted by timestamp)
{
pk: "USER#123",
sk: "POST#2025-01-02T10:30:00Z#post1" // ISO timestamp for sorting
}
await client.send(new QueryCommand({
TableName: Resource.Database.name,
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":pk": "USER#123",
":sk": "POST#"
},
ScanIndexForward: false // Descending order
}));Pattern 4: Global Secondary Index (GSI)
模式4:全局二级索引(GSI)
typescript
// GSI for querying posts by status across all users
// GSI: gsi1pk = "POST#STATUS#published", gsi1sk = timestamp
// In SST config
const table = new sst.aws.Dynamo("Database", {
fields: {
pk: "string",
sk: "string",
gsi1pk: "string",
gsi1sk: "string"
},
primaryIndex: { hashKey: "pk", rangeKey: "sk" },
globalIndexes: {
gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk" }
}
});
// Query all published posts
await client.send(new QueryCommand({
TableName: Resource.Database.name,
IndexName: "gsi1",
KeyConditionExpression: "gsi1pk = :pk",
ExpressionAttributeValues: {
":pk": "POST#STATUS#published"
}
}));typescript
// GSI for querying posts by status across all users
// GSI: gsi1pk = "POST#STATUS#published", gsi1sk = timestamp
// In SST config
const table = new sst.aws.Dynamo("Database", {
fields: {
pk: "string",
sk: "string",
gsi1pk: "string",
gsi1sk: "string"
},
primaryIndex: { hashKey: "pk", rangeKey: "sk" },
globalIndexes: {
gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk" }
}
});
// Query all published posts
await client.send(new QueryCommand({
TableName: Resource.Database.name,
IndexName: "gsi1",
KeyConditionExpression: "gsi1pk = :pk",
ExpressionAttributeValues: {
":pk": "POST#STATUS#published"
}
}));Common Patterns
常见模式
Pattern: User with Posts and Comments
模式:包含帖子与评论的用户模型
typescript
// User profile
{
pk: "USER#userId",
sk: "PROFILE",
name: "John",
email: "john@example.com",
createdAt: "2025-01-01T00:00:00Z"
}
// User's post
{
pk: "USER#userId",
sk: "POST#postId",
title: "My Post",
content: "...",
createdAt: "2025-01-02T00:00:00Z"
}
// Post metadata (for reverse lookup)
{
pk: "POST#postId",
sk: "METADATA",
userId: "userId",
title: "My Post",
content: "...",
commentCount: 5
}
// Post comments
{
pk: "POST#postId",
sk: "COMMENT#2025-01-02T10:00:00Z#commentId",
userId: "commenterId",
text: "Great post!",
createdAt: "2025-01-02T10:00:00Z"
}
// Commenter profile (denormalized for display)
{
pk: "POST#postId",
sk: "COMMENT#2025-01-02T10:00:00Z#commentId",
userId: "commenterId",
userName: "Jane", // Denormalized!
userAvatar: "https://...", // Denormalized!
text: "Great post!"
}Access Patterns:
- Get user profile:
GetItem(pk="USER#userId", sk="PROFILE") - Get user's posts:
Query(pk="USER#userId", sk begins_with "POST#") - Get post with comments:
Query(pk="POST#postId") - Get recent comments: Sort by timestamp in sk
typescript
// User profile
{
pk: "USER#userId",
sk: "PROFILE",
name: "John",
email: "john@example.com",
createdAt: "2025-01-01T00:00:00Z"
}
// User's post
{
pk: "USER#userId",
sk: "POST#postId",
title: "My Post",
content: "...",
createdAt: "2025-01-02T00:00:00Z"
}
// Post metadata (for reverse lookup)
{
pk: "POST#postId",
sk: "METADATA",
userId: "userId",
title: "My Post",
content: "...",
commentCount: 5
}
// Post comments
{
pk: "POST#postId",
sk: "COMMENT#2025-01-02T10:00:00Z#commentId",
userId: "commenterId",
text: "Great post!",
createdAt: "2025-01-02T10:00:00Z"
}
// Commenter profile (denormalized for display)
{
pk: "POST#postId",
sk: "COMMENT#2025-01-02T10:00:00Z#commentId",
userId: "commenterId",
userName: "Jane", // Denormalized!
userAvatar: "https://...", // Denormalized!
text: "Great post!"
}访问模式:
- 获取用户资料:
GetItem(pk="USER#userId", sk="PROFILE") - 获取用户的所有帖子:
Query(pk="USER#userId", sk begins_with "POST#") - 获取帖子及其评论:
Query(pk="POST#postId") - 获取最新评论:通过sk中的时间戳排序
Pattern: Many-to-Many (Users and Groups)
模式:多对多关系(用户与群组)
typescript
// User membership in group
{
pk: "USER#userId",
sk: "GROUP#groupId",
groupName: "Developers", // Denormalized
role: "admin",
joinedAt: "2025-01-01"
}
// Group membership list
{
pk: "GROUP#groupId",
sk: "USER#userId",
userName: "John", // Denormalized
role: "admin",
joinedAt: "2025-01-01"
}
// Group metadata
{
pk: "GROUP#groupId",
sk: "METADATA",
name: "Developers",
description: "...",
memberCount: 42
}Access Patterns:
- Get user's groups:
Query(pk="USER#userId", sk begins_with "GROUP#") - Get group's members:
Query(pk="GROUP#groupId", sk begins_with "USER#") - Check membership:
GetItem(pk="USER#userId", sk="GROUP#groupId")
typescript
// User membership in group
{
pk: "USER#userId",
sk: "GROUP#groupId",
groupName: "Developers", // Denormalized
role: "admin",
joinedAt: "2025-01-01"
}
// Group membership list
{
pk: "GROUP#groupId",
sk: "USER#userId",
userName: "John", // Denormalized
role: "admin",
joinedAt: "2025-01-01"
}
// Group metadata
{
pk: "GROUP#groupId",
sk: "METADATA",
name: "Developers",
description: "...",
memberCount: 42
}访问模式:
- 获取用户所属群组:
Query(pk="USER#userId", sk begins_with "GROUP#") - 获取群组的所有成员:
Query(pk="GROUP#groupId", sk begins_with "USER#") - 检查成员关系:
GetItem(pk="USER#userId", sk="GROUP#groupId")
Pattern: Hierarchical Data (Folders and Files)
模式:层级数据(文件夹与文件)
typescript
// Folder
{
pk: "FOLDER#folderId",
sk: "METADATA",
name: "Documents",
parentId: "parentFolderId",
path: "/Documents"
}
// Files in folder
{
pk: "FOLDER#folderId",
sk: "FILE#2025-01-02#fileId", // Sorted by date
name: "report.pdf",
size: 1024000,
uploadedAt: "2025-01-02T10:00:00Z"
}
// File metadata (for direct access)
{
pk: "FILE#fileId",
sk: "METADATA",
name: "report.pdf",
folderId: "folderId",
size: 1024000
}typescript
// Folder
{
pk: "FOLDER#folderId",
sk: "METADATA",
name: "Documents",
parentId: "parentFolderId",
path: "/Documents"
}
// Files in folder
{
pk: "FOLDER#folderId",
sk: "FILE#2025-01-02#fileId", // Sorted by date
name: "report.pdf",
size: 1024000,
uploadedAt: "2025-01-02T10:00:00Z"
}
// File metadata (for direct access)
{
pk: "FILE#fileId",
sk: "METADATA",
name: "report.pdf",
folderId: "folderId",
size: 1024000
}Pattern: Time Series Data
模式:时间序列数据
typescript
// Metrics by date
{
pk: "METRICS#resourceId",
sk: "2025-01-02T10:00:00Z",
cpu: 45.2,
memory: 67.8,
requests: 1234
}
// Query metrics for a time range
await client.send(new QueryCommand({
TableName: Resource.Database.name,
KeyConditionExpression: "pk = :pk AND sk BETWEEN :start AND :end",
ExpressionAttributeValues: {
":pk": "METRICS#resourceId",
":start": "2025-01-01T00:00:00Z",
":end": "2025-01-02T00:00:00Z"
}
}));typescript
// Metrics by date
{
pk: "METRICS#resourceId",
sk: "2025-01-02T10:00:00Z",
cpu: 45.2,
memory: 67.8,
requests: 1234
}
// Query metrics for a time range
await client.send(new QueryCommand({
TableName: Resource.Database.name,
KeyConditionExpression: "pk = :pk AND sk BETWEEN :start AND :end",
ExpressionAttributeValues: {
":pk": "METRICS#resourceId",
":start": "2025-01-01T00:00:00Z",
":end": "2025-01-02T00:00:00Z"
}
}));Implementation with SST
基于SST的实现
Basic Setup
基础配置
typescript
// sst.config.ts
const table = new sst.aws.Dynamo("Database", {
fields: {
pk: "string",
sk: "string",
gsi1pk: "string",
gsi1sk: "string",
gsi2pk: "string",
gsi2sk: "string"
},
primaryIndex: { hashKey: "pk", rangeKey: "sk" },
globalIndexes: {
gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk" },
gsi2: { hashKey: "gsi2pk", rangeKey: "gsi2sk" }
},
stream: "new-and-old-images" // For event-driven updates
});typescript
// sst.config.ts
const table = new sst.aws.Dynamo("Database", {
fields: {
pk: "string",
sk: "string",
gsi1pk: "string",
gsi1sk: "string",
gsi2pk: "string",
gsi2sk: "string"
},
primaryIndex: { hashKey: "pk", rangeKey: "sk" },
globalIndexes: {
gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk" },
gsi2: { hashKey: "gsi2pk", rangeKey: "gsi2sk" }
},
stream: "new-and-old-images" // For event-driven updates
});Type-Safe Helpers
类型安全助手
typescript
// src/lib/db.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { Resource } from "sst";
export const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
marshallOptions: {
removeUndefinedValues: true
}
});
export const TableName = Resource.Database.name;
// Key builders
export const keys = {
user: (userId: string) => ({
profile: { pk: `USER#${userId}`, sk: "PROFILE" },
post: (postId: string) => ({ pk: `USER#${userId}`, sk: `POST#${postId}` })
}),
post: (postId: string) => ({
metadata: { pk: `POST#${postId}`, sk: "METADATA" },
comment: (commentId: string, timestamp: string) => ({
pk: `POST#${postId}`,
sk: `COMMENT#${timestamp}#${commentId}`
})
})
};typescript
// src/lib/db.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { Resource } from "sst";
export const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
marshallOptions: {
removeUndefinedValues: true
}
});
export const TableName = Resource.Database.name;
// Key builders
export const keys = {
user: (userId: string) => ({
profile: { pk: `USER#${userId}`, sk: "PROFILE" },
post: (postId: string) => ({ pk: `USER#${userId}`, sk: `POST#${postId}` })
}),
post: (postId: string) => ({
metadata: { pk: `POST#${postId}`, sk: "METADATA" },
comment: (commentId: string, timestamp: string) => ({
pk: `POST#${postId}`,
sk: `COMMENT#${timestamp}#${commentId}`
})
})
};CRUD Operations
CRUD操作
typescript
import { GetCommand, PutCommand, UpdateCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
import { dynamodb, TableName, keys } from "./db";
// Create user
export async function createUser(userId: string, data: UserData) {
await dynamodb.send(new PutCommand({
TableName,
Item: {
...keys.user(userId).profile,
...data,
createdAt: new Date().toISOString()
}
}));
}
// Get user
export async function getUser(userId: string) {
const result = await dynamodb.send(new GetCommand({
TableName,
Key: keys.user(userId).profile
}));
return result.Item as User | undefined;
}
// Update user
export async function updateUser(userId: string, updates: Partial<UserData>) {
await dynamodb.send(new UpdateCommand({
TableName,
Key: keys.user(userId).profile,
UpdateExpression: "SET #name = :name, #email = :email",
ExpressionAttributeNames: {
"#name": "name",
"#email": "email"
},
ExpressionAttributeValues: {
":name": updates.name,
":email": updates.email
}
}));
}
// Get user's posts
export async function getUserPosts(userId: string) {
const result = await dynamodb.send(new QueryCommand({
TableName,
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":pk": `USER#${userId}`,
":sk": "POST#"
}
}));
return result.Items as Post[];
}typescript
import { GetCommand, PutCommand, UpdateCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
import { dynamodb, TableName, keys } from "./db";
// Create user
export async function createUser(userId: string, data: UserData) {
await dynamodb.send(new PutCommand({
TableName,
Item: {
...keys.user(userId).profile,
...data,
createdAt: new Date().toISOString()
}
}));
}
// Get user
export async function getUser(userId: string) {
const result = await dynamodb.send(new GetCommand({
TableName,
Key: keys.user(userId).profile
}));
return result.Item as User | undefined;
}
// Update user
export async function updateUser(userId: string, updates: Partial<UserData>) {
await dynamodb.send(new UpdateCommand({
TableName,
Key: keys.user(userId).profile,
UpdateExpression: "SET #name = :name, #email = :email",
ExpressionAttributeNames: {
"#name": "name",
"#email": "email"
},
ExpressionAttributeValues: {
":name": updates.name,
":email": updates.email
}
}));
}
// Get user's posts
export async function getUserPosts(userId: string) {
const result = await dynamodb.send(new QueryCommand({
TableName,
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":pk": `USER#${userId}`,
":sk": "POST#"
}
}));
return result.Items as Post[];
}Best Practices
最佳实践
1. Design for Access Patterns First
1. 优先围绕访问模式设计
❌ Don't design entities first:
Users table, Posts table, Comments table...✅ Do design access patterns first:
1. Get user profile
2. Get user's posts
3. Get post with comments
4. Get all published posts
// Then design keys to support these❌ 不要先设计实体:
用户表、帖子表、评论表...✅ 应先设计访问模式:
1. 获取用户资料
2. 获取用户的所有帖子
3. 获取帖子及其评论
4. 获取所有已发布的帖子
// 然后设计键以支持这些模式2. Use Sparse Indexes
2. 使用稀疏索引
Only items with GSI keys appear in the index:
typescript
// Only published posts have gsi1pk
{
pk: "POST#123",
sk: "METADATA",
status: "published",
gsi1pk: "POST#STATUS#published", // Only published posts have this
gsi1sk: "2025-01-02T10:00:00Z"
}只有包含GSI键的项才会出现在索引中:
typescript
// Only published posts have gsi1pk
{
pk: "POST#123",
sk: "METADATA",
status: "published",
gsi1pk: "POST#STATUS#published", // Only published posts have this
gsi1sk: "2025-01-02T10:00:00Z"
}3. Denormalize Strategically
3. 策略性地进行反规范化
Duplicate data to avoid secondary queries:
typescript
// Comment with user info denormalized
{
pk: "POST#postId",
sk: "COMMENT#commentId",
userId: "userId",
userName: "John", // From users table
userAvatar: "...", // From users table
text: "Great post!"
}通过数据冗余避免二次查询:
typescript
// Comment with user info denormalized
{
pk: "POST#postId",
sk: "COMMENT#commentId",
userId: "userId",
userName: "John", // From users table
userAvatar: "...", // From users table
text: "Great post!"
}4. Use Transactions for Related Items
4. 对相关项使用事务
typescript
import { TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
await dynamodb.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName,
Item: { pk: "POST#123", sk: "METADATA", ... }
}
},
{
Update: {
TableName,
Key: { pk: "USER#userId", sk: "PROFILE" },
UpdateExpression: "SET postCount = postCount + :inc",
ExpressionAttributeValues: { ":inc": 1 }
}
}
]
}));typescript
import { TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
await dynamodb.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName,
Item: { pk: "POST#123", sk: "METADATA", ... }
}
},
{
Update: {
TableName,
Key: { pk: "USER#userId", sk: "PROFILE" },
UpdateExpression: "SET postCount = postCount + :inc",
ExpressionAttributeValues: { ":inc": 1 }
}
}
]
}));5. Handle Hot Partitions
5. 处理热点分区
Distribute writes using suffixes:
typescript
// Instead of: pk: "METRICS"
// Use: pk: "METRICS#0", "METRICS#1", ..., "METRICS#9"
const suffix = Math.floor(Math.random() * 10);
const pk = `METRICS#${suffix}`;通过后缀分散写入:
typescript
// Instead of: pk: "METRICS"
// Use: pk: "METRICS#0", "METRICS#1", ..., "METRICS#9"
const suffix = Math.floor(Math.random() * 10);
const pk = `METRICS#${suffix}`;Common Gotchas
常见陷阱
1. Sort Key is Required for Queries
1. 查询需要排序键
typescript
// ❌ This won't work
Query(pk = "USER#123")
// ✅ Use begins_with
Query(pk = "USER#123" AND sk begins_with "POST#")typescript
// ❌ This won't work
Query(pk = "USER#123")
// ✅ Use begins_with
Query(pk = "USER#123" AND sk begins_with "POST#")2. GSI Consistency is Eventually Consistent
2. GSI的一致性是最终一致性
typescript
// After writing to main table
await putItem({ pk: "USER#123", gsi1pk: "ACTIVE" });
// GSI query might not see it immediately
const result = await query({ IndexName: "gsi1", gsi1pk: "ACTIVE" });
// May not include the item yet!typescript
// After writing to main table
await putItem({ pk: "USER#123", gsi1pk: "ACTIVE" });
// GSI query might not see it immediately
const result = await query({ IndexName: "gsi1", gsi1pk: "ACTIVE" });
// May not include the item yet!3. Item Size Limit is 400KB
3. 项大小限制为400KB
typescript
// Don't store large data in items
❌ { pk: "POST#123", content: "<10MB of text>" }
// Store large data in S3
✅ { pk: "POST#123", contentUrl: "s3://bucket/key" }typescript
// Don't store large data in items
❌ { pk: "POST#123", content: "<10MB of text>" }
// Store large data in S3
✅ { pk: "POST#123", contentUrl: "s3://bucket/key" }4. Projection of GSI Matters
4. GSI的投影方式很重要
typescript
// GSI with ALL projection (expensive)
globalIndexes: {
gsi1: {
hashKey: "gsi1pk",
projection: "all" // Copies all attributes
}
}
// GSI with KEYS_ONLY (cheaper)
globalIndexes: {
gsi1: {
hashKey: "gsi1pk",
projection: "keys_only" // Only pk, sk, gsi keys
}
}typescript
// GSI with ALL projection (expensive)
globalIndexes: {
gsi1: {
hashKey: "gsi1pk",
projection: "all" // Copies all attributes
}
}
// GSI with KEYS_ONLY (cheaper)
globalIndexes: {
gsi1: {
hashKey: "gsi1pk",
projection: "keys_only" // Only pk, sk, gsi keys
}
}Testing Single Table Design
单表设计测试
typescript
// Use local DynamoDB for tests
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({
endpoint: "http://localhost:8000"
});
describe("User operations", () => {
test("creates user and retrieves profile", async () => {
await createUser("123", { name: "John", email: "john@example.com" });
const user = await getUser("123");
expect(user?.name).toBe("John");
});
test("queries user posts", async () => {
await createPost("123", "post1", { title: "First Post" });
await createPost("123", "post2", { title: "Second Post" });
const posts = await getUserPosts("123");
expect(posts).toHaveLength(2);
});
});typescript
// Use local DynamoDB for tests
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({
endpoint: "http://localhost:8000"
});
describe("User operations", () => {
test("creates user and retrieves profile", async () => {
await createUser("123", { name: "John", email: "john@example.com" });
const user = await getUser("123");
expect(user?.name).toBe("John");
});
test("queries user posts", async () => {
await createPost("123", "post1", { title: "First Post" });
await createPost("123", "post2", { title: "Second Post" });
const posts = await getUserPosts("123");
expect(posts).toHaveLength(2);
});
});Migration Strategy
迁移策略
If migrating from multiple tables:
- Identify access patterns in existing code
- Design new key structure to support patterns
- Create migration scripts to transform data
- Run in parallel (dual writes during transition)
- Verify data integrity before cutover
- Switch to single table atomically
如果从多表架构迁移:
- 识别现有代码中的访问模式
- 设计新的键结构以支持这些模式
- 创建迁移脚本转换数据
- 并行运行(迁移期间双写)
- 切换前验证数据完整性
- 原子性切换到单表
Further Reading
扩展阅读
- AWS DynamoDB Best Practices: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html
- Alex DeBrie's "DynamoDB Book": https://www.dynamodbbook.com/
- Rick Houlihan's re:Invent talks on YouTube
- SST DynamoDB docs: https://sst.dev/docs/component/aws/dynamo
- AWS DynamoDB最佳实践:https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html
- Alex DeBrie的《DynamoDB Book》:https://www.dynamodbbook.com/
- Rick Houlihan在YouTube上的re:Invent演讲
- SST DynamoDB文档:https://sst.dev/docs/component/aws/dynamo