Claude CodeでDynamoDBのテーブル設計 — アクセスパターンから詰める手順
DynamoDBはアクセスパターンを決めてからキーを決めます。Claude Codeに聞き取らせて一覧にし、GSIを割り当て、ホットパーティションを検算スクリプトで確かめる手順と、Stop hookで検算を強制する設定をまとめます。
DynamoDBのテーブル設計をClaude Codeに任せるときは、順番を固定します。アクセスパターンを列挙し、キーとGSIを割り当て、最後にホットパーティションを数字で検算します。この順を崩して「注文テーブルを作って」と頼むと、RDBMSの感覚で正規化されたテーブルが出てくることがあります。
ここでは、その3段階をClaude Codeの機能(インタビュー、CLAUDE.md、Stop hook、permissions)に割り当てる方法を示します。検算には、実行例の出力を載せたスクリプトを使います。
DynamoDBでは、なぜキーより先にアクセスパターンなのか
AWSのモデリング指針には、はっきりした注意書きがあります。RDBMSなら、アクセスパターンを考えずに正規化したデータモデルを作り、後から問い合わせに合わせて拡張できます。DynamoDBでは、テーブルが答えるべき問いが分かるまでスキーマの設計を始めるべきではない、という立場です。
アクセスパターンの集め方も書かれています。新規のアプリならユーザーストーリーからユースケースを洗い出し、必要な検索を分析します。既存のアプリならクエリログを調べて、実際の使われ方と主要なパターンを見つけます。
出来上がる一覧は、たとえば次のような形です(AWSのガイドにある注文管理アプリの例の一部)。
| # | アクセスパターン |
|---|---|
| 1 | アクセスパターン従業員IDで従業員の詳細を引く |
| 5 | アクセスパターン顧客の注文を日付範囲で取得する |
| 6 | アクセスパターン日付範囲内の未処理注文をすべて表示する |
| 9 | アクセスパターン商品に紐づく注文明細をすべて取得する |
| 15 | アクセスパターン商品の在庫総数を取得する |
この一覧を基にして、1つのアクセスパターンを1回のクエリで取得できるようにデータを構造化します。これがDynamoDB設計のゴールです。
Claude Codeと組むうえでの要点は1つです。この一覧を作る材料(誰がどの画面で何を引くか、ピーク時に毎秒何回か)は、リポジトリを読んでも出てきません。Claude Codeの運用指針も、リポジトリにない情報、たとえば会議で決めた判断を、Claudeが自力で見つけられない情報として挙げています。頻度やピーク値は人間が持っている情報なので、ここは人間が答える側に回ります。
手順1: Claude Codeに聞き取らせ、ACCESS_PATTERNS.mdにする
最初の1手は、設計ではなくインタビューです。Claude Codeの運用指針には、考えがまとまっていないときにClaudeへ質問させ、仕様をファイルに書かせる型が載っています。その型をDynamoDB向けに替えたプロンプトが次です。
注文管理アプリのDynamoDBテーブルを設計したいが、アクセスパターンがまだ整理できていない。テーブルやキーはまだ書かないで。画面・バッチ・外部連携ごとに「何をキーに、何件、どの順で引くか」をインタビューして、ACCESS_PATTERNS.mdに書き出して。
各パターンに、読み書きの別、ピーク時の毎秒リクエスト数、1項目のサイズ(KB)、強い整合性が要るかどうかを必ず入れて。分からない値は空欄にせず、私に聞き返して。「テーブルやキーはまだ書かないで」の一文が肝です。これがないと、Claudeがテーブル定義まで先に書いてしまうことがあり、その場合アクセスパターンが後付けになります。
書き出されたACCESS_PATTERNS.mdは、次の列を持つ表にしておくと後の検算が楽です。
| # | パターン | 読み書き | ピークrps | 項目サイズ | 強い整合性 |
|---|---|---|---|---|---|
| 1 | パターン注文IDで1件取得 | 読み書き読み | ピークrps200 | 項目サイズ2KB | 強い整合性要 |
| 2 | パターン顧客の注文を日付範囲で一覧 | 読み書き読み | ピークrps300 | 項目サイズ8KB | 強い整合性不要 |
| 3 | パターン注文の書き込み | 読み書き書き | ピークrps800 | 項目サイズ2KB | 強い整合性- |
| 4 | パターン日付範囲の注文を全件(バッチ) | 読み書き読み | ピークrps5 | 項目サイズ2KB | 強い整合性不要 |
数値は例です。実際の値は、既存システムならクエリログから、新規ならユーザーストーリーと想定負荷から人間が決めます。
仕様ファイルができたら、設計は新しいセッションで始めます。運用指針も、仕様ができたら新しいセッションでそのファイルから作り始めることを勧めています。聞き取りで膨らんだ会話を引きずらず、「完成の定義」が書かれたファイルだけを手がかりにできるからです。
手順2: パターンごとにキーとGSIを割り当てさせる
新しいセッションでは、1パターンずつ「どのキー条件でQuery 1回になるか」を書かせます。依頼文はこうです。
ACCESS_PATTERNS.mdの全パターンについて、ベーステーブルかGSIのどちらで、どのパーティションキーとソートキーの条件なら1回のQueryで取れるかを表にしてDESIGN.mdへ。ベーステーブルで満たせないパターンだけをGSI候補にして、理由を1行ずつ付けて。GSIを足す前に、同じ目的に使い回せる既存のGSIがないか確認して。出てきた案を読むときは、AWSの指針に沿って次の点を見ます。
ソートキーで範囲を表現しているか。良いソートキーは、関連する項目を1か所に集め、begins_withやbetween、>、<などの範囲クエリで取り出せるようにします。[country]#[region]#[state]のような複合ソートキーなら、どの階層でも範囲クエリが成立します。パターン2なら、たとえばパーティションキーをCUSTOMER#<顧客ID>、ソートキーをORDER#<注文日時>#<注文ID>にすれば、期間指定の一覧は1回のQueryです。
単一テーブルか複数テーブルか。AWSのガイドは3つの設計を並べています。アクセスの相関が高い複数のエンティティを1つのテーブルに載せる単一テーブル設計、運用特性が独立したエンティティを別テーブルに分ける複数テーブル設計、そして常に一緒に読むデータを埋め込む集約設計です。どれを選ぶかは、アクセスパターンとデータの性質と運用要件で決まる、とされています。ガイド自身の例は複数テーブルに集約と非正規化を組み合わせています。Claudeが単一テーブル一択の案を出してきたら、根拠をアクセスパターンの番号で説明させてください。
GSIが増えすぎていないか。GSIはベーステーブルと別のキーを持ち、読み書きのスループット設定も別です。1テーブルに作れるGSIは既定の上限で20、LSIは5です。ただし上限に届く前に費用が効きます。AWSの指針は、あまり問い合わせない属性にインデックスを張らないこと、射影する属性は必要なものだけにすることを求めています。ALLを指定するとテーブルアイテム全体を別のソートキーで引けますが、多くの場合は保存と書き込みのコストが倍になります。LSIは、同じパーティションキー値のデータ合計が10GBを超えられない点も効きます。
属性の使い回し(GSIオーバーロード)を試す余地があるか。同じ属性に項目の種類ごとに別の内容を入れ、1つのGSIで複数種類の検索を捌く手法です。AWSのガイドでは、従業員名の検索、倉庫IDによる従業員検索、最近の入社日の検索を1つのGSIで行う例を示しています。GSIの本数を減らせますが、属性の意味がテーブル内で揺れるので、DESIGN.mdに「この属性は項目種別ごとに何が入るか」を書かせておくと、後から読む人が迷いません。
ここまでをDESIGN.mdに残させれば、レビューも楽になります。別のサブエージェントに、仕様と照らし合わせた確認を頼みます。
サブエージェントで、DESIGN.mdをACCESS_PATTERNS.mdと突き合わせてレビューして。全パターンがQuery 1回で満たされているか、使われていないGSIがないかを確認。スタイルの指摘は不要。「スタイルの指摘は不要」と明示するのは、運用指針の注意書きによるものです。問題を探せと言われたレビュアーは必ず何かを見つけるため、全部直すと不要な抽象化が増えます。
手順3: ホットパーティションを数字で検算する
キーの割り当てが論理的に正しくても、負荷が1つのキー値に集中すれば詰まります。ここを「たぶん大丈夫」で通さないために、数字で見ます。
検算に使う上限と、偏りやすいキー
AWSの指針では、パーティションの上限は、読み込みが毎秒3,000読み込みユニット、書き込みが毎秒1,000書き込みユニットです。1読み込みユニットは、4KBまでの項目の強い整合性読み取りを毎秒1回(結果整合性なら2回)、1書き込みユニットは1KBまでの項目の書き込みを毎秒1回です。項目サイズも効きます。ガイドの例では、20KBの項目の強い整合性読み取り1回が5読み込みユニットを消費し、1項目に毎秒600回の読み取りが当たると上限に届きます。
キーの良し悪しも、ガイドの対比表が基準になります。
| パーティションキーの値 | 均一性 |
|---|---|
| 多数のユーザーを持つアプリのユーザーID | 均一性良い |
| 取りうる値が少ないステータスコード | 均一性悪い |
| 日・時・分に丸めた項目作成日時 | 均一性悪い |
| 同程度の間隔でアクセスする多数のデバイスのID | 均一性良い |
| 台数が多くても、1台だけ突出して人気があるデバイスID | 均一性悪い |
たとえばパターン3(注文の書き込み)のパーティションキーを「注文日」にすると、表の3行目そのものになります。同じ日の書き込みがすべて1つのキー値、つまり1つのパーティションに集まります。
なお、この均一性はテーブルだけでなくセカンダリインデックスにも求められます。ガイドは、テーブルとそのインデックスのすべてのパーティションキーで均一なアクティビティを前提に設計するよう述べています。日付をキーにしたGSIを足すと、その書き込みも日付のキーに集まります。
検算スクリプトを置く
数字の突き合わせはClaudeの暗算に任せず、スクリプトにします。次のスクリプトは、design/patterns.jsonのパターンごとに、最も当たりが集中するキー値1つの負荷を求めて上限と比べます(hot_shareは、そのピーク負荷のうち最も忙しいキー値が受ける割合の見積もりです)。
import json, math, sys
WRITE_LIMIT = 1000 # 1パーティションあたりの書き込みユニット/秒
READ_LIMIT = 3000 # 1パーティションあたりの読み込みユニット/秒
failed = False
for p in json.load(open("design/patterns.json")):
kb = p["item_kb"]
if p["op"] == "write":
units, limit = math.ceil(kb / 1), WRITE_LIMIT
else:
units = math.ceil(kb / 4)
if not p.get("consistent", False):
units /= 2 # 結果整合性読み取りは半分
limit = READ_LIMIT
hot = p["peak_rps"] * p["hot_share"] * units
ok = hot <= limit
failed |= not ok
print(f'#{p["id"]} {p["name"]}: {hot:g}/{limit} {"OK" if ok else "NG"}')
sys.exit(1 if failed else 0)design/patterns.jsonに、日付をキーにした書き込み案(毎秒800回、2KB、全件が同じキー値に集中する=hot_shareが1.0)と、顧客ごとの一覧(毎秒300回、8KB、結果整合性、最も忙しい顧客に2%が集中)を書いて実行した出力が次です。
python3 scripts/check_hot_partition.py#3 注文の書き込み(日付をキーにした案): 1600/1000 NG
#2 顧客の注文一覧: 6/3000 OK2KBの項目は2書き込みユニットで、毎秒800回なら1,600ユニットです。1パーティションの上限1,000を超えるため、パターン3はNGです。顧客単位の読み取りは、8KBなら2読み込みユニットで、結果整合性なら半分の1。毎秒300回のうち2%が集中して6ユニットなので、余裕があります。
このスクリプトは設計の代わりに判断するものではなく、Claudeが書いた案の前提を数字として表に出すための道具です。hot_shareは見積もりなので、実際の分布を知っている人間が決めます。
NGが出たら、書き込みシャーディングで広げる
AWSのガイドは、日付キーに乱数の接尾辞(1〜200など)を付けて書き込みを広げる方法を示しています。2014-07-09.1から2014-07-09.200まで、キーが200通りに分かれます。ただし読む側は、すべての接尾辞に対してQueryを発行し、結果を結合しなければなりません。
読み取りたい項目を特定したいなら、計算された接尾辞を使います。注文IDから200通りの値を計算し、日付に付けます。注文IDと日付が分かればGetItemでキーを再現できます。その日の全件を読むときは、やはり全接尾辞を引いて結合します。
patterns.jsonのhot_shareを0.005(200分割で均等と仮定)に直せば、パターン3は800 × 0.005 × 2 = 8ユニットで収まります。そのかわり、パターン4(日付範囲の全件取得)は200回のQueryとマージを抱えます。これを許容できるかは、バッチ処理のパターン4を毎秒5回しか使わないという前提と照らして判断するところです。Claudeには、シャーディングの案とあわせて「読み取り側の増える呼び出し数」を必ず書かせてください。
落とし穴
ホットキーの見積もりをClaudeに埋めさせる
hot_shareやpeak_rpsをClaudeが「妥当そうな値」で埋めると、検算は常にOKになります。数字の出どころは、クエリログ、既存システムの計測値、人間の想定のどれかに固定し、patterns.jsonに出どころをコメントで残してください。見積もりの源が分からない値は、空欄のまま人間に戻すよう最初のプロンプトで指示してあります。
手順4: 検算をStop hookで強制し、権限で本番を守る
「設計を終える前に必ず検算を通す」は、例外のない規則です。Claude Codeの運用指針は、毎回必ず実行したいことはCLAUDE.mdに書くのではなく、hookやdenyルールに移すよう勧めています。CLAUDE.mdにはガイドラインを書き、Claudeはそれを適用しないと判断できるためです。
CLAUDE.mdには、コマンドだけを短く置きます。
# Commands
- DynamoDB設計の検算: python3 scripts/check_hot_partition.py
# Gotchas
- design/patterns.json の peak_rps と hot_share は人間が決めた値。勝手に書き換えないそのうえで、検算を通らないかぎりClaudeがターンを終えられないよう、Stop hookを設定します。形は運用指針のtscの例と同じです。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "python3 scripts/check_hot_partition.py 1>&2 || exit 2" }
]
}
]
}
}終了コード2がターン終了のブロックで、標準エラーに出した内容がClaudeへ「まだ終われない理由」として返ります。スクリプトの出力を1>&2で標準エラーへ回しているのはそのためです。8回連続でブロックされると、Claude Codeが次のブロックを無効にしてターンを終えます。上限を変えるにはCLAUDE_CODE_STOP_HOOK_BLOCK_CAPを設定します。この上限の挙動はStop hookの無限ブロックを止める環境変数の記事にまとめてあります。テストや型検査をhookで回す場合との使い分けは、hooksでテストを自動実行する記事が扱っています。
最後に、AWSのリソースに触れるコマンドの権限です。設計の議論だけなら、Claudeにテーブルを作らせる必要はありません。作らせる段階でも、削除は止めておきます。
{
"permissions": {
"allow": ["Bash(python3 scripts/check_hot_partition.py)"],
"ask": ["Bash(aws dynamodb create-table *)"],
"deny": ["Bash(aws dynamodb delete-table *)"]
}
}denyルールは、autoやbypassPermissionsを含むすべての権限モードで効きます。一方でルールが照合するのはツール呼び出しであり、意図ではありません。シェルスクリプトの中でAWS CLIを呼ばれる経路までは、Bashのルールでは止まりません。本番のテーブルに対しては、IAMの権限側でも書き込みと削除を絞っておくと安全です。
設計の途中でつまずきやすい点
Claudeが正規化したテーブルを作る。アクセスパターンの一覧を渡さず、エンティティの説明だけを渡したときに起きます。DESIGN.mdの依頼文に「全パターン番号を参照して」と入れ、参照されていない番号があれば差し戻します。
パターンが後から増える。DynamoDBでは新しい問い合わせがキー設計の変更に直結します。リリース後に増えたパターンは、ACCESS_PATTERNS.mdへの追記から始め、検算とレビューの手順をもう一度通します。既存のキーで満たせない場合のGSI追加は、射影する属性を最小にして、書き込みコストの増加をDESIGN.mdに書き残します。
検算が通ったのに本番で詰まる。hot_shareが現実より小さかった可能性があります。上の表のとおり、台数が多くても1台だけ突出して人気があるデバイスIDは「悪い」側に入ります。平均で見て均一でも、突出する1つのキー値が全体の上限を決めます。見積もりの更新は、計測値が手に入った時点で人間が行います。
認可や費用の制約が設計に混ざる。読み取り専用のユーザーが引くパターンと、書き込みをするバッチは、ACCESS_PATTERNS.mdの別の行に分けておきます。IAMの境界を分けたいときに、どのパターンが誰のものかが読み取れます。
次の一手
既存のアプリがある場合は、クエリログからパターン候補を抜き出す作業をClaude Codeに任せられます。ログの出力をcat query.log | claude -p "..."のようにパイプで渡せば、パターンの候補と頻度の集計が手に入ります。ただし、その集計が業務上の意味を持つパターンかどうかは、画面や機能を知っている人間が判断します。
AWS上で別のデータストアを検討している段階なら、databases-on-awsプラグインでAurora DSQLを設計する記事が参考になります。DynamoDBと違い、そちらはSQLのスキーマ設計が中心で、同じ「Claudeに設計を任せる」でも守らせるルールが変わります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/best-practices
- docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-modeling-nosql.html
- docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-unif…
- docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-shar…
- docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-indexes-general.ht…