1. 結論(この記事で得られること)
この記事では、DynamoDBのsort key設計で実務レベルでよく使われる一般化パターンを体系的に整理します。
具体的には:
- 複合sort keyの設計原則(区切り文字、階層表現、時系列の扱い)
- 実務でよく失敗する5つのアンチパターンとその回避方法
- AI(Claude/GPT)を使った設計レビューの自動化手法
- 本番環境で安全にスキーマ変更する手順とロールバック戦略
昔、私もEコマース案件でsort keyを「ITEM#」みたいに設計して、後から「商品カテゴリ × 登録日時でソートしたい」という要件が来て、全テーブルスキャンするLambdaを夜な夜な書いた経験があります。あの悪夢を皆さんには味わってほしくない。
2. 前提(環境・読者層)
想定読者
- DynamoDBの基本(partition key / sort key / GSI)は理解している
- 実際のプロジェクトでテーブル設計を任されているor任される予定
- 「なんとなく動く」から「拡張性のある設計」にレベルアップしたい
技術環境
- AWS DynamoDB(従量課金 or オンデマンドモード)
- 言語例:TypeScript(Node.js)、Python
- AWS SDK v3前提(v2でも考え方は同じ)
3. Before:よくあるつまずきポイント
実務で見てきたsort key設計の失敗パターンを3つ紹介します。
失敗①:sort keyに何も考えずUUIDを入れる
// ❌ Bad
{
PK: "USER#123",
SK: "c5e1a8f3-4d2b-4e8a-9c7f-1a2b3c4d5e6f", // ランダムUUID
createdAt: "2025-01-15T10:00:00Z"
}
何が問題か:
- sort keyでソートする意味がない(UUIDはランダム)
- 時系列順に取得したいときに「createdAt」でフィルタ → 全件スキャン
失敗②:区切り文字を統一しない
// ❌ Bad(案件内で混在)
SK: "METADATA#2025-01-15" // `#`で区切り
SK: "ORDER_2025-01-15" // `_`で区切り
SK: "ITEM::electronics" // `::`で区切り
何が問題か:
- チーム内で分割ロジックがバラバラになる
- パース処理が属人化 → 障害時に調査コストが跳ね上がる
失敗③:将来のクエリパターンを考慮しない
// ❌ Bad:最初は「ユーザーごとの注文一覧」だけ想定
{
PK: "USER#123",
SK: "ORDER#456"
}
// 後から「特定期間の注文だけ取得したい」→ できない
// 後から「ステータスごとに絞りたい」→ GSI追加 → コスト増
この設計だと、Query条件に柔軟性がなく、後からGSIを乱立させることになります。
4. After:基本的な解決パターン
実務でよく使われる一般化パターンを3つ紹介します。
パターン①:時系列 prefix 型(最頻出)
// ✅ Good
{
PK: "USER#123",
SK: "ORDER#2025-01-15T10:30:00Z#456", // タイプ + ISO8601 + ID
status: "shipped",
totalAmount: 15000
}
メリット:
- 「begins_with("ORDER#2025-01")」で1月の注文だけQueryできる
- 自然に時系列でソートされる
- IDを末尾に置くことで一意性も担保
Query例:
const params = {
TableName: "Orders",
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk_prefix)",
ExpressionAttributeValues: {
":pk": "USER#123",
":sk_prefix": "ORDER#2025-01"
}
};
パターン②:階層型(カテゴリ・地域など)
// ✅ Good
{
PK: "SHOP#tokyo",
SK: "CATEGORY#electronics#ITEM#item-123",
price: 29800,
stock: 5
}
メリット:
- 「begins_with("CATEGORY#electronics")」で電化製品だけ取得
- 階層を増やしても後方互換性がある
使い所:
- EC:カテゴリ → サブカテゴリ → 商品
- SaaS:組織 → チーム → メンバー
パターン③:複合条件型(ステータス × 時刻)
// ✅ Good
{
PK: "TENANT#abc",
SK: "TICKET#open#2025-01-15T10:00:00Z#ticket-789",
assignee: "user-456"
}
メリット:
- 「begins_with("TICKET#open")」でオープンチケットだけQuery
- ステータスが変わったらSKごと更新(後述)
注意点:
- ステータス変更時はUpdateではなくDelete + Putが必要
- トランザクション必須(後ほど詳述)