Mình mất khá nhiều thời gian để hiểu tại sao cùng một table query nhanh chỗ này mà chậm chỗ kia, và câu trả lời nằm ở key design. DynamoDB khác RDS tận gốc: không connection (HTTP API), không schema cố định (mỗi item có attribute riêng), không provisioning (On-Demand), scale ngang tự động. Với Lambda + API Gateway, DynamoDB là database tự nhiên nhất: pay-per-request, không connection pool, latency single-digit ms ở mọi scale.
Nhưng sức mạnh đi kèm learning curve: key design, GSI, capacity mode. Single-table nâng cao ở Bài 41.
flowchart TB
Table["DynamoDB Table"]
PK["Partition Key (PK)<br/>quyết định partition vật lý"]
SK["Sort Key (SK)<br/>range query trong partition"]
GSI["GSI<br/>query theo key khác<br/>eventual consistency"]
Stream["DynamoDB Stream<br/>capture mọi thay đổi<br/>→ Lambda trigger"]
Table --> PK
Table --> SK
Table --> GSI
Table --> Stream
Key design: PK quyết định partition
import {
DynamoDBDocumentClient,
PutCommand,
QueryCommand,
} from "@aws-sdk/lib-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
// Put item — PK + SK xác định vị trí duy nhất
await ddb.send(
new PutCommand({
TableName: "myapp",
Item: {
PK: "USER#alice",
SK: "PROFILE#main",
name: "Alice",
email: "[email protected]",
team: "backend",
},
})
);
// Query theo PK — lấy tất cả item của user
const { Items } = await ddb.send(
new QueryCommand({
TableName: "myapp",
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
ExpressionAttributeValues: { ":pk": "USER#alice", ":sk": "TASK#" },
})
);
Capacity modes
| On-Demand | Provisioned | |
|---|---|---|
| Giá | $1.25/1M WRU | $0.00065/WCU-hr (~$0.47/1M) |
| Throttle | Hiếm (spike >2x peak) | Có nếu traffic > capacity |
| Dùng khi | Traffic unpredictable, dev | Traffic ổn định, cost-sensitive |
Bắt đầu On-Demand → chuyển Provisioned khi hiểu access pattern. Switch được bất kỳ lúc nào.
GSI + TTL + Stream
// GSI: query theo status (không phải PK chính)
await ddb.send(
new QueryCommand({
TableName: "myapp",
IndexName: "GSI1",
KeyConditionExpression: "GSI1PK = :status",
ExpressionAttributeValues: { ":status": "TASK_STATUS#pending" },
})
);
// TTL: auto-expire session
await ddb.send(
new PutCommand({
TableName: "myapp",
Item: {
PK: "SESSION#abc",
SK: "TOKEN#xyz",
expiresAt: Math.floor(Date.now() / 1000) + 86400,
},
})
);
// DynamoDB Stream → Lambda
export const handler = async (event: DynamoDBStreamEvent) => {
for (const record of event.Records) {
if (record.eventName === "INSERT") {
const item = unmarshall(record.dynamodb!.NewImage!);
await es.index({ index: "tasks", id: item.taskId, body: item });
}
}
};
Key design: PK quyết định partition, SK cho range query
Mình thấy key design là skill quan trọng nhất khi làm việc với DynamoDB. On-Demand hay Provisioned chỉ là chuyện nhỏ sau khi đã thiết kế đúng access pattern.
- On-Demand → Provisioned khi access pattern ổn định
- GSI: query theo key khác, eventual consistency, thêm cost
- Single-table: PK/SK prefix phân biệt entity — Bài 41 deep dive
- TTL: auto expire cho session, cache, token
- Stream → Lambda: search index, notification, cache invalidation
Bài sau: Phần 14: Route 53 — domain, DNS, health check, routing policy