この記事は GMOインターネットグループ Advent Calendar 2025 11日目の記事です。
こんにちは。GMOペパボ株式会社のyumuです。今回、リワード広告システムの開発で、スタンプデータの管理にDynamoDBを採用しました。
「DynamoDBを使ってみたい!」という興味本位な気持ちもあって採用を決めたのですが、RDBの設計経験しかなかった私は、いくつかの落とし穴にハマることになりました。
この記事では、DynamoDB設計で実際に遭遇した課題と、そこから学んだ設計のポイントを共有します。
目次
なぜDynamoDBを選んだのか
私たちはハンドメイドECサービス「minne byGMOペパボ」を運営しており、今回リワード広告機能を追加することになりました。ユーザーがアプリ内で広告を閲覧すると、スタンプがもらえる仕組みです。
このスタンプデータの保存先として、DynamoDBを選択しました。理由は以下の通りです。
- スタンプデータはユーザーIDに紐づくシンプルな構造で、NoSQLとの相性が良さそう
- 読み書きのパフォーマンスが重要
- DynamoDBを実戦で使ってみたい
しかし、この「軽い気持ち」が後々、設計の甘さとして跳ね返ってくることになります。
RDB脳による設計ミス
DynamoDBのテーブルを作成する際、私はRDBと同じ感覚で設計を進めていました。
「とりあえずユーザーIDとスタンプ情報を保存できるテーブルを作って、必要になったらクエリすればいいか」
この考え方が大きな間違いでした。
クエリできない!
実装を進めて、「特定期間に獲得されたスタンプの一覧を取得する」というクエリを書こうとした時、気づきました。
DynamoDBでは、パーティションキーを指定せずに範囲検索することができない。
RDBであれば SELECT * FROM stamps WHERE created_at BETWEEN '2025-12-01' AND '2025-12-31' と書けば済む話ですが、DynamoDBではパーティションキーの指定が必須です。パーティションキーをuser_idに設定していたため、特定ユーザーのスタンプは取得できても、全ユーザーを横断して期間指定で取得することができませんでした。
意図せずScanになっていて、めちゃくちゃ遅い!
インデックスを設定して実装したつもりだったのですが、実際に動かしてみるとクエリの実行時間が著しく長いという問題に直面しました。
データ量が少ないうちは気にならなかったのですが、テストデータを増やすと、どんどん遅くなっていきます。ログを見てみると、意図に反してScanが実行されていたのです。Scanはテーブル全体をフルスキャンするため、データ量に比例して時間がかかります。
原因は、クエリ時のインデックス指定ミスでした。Queryのつもりで書いたコードが、実際にはScanになっていたのです。
結局、設計し直し
幸い実装を開始した直後に気づいたので大事には至りませんでしたが、アクセスパターンを洗い出して、カラムやLSI、GSIを設計し直すことになりました。
DynamoDB設計とRDB設計の決定的な違い
この失敗を通して、ようやく「DynamoDBはRDBとは根本的に設計思想が違う」ということに気づきました。具体的にどう違うのか、整理してみます。
正規化ではなく、アクセスパターン優先
RDBでは、データの重複を避けるために正規化を行い、テーブルを分割します。そして必要に応じてJOINで結合します。
しかしDynamoDBにはJOINという概念がありません。そのため、以下のような設計が求められます。
- アクセスパターンを事前に洗い出す
- そのパターンに最適化した形でデータを配置する
- 場合によってはデータの重複を許容する
つまり、「どんなデータを保存するか」ではなく、「どうやってデータを取得するか」から設計を始める必要があるのです。私が最初にやってしまった「とりあえず必要なデータを入れるテーブルを作る」というアプローチでは上手くいきませんでした。
QueryとScanの違い
DynamoDBには、データ取得の方法が2つあります。
⭐️ Query
- パーティションキー(とソートキー)を指定して取得
- 効率的で高速
⭐️ Scan
- テーブル全体をスキャンして条件に合うものを取得
- 非効率で遅い
- データ量に比例して時間がかかる
私が実際に遭遇したのは、インデックス指定を誤ったことで意図せずScanが実行され、クエリの実行時間が著しく伸びてしまったというケースでした。
効率的なQueryを使うためには、適切なインデックス設計が不可欠です。
インデックス設計の制約
RDBでは、後からでも比較的自由にインデックスを追加できます。しかしDynamoDBでは、インデックスの種類ごとに厳格な制約があります。
⭐️ プライマリキー(パーティションキー + ソートキー)
- テーブル作成時に決定
- 変更不可
⭐️ LSI(ローカルセカンダリインデックス)
- テーブル作成時にしか追加できない
- パーティションキーはテーブルのプライマリキーと同じで、ソートキーだけを変えたクエリに使用
- テーブルあたり最大5個
⭐️ GSI(グローバルセカンダリインデックス)
- 後から追加可能
- パーティションキーもソートキーも自由に設定できる
- テーブルあたり最大20個
この制約があるため、「後で必要になったらインデックスを追加すればいい」という考え方は通用しません。
実践:失敗から学んだ設計プロセス
失敗を経て、私が学んだDynamoDB設計のプロセスは以下の通りです。
Step 1:アクセスパターンを徹底的に洗い出す
RDB脳での考え方🙅♀️:「ユーザーとスタンプのデータを保存しよう。クエリは後で考えればいいや」
DynamoDB的な考え方🙆♀️:「どんな場面で、どんなクエリが必要になるか?を最初に列挙しよう」
リワード広告システムでは、以下のようなアクセスパターンを洗い出しました。
ユーザーIDをもとに、ユーザーが獲得したスタンプの一覧を取得ユーザーIDをもとに、ユーザーが特定期間に獲得したスタンプ数を取得ユーザーIDと広告IDをもとに、特定の広告のスタンプ獲得状況を取得スタンプ獲得日をもとに、特定期間に獲得されたスタンプの一覧を取得
この洗い出しが甘いと、後で「このクエリができない!」となってしまいます。
Step 2:プライマリキーの決定
洗い出したアクセスパターンから、最も頻繁に使われるクエリを基準に決定します。
- パーティションキー:
user_id(ユーザーごとにデータが分散される) - ソートキー:
timestamp(時系列でのソート・範囲検索が可能)
この設計により、「ユーザーIDで取得」「特定期間のスタンプを取得」といったクエリが効率的に実行できます。
Step 3:LSI/GSIの設計
プライマリキーだけでカバーできないアクセスパターンには、LSIやGSIを使います。
例えば、「スタンプ獲得日をもとに、特定期間に獲得されたスタンプの一覧を取得したい」というパターンがある場合は以下のようなGSIが必要です。
- GSI:パーティションキーを
date、ソートキーをuser_idに設定
特にLSIはテーブル作成時にしか追加できないため、慎重に検討します。「後で必要になるかも?」と思ったら、最初に設定しておく方が安全です。
ただし、インデックスが増えると読み込み/書き込みキャパシティの消費量も増えるため注意が重要です。
実際の設計例
最終的に、以下のような設計になりました。
| Column Name | Data Type | Primary Key | LSI#1 | GSI#1 |
|---|---|---|---|---|
| user_id | Number | Partition Key | Partition Key | Sort Key |
| ad_id | String | Sort Key | ||
| date | String | Partition Key | ||
| timestamp | String | Sort Key |
この設計により、主要なアクセスパターンをカバーできるようになりました。
運用で気づいた落とし穴
GSIを設定しているのにインデックスが使われない
minneではRubyを使っており、DynamoDBのORMとしてDynamoidを採用しました。
Dynamoidでは、GSIを定義する際にprojected_attributes: :keys_onlyを指定している場合、DynamoDB側でGSIが設定されていても、Queryで使ってくれないことがありました。
class UserStamp
# (省略)
# NG
global_secondary_index hash_key: :date,
range_key: :user_id,
projected_attributes: :keys_only,
name: :date_user_gsi
# OK
global_secondary_index hash_key: :date,
range_key: :user_id,
projected_attributes: :all,
name: :date_user_gsi
endこの設定を:keys_onlyにしていたことが原因で、Scanが実行され、クエリの実行時間が異常に長くなってしまっていました。
パーティションキーなしでの範囲検索ができない
DynamoDBでは、パーティションキーを指定せずに範囲検索することができません。
例えば、「今日獲得されたスタンプを全ユーザー横断で取得したい」というケースを考えてみます。timestampをソートキーに設定していても、パーティションキー(user_id)を指定しない限り、Queryは使えません。
この問題に対して、私たちは日付用のカラムを追加し、それをパーティションキーとするGSIを作成するという対策を取りました。
# timestampとは別に、date(YYYY-MM-DD形式)カラムを追加
# GSIでdateをパーティションキー、user_idをソートキーに設定
UserStamp.where(date: '2025-12-15')このように、範囲検索が必要な場合は、検索に使う粒度に合わせたカラムを用意し、それをパーティションキーとするGSIを設計時に用意しておくことが必要です。
開発中はこまめにログを確認する
開発中は、意図通りQueryが使われているのか、Scanになっているのかをログで確認する癖をつけましょう。
# ログレベルを設定
Dynamoid.config.logger.level = :debugこれにより、実行されたDynamoDBのAPIコール(Query/Scan)が確認できます。
まとめ:DynamoDB設計の心得
DynamoDBを使ってみて、痛い目に遭いながら学んだことをまとめます。
1. 設計は「データ」ではなく「クエリ」から始める
RDBの正規化思考を一旦忘れて、「どうデータを取得するか」を最優先に考えましょう。アクセスパターンの洗い出しが甘いと、後で必ず後悔します。
2. インデックス設計は慎重に、そして早めに
特にLSIはテーブル作成時にしか追加できません。「後で必要になるかも」と少しでも思ったら、最初から設定しておくことをお勧めします。
3. QueryとScanの違いを意識する
インデックスを適切に設定し、Queryで取得できるようにすることが、パフォーマンスとコストの両面で重要です。ORMを使う場合は、意図せずScanになっていないか、ログで確認する習慣をつけましょう。
おわりに
「使ってみたい」と軽い気持ちで始めたDynamoDBでしたが、RDBとは全く異なる設計思想に戸惑いました。しかし、その特性を理解して適切に設計すれば、非常に強力なデータベースです。
今回の経験が、これからDynamoDBを使おうとしている方の参考になれば幸いです。そして、もし同じような失敗をしている方がいたら、「自分だけじゃなかった」と安心してもらえればと思います。
最後まで読んでいただき、ありがとうございました!
ブログの著者欄
採用情報
関連記事
KEYWORD
CATEGORY
-
技術情報(531)
-
イベント(198)
-
カルチャー(54)
-
デザイン(52)
TAG
- "eVTOL"
- "Japan Drone"
- "ロボティクス"
- "空飛ぶクルマ"
- 5G
- Adam byGMO
- AdventCalender
- AGI
- AI
- AI人財
- APT攻撃
- AWX
- BIT VALLEY
- Blade
- blockchain
- Canva
- ChatGPT
- ChatGPT Team
- Claude Team
- cloudflare
- cloudnative
- CloudStack
- CM
- CNDO
- CNDT
- CODEBLUE
- CODEGYM Academy
- ConoHa
- ConoHa、Dify
- CS
- CSS
- CTF
- DC
- design
- Designship
- Desiner
- DeveloperExper
- DeveloperExpert
- DevRel
- DevSecOpsThon
- DiceCTF
- Dify
- DNS
- Docker
- DTF
- Excel
- Expert
- Experts
- Felo
- GitLab
- GMO AIR
- GMO AIロボティクス大会議&表彰式
- GMO DESIGN AWARD
- GMO Developers Day
- GMO Developers Night
- GMO Developers ブログ
- GMO Flatt Security
- GMO GPUクラウド
- GMO Hacking Night
- GMO kitaQ
- GMO SONIC
- GMOアドパートナーズ
- GMOアドマーケティング
- GMOイエラエ
- GMOインターネット
- GMOインターネットグループ
- GMOクラウド]
- GMOグローバルサイン
- GMOサイバーセキュリティbyイエラエ
- GMOサイバーセキュリティ大会議
- GMOサイバーセキュリティ大会議&表彰式
- GMOソリューションパートナー
- GMOデジキッズ
- GMOブランドセキュリティ
- GMOペイメントゲートウェイ
- GMOペパボ
- GMOメディア
- GMOリサーチ
- GMO大会議
- Go
- GPU
- GPUクラウド
- GTB
- Hardning
- Harvester
- HCI
- iOS
- IoT
- ISUCON
- JapanDrone
- Java
- JJUG
- K8s
- Kaigi on Rails
- Kids VALLEY
- KidsVALLEY
- Linux
- LLM
- MCP
- MetaMask
- MySQL
- NFT
- NVIDIA
- NW構成図
- NW設定
- Ollama
- OpenStack
- Perl
- perplexity
- PHP
- PHPcon
- PHPerKaigi
- PHPカンファレンス
- Python
- QUIC
- Rancher
- RPA
- Ruby
- Selenium
- Slack
- Slack活用
- Spectrum Tokyo Meetup
- splunk
- SRE
- sshd
- SSL
- Terraform
- TLS
- TypeScript
- UI/UX
- vibe
- VLAN
- VS Code
- Webアプリケーション
- WEBディレクター
- XSS
- アドベントカレンダー
- イベントレポート
- インターンシップ
- インハウス
- オブジェクト指向
- オンボーディング
- お名前.com
- カルチャー
- クリエイター
- クリエイティブ
- コーディング
- コンテナ
- サイバーセキュリティ
- システム研修
- スクラム
- スペシャリスト
- セキュリティ
- ソフトウェアテスト
- チームビルディング
- デザイン
- ドローン
- ネットのセキュリティもGMO
- ネットワーク
- ビジネス職
- ヒューマノイド
- ヒューマノイドロボット
- プログラミング教育
- ブロックチェーン
- ベイズ統計学
- マルチプレイ
- ミドルウェア
- モバイル
- ゆめみらいワーク
- リモートワーク
- レンタルサーバー
- ロボット
- 京大ミートアップ
- 人材派遣
- 出展レポート
- 動画
- 協賛レポート
- 基礎
- 多拠点開発
- 大学授業
- 宮崎オフィス
- 展示会
- 広告
- 形
- 応用
- 情報伝達
- 技育プロジェクト
- 技術広報
- 技術書典
- 採用
- 採用サイトリニューアル
- 採用活動
- 新卒
- 新卒研修
- 日本科学未来館
- 映像
- 映像クリエイター
- 暗号
- 業務効率化
- 業務時間削減
- 機械学習
- 決済
- 物理暗号
- 生成AI
- 色
- 視覚暗号
- 開発生産性
- 開発生産性向上
- 階層ベイズ
- 高機能暗号
PICKUP
-
DynamoDB設計で痛い目にあった話 – RDB脳から抜け出すための実践ガイド
技術情報
-
【協賛レポート・後編】Designship 2025|“勝つデザイン”と“やさしいデザイン”──現場から見えたこれからの指針
デザイン
-
説明が可能なプロダクトセキュリティについての一考察
技術情報
-
「ユーザー目線」を習得する!UI/UX向上業務に初めて取り組んだビギナーがニールセンの10原則を「調査・改善提案の指針」にした話
技術情報
-
「LLMにファイルとプロンプトを投げるだけ」では失敗する。Excelデータ抽出に学ぶコンテキストエンジニアリングの勘所
技術情報
-
AWS認定の沼へようこそ!AWS全冠取得へ!
技術情報