タイトル
すみません、怒られたわけではありません。
詳しい方にレビューしていただき、「だめっすね」と愛のある指摘をいただいただけです☺️
今回は、DynamoDBでScanを使ったときにいただいたアドバイスと、そこから学んだことをまとめます。
これからDynamoDBを使う方の参考になれば嬉しいです!
前提
私は、先月分のデータを定期的に取得するバッチ処理を作成していました。
対象となる属性はCreateAtで、これはレコードの作成日時を表しています。
当時のDynamoDBテーブル設計は以下の通りです。
- パーティションキー:ClientID
- ソートキー:SessionID
- その他属性:CreateAt(今回使用したいデータ)
このテーブルは、機能実装を最優先にして作成したため、
分析やデータ取得の観点はほとんど考慮できていませんでした。
そもそもScanとは?
DynamoDBには、データ取得手段としてScanとQueryの2種類があります。
-
Scan
→ テーブル全体をなめる(フルスキャン) -
Query
→ パーティションキーを指定して、そこから効率的にデータを取得
Scanは非常にシンプルにデータが取れる反面、コストが高く、パフォーマンスも悪化しやすい特徴があります。
公式でも「できるだけQueryを使いましょう」と推奨されています👇
DyanamoDBのしくみ
DynamoDBは 水平分散(シャーディング) してデータを保存しています。
つまり、テーブル全体が複数のパーティション(物理サーバー)に分かれています。
パーティションごとに並列でデータを取りに行きますが、アクセス量もトータルでは大きくなります。
イメージ図にする
Scan
[パーティション1] ←
[パーティション2] ← Scan要求
[パーティション3] ←
[パーティション4] ←
Query
[パーティション3] ← Query このパーティションだけ
特定のパーティションだけ狙い撃ちできるので、めちゃくちゃ効率が良いです。
なぜScanを使ったのか?
理由は単純で、CreateAt
が キー項目ではなかった からです。
Queryは「パーティションキーを指定すること」が前提条件です。
一方で、今回絞り込みたかったのはCreateAt
。
パーティションキーでもソートキーでもない普通の属性だったため、まずバッチ処理を完成させることを目的に Scanを選択していました。
なぜ指摘されたのか?
Scanを使うと、テーブル全体に対して読み取りが発生します。
これにより、
- 読み取りコストが大幅に上昇
- 処理時間も長くなり、パフォーマンス悪化
- テーブルが大きくなるほど影響が深刻化
といった問題が発生します。
「このままだと将来的にまずいよ」という、運用目線からのありがたいアドバイスでした。
対応方法:GSIの活用
この問題に対しては、GSI を利用するのが有効です。
-
GSI(グローバルセカンダリインデックス):
本来のパーティションキー・ソートキーとは別に、任意の属性でインデックスを作成できる機能 -
LSI(ローカルセカンダリインデックス):
ソートキーだけを別で指定するインデックス。ただし、テーブル作成時にしか設定できない制約あり
つまり、運用中のテーブルに後付けできるのはGSIだけです。
今回の場合は、CreateAt
をキーとするGSIを作成し、そこに対してQueryを投げることで、
効率よくデータを取得できるようにするのがベストな方法でした。
参考コード
データ取得比較
Sqanのとき
const command = new ScanCommand({
TableName: tableName,
FilterExpression: 'createdAt BETWEEN :start AND :end',
ExpressionAttributeValues: {
':start': lastMonthStartISO,
':end': lastMonthEndISO,
},
});
const result = await ddbClient.send(command);
const items = result.Items || [];
Queryのとき
const command = new QueryCommand({
TableName: tableName,
IndexName: 'CreatedAtIndex',
KeyConditionExpression: 'createdAt BETWEEN :start AND :end',
ExpressionAttributeValues: {
':start': lastMonthStartISO,
':end': lastMonthEndISO,
},
});
const result = await ddbClient.send(command);
const items = (result.Items || []) as ToJsonL[];
Scanには FilterExpression というオプションがあります。
ただ、フィルタ実行タイミングが読み込んだ後にフィルタリングするのです。
なのでScanは一旦全部読んでから、あとでフィルタするので、
「取ってきたけど使わないデータ」にもコストがかかります。
Queryは最初から絞って読み取るから、そもそも読み取りコストが小さいです。
CDK
import { RemovalPolicy, aws_dynamodb } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class DynamoDbConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const table = new aws_dynamodb.Table(this, 'ExampleTable', {
partitionKey: { name: 'clientId', type: aws_dynamodb.AttributeType.STRING },
sortKey: { name: 'sessionId', type: aws_dynamodb.AttributeType.STRING },
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: RemovalPolicy.DESTROY,
});
table.addGlobalSecondaryIndex({
indexName: 'CreatedAtIndex',
partitionKey: { name: 'clientId', type: aws_dynamodb.AttributeType.STRING },
sortKey: { name: 'createdAt', type: aws_dynamodb.AttributeType.STRING },
projectionType: aws_dynamodb.ProjectionType.ALL,
});
}
}
感想
正直、当初はあまり意識していませんでした。
DynamoDBはスキーマ定義がゆるく、型も厳密ではないため、気軽に使えるというメリットがあります。
しかし、データ取得時には「リクエストユニット(RCU)=お金」が直接関わってくるため、
最初の段階からアクセスパターンを意識して設計することの重要性を痛感しました。
個人的には、
- 気軽に使えるのに、設計はシビア
- 気楽そうに見えて、油断できない
という、DynamoDBのちょっと矛盾しているような難しさを感じました。
また、プロダクトの規模や将来性によっては、リレーショナルDB(RDB)のほうが柔軟で楽なケースもあるな、と改めて思いました。
でもDynamoDBは、やはりメリット大きいです。
理由は、 運用がラクで、少量アクセスなら非常に安く済む からです。
例えば、読み取り・書き込みが少ないプロダクトなら、月数百円で運用できることもあります。
スケールしない前提なら、むしろコスパ重視で大いに使えるDBです。
まとめ
- DynamoDBでScanは極力使わない
- アクセスパターンを想定して、最初から設計する
- 運用中ならGSIで対応する
- 小さなプロダクトなら、RDBも選択肢に
- そして、怒られてない☺️
おわりに
今回は、DynamoDBでの失敗談(?)と学びを共有しました。
これから設計・開発される方の参考になれば幸いです🙇♀️
おまけ コストざっくり試算
ここでは、ざっくりとScanした場合のコストイメージを紹介します。
想定条件
- テーブルデータ総量:10GB
- 1レコードサイズ:1KB
- 総レコード数:約1,000万件
- 結果整合性あり読み取り(通常の読み取り)
計算
- 1RCUで読める量:8KB(結果整合性ありの場合)
- 必要なRCU:10GB ÷ 8KB = 1,250,000 RCU
- オンデマンド読み取り料金:1M RCUあたり約1.19 USD(東京リージョン)
よって、
1,250,000RCU × 1.19 USD = 約1.5 USD(約230円)
が1回のScanでかかります。
実際の運用を考えると?
仮に毎日バッチを動かすとすると、
- 1.5 USD × 30日 = 約45 USD(約7,000円)/月
これだけで、ちょっとしたコストになります。
さらに、
- データ量が増える
- リトライやスロットリングが発生する
- 頻繁にスキャンを回す
といった要因で、実際のコストはさらに膨らみやすいです。