Bạn có 3 bảng users, tasks, comments. Mỗi lần load user profile phải JOIN 3 table, latency 50ms. DynamoDB single-table design gom tất cả vào 1 bảng, 1 query lấy hết – latency 5ms.
Thay vì 3 table users, tasks, comments, single-table lưu tất cả trong 1 table. Lý do: DynamoDB tối ưu cho single-table query: 1 request = 1 partition = tất cả data liên quan.
PK: USER#alice SK: PROFILE#main name, email PK: USER#alice SK: TASK#2026-06-21T08:00 title, status PK: USER#alice SK: TASK#2026-06-20T15:00 title, status PK: TASK#001 SK: COMMENT#2026-06-21T09 text, author
1 query PK=USER#alice lấy tất cả: profile + mọi tasks. Task lưu dưới PK=USER#id nên cần 2 query riêng: 1 query PK=USER#alice, SK=bắt đầu TASK# lấy task, và 1 query PK=TASK#001 lấy comments.
Access-pattern-first design
- Get user profile → PK: USER#id, SK: PROFILE#main
- List tasks by user → PK: USER#id, SK: begins_with TASK#
- Get task → PK: USER#id, SK: TASK#taskId (hoặc dùng GSI)
- List comments → PK: TASK#taskId, SK: begins_with COMMENT#
- List tasks by status (admin) → GSI1 PK: STATUS#pending, SK: createdAt
- Tasks due today → GSI1 PK: DUEDATE#2026-06-21, SK: USER#id
GSI Overload: một index cho nhiều pattern
Cùng GSI1, khác prefix → phục vụ nhiều access pattern:
// Pattern 5: tasks by status
GSI1PK: "STATUS#pending", GSI1SK: "2026-06-21T08:00:00Z"
// Pattern 6: tasks due today
GSI1PK: "DUEDATE#2026-06-21", GSI1SK: "USER#alice"
// Cùng GSI1, query với prefix khác nhau
await ddb.send(new QueryCommand({
TableName: "myapp", IndexName: "GSI1",
KeyConditionExpression: "GSI1PK = :pk",
ExpressionAttributeValues: { ":pk": "STATUS#pending" },
}));
GSI Overload dùng prefix khác nhau trên cùng GSI1PK để phục vụ nhiều access pattern, tiết kiệm WCU và đơn giản hóa code.
## Sparse Index và DAX
Item không cần GSI → không set GSI key → không tốn GSI capacity. DAX = in-memory cache cho DynamoDB, latency từ ~10ms → <1ms cho read.
Bài sau: [Phần 42: Serverless nâng cao -- event-driven patterns](/posts/aws/42-serverless-nang-cao-event-driven-patterns/)