dynamodbでテーブル設計する際に気にしておくことを整理

Reference

アクセスパターンの整理
dynamodb の設計を行う際に考慮する点
RDBMS のように「テーブルを正規化して JOIN」するのではなく、アクセスパターンに応じて非正規化したり、項目を集約して保存する設計を行うのが基本となる。
観点 | 内容 |
---|---|
アクセスパターン | 誰が、どんなキーで、どんなデータを、どの頻度で取得/更新するか? |
スケーラビリティ | 同一パーティションキーにアクセスが集中しすぎないようにする(パーティション分散)。 |
アクセスパターンを考慮してテーブル設計する
RDBMSのテーブルを例に考える (例: ゲームプロダクトにありそうなテーブル)
users テーブル (AIが作成)
CREATE TABLE IF NOT EXISTS `users` (
`id` VARCHAR(255) PRIMARY KEY COMMENT 'ユーザーID',
`provider_id` VARCHAR(255) PRIMARY KEY COMMENT '外部の認証サービスで払い出されるユーザを特定するための一意のID',
`username` VARCHAR(255) COMMENT 'ユーザー名',
`email` VARCHAR(255) COMMENT 'ユーザーのメールアドレス',
`role` ENUM('general', 'admin', 'beta_tester') NOT NULL COMMENT 'ユーザーの権限',
`status` ENUM('active', 'inactive', 'banned') NOT NULL COMMENT 'ユーザーのステータス',
`rank` SMALLINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '現在のユーザーランク',
`exp` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '累積経験値',
`stamina_max` SMALLINT UNSIGNED NOT NULL DEFAULT 100 COMMENT '最大スタミナ',
`stamina_current` SMALLINT UNSIGNED NOT NULL DEFAULT 100 COMMENT '現在のスタミナ',
`team_slot_limit` TINYINT UNSIGNED NOT NULL DEFAULT 5 COMMENT 'ユーザーが保有できるチーム数上限',
`last_login_at` DATETIME NOT NULL COMMENT '最終ログイン日時',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_email` (`email`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
アクセスパターンをまとめると以下のようなイメージになる
各カラムのアクセスパターン
カラム名 | 予想されるアクセスパターン・用途 | DynamoDBでの扱い・設計上の考慮点 |
---|---|---|
id |
ユーザー固有IDでユーザー情報を一意に取得・更新 | パーティションキー(主キー)に設定。最も基本的なアクセス。 |
provider_id |
Firebaseなど外部認証のIDからユーザー情報を取得 | GSIのハッシュキーに設定。外部認証IDからの検索に対応。 |
username |
ユーザー名による検索や一覧表示(頻度は低め、名前重複も想定) | 基本は属性。名前検索が多い場合はGSIを検討。 |
email |
ログインや通知送信時にメールアドレスからユーザーを検索 | GSI設定候補。認証の外部の機構になる場合は不要になる想定。 |
role |
管理者やベータテスター等の権限別にユーザーをフィルタリング(管理画面や分析など) | 頻繁ならGSIやセカンダリインデックスを検討。ただしパーティション分散に注意が必要。 |
status |
ユーザーの状態(active/bannedなど)によるフィルタリング |
role と同様。利用頻度・用途に応じてGSI追加を検討。 |
rank |
ゲーム内のランキングや条件付き検索(例: rank >= 1000のユーザー) | そのような範囲クエリが多い場合、GSIにソートキーとして組み込むことを検討。 |
exp |
経験値によるランキングや条件絞り(上位何%など) |
rank と同様。範囲検索が必要ならGSIのソートキーとして設定。 |
stamina_max / stamina_current
|
スタミナの現在値・最大値。ユーザー状態管理・スタミナ消費処理に使用 | 属性として保持。更新頻度が高いため、書き込みパターンに注意。 |
team_slot_limit |
チーム編成の最大数の取得 | 属性で十分。 |
last_login_at |
最終ログイン日時によるユーザーアクティブ判定やログイン履歴分析 | たまにログ検索する用途ならGSIも検討可能。 |
created_at / updated_at
|
作成・更新日時。監査ログや履歴管理、レポート作成時に利用 | 属性として保持。updated_at は更新タイミングに応じて書き換えられる。 |

PK/SKの選択
プライマリーキーとパーティションキーの違い
まずざっくり両者の違い
プライマリーキー:
- データを一意に識別するキーのこと。
- パーティションキー単独でもPKになるし、パーティションキーとソートキーの組み合わせでもPKとすることができる。
- 単一キーの場合: user_id
- 複合キーの場合: user_id & timestamp (2025-07-18T10:00:00)
パーティションキー:
- データを物理的にどのパーティション(ストレージ)に保存するかを決めるキー
- 内部ではこのキーをhashして分散配置される。
- 同じパーティションキーのレコードは同じ物理パーティションに保存されるイメージ
整理するとこのようなイメージ
観点 | パーティションキー | プライマリーキー |
---|---|---|
定義上の役割 | データの物理分散先を決定 | テーブル内でのデータ一意性を保証する |
種類 | 単一キー | 単一キーまたは複合キー(+ ソートキー) |
一意性の保証 | しない | 保証する(1テーブルに同一プライマリーキーは1件) |
主な用途 | スケーリング・分散 | データ識別・検索 |
プライマリーキーの詳細
When you create a table, in addition to the table name, you must specify the primary key of the table. The primary key uniquely identifies each item in the table, so that no two items can have the same key.
- テーブル作成時には、テーブル名に加えてプライマリキーの指定が必須。
- プライマリキーはアイテムを一意に識別する役割を持ち、同じキーのアイテムは複数存在できない。
プライマリキーの種類
単一プライマリーキーと複合プライマリーキーの2種類が存在。
種類 | 構成 | 特徴 |
---|---|---|
単一プライマリキー | パーティションキーのみ | ハッシュ関数を用いて物理ストレージに分散保存。同一パーティションキーを持つアイテムは1つだけ。 |
複合プライマリキー | パーティションキー + ソートキー | 同じパーティションキーのアイテムが複数存在可能。ソートキーにより物理的に近接・順序付き保存。 |
アクセスやクエリの柔軟性
- 単一キーテーブル:
- パーティションキー(例: user_id)を指定すれば直接アクセス可能。
- 複合キーテーブル:
- パーティションキー(例: artist_id)とソートキー(例: song_title)の組み合わせでアクセス。
- artist_id だけ指定 → そのアーティストのすべての曲が取得可能
- artist_id + song_title の範囲指定 → 曲の一部を絞り込んで取得可能
補足事項
Each primary key attribute must be a scalar (meaning that it can hold only a single value). The only data types allowed for primary key attributes are string, number, or binary. There are no such restrictions for other, non-key attributes.
-
プライマリーキーの属性はスカラ値(単一値)のみ設定が可能。
-
利用可能な型としては以下
- string: 文字列
- number: 数値
- binary: バイト列(いわゆる文字列ではない生のデータ、Goだと[]byte、Javaなら
- ByteBuffer、JSONではBase64エンコードされた文字列)
-
非キー属性はプライマリーキーでは使用できない。
- List, Map, Boolean, Null, Set
jsonのイメージ
{
"user_id": "123", // プライマリキー(String型)
"profile": {
"name": "Alice",
"age": 30,
"hobbies": ["tennis", "reading"],
"address": {
"city": "Tokyo",
"zip": "123-4567"
}
}
}
ソートキーの詳細
ソートキーについて
- パーティションキーと組み合わせてアイテムを一意に識別できるようにすることを目的として利用されるキーのこと。
- 例えば、チャットテーブル等を例にした場合、以下のように user_id と timestamp の 2つでアイテムが一意になる。※timestamp がソートキーになる。
user_id(partition key) | timestamp(sort key) | message |
---|---|---|
user123 | 2025-07-01T10:00:00Z | Hello! |
user123 | 2025-07-02T12:30:00Z | How are you? |
user456 | 2025-07-01T09:00:00Z | Hi! |
※本番だと誰が誰に送ったか、等の情報も必要だが便宜上簡単なテーブルにしている
ソートキーの役割や利点を整理すると以下のようなイメージになる。
目的 | 説明 |
---|---|
データの並び順の制御 | 同じパーティションキーのデータがソートキー順に物理的に近接&順序付きで保存される |
範囲クエリが可能 | ソートキーに対して begins_with, between, >, < などが使える |
セカンダリ的な識別子 | パーティションキーでグループ化し、ソートキーでその中のレコードを一意に識別 |
ソートキーを生かした検索を行う際の Query 例は以下のようなイメージになる。
case1: 1人のユーザの全メッセージを取得する
PartitionKey = 'user123'
→ user123 の全メッセージが ソートキー順(つまり日時順)で取得できる
case2: 特定期間のデータだけ取得(日時の範囲指定)
PartitionKey = 'user123'
AND SortKey BETWEEN '2025-07-01' AND '2025-07-31'
→ user123 のメッセージのうち、2025年7月中のメッセージのみを取得できる。
Query 使用可能なソートキーを用いた条件指定は以下
条件 | 使用例 | 実際の挙動(例) |
---|---|---|
=(等しい) | SortKey = "2025-07-01T10:00:00Z" |
user123 の "Hello!" メッセージのみ取得される |
begins_with | begins_with(SortKey, "2025-07") |
user123 の 7月の全メッセージ("Hello!" と "How are you?")が取得される |
between | SortKey BETWEEN "2025-07-01" AND "2025-07-01T12:00:00Z" |
"Hello!" のみ取得(範囲は文字列としての比較で評価される) |
> / < / >= | SortKey > "2025-07-01T11:00:00Z" |
"How are you?" のみ取得(2025-07-02T12:30:00Z は指定より後のため) |
※between や > などの条件は、ISO8601形式(YYYY-MM-DDTHH:MM:SSZ)だと文字列比較でも時系列順で動作する模様。※日付 → 時 → 分 → 秒 と先頭から順番に並んでいるので、文字列比較がそのまま時系列比較になるのかも?
※ begins_with は 前方一致 で、"2025-07" のように年月単位でまとめて取得するのに便利。
ソートキーについて整理
項目 | 内容 |
---|---|
必須条件 | 複合プライマリキーのときのみ使用 |
主な役割 | 同一PartitionKey内での並び順制御・範囲クエリ・一意識別の補助 |
クエリの柔軟性 | begins_with, between, >, < などの範囲・前方一致クエリが可能 |
データ格納 | 同一PartitionKey内ではSortKey順で物理的に格納される |

ソートキー活用のベストプラクティスについて整理
ソートキーを上手く設計するために考慮すべき点など
Well-designed sort keys have two key benefits:
They gather related information together in one place where it can be queried efficiently. Careful design of the sort key lets you retrieve commonly needed groups of related items using range queries with operators such as begins_with, between, >, <, and so on.
Composite sort keys let you define hierarchical (one-to-many) relationships in your data that you can query at any level of the hierarchy.
- 関連データを同一パーティション内に集約させることができれば、begins_with、between、>、< などの演算子で効率的にアイテムを取得できるようになる。
-
begins_with() クエリは文字列ベースで範囲検索ができるため、
文字列階層を「#」などのセパレータで構築することで、任意の粒度で柔軟な検索が可能になる(1対多関係のような階層構造の表現が可能)
キー設計例1: 機能追加などで初期検証時点など
pk | sk | data |
---|---|---|
player#1234 | char#01#inventory#weapon#sword001 | +10 Sword |
player#1234 | char#01#inventory#armor#helmet001 | Iron Helmet |
player#1234 | char#01#enhance#sword001#2025-01-01 | +1 |
player#5678 | char#02#inventory#armor#robe001 | Magic Robe |
- 各プレイヤーが個別のパーティション(pk)を持つ
- sk に階層構造を保持しつつ、分散性能も確保できる
query例
- プレイヤー1234のキャラ01のインベントリアイテムを全部取得
pk = "player#1234"
begins_with(sk, "char#01#inventory")
- プレイヤー1234のキャラ01の剣(sword001)の強化履歴を取得
pk = "player#1234"
begins_with(sk, "char#01#enhance#sword001")
- プレイヤー1234のメタ情報やキャラクター情報をまとめて取得
pk = "player#1234"
(sk = "metadata" OR begins_with(sk, "char#"))
- プレイヤー1234のキャラ01に関するすべての情報を取得
pk = "player#1234"
begins_with(sk, "char#01")
キー設計例2: 運用後ある程度ドメイン境界が固まってきたら
pk | sk | type | data |
---|---|---|---|
player#1234 | metadata | Player | 名前やレベルなど |
player#1234 | char#01 | Character | キャラ情報 |
player#1234 | char#01#inventory#sword001 | Inventory | +10 Sword |
player#1234 | char#01#enhance#sword001#2025-01-01 | Enhance | 強化履歴 |
player#1234 | char#02 | Character | 別キャラ |
- プレイヤーごとにパーティションキーが割り当てられ、個別のパーティションで管理
- sk に階層構造や種類を含めてデータを分類
- メタ情報やキャラクター情報、装備や強化履歴などを柔軟に取得可能
query例
- プレイヤー1234のメタ情報を取得
pk = "player#1234"
sk = "metadata"
- プレイヤー1234のキャラクター一覧を取得
pk = "player#1234"
begins_with(sk, "char#")
- プレイヤー1234のキャラ01の装備(インベントリ)を全部取得
pk = "player#1234"
begins_with(sk, "char#01#inventory")
- プレイヤー1234のキャラ01の剣(sword001)の強化履歴を取得
pk = "player#1234"
sk = "char#02"
まとめ
特徴 | 内容 |
---|---|
プレイヤー内の階層構造に強い | キャラ → インベントリ → 装備・強化履歴などのネスト構造を sk で表現できる |
範囲・部分検索に強い |
begins_with(sk, "char#01#inventory") などでキャラごとの装備一覧を効率的に取得可能 |
データ構造の可読性が高い |
sk に階層構造が文字列で表現されるため、構造の把握・メンテナンスがしやすい |
GSI不要で柔軟なクエリ実現 | 多くのユースケースは pk + sk だけでカバーでき、追加のインデックス設計が不要 |
アクセスパターン主導の設計 | 強化履歴取得、キャラ別装備一覧など、想定されるアクセスパターンをそのまま sk に反映しやすい |
高いスケーラビリティ |
pk (player#1234 など)でプレイヤーごとに分散されるため、プレイヤー数が増えてもスループットを維持しやすい |

ホットパーティションを考慮する
前提として
The partition key portion of a table's primary key determines the logical partitions in which a table's data is stored. This in turn affects the underlying physical partitions. A partition key design that doesn't distribute I/O requests effectively can create "hot" partitions that result in throttling and use your provisioned I/O capacity inefficiently.
- 各パーティションへのアクセスが偏り過ぎないように設計する必要あり。
- dynamodb はデータを複数のパーティションに分けて保存するため、一部のデータばかりにアクセスが集中するとレイテンシの原因となる。
- よって、どのデータにも均一にアクセスされるような主キーをまずは検討するところから始まる。
スループット(処理能力)の考慮
- 1つのパーティションで1秒間に処理できる作業量が決まっている。
- 以下は比較的少量のデータ(1~4KB程度)の場合の制限
- 読み取り: 1秒間に最大3,000回
- 書き込み: 1秒間に最大1,000回
パーティションキーが担う役割を理解する
偏ったパーティションキーになってしまった場合(つまり一部のパーティションばかりに頻繁にアクセスされる状態にしてしまった場合)は以下のようなケースが考えられる。
- その物理パーティションだけにリクエストが集中してしまう。
- スロットリング(処理制限)が発生しやすくなってしまう。
- 結果的に、用意したプロビジョン済みスループット(またはオンデマンドの能力)を無駄にしてしまう。※DynamoDB全体としてはまだ余裕があるにも関わらず、一部のパーティションだけが詰まってしまい処理できなくなる(スロットルされる)という状況
凡例(パーティションが4つに分かれているとした場合)
[全体スループット] [使われ方]
------------------ --------------------------
最大書き込み能力 Aパーティション:1000 WCU ← ✕ 上限到達でスロットル
= 4000 WCU Bパーティション:200 WCU
Cパーティション:50 WCU
Dパーティション:0 WCU
非効率な設計の例
極端な例で、例えばテーブルが4つの物理パーティションに分割されていることを前提として、全ユーザーのレコードが以下のような形で保存されているとした場合
{
"status_code": "active", // パーティションキー
"user_id": "user_12345",
...
}
結果として起き得ること
活性化したプロダクトの場合、ユーザの多くは active
が大半を占めることになるため、リクエストが active
という1つのパーティションキーに集中する。
→その1つの物理パーティションだけが高負荷になり、最大スループットの1,000WCUに即座に到達してしまう。
※ほか3つの物理パーティションはほとんど使われずにいる状態。
ホットパーティションを考慮した設計パターンの例
パターン1: user_idにした場合はユーザ数が多ければ自然に分散される
想定シナリオ
- アプリに 100 万人のユーザがいて、それぞれがプロフィールデータを持つ。
- パーティションキーは user_id
user_id | name | |
---|---|---|
100001 | Alice | alice@example.com |
100002 | Bob | bob@example.com |
100003 | Charlie | charlie@example.com |
... | ... | ... |
999999 | Zoe | zoe@example.com |
分散される理由
- 各 user_id がユニークであり、アクセスも均等に分散されやすいため、リクエストが物理パーティション全体にバランスよく分散される。
- 特定ユーザだけが爆発的にアクセスされない限り「ホットパーティション」になりにくい構成になる。
terraform でテーブル定義するとこのような形式になる
resource "aws_dynamodb_table" "users" {
name = "Users"
billing_mode = "PAY_PER_REQUEST"
hash_key = "user_id"
attribute {
name = "user_id"
type = "N" # 数値型
}
tags = {
Environment = "dev"
Project = "GameBackend"
}
}
データ形式
{
"Item": {
"user_id": { "N": "100001" },
"name": { "S": "Alice" },
"email": { "S": "alice@example.com" }
}
}
パターン2: device_id + ランダムなsufix の場合はキー空間を広げられる
想定シナリオ
- IoTデバイス(例えば温度センサなど)が1分毎にデータ送信される。
- 特定の device_id にアクセスが集中してスループットが偏るのを防ぐ必要がある。
- device_id#ランダム文字列 にすることでパーティションキーを設計することで回避できる。
partition_key | timestamp | temperature |
---|---|---|
device-abc#1 | 2025-07-17T00:01:00 | 24.3°C |
device-abc#2 | 2025-07-17T00:01:00 | 24.5°C |
device-abc#3 | 2025-07-17T00:01:00 | 24.4°C |
device-xyz#1 | 2025-07-17T00:01:00 | 27.1°C |
device-xyz#2 | 2025-07-17T00:01:00 | 27.2°C |
分散される理由
- 本来、device-abc 1個に集中するアクセスが、device-abc#1, device-abc#2, device-abc#3 のように異なるキーに分かれることで物理パーティションにも分散されやすくなる。
- 注意点として、クエリで取得する際には、begins_with(device_id, "device-abc") 等でデータアクセスする工夫が必要。
terraform でテーブル定義するとこのような形式になる
resource "aws_dynamodb_table" "device_temperature_table" {
name = "DeviceTemperature"
billing_mode = "PAY_PER_REQUEST"
hash_key = "partition_key"
attribute {
name = "partition_key"
type = "S"
}
}
データ形式
{
"partition_key": { "S": "device-abc#1" },
"timestamp": { "S": "2025-07-17T00:01:00" },
"temperature": { "N": "24.3" }
}
パターン3: created_at#user_id などの時間 + ユーザで自然な分散を狙う
想定シナリオ
- ユーザが予約操作を行うたびにイベントが記録される。
- 書き込みが「ある時間帯」に集中しやすいので、created_at 単独だとホットパーティションになる。
- そこで created_at#user_id をパーティションキーにすることで偏りが出ないようにする。
partition_key | reservation_id | status |
---|---|---|
2025-07-17T09#100001 | R001 | booked |
2025-07-17T09#100002 | R002 | booked |
2025-07-17T09#100003 | R003 | failed |
2025-07-17T10#100001 | R004 | booked |
2025-07-17T10#100002 | R005 | failed |
分散される理由:
- 単なる 2025-07-17T09 だけでは1つのパーティションに全てのリクエストが集中してしまう。
- #user_id をつけることで、同一時間帯でもアクセスが複数のキーに分散され、ホットパーティションを防ぐことができる。
terraform でテーブル定義するとこのような形式になる
resource "aws_dynamodb_table" "reservation_table" {
name = "Reservation"
billing_mode = "PAY_PER_REQUEST"
hash_key = "created_at"
range_key = "user_id"
attribute {
name = "created_at"
type = "S"
}
attribute {
name = "user_id"
type = "N"
}
}
データ形式
{
"created_at": { "S": "2025-07-17T09" },
"user_id": { "N": "100001" },
"reservation_id": { "S": "R001" },
"status": { "S": "booked" }
}

インデックスについて
LSI/GSI の共通点・特徴・相違点
共通点
項目 | 説明 |
---|---|
目的 | 主キー以外の属性で効率的にクエリを行うための手段 |
利用可能な操作 |
Query , Scan
|
投影(Projection) | 必要な非キー属性を選択的に複製可能(KEYS_ONLY / INCLUDE / ALL ) |
直接の書き込み不可 | インデックスはテーブルへの書き込みに連動して自動更新される |
ストレージ・スループット課金あり | インデックス用のストレージとRCU/WCUが別途かかる(オンデマンド時も考慮) |
LSIの特徴
項目 | 内容 |
---|---|
パーティションキー | ベーステーブルと 同じ |
ソートキー | 異なるソートキー を指定可能 |
作成タイミング | テーブル作成時のみ 作成可能(後から追加不可) |
最大数 | テーブル1つあたり 最大5個 |
整合性 | 強い整合性(strongly consistent reads) が使用可能 |
ユースケース | 同一パーティションキー内でソート条件を変えて柔軟に取得したいとき 例: UserId ごとに「投稿順」「評価順」「更新日時順」など |
GSIの特徴
項目 | 内容 |
---|---|
パーティションキー | 異なるパーティションキー を指定可能 |
ソートキー | 任意(なくてもよい) |
作成タイミング | 後から追加可能(運用中のテーブルでも追加できる) |
最大数 | テーブル1つあたり 最大20個 |
整合性 | 最終的整合性(eventually consistent) のみ |
ユースケース | ベーステーブルとは異なるキー構成でクエリしたいとき 例: GameTitle ごとにランキングを出す・Email でユーザーを検索する |
相違点
比較項目 | LSI | GSI |
---|---|---|
パーティションキー | テーブルと同一 | テーブルと異なるキーを使える |
ソートキー | テーブルとは異なるキーを指定可能 | 任意(無くても可) |
作成タイミング | テーブル作成時のみ | 作成後でも追加可能 |
最大数 | 最大5個 | 最大20個 |
整合性の読み取り | 強い整合性が利用可能 | 最終的整合性のみ |
読み取り性能 | 同一パーティション内の並び替えなどに向いている | 異なる視点での検索に向いている |
典型的な用途 | 特定ユーザーのデータを別の順序で並び替える | サービス全体のデータを別の属性で検索する |
LSI と GSI を構造的に見る
インデックスの種別
dynamodb には3種類のインデックスが存在。
インデックス種類 | 概要 | 主な用途例 |
---|---|---|
Primary Index | テーブル定義時の partition key + sort key
|
基本のアクセス(例: プレイヤー単位) |
LSI(Local Secondary Index) | 同じパーティションキーで別のソートキーを持つ | 時系列・履歴管理(例: 強化履歴など) |
GSI(Global Secondary Index) | 任意のキーで検索可能なインデックス | 別の観点での検索(例: キャラ一覧取得) |
LSI と GSI 構造上の違いについて
特徴 | LSI(Local Secondary Index) | GSI(Global Secondary Index) |
---|---|---|
定義タイミング | テーブル作成時のみ定義可能 | テーブル作成後でも追加可能 |
データ格納場所 | 同じパーティションに格納(=テーブルと同一の物理領域) | 別の専用領域に格納(=GSI専用テーブル) |
パーティションキー | テーブルと 同じパーティションキー(PK) | テーブルと異なる PK を定義可能 |
ソートキー | テーブルとは 別のソートキー(SK) を定義できる | PK だけでなく SK も自由に定義可能 |
書き込みスループット | テーブルと共有(スロットリングの影響を受けやすい) | 独立したスループット(専用の WCU/RCU が必要) |
LSI の場合
-
用途としては、同一PK内のソート順を変えたいときに用いられる。
→ 例: あるプレイヤーのキャラクターを、強化した日時で並び替えたい時などに最適 -
テーブルと物理的に同じパーティションで管理されるため、GSIと比較して処理時間やトランザクション面で利点。
-
一方で、インデックスの設定がテーブル作成時しか定義できなかったり、1テーブルにつき最大5つまでしか設定できないなどの制約があるため、使いどころは中々難しい。
GSI の場合
-
用途としては別テーブルのように管理される自由度の高いインデックス として用いられる。
→ 例:キャラクター一覧を全プレイヤー横断で取得したいときなどに最適 -
テーブル作成後でも、パーティションキー、ソートキーともに自由に追加できるためアクセスパターンに応じて最適な選択が可能になるのが利点。
-
一方で、データ自体が保存されているパーティションとは別のパーティションにデータが保存されることになるため、結果整合性(Eventually Consisten)を許容できないプロダクトの場合は取り扱いが難しい。ほか、書き込み時にGSIに対しての更新処理も行われるため、コスト面においてもLSIと比較して高くつくのが難点。
両インデックスのメリット・デメリット
LSI のメリット・デメリット
観点 | pros | cons |
---|---|---|
構造 | テーブルと同じパーティションキーを使うため、一貫性が保ちやすい | PKを変えられない(≒同一エンティティ内の並び替えや絞り込みに限定される) |
クエリ性能 | 同一PK内で異なるソートキーによる検索が可能 → 範囲検索や並び替えに強い | PKが同一なため、アクセスパターンが増えると対応できないケースがある |
性能 | データが同じパーティションにあるため、高速なアクセスが可能 | 書き込み時にLSI分も同時更新されるため、スループットが競合しやすい(WCU共有) |
容量・コスト | データは同一領域に保持 → コストは安定しやすい | 1テーブルにつき 最大5つまで しか作成できず、スキーマ設計に制限がある |
制約 | 書き込み一貫性が強く、トランザクションとの親和性が高い | テーブル作成時にしか定義できない → 後から追加・変更不可 |
GSI のメリット・デメリット
観点 | pros | cons |
---|---|---|
構造 | パーティションキーもソートキーも自由に設計できる → 柔軟なアクセスパターンを実現 | 本体テーブルとは別領域にデータを複製するため、整合性や更新のタイミングに注意 |
クエリ性能 | 異なるキー構造での検索が可能 → ユースケースごとに最適化された検索が実現 | 予期せぬスロットリングやホットパーティションの原因になることがある |
性能 | テーブルとは独立したスループット制御(WCU/RCUを分離可能)で高負荷処理にも対応 | 書き込み時にGSIの更新も走るため、内部的なコストやレイテンシが上がることがある |
容量・コスト | 必要に応じて後から追加可能 → スキーマ変更が柔軟 | GSI用のRCU/WCUが別途必要 → 大量データの場合はコストに注意 |
制約 | ユーザー行動やデータ種別ごとにGSIを設計でき、マイクロサービス志向の設計に適応しやすい | 一時的に整合性が崩れる(Eventually Consistent) ことがある |
DynamoDB automatically synchronizes each global secondary index with its base table. When an application writes or deletes items in a table, any global secondary indexes on that table are updated asynchronously, using an eventually consistent model. Applications never write directly to an index. However, it is important that you understand the implications of how DynamoDB maintains these indexes.
→アプリケーションがテーブルに書き込みをした際、そのテーブルは結果整合性モデルを使用して非同期的に GSI にも反映する。※アプリが直接 GSI にデータを書き込むことはない。
Write capacity units
When an item in a table is added, updated, or deleted, and a global secondary index is affected by this, the global secondary index consumes provisioned write capacity units for the operation. The total provisioned throughput cost for a write consists of the sum of write capacity units consumed by writing to the base table and those consumed by updating the global secondary indexes. If a write to a table does not require a global secondary index update, no write capacity is consumed from the index.
→テーブルに何らか書き込み操作が GSI にも影響がある場合、テーブルだけでなく、GSI でも書き込みコスト(WCU)を消費する。つまり、書き込みに要するコストは テーブルとそのテーブルを更新した際に影響を受けるGSIのそれぞれのWCUの合計値となる。※テーブルを更新するもGSIに影響がない場合はテーブル更新時のみのWCUが消費コストとなる。
インデックス選定早見表
条件 | 推奨インデックス |
---|---|
同一プレイヤー内の並び替えや履歴表示 | LSI(PK同一+時系列SKなど) |
異なるPK構造での検索が必要 | GSI(柔軟にPK/SK設計可) |
書き込み負荷が高く、独立したスループットが必要 | GSI |
後からインデックスを追加したい | GSI |
トランザクションや厳密な整合性が重要 | LSI(同一領域で一貫性高) |
インデックス選定の判断軸
- dynamodb では「インデックス ≒ クエリのための設計」
- アクセスパターンを洗い出し、インデックスが不要な形にまず設計するのが理想。
- 必要に応じて GSI(横断検索)、LSI(時系列) を導入を検討する。
属性の射影について(Projection)
セカンダリインデックスは、ベーステーブルにある全ての属性を自動的に保持するわけではない。
検索を高速化しつつ、ストレージ使用量を最小限に抑えるために、必要な属性のみをindexに設定する必要がある。
When you create a secondary index, you need to specify the attributes that will be projected into the index. DynamoDB provides three different options for this:
KEYS_ONLY – Each item in the index consists only of the table partition key and sort key values, plus the index key values. The KEYS_ONLY option results in the smallest possible secondary index.
INCLUDE – In addition to the attributes described in KEYS_ONLY, the secondary index will include other non-key attributes that you specify.
ALL – The secondary index includes all of the attributes from the source table. Because all of the table data is duplicated in the index, an ALL projection results in the largest possible secondary index.
射影に選択できる種別
種別 | 内容 | つかいどころ |
---|---|---|
KEYS_ONLY |
インデックスのパーティションキーとソートキーのみを保持。非キー属性は保持しない。 | 結果から必要な値は全てテーブルから再取得(Fetch) する前提の場合。 |
INCLUDE |
キー属性 + 指定した非キー属性のみを保持。 | 検索時に特定の属性(例: Title , Category など)だけあれば十分なケース。 |
ALL |
テーブルのすべての属性を保持(インデックスがフルコピーを持つ)。 | パフォーマンス重視で再取得を避けたい場合や、テーブル項目数が少ない場合に有効。 |
ユースケース別に考えた際に選択する射影
シナリオ | 推奨される射影 |
---|---|
商品IDと価格だけで検索・表示できる | INCLUDE |
検索後に詳細ページ表示のために、結局全項目を取ってくる必要がある | KEYS_ONLY |
検索結果一覧で多数の項目(カテゴリ、レビュー数、在庫数など)を表示したい | ALL |
選択した射影の種類に応じて、ストレージや課金にどのような作用があるのか
種別 | ストレージ使用量 | 課金対象 | 特徴・備考 |
---|---|---|---|
KEYS_ONLY |
最小 (インデックスキーのみ) |
読み書きキャパシティ/キー属性のストレージ | 最も軽量。 ただし、非キー属性は都度テーブルから再取得が必要になるためReadコスト増の可能性あり。 |
INCLUDE |
中程度 (キー + 指定属性のみ) |
読み書きキャパシティ/保持属性分のストレージ | アクセスパターンに必要な属性のみ保持。 パフォーマンスとコストのバランスが良い。 |
ALL |
最大 (テーブル全属性を複製) |
読み書きキャパシティ/全属性分のストレージ | 読み込み時にテーブルアクセス不要で高速 ただし、ストレージ・コストが最も高くなる。 |
1点注意しておくべき点として、GSIの場合、セカンダリインデックスはベーステーブルとは異なる領域に保持する構造であるため、射影種別を ALL
にした場合 GSI の読み書き容量・ストレージも個別に課金対象 となる点を認識しておく必要がある。
※LSIの場合はベーステーブルと同じ領域に保持されるため別課金にはならない模様
When you choose the attributes to project into a global secondary index, you must consider the tradeoff between provisioned throughput costs and storage costs:
If you need to access most of the non-key attributes on a frequent basis, you can project these attributes—or even the entire base table— into a global secondary index. This gives you maximum flexibility. However, your storage cost would increase, or even double.
ベーステーブルとインデックステーブルへのアクセスパターンと判断軸
ALL の処理フロー
INCLUDE の処理フロー
KEYS_ONLY の処理フロー
投影種別の判断軸

トランザクションについて
dynamodb におけるトランザクションの概要
Amazon DynamoDB transactions simplify the developer experience of making coordinated, all-or-nothing changes to multiple items both within and across tables. Transactions provide atomicity, consistency, isolation, and durability (ACID) in DynamoDB, helping you to maintain data correctness in your applications.
You can use the DynamoDB transactional read and write APIs to manage complex business workflows that require adding, updating, or deleting multiple items as a single, all-or-nothing operation. For example, a video game developer can ensure that players’ profiles are updated correctly when they exchange items in a game or make in-game purchases.
- 複数のアイテムに跨る変更はトランザクション用の読み書きAPIを使用することで all-or-nothing(原子性を保証しながら一括で変更、失敗した場合は変更しない) のオペレーションが可能。
With the transaction write API, you can group multiple Put, Update, Delete, and ConditionCheck actions. You can then submit the actions as a single TransactWriteItems operation that either succeeds or fails as a unit. The same is true for multiple Get actions, which you can group and submit as a single TransactGetItems operation.
-
TransactWriteItems
: Put、Update、Delete、ConditionCheck の複数アクションを一つにまとめて、全体として成功または失敗させることが可能。 -
TransactGetItems
: 複数の Get 操作をまとめて実行することが可能。
There is no additional cost to enable transactions for your DynamoDB tables. You pay only for the reads or writes that are part of your transaction. DynamoDB performs two underlying reads or writes of every item in the transaction: one to prepare the transaction and one to commit the transaction. These two underlying read/write operations are visible in your Amazon CloudWatch metrics.
- トランザクションの利用による追加コストはなく、通常の読み書きした分が課金対象となる。
- ただし、少々ややこしいのがトランザクション実行時には各アイテムに対して、トランザクションの準備フェーズとコミットフェーズの2段階の処理が行われるため、通常の単一アイテムの読み書きの2倍のリソースを消費することになる。
おおまかな整理
TransactWriteItems API について
TransactWriteItems is a synchronous and idempotent write operation that groups up to 100 write actions in a single all-or-nothing operation. These actions can target up to 100 distinct items in one or more DynamoDB tables within the same AWS account and in the same Region. The aggregate size of the items in the transaction cannot exceed 4 MB. The actions are completed atomically so that either all of them succeed or none of them succeeds.
- 同期的、且つ冪等性を担保する書き込み処理のこと。
- 最大で100件の書き込み処理(Put/Update/Delete/ConditionCheck)を1つのトランザクションにまとめて全て成功するか全て失敗するかの原子操作を実行する。※ConditionCheck: アイテムの存在確認/属性条件の検証等
- トランザクション内の対象アイテムは1つ以上のテーブルに跨っていても良い(ただし、同じAWSアカウント且つ、同一リージョン内に限定される)
- 合計データサイズは4MBまで。
注意点
A TransactWriteItems operation differs from a BatchWriteItem operation in that all the actions it contains must be completed successfully, or no changes are made at all. With a BatchWriteItem operation, it is possible that only some of the actions in the batch succeed while the others do not.
Transactions cannot be performed using indexes.
You can't target the same item with multiple operations within the same transaction. For example, you can't perform a ConditionCheck and also an Update action on the same item in the same transaction.
- BatchWriteItem API は一部成功を容認するためこのオペレーションとは異なることを認識しておく必要がある。
- セカンダリインデックス(LSIとGSI)に対してはトランザクション操作を行うことはできない。※セカンダリインデックスはベーステーブルの属性を自動的に反映する構造になっているので、ユーザ側で明示的に変更することはできないのかと
- 同一トランザクション内で同じアイテムに複数の操作を行うことはできない。※例えば delete してから Update する操作はできないということ
トランザクション完了後の処理について
When a transaction completes in DynamoDB, its changes start propagating to global secondary indexes (GSIs), streams, and backups. This propagation occurs gradually: stream records from the same transaction might appear at different times and could be interleaved with records from other transactions. Stream consumers shouldn't assume transaction atomicity or ordering guarantees.
-
トランザクション処理が完了した後、変更は段階的に GSI や DynamoDB Stream 等に伝搬される仕組みになっている。
-
Stream のレコードは順不同となるため、トランザクションの原始性や順序性は保証されないことに注意する必要がある。※Stream のコンシューマーの設計はその前提で行う必要がある。
-
イメージとしては以下
TransactWriteItems
- Put item A
- Update item B
- Delete item C
この時、DynamoDB Stream 上では、
- item B の更新イベントが先に届く。
- item A の作成イベントがその次に届く。
- item C の削除イベントがその次に届く。
といった順番でイベントが流れてくるので、コンシューマ側ではイベントが流れてきたら際にはまず状態チェックのような機構を設ける必要があるということ。
冪等性(Idempotency)について意識しておくべきこと
You can optionally include a client token when you make a TransactWriteItems call to ensure that the request is idempotent. Making your transactions idempotent helps prevent application errors if the same operation is submitted multiple times due to a connection time-out or other connectivity issue.
- TransactWriteItems は、クライアントトークンを任意で指定することで、リクエストの冪等性を保証することが可能。
- クライアントトークンは dynamodb の api を実行する際に、リクエストに含めることができる。
- ユーザ側で任意の値を生成して実行することが可能。※Go製のプログラム例を後述
Important points about idempotency
A client token is valid for 10 minutes after the request that uses it finishes. After 10 minutes, any request that uses the same client token is treated as a new request. You should not reuse the same client token for the same request after 10 minutes.
If you repeat a request with the same client token within the 10-minute idempotency window but change some other request parameter, DynamoDB returns an IdempotentParameterMismatch exception.
- クライアントトークンの有効期限は10分。
- 最初のリクエストが完了してから10分以内であれば、同じトークンを使った再送リクエストは変更を加えず成功として返される。
- 10分を過ぎると、そのリクエストは新しいリクエストとして扱われる。※つまり更新処理が行われることになる
- 10分以内に、同じクライアントトークンで再リクエストしたとしても、リクエストのパラーメータが一部変わっていたりすると IdempotentParameterMismatch という例外を返す模様。※例えば post リクエストの一部のパラメータ変えてたりした場合がこれにあたる
dynamodb のトランザクション処理でクライアントトークンを含めた状態でリクエストする例
// dynamodb のクライアント生成
func createDynamoDBClient(ctx context.Context) (*dynamodb.Client, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, err
}
return dynamodb.NewFromConfig(cfg), nil
}
// 冪等性のためのクライアントトークンを生成 ※これのこと
func generateClientToken() string {
return uuid.New().String()
}
// トランザクションに含める操作(例: ユーザーのステータス更新)を構築
func buildTransactItems(tableName, userID, newStatus string) []types.TransactWriteItem {
return []types.TransactWriteItem{
{
Update: &types.Update{
TableName: aws.String(tableName),
Key: map[string]types.AttributeValue{
"user_id": &types.AttributeValueMemberS{Value: userID},
},
UpdateExpression: aws.String("SET #status = :newStatus"),
ExpressionAttributeNames: map[string]string{
"#status": "status",
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":newStatus": &types.AttributeValueMemberS{Value: newStatus},
},
},
},
}
}
// トランザクションを実行
func executeTransaction(ctx context.Context, client *dynamodb.Client, items []types.TransactWriteItem, token string) error {
input := &dynamodb.TransactWriteItemsInput{
TransactItems: items,
ClientRequestToken: aws.String(token), // 実際に dynamodb の api 実行する際に、事前に生成しておいたクライアントトークンを含めた状態で実行する
ReturnConsumedCapacity: types.ReturnConsumedCapacityTotal,
}
output, err := client.TransactWriteItems(ctx, input)
if err != nil {
return err
}
fmt.Printf("Consumed capacity: %+v\n", output.ConsumedCapacity)
return nil
}
書き込みトランザクションが失敗するケースについて
他トランザクションと競合が発生した場合
When a TransactWriteItems request conflicts with an ongoing TransactWriteItems operation on one or more items in the TransactWriteItems request. In this case, the request fails with a TransactionCanceledException.
- 同じアイテムに対して同時に別のトランザクションが実行されていると、競合が起きて TransactionCanceledException で失敗する。
- 冪等性トークンの使用や、リトライ処理(指数バックオフなど)などの考慮が必要。
プロビジョニングされたキャパシティが不足している場合
When there is insufficient provisioned capacity for the transaction to be completed.
- 特にプロビジョンモードでのスループット上限値を越えてしまった場合など。
- プロビジョニング設定を増強、またはオンデマンド設定に切り替える等の検討が必要。※オンデマンド設定の場合はユーザ側で RCU/WCU は設定せず、書き込み、読み込みリクエストをdynamodb 側が自動で処理するので
アイテムサイズやインデックスサイズが制限を超えた場合
When an item size becomes too large (larger than 400 KB), or a local secondary index (LSI) becomes too large, or a similar validation error occurs because of changes made by the transaction.
- アイテムが400KB以上、またはLSIのサイズが制限を超えた場合など。
- データ構造の見直しや、既存の項目をバイナリで圧縮する等の代替策の検討が必要。
トランザクションの分離レベルについて
Serializable(直列化可能)
Serializable isolation ensures that the results of multiple concurrent operations are the same as if no operation begins until the previous one has finished.
- もっとも強い分離レベルであり、すべての操作が1つずつ順番に実行されたように見える状態。
- つまり、同時に複数の操作が走っていても、結果はまるで1つずつ順番に処理されたかのように整合性を保つ仕組みのこと。
以下のようなイメージ
-
状況:
- userA, userB が同一商品の在庫を同タイミングで以下のように変更しようとする。
- userA:「在庫を5減らす」
- userB: 「在庫を10減らす」
- userA, userB が同一商品の在庫を同タイミングで以下のように変更しようとする。
-
非Serializable(弱い整合性)の場合:
- どちらの操作が先に実行された(処理された)かにより、不安定な結果になってしまう。
- よくある在庫の整合性が合わなくなってしまう問題。
-
Serializable(直列化可能)の場合:
- たとえ同時に実行していたとしても、内部的には「どちらかを先に、どちらかを後に」実行したように扱うので、不整合の事態を回避することができるようになる。
非Serializable の例(整合性が崩れる可能性)
Serializable の例(整合性が保証される)
Read-Committed(コミット済み読み取り)
Read-committed isolation ensures that read operations always return committed values for an item - the read will never present a view to the item representing a state from a transactional write which did not ultimately succeed. Read-committed isolation does not prevent modifications of the item immediately after the read operation.
- 「読み取り時点での正しい状態を返す」といった保証はあるが、「読み取り直後もその状態が変わらないことまでは保証しない」ということ。
以下のようなイメージ
- userA は「商品Aの在庫数」を参照した。
- この時点で読み取った在庫数は、その瞬間にコミットされている最新の値(コミットされたのは1秒前かもしれないし、昨日かもしれないし、1年前かもしれない)
- ただし、その読み取り操作を行った直後に、別のユーザ、またはバッチ処理が商品Aの在庫数を変更することはあるので、userA が参照した情報は古い情報になるかもしれない、ということ。
トランザクションと単一操作間、バッチ操作間の分離レベル
単一操作間では Serializable な分離レベル が保証される。
操作の組み合わせ | 分離レベル | 意味 |
---|---|---|
TransactWriteItems ↔ 標準の PutItem , UpdateItem , DeleteItem
|
Serializable | トランザクションが完了するまで、他の書き込み操作はそれを「見て」しまうことはない |
TransactWriteItems ↔ GetItem
|
Serializable | 一貫性のある状態で読み書きされる |
TransactWriteItems ↔ TransactGetItems
|
Serializable | 同じように順序どおりに扱われる |
ただし、以下のような「バッチ系の操作」全体とは Serializable ではない。
操作の組み合わせ | 分離レベル | 意味 |
---|---|---|
TransactWriteItems ↔ BatchWriteItem
|
Serializable ではない(個々の書き込みは別々に実行) | 全体としては原子的ではない |
TransactWriteItems ↔ BatchGetItem , Query , Scan
|
Read-committed(後述) | 読み込みはコミット済みの値を返すが一部古い値が混ざる可能性あり |
分離レベルの特性比較
特性 | Serializable | Read-Committed |
---|---|---|
同時実行でも順序通りに見える? | ✅ はい | ❌ いいえ |
中途半端なデータを読む心配は? | ❌ ない | ❌ ない(ただし、古い値が混ざる可能性あり) |
データが途中で変わっても? | ❌ 見えない | ✅ 一部見える可能性あり |
- dynamodb のトランザクション操作(TransactWriteItems, TransactGetItems)は基本的には、Serializable なレベルで処理を行う。※つまり他の処理と順序が入れ替わっても結果が変わらないということ
- パフォーマンス重視で、整合性はそこまで重要視されてない場合は Query や BatchGetItem でも良いが、整合性が重要な機能を扱う場合は基本的には TransactWriteItems, TransactGetItems を選択しておくのが良い。
大量のデータを取り扱う場合は BatchWriteItem を使った方が良い
Avoid using transactions for ingesting data in bulk. For bulk writes, it is better to use BatchWriteItem.
大量のデータを一括で書き込む場合は、極力トランザクション操作は行わず、BatchWriteItem を使用することが推奨されている。
dynamodb のトランザクションはオーバーヘッドが大きく、bulk insert には不向きな模様。
項目 | TransactWriteItems |
BatchWriteItem |
---|---|---|
書き込みの整合性 | ACID保証(Atomicity, Consistency など) | ベストエフォート型(部分成功もあり) |
内部処理 | 複数アイテムを直列的に処理 事前検証・ロック制御あり |
複数アイテムを並列で処理 |
書き込み制限(1リクエスト) | 最大 25 件 / 4MB | 最大 25 件 / 16MB |
リトライの挙動 | 一括ロールバックが必要 | 失敗したアイテムのみ個別に再試行可能 |
処理速度・スループット | 遅い(整合性を保証するためのオーバーヘッドあり) | 速い(高速なバルク処理に適する) |
- 内部的に各アイテムに対して、トランザクションの準備フェーズとコミットフェーズの2段階の処理が行われるため処理が重くなりがち。※これに加えて RCU/WCU の消費量が2倍になるのも痛い。
- 整合性確保するために、直列性を強制することも理由に挙がるが、そもそもスケーラビリティを視野に入れた使い方をすることが多いはずで、並列実行の恩恵を受けにくくするのはややアンチパターンに近い。※RDBMSのほうが向いている