DynamoDB2年本番運用で痛感した、Single Table Designの本当の使い所
RDS感覚で設計したら月60万円の請求。DynamoDBで2年失敗と工夫を重ねて見えた、本当に必要な設計パターンと避けるべき落とし穴を実例で解説します。
DynamoDBとの付き合い始めた当初の失敗
うちのチームが本格的にDynamoDBを導入したのは2024年の春。マイクロサービスのプロジェクトで、従来のRDSだと遅い・スケールしない、みたいな判断だった。だけど正直、最初の3ヶ月は地獄だった。
テーブル設計を甘く見てました。RDSの感覚で「ユーザーテーブル」「注文テーブル」「アイテムテーブル」みたいに分けて、複数のGSI(グローバルセカンダリインデックス)を張りまくったんです。そしたら本番で請求書を見て絶望。月60万円ですよ。
原因は簡単で、無駄なキャパシティプロビジョニングと、読み書き両方にプロビジョニング容量を確保してたこと。オンデマンドモード使えばよかった、と何度思ったか。
Single Table Design、本当に必要な時とそうじゃない時
先日、チームで読み直す機会があって思ったのは、Single Table Designは「銀の弾」じゃないってこと。巷では「DynamoDBならSingle Table Design!」みたいに言われますが、実際に運用してみるとめっちゃ複雑になります。
去年の中盤、僕らが試したのはこんな構成。
テーブル構成:
PK: EntityType#EntityId (例: USER#user123, ORDER#order456)
SK: Timestamp#DataType (例: 2025-06-13T10:30:00Z#PROFILE, 2025-06-13T10:30:00Z#ITEMS)
GSI1PK: EntityType
GSI1SK: CreatedAt
GSI2PK: Status
GSI2SK: UpdatedAt
理屈としては効率的です。でも運用してて気づいたのは、複数のエンティティタイプを同じテーブルに詰め込むと、以下の問題が出てくる:
1. アクセスパターンが複雑になる
クエリが増えれば増えるほど、どのGSIを使うべきか判断が難しくなる。チーム内で「あ、このクエリはGSI2の方が効率的だった」みたいな知見が共有されないまま、無駄な読み込みが発生する。
2. 容量計画が困難
複数のエンティティを詰め込むと、どのタイプのアクセスが多いのか把握しにくくなります。結果、オーバープロビジョニングになりやすい。
3. フィルタリングが増える
DynamoDBはクエリ結果のフィルタリングが無料じゃない。取得後に EntityType でフィルタリングすると、読み込み容量を無駄に消費しちゃうんです。
4ヶ月運用した結果、僕らは戦略を変更しました。「アクセスパターンが明確に分離できるエンティティはテーブルを分ける」 という判断ですね。
現状こんな感じで運用してます。
| テーブル名 | 用途 | PK | SK | キャパシティモード |
|---|---|---|---|---|
| users | ユーザーマスタ | UserId | CreatedAt | オンデマンド |
| orders | 注文データ | OrderId | CreatedAt | オンデマンド |
| user-orders | ユーザー別注文一覧 | UserId | OrderId | オンデマンド |
| inventory | 在庫管理 | ProductId | LastUpdated | プロビジョニング(予測可能) |
実装としては複数テーブルにはなりますが、各テーブルのアクセスパターンが明確になって、容量計画が圧倒的に楽になりました。月の請求額も40万円まで下がったので、これはデカい。
キャパシティモード、本当の使い分け
オンデマンドとプロビジョニングで2年悩み続けてます。
オンデマンドはシンプルですよ。スパイク対応が必要な場合、読み書きのトラフィックが予測不可能な場合、本当に最高です。ただしコストは割高。オンデマンドでの読み込みは $1.25/100万RCU だから、トラフィックが多いと痛い。
プロビジョニングは安いけど、キャパシティを正確に予測しないと地獄。去年、在庫テーブルで朝9時に定期バッチが走るんですけど、それを考慮せずプロビジョニング構成にしたら、その時間帯だけ ProvisionedThroughputExceededException が頻発して、本番障害になりました。運用チームから怒られました。
現在のうちのチーム基準はこう。
- オンデマンド推奨:API系(ユーザー検索、注文取得)、トラフィックスパイクが大きい場合
- プロビジョニング推奨:バッチ処理、定時実行ジョブ、予測可能なトラフィックパターン
ただし、Autoscaling設定を絶対忘れるな という教訓。プロビジョニング選んだら、CloudWatchメトリクスを見て自動スケーリング設定するのが必須です。
# boto3でプロビジョニング設定
dynamodb = boto3.client('dynamodb')
dynamodb.update_table(
TableName='inventory',
ProvisionedThroughputDescription={
'ReadCapacityUnits': 10,
'WriteCapacityUnits': 10
}
)
# オートスケーリング設定(別途)
autoscaling = boto3.client('application-autoscaling')
autoscaling.register_scalable_target(
ServiceNamespace='dynamodb',
ResourceId='table/inventory',
ScalableDimension='dynamodb:table:WriteCapacityUnits',
MinCapacity=10,
MaxCapacity=1000
)
ローカル開発、DynamoDB Localは本当に使えるのか
DynamoDB Localで開発してから本番に上げたら動かない、みたいな経験ありませんか? 僕らもあります。まじで。
Localで問題になるのはこんなところ:
1. スループットモデルの差異
Localはスループット制限がないので、本番で容量超過になることを検出できない。開発では余裕だったのに本番でエラーになるんです。
2. TTL機能
Localはスキャンすると有効期限切れアイテムが削除されない。本番と挙動が違う。
3. GSIの一貫性
Localのeventually consistent読み込みは、実装レベルでは違ってくることがある。
正直、LocalでOKでも本番で失敗することはある。だから今は、本番環境のDynamoDBをテーブル単位で作って、テスト時にそこに直結させてます。開発用AWS環境のコストは増えますが、本番障害を減らす方が安いってのが判断ですね。
# テスト用に開発環境DynamoDBを使う例
import boto3
from moto import mock_dynamodb # Localテストは軽い検証だけ
@mock_dynamodb
def test_query_locally():
"""LocalテストはAPIロジックの検証程度"""
pass
def test_integration():
"""本物のDynamoDBで統合テスト"""
table = boto3.resource('dynamodb', region_name='ap-northeast-1').Table('dev-users')
# 実際のテーブルに対してテスト
TTL・Global Tables、本番で痛感した落とし穴
TTL設定は簡単に見えます。属性に ExpirationTime を足して、自動削除させるだけ。でも本番で問題が出たのは、削除にラグがある ってこと。
公式ドキュメントでは「通常、有効期限切れアイテムはExpiration時刻から48時間以内に削除される」って書いてあります。つまり、削除されるまでに数時間〜48時間かかる可能性があるわけですよ。
僕らの場合、セッションテーブルでTTLを使ってたんですけど、ある日ユーザーが「ログアウト後、60時間たった後でもセッション有効だった」って報告が来た。確認したら、TTLで消える前にスキャンで引っかかってたんです。げんなりしました。
対策としては、クエリする時に有効期限を確認する ロジックを入れることにしました。
from datetime import datetime, timezone
def get_session(user_id, session_id):
table = boto3.resource('dynamodb').Table('sessions')
response = table.get_item(Key={'UserId': user_id, 'SessionId': session_id})
if 'Item' in response:
item = response['Item']
# TTL確認:削除されるまでの間、無効判定する
if item.get('ExpirationTime', 0) < int(datetime.now(timezone.utc).timestamp()):
return None # セッション無効
return item
return None
Global Tablesについても、マルチリージョン対応で導入したんですが、これは 最終的一貫性 が想像以上に難しい。レプリケーション遅延は通常100ms以下ですが、ネットワーク混雑時は1秒超えることもあるんです。クライアント側で書き込み直後に別リージョンから読むと、古いデータが返ってくる可能性がある。
そういう場合は、クライアント側で読み込み時に「書き込み時刻」を保存しておいて、レプリケーション完了を待つロジックを入れるしかない。めんどくさいですが、マルチリージョン対応するなら必須だと思います。
スケーリング、予測できない急増への対応
2025年年末に、新機能ローンチで急にトラフィック3倍になったことがありました。プロビジョニング容量では足りず、オンデマンドに一時的に切り替えた。その時のコストが月150万円。地獄ですよ。
急速なスケーリングに対応するには、こんな対策が有効。
1. 事前にオンデマンドへ切り替え
スパイク予測がある場合、あらかじめオンデマンドに変更しておく。スイッチは数分で可能だから、リスク回避としては強い。
2. DAX(DynamoDB Accelerator)の導入
キャッシュレイヤーを入れると、読み込み頻度の高いアイテムへのアクセスが圧倒的に早い。月の追加コストは5〜10万円程度ですが、本番障害を避けられるなら安い投資。
# DAXの設定例
import amazondax
client = amazondax.AmazonDaxClient.resource(
region_name='ap-northeast-1',
endpoints=['my-dax-cluster.2fdiqe.clustercfg.dax.amazonaws.com:8111']
)
response = client.Table('users').get_item(Key={'UserId': 'user123'})
# DAXがキャッシュしてるので、2回目以降のリクエストは数msで返る
実装してみた結果、読み込みレイテンシが平均40msから5msに改善。本当に効きます。
2026年時点でのベストプラクティス、正直な感想
DynamoDBの設計ベストプラクティスは毎年変わっています。2026年現在のうちのチームの基準はこんな感じ。
graph TB
Start[DynamoDB導入検討]
Start --> Q1{アクセスパターン<br/>予測可能?}
Q1 -->|Yes| Q2{QPS<br/>1000以上?}
Q1 -->|No| OD[オンデマンド推奨]
Q2 -->|Yes| Prov[プロビジョニング推奨]
Q2 -->|No| Q3{複数テーブルで<br/>分離可能?}
Q3 -->|Yes, Clear Separation| Multi[マルチテーブル設計]
Q3 -->|No or Unclear| Single[Single Table Design]
Prov --> AS[Autoscaling設定必須]
Multi --> GSI[GSI設計を慎重に]
Single --> Idx[インデックス戦略を整理]
AS --> DAX{読み込み<br/>集中型?}
GSI --> DAX
DAX -->|Yes| DaxImpl[DAX導入]
DAX -->|No| Monitoring[CloudWatch監視]
DaxImpl --> Monitoring
Monitoring --> Done[本番投入]
実装のコツとしては、こんなところが大事だと思う。
- テーブル設計は最初が全て。後から変更するのはめちゃくちゃ大変(実際に2回壊した)
- クエリアクセスパターンを最初にドキュメント化。それが設計の指針になる
- 本番環境でカオスエンジニアリングを実施。スパイク想定で容量超過テストをやる
- オンデマンドとプロビジョニングの切り替えは簡単だから、最初はオンデマンドで様子見する が無難
実務としては、「DynamoDBはスケーラブル」というのは正しいですが、「設計次第」という条件付きなんです。設計が悪いと、PostgreSQLより遅くなることもありますからね。
まとめ
DynamoDBを本番で2年運用して学んだことは、結局 設計が全て ということ。NoSQL自体の特性もあるけど、アクセスパターン理解が甘いと本番で大火傷します。
-
Single Table Designは銀の弾じゃない。アクセスパターンが明確に分離できるなら、テーブルを分ける方が運用は楽だし、コストも削減できる
-
キャパシティモードの選択は重要。オンデマンドで安心買うか、プロビジョニングでコスト削減するか、トレードオフを理解してから選ぶ必要がある
-
TTL・Global Tables・Autoscalingは細部が重要。ドキュメント読むだけじゃダメ。本環境で検証必須だと痛感した
-
DAXはコスパ最強。読み込み集約的なワークロードなら、本当に検討する価値あり
-
ローカル開発と本番の環境差異は大きい。テスト時は実環境のDynamoDBを使うくらいの覚悟で
DynamoDBは本当に強力ですが、RDBの感覚では設計できません。これから導入する人は、小さいテーブルから始めて、失敗を重ねる方が結果的に近道だと思いますよ。