Day 13: DynamoDB実践:テーブル設計からCRUD操作まで
皆さん、こんにちは!「AWSデータベース・ストレージ完全攻略」のDay 13へようこそ!
昨日のDay 12では、NoSQLデータベースの概念と、AWSのフルマネージドNoSQLデータベースであるAmazon DynamoDBの概要と特徴について学びました。DynamoDBが持つ高いスケーラビリティ、パフォーマンス、そして柔軟なスキーマは、現代の多様なアプリケーション要件に対応するための強力な武器となることを理解できたはずです。
今日は、そのDynamoDBの知識をさらに深掘りし、実際にテーブルを設計し、基本的なCRUD(Create, Read, Update, Delete)操作を行う方法を実践的に学んでいきましょう。DynamoDBの真価を引き出すには、適切なテーブル設計が不可欠です。
1. DynamoDBテーブル設計の重要性
リレーショナルデータベースでは、正規化されたスキーマ設計が一般的であり、テーブル間の結合(JOIN)によってデータを取得します。しかし、DynamoDBはJOIN操作をサポートしていません。そのため、アプリケーションのアクセスパターン(データがどのように読み書きされるか)を事前に深く理解し、それに合わせてテーブルを設計することが極めて重要になります。
不適切な設計は、パフォーマンスの低下、コストの増加、あるいは必要なデータが効率的に取得できないといった問題を引き起こします。
DynamoDB設計の基本原則:
- アクセスパターンファースト: まず、アプリケーションがどのようなデータを、どのようなキーで、どのように取得したいのかを明確にします。
- 非正規化の活用: 関連するデータを1つのアイテムにまとめて格納することで、読み込み時のI/Oを削減します。
- プライマリキーの適切な選択: 最も頻繁なアクセスパターンに対応できるようにプライマリキーを設計します。
- セカンダリインデックスの活用: プライマリキーでは対応できないアクセスパターンに対応するために、必要に応じてインデックスを追加します。
2. プライマリキーの設計
DynamoDBのプライマリキーは、テーブル内の各アイテムを一意に識別するために使用され、データの物理的な配置とアクセス効率に直接影響します。
a. パーティションキー (Partition Key) のみ
- 定義: 各アイテムを一意に識別する単一の属性。ハッシュキーとも呼ばれます。
- 特徴: DynamoDBはパーティションキーのハッシュ値に基づいてデータを複数のストレージパーティションに分散します。
- ユースケース: 特定のアイテムをキーで直接取得する(例: ユーザーIDでユーザー情報を取得)。
-
設計のポイント:
- カーディナリティ(値の多様性)が高い属性を選ぶ: データが均等に分散され、ホットパーティション(特定のパーティションにアクセスが集中する状態)を避けるため。
-
アクセスパターンで頻繁に利用される属性を選ぶ:
GetItem
やQuery
操作で効率的にデータにアクセスするため。
例:Users
テーブル
- アクセスパターン: ユーザーIDでユーザー情報を取得する。
-
プライマリキー:
UserId
(パーティションキー) -
アイテム例:
{ "UserId": "user123", "Username": "Alice", "Email": "alice@example.com", "RegistrationDate": "2023-01-15" }
b. パーティションキー + ソートキー (Sort Key)
- 定義: パーティションキーとソートキーの組み合わせでアイテムを一意に識別します。複合プライマリキーとも呼ばれます。
- 特徴: 同じパーティションキーを持つアイテムは、ソートキーの値に基づいてソートされて格納されます。これにより、特定のパーティションキー内のアイテムに対して範囲クエリやソートされた取得が可能です。
- ユースケース: 特定のエンティティ(例: ユーザー)に関連する複数のアイテムを、ある属性(例: タイムスタンプ)でソートして取得する。
-
設計のポイント:
- パーティションキー: アクセスが分散されるようにカーディナリティの高い属性を選ぶ。
- ソートキー: 範囲クエリやソートが必要な属性を選ぶ。時系列データ(タイムスタンプ)がよく使われます。
例:Orders
テーブル
- アクセスパターン: 特定のユーザーの注文履歴を、注文日時でソートして取得する。
-
プライマリキー:
UserId
(パーティションキー),OrderId
(ソートキー) -
アイテム例:
{ "UserId": "user123", "OrderId": "order-20230729-001", "OrderDate": "2023-07-29T10:00:00Z", "TotalAmount": 150.00, "Status": "Completed" }, { "UserId": "user123", "OrderId": "order-20230730-002", "OrderDate": "2023-07-30T14:30:00Z", "TotalAmount": 25.50, "Status": "Pending" }
-
Query
操作でUserId = "user123"
を指定し、OrderId
の範囲条件やOrderDate
でソートして取得できます。
-
3. セカンダリインデックスの設計
プライマリキーでは対応できないアクセスパターンがある場合、セカンダリインデックスを利用します。
a. グローバルセカンダリインデックス (Global Secondary Index: GSI)
- 定義: テーブルのプライマリキーとは異なるパーティションキーとソートキーを持つインデックスです。
-
特徴:
- テーブル全体にわたってクエリを実行できます。
- 元のテーブルとは物理的に独立したストレージに格納されます。
- データは最終的な一貫性でレプリケートされます(強い一貫性での読み込みはできません)。
- GSIにもキャパシティユニットをプロビジョニング(またはオンデマンド)する必要があります。
-
ユースケース:
- プライマリキー以外の属性でアイテムを検索したい場合。
- 特定のステータスのアイテムを検索したい場合。
例:Users
テーブルにGSIを追加
-
テーブルのプライマリキー:
UserId
(パーティションキー) - アクセスパターン: メールアドレスでユーザーを検索したい。
-
GSIの設計:
EmailIndex
-
GSIパーティションキー:
Email
-
GSIソートキー: なし(または必要に応じて
Username
など)
-
GSIパーティションキー:
-
プロジェクション (Projection): GSIに含める属性を選択できます。
- KEYS_ONLY: インデックスキーの属性のみをコピー。最もコスト効率が良い。
- ALL: 元のアイテムのすべての属性をコピー。最も柔軟だが、ストレージとI/Oコストが増加。
- INCLUDE: インデックスキーと、指定した非キー属性のみをコピー。
b. ローカルセカンダリインデックス (Local Secondary Index: LSI)
- 定義: テーブルと同じパーティションキーを持ち、ソートキーのみが異なるインデックスです。
-
特徴:
- 同じパーティションキー内のアイテムに限定してクエリを実行できます。
- 元のテーブルと同じパーティションに物理的に格納されます。
- 強い一貫性での読み込みが可能です。
- テーブル作成時にのみ定義でき、後から追加・削除はできません。
- ユースケース: 特定のパーティションキー内で、プライマリキーのソートキーとは異なる属性でソートして取得したい場合。
例:Orders
テーブルにLSIを追加
-
テーブルのプライマリキー:
UserId
(パーティションキー),OrderId
(ソートキー) - アクセスパターン: 特定のユーザーの注文を、注文ステータスと注文日時でソートして取得したい。
-
LSIの設計:
OrderStatusIndex
-
LSIパーティションキー:
UserId
(テーブルと同じ) -
LSIソートキー:
Status
-
LSIパーティションキー:
-
プロジェクション: 必要に応じて
OrderDate
などをINCLUDE。
4. DynamoDBのCRUD操作の実践
ここでは、AWS CLIとPython SDK (Boto3) を使った基本的なCRUD操作の例を示します。
事前準備:テーブルの作成
AWSマネジメントコンソールで、以下のようなテーブルを作成しておきましょう。
テーブル名: Users
プライマリキー: UserId
(パーティションキー)
オンデマンドキャパシティモード
テーブル名: Orders
プライマリキー: UserId
(パーティションキー), OrderId
(ソートキー)
オンデマンドキャパシティモード
a. Create / PutItem (新しいアイテムの作成または既存アイテムの更新)
PutItem
は、指定したプライマリキーのアイテムが存在しない場合は新規作成し、存在する場合は既存のアイテムを上書きします。
AWS CLI:
# Users テーブルにアイテムを追加
aws dynamodb put-item \
--table-name Users \
--item '{
"UserId": {"S": "user001"},
"Username": {"S": "Charlie"},
"Email": {"S": "charlie@example.com"},
"RegistrationDate": {"S": "2024-01-01"}
}' \
--return-consumed-capacity TOTAL
# Orders テーブルにアイテムを追加
aws dynamodb put-item \
--table-name Orders \
--item '{
"UserId": {"S": "user001"},
"OrderId": {"S": "order-20240730-001"},
"OrderDate": {"S": "2024-07-30T10:00:00Z"},
"TotalAmount": {"N": "120.50"},
"Status": {"S": "Processing"}
}' \
--return-consumed-capacity TOTAL
Python (Boto3):
import boto3
dynamodb = boto3.resource('dynamodb')
# Users テーブルにアイテムを追加
users_table = dynamodb.Table('Users')
response = users_table.put_item(
Item={
'UserId': 'user001',
'Username': 'Charlie',
'Email': 'charlie@example.com',
'RegistrationDate': '2024-01-01'
}
)
print("PutItem to Users successful:", response)
# Orders テーブルにアイテムを追加
orders_table = dynamodb.Table('Orders')
response = orders_table.put_item(
Item={
'UserId': 'user001',
'OrderId': 'order-20240730-001',
'OrderDate': '2024-07-30T10:00:00Z',
'TotalAmount': 120.50, # Pythonの数値型はDynamoDBのNumber型に自動変換
'Status': 'Processing'
}
)
print("PutItem to Orders successful:", response)
b. Read / GetItem (プライマリキーによる単一アイテムの取得)
GetItem
は、プライマリキー(パーティションキー、またはパーティションキーとソートキーの組み合わせ)を正確に指定して、単一のアイテムを取得する最も効率的な方法です。
AWS CLI:
# Users テーブルからアイテムを取得
aws dynamodb get-item \
--table-name Users \
--key '{"UserId": {"S": "user001"}}' \
--return-consumed-capacity TOTAL
# Orders テーブルからアイテムを取得 (複合プライマリキー)
aws dynamodb get-item \
--table-name Orders \
--key '{"UserId": {"S": "user001"}, "OrderId": {"S": "order-20240730-001"}}' \
--return-consumed-capacity TOTAL
Python (Boto3):
# Users テーブルからアイテムを取得
response = users_table.get_item(
Key={
'UserId': 'user001'
}
)
item = response.get('Item')
print("GetItem from Users:", item)
# Orders テーブルからアイテムを取得
response = orders_table.get_item(
Key={
'UserId': 'user001',
'OrderId': 'order-20240730-001'
}
)
item = response.get('Item')
print("GetItem from Orders:", item)
c. Query (パーティションキーによるアイテムの検索)
Query
は、パーティションキーを正確に指定し、オプションでソートキーに対する条件(範囲、前方一致など)を指定して、特定のパーティション内のアイテムを検索します。GSIに対してもQuery
を実行できます。
AWS CLI:
# Orders テーブルから特定のユーザーの全ての注文を取得
aws dynamodb query \
--table-name Orders \
--key-condition-expression "UserId = :uid" \
--expression-attribute-values '{":uid": {"S": "user001"}}' \
--return-consumed-capacity TOTAL
# Orders テーブルから特定のユーザーの特定の範囲の注文を取得
aws dynamodb query \
--table-name Orders \
--key-condition-expression "UserId = :uid AND OrderId BETWEEN :order_start AND :order_end" \
--expression-attribute-values '{
":uid": {"S": "user001"},
":order_start": {"S": "order-20240701-000"},
":order_end": {"S": "order-20240731-999"}
}' \
--return-consumed-capacity TOTAL
Python (Boto3):
# Orders テーブルから特定のユーザーの全ての注文を取得
response = orders_table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('UserId').eq('user001')
)
items = response.get('Items')
print("Query from Orders (all for user001):", items)
# Orders テーブルから特定のユーザーの特定の範囲の注文を取得
response = orders_table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('UserId').eq('user001') &
boto3.dynamodb.conditions.Key('OrderId').between('order-20240701-000', 'order-20240731-999')
)
items = response.get('Items')
print("Query from Orders (range for user001):", items)
d. Scan (テーブル全体の検索 - 注意!)
Scan
は、テーブルまたはセカンダリインデックスのすべてのアイテムを読み込みます。非常に柔軟な検索が可能ですが、データ量が多いテーブルに対して実行すると、非常にコストがかかり、パフォーマンスも低下します。本番環境での使用は極力避けるべきです。
AWS CLI:
# Users テーブルの全てのアイテムをスキャン (開発/テスト用途のみ)
aws dynamodb scan \
--table-name Users \
--return-consumed-capacity TOTAL
Python (Boto3):
# Users テーブルの全てのアイテムをスキャン (開発/テスト用途のみ)
response = users_table.scan()
items = response.get('Items')
print("Scan from Users:", items)
e. UpdateItem (既存アイテムの属性更新)
UpdateItem
は、既存のアイテムの特定の属性を更新します。PutItem
と異なり、アイテム全体を上書きすることなく、部分的な更新が可能です。
AWS CLI:
# Users テーブルの user001 の Email を更新
aws dynamodb update-item \
--table-name Users \
--key '{"UserId": {"S": "user001"}}' \
--update-expression "SET Email = :e" \
--expression-attribute-values '{":e": {"S": "charlie.new@example.com"}}' \
--return-values ALL_NEW \
--return-consumed-capacity TOTAL
Python (Boto3):
# Users テーブルの user001 の Email を更新
response = users_table.update_item(
Key={
'UserId': 'user001'
},
UpdateExpression="SET Email = :e",
ExpressionAttributeValues={
':e': 'charlie.new@example.com'
},
ReturnValues="ALL_NEW" # 更新後のアイテム全体を返す
)
print("UpdateItem for Users:", response.get('Attributes'))
f. DeleteItem (アイテムの削除)
DeleteItem
は、プライマリキーを指定して単一のアイテムを削除します。
AWS CLI:
# Users テーブルから user001 を削除
aws dynamodb delete-item \
--table-name Users \
--key '{"UserId": {"S": "user001"}}' \
--return-consumed-capacity TOTAL
Python (Boto3):
# Users テーブルから user001 を削除
response = users_table.delete_item(
Key={
'UserId': 'user001'
}
)
print("DeleteItem from Users successful:", response)
5. 設計のベストプラクティスと注意点
- アクセスパターンを理解する: 何度も繰り返しますが、これが最も重要です。アプリケーションがどのようにデータにアクセスするかを明確に定義してから設計を開始します。
- ホットパーティションの回避: 特定のパーティションキーにアクセスが集中すると、そのパーティションがボトルネックになり、スループットが低下します。パーティションキーは、アクセスが均等に分散されるように設計しましょう。UUIDやハッシュ値など、ランダム性の高い値を含めることが有効な場合があります。
-
キャパシティユニットの最適化:
- オンデマンドモード: アクセスパターンが予測できない場合や、トラフィックが急増・急減する場合に最適です。使った分だけ課金されます。
- プロビジョニング済みモード: アクセスパターンが予測可能で、安定したトラフィックがある場合にコスト効率が良いです。RCU/WCUを適切に設定する必要があります。
- Item Collection Size Limit (LSI): LSIを使用する場合、同じパーティションキーを持つ全てのアイテムとLSIの合計サイズは10GBを超えてはなりません。この制限はLSIの設計において重要です。
-
Scan操作の回避:
Scan
は非常に非効率的です。もしScan
が必要なアクセスパターンがあるなら、GSIの追加や、DynamoDB StreamsとLambdaを使ってデータを別の分析用データストア(例: S3 + Athena, OpenSearch Service)にエクスポートすることを検討してください。
まとめとDay 14への展望
今日のDay 13では、Amazon DynamoDBのテーブル設計の重要性を理解し、プライマリキーとセカンダリインデックスの設計原則を学びました。そして、実際にAWS CLIとPython Boto3を使って、DynamoDBの基本的なCRUD操作(PutItem
, GetItem
, Query
, Scan
, UpdateItem
, DeleteItem
)を実践しました。
- DynamoDBの設計は、アプリケーションのアクセスパターンに強く依存すること。
- プライマリキー(パーティションキーとソートキー)の選択がパフォーマンスとコストに大きく影響すること。
- セカンダリインデックス(GSIとLSI)が、プライマリキーでは対応できないアクセスパターンを可能にすること。
-
Scan
操作は避けるべきであること。
これらの実践的な知識は、DynamoDBを効果的に活用するための基盤となります。
明日のDay 14では、DynamoDBのさらに高度な機能と、他のAWSサービスとの連携に焦点を当てます。具体的には、DynamoDB StreamsとLambdaによるイベント駆動型アーキテクチャ、DAXによるキャッシング、そしてグローバルテーブルについて学び、DynamoDBの可能性をさらに広げていきましょう。
それでは、また明日お会いしましょう!