DynamoDB のテーブル設計について色々調べました。
最初にまとめ
- スキーマレスだからといって設計いらない訳ではない
- 検索条件を事前に洗い出したうえで設計する
- ベストプラクティスはシングルテーブル設計、もしくは出来るだけ少ないテーブル
- 種類の異なるレコードを横ではなく縦に格納するイメージで設計するとよい
DynamoDB とは?
DynamoDB は、AWS によって提供されているフルマネージドな NoSQL データベースサービスです。
従来のリレーショナルデータベースとは異なり、スケーラビリティと柔軟性に優れており、非常に高い可用性とパフォーマンスを提供します。
DynamoDB はスキーマレスな設計を採用しており、データモデルを固定せずに、キー・値ペアやドキュメントデータを効率的に格納できることが特徴です。
特に、急速に変動するアクセスパターンに対しても安定したパフォーマンスを発揮し、データが大規模になるほどそのメリットが際立ちます。
また、プロビジョニングされた読み書きキャパシティに基づいて自動的にスケーリングを行う機能を備えているため、運用管理の手間を大幅に削減できます。
DynamoDB とリレーショナルデータベースの違い
DynamoDB とリレーショナルデータベースの最も大きな違いは、データモデルとスケーラビリティのアプローチにあります。
リレーショナルデータベースでは、データをテーブル、行、列といった厳格なスキーマに基づいて構成します。
これに対して、DynamoDB はスキーマレスであり、柔軟なデータ構造を許容します。そのため、特定のクエリやアクセスパターンに最適化された設計が必要となります。
また、DynamoDB は分散型のデータベースで、データを複数のパーティションに自動的に分割して保存します。
これにより、リレーショナルデータベースがスケールアップに依存するのに対し、DynamoDB はスケールアウトによるパフォーマンス向上を目指しています。
リレーショナルデータベースでは強いトランザクション整合性が重視される一方で、DynamoDB では最終的整合性をデフォルトとし、スループットを最大化する設計がなされています。
これらの違いから、アプリケーションの要件に応じたデータベース選定が必要です。
DynamoDB のテーブル設計について
用語と検索条件おさらい
用語
- プライマリーキー
- DynamoDB 内で登録されている Item(レコード)を一意に識別するキー
- 後述するパーティションキーとソートキーが使用される
- パーティションキー = プライマリーキーにすることも可能
- パーティションキー
- データを格納する場所をパーティションで区切っておくようなイメージ
- ソートキー
- パーティション内のデータをソートしておくキー
- 設定なしでも一応テーブルは作れる
- グローバルセカンダリインデックス
- 通常のPK,SKとは別にPK,SKを設定できる機能
- 属性の検索条件を増やしたい時に使う。コストは増える
検索条件について
- 基本的にパーティションキーおよびソートキーにしか検索条件をかけられません。属性に対して検索をかけることはできません
- Scan とフィルターを使用すれば一応は可能ですがデータ量が多いとパフォーマンスが悪くなります
- グローバルセカンダリインデックスを活用すれば属性に対して検索をかけることも可能です。その場合コストは上がります
- パーティションキーは必ず検索条件に指定する必要があります。ソートキーのみを検索条件にすることはできません
- パーティションキーには、完全一致条件のみ指定できます
- 「B から始まる」というような部分一致条件は指定できません
- ソートキーには「1 から始まる」というような部分一致条件が指定できます
設計例
1. 複合キー
DynamoDB では、一つのパーティションキー(PK)に複数の異なるソートキー(SK)を持つことができるため、複合キー を利用して一対多のデータを表現する方法がよく使われます。
この方法では、親アイテム(1 側)と子アイテム(多側)を同じパーティションキーで管理し、ソートキーで区別します。
例: ユーザーとその所属チームの一対多関係
- 1 人のユーザーが複数のチームに所属するという一対多の関係を表現する
データ例:
PK(パーティションキー) | SK(ソートキー) | 属性 1(TeamName) | 属性 2(UserName) |
---|---|---|---|
USER#002 | TEAM#001 | Developers | |
USER#001 | TEAM#001 | Developers | |
USER#002 | USER#METADATA | てすと じろう | |
USER#001 | TEAM#002 | Designers | |
USER#001 | USER#METADATA | てすと たろう |
クエリ方法:
- 特定のユーザーが所属しているすべてのチームを取得するクエリは、以下のように
PK
に基づいてクエリを実行します
const params = {
TableName: "TeamUserTable",
KeyConditionExpression: "PK = :userId",
ExpressionAttributeValues: {
":userId": "USER#001",
},
};
// USER#METADATAがいらない場合は以下のように前方一致条件をいれる
// const params = {
// TableName: "TeamUserTable",
// KeyConditionExpression: "PK = :userId AND begins_with(SK, :Prefix)",
// ExpressionAttributeValues: {
// ":userId": "USER#001",
// ":Prefix": "TEAM#",
// },
// };
const result = await dynamo.send(new QueryCommand(params));
console.log(result);
結果
{
"Count": 3,
"Items": [
{
"PK": "USER#001",
"SK": "TEAM#001",
"TeamName": "Developers"
},
{
"PK": "USER#001",
"SK": "TEAM#002",
"TeamName": "Designers"
},
{
"PK": "USER#001",
"SK": "USER#METADATA",
"UserName": "てすと たろう"
}
]
}
メリット:
- シンプルなクエリ: 1 つのクエリでユーザーとそのチーム情報を一度に取得可能
- 高いパフォーマンス: 1 つのパーティション内で関連データがすべて集約されるため、読み込みが効率的
デメリット:
- データの重複: 非正規化のため、データの重複が生じることがありますが、DynamoDB ではこれは一般的なトレードオフです
- チームの情報取得の課題: 特定のチームにどのユーザーが所属しているかは取得できない
2. GSI(グローバルセカンダリインデックス)を活用するパターン
複数のクエリパターンに対応する必要がある場合、GSI
を使って効率的な検索を可能にする方法があります。
例えば、ユーザーが所属するチームを取得したい場合、PK
にはユーザー ID、SK
にはチーム ID を設定し、GSI
を使って逆方向のクエリを実行します。
データ例:
PK(パーティションキー) | SK(ソートキー) | TeamName | UserName | GSI1PK | GSI1SK |
---|---|---|---|---|---|
USER#001 | TEAM#001 | Developers | てすと たろう | TEAM#001 | USER#001 |
USER#001 | TEAM#002 | Designers | てすと たろう | TEAM#002 | USER#001 |
クエリ方法:
- ユーザーに基づいたチーム情報のクエリは、通常の
PK
を使用します - チームに基づいたメンバー情報のクエリは、
GSI
を利用します
const params = {
TableName: "TeamUserTable",
IndexName: "GSI1", // GSIを使用
KeyConditionExpression: "GSI1PK = :teamId",
ExpressionAttributeValues: {
":teamId": "TEAM#001",
},
};
const result = await dynamo.send(new QueryCommand(params));
console.log(result.Items);
結果
{
"Count": 1,
"Items": [
{
"PK": "USER#001",
"SK": "TEAM#001",
"TeamName": "Developers",
"GSI1PK": "TEAM#001",
"GSI1SK": "USER001"
}
]
}
メリット:
- 柔軟なクエリパターン: GSI を使うことで、一対多の関係を逆方向からもクエリできます
- スケーラビリティ: GSI を使用することで、大量のデータでも効率的に処理が可能です
デメリット:
- コスト増加: GSI の利用には追加のストレージとクエリコストが発生します
3. アドジャセンシーリスト(隣接リスト)を活用するパターン
多対多の関係を扱う場合、リレーショナルデータベースでは中間テーブルを用いることが一般的です。
DynamoDB でも同様のアプローチを取り、アドジャセンシーリスト(隣接リスト)という設計パターンを使用します。
データ例
以下のように、パーティションキー(PK) と ソートキー(SK) を使ってチームとユーザーの関係を表現します。
PK | SK | TeamName | UserName |
---|---|---|---|
TEAM#001 | TEAM#METADATA | Developers | |
TEAM#001 | USER#001 | てすと たろう | |
TEAM#002 | TEAM#METADATA | Designers | |
USER#001 | TEAM#002 | Designers | |
USER#001 | USER#METADATA | てすと たろう | |
USER#002 | TEAM#001 | Developers | |
USER#002 | USER#METADATA | てすと じろう |
上記の データのアイテムは、以下の情報を保持しています:
- prefixがTEAM#のアイテムは、「チーム ID」に所属する「ユーザー ID」を表します
- prefixがUSER#のアイテムは、「ユーザー ID」が所属している「チーム ID」を表します
このように、1 つのエンティティに対して複数のレコードを縦に格納することで、多対多の関係をシンプルに表現できます。
要件に基づいた DynamoDB のデータモデリングは、シングルテーブル設計を用いることで効率的に実現できます。
以下のアプローチは、ユーザー、チーム、そしてそれらの多対多の関係を効率的に管理し、パフォーマンスも最適化できる設計です。
クエリ例
- チームに所属するユーザーのリストを取得
const params = {
TableName: "TeamUserTable",
KeyConditionExpression: "PK = :teamId AND begins_with(SK, :userPrefix)",
ExpressionAttributeValues: {
":teamId": "TEAM#001",
":userPrefix": "USER#",
},
};
const result = await dynamo.send(new QueryCommand(params));
console.log(result.Items);
- ユーザーが所属しているチームのリストを取得
const params = {
TableName: "TeamUserTable",
KeyConditionExpression: "PK = :userId AND begins_with(SK, :teamPrefix)",
ExpressionAttributeValues: {
":userId": "USER#001",
":teamPrefix": "TEAM#",
},
};
const result = await dynamo.send(new QueryCommand(params));
console.log(result.Items);
この設計では、ユーザーやチームに関する情報、さらにユーザーとチームの多対多の関係を簡潔に管理できます。
シングルテーブル設計を活用し、クエリのパフォーマンスを最適化しながら要件を満たすことができます。
また、必要に応じてグローバルセカンダリインデックス(GSI)を導入することで、柔軟な検索機能を強化できます。
中間テーブルの設計とクエリのポイント
上記のような中間テーブルの設計を行う際のポイントは、パーティションキーとソートキーの役割を明確にすることです。
パーティションキーはデータの主軸となるエンティティを指定し、ソートキーでそのエンティティに関連する他のエンティティを紐づけます。
例えば、チームに対してユーザーを検索する場合、チーム ID(TEAM#
)をパーティションキーとして設定し、そのチームに属するユーザーをソートキーとして格納します。
これにより、クエリを行った際に効率よくデータを取得できます。
また、この設計ではリレーショナルデータベースのように JOIN を使わないため、クエリ回数を増やすことなく、一度のクエリで関連するデータを取得することが可能です。
まとめ
DynamoDB のテーブル設計において、パーティションキーとソートキーをどのように設計するかが非常に重要です。
また、クエリパターンに応じた設計を行うことで、複雑な多対多の関係もシンプルに管理できます。
シングルテーブル設計を活用し、種類の異なるレコードを縦に格納するアプローチは、スケーラビリティとパフォーマンスを向上させるための効果的な手法です。