電通総研 テックブログ

電通総研が運営する技術ブログ

DynamoDB初心者がチャット機能のテーブル設計で学んだこと

金融IT本部 入社1年目の河岸歩希です。
会社の同期と個人開発に取り組んでいます。
その過程で「LINEのような個別チャット機能」を実装するにあたり、AWSのサーバーレス構成(Lambda + DynamoDB)の採用を検討することになりました。
今回は実際に調査と設計を行う中で得られた気づきについて共有させていただきます。

はじめに

想定読者

  • RDBは触ったことがあるけど、DynamoDBは初めての方
  • 「DynamoDBって何が嬉しいの?」という疑問をお持ちの方
  • サーバーレス構成でチャット機能を作りたい方

本記事の目的

  • DynamoDBとRDBの設計思想の違いを理解する

チャット機能の要件

今回調査したのは、LINEのような1対1の個別チャット機能です。
個別チャット機能はアプリケーション全体のコア機能ではありませんが、初めて実装する機能だったため、先行して調査を行いました。
チャット機能を実装するにあたり、以下のような要件を整理しました。

要件 理由
リアルタイム性 メッセージは送信後すぐに相手に届いてほしい
高頻度の書き込み チャットはメッセージの追加が頻繁に発生する
スケーラビリティ 現状は30人規模だが、将来的な拡大も想定したい
高可用性 業務時間中はいつでも利用できる状態を維持したい

DynamoDBの設計を理解する

DynamoDBはNoSQL(Not Only SQL)データベースの一種で、キーバリュー型に分類されます。
キーバリュー型の特徴は、キーを指定して値を取得するシンプルな構造です。

DynamoDBでは、データは以下のような構造で格納されます。

テーブル
└── 項目(Item)← 1件のデータ
    ├── パーティションキー: "UserA"  ← キー(必須)
    ├── ソートキー: "2026-01-17"     ← キー(任意)
    ├── Name: "鈴木一郎"             ← 属性
    ├── Email: "suzuki@example.com"  ← 属性
    └── Department: "営業部"         ← 属性
  • 項目(Item): 1件のデータのまとまり(RDBでいう「行」)
  • キー(プライマリキー): 項目を一意に特定するもの
  • 属性(Attribute): 項目が持つ各フィールドのこと

基本的な仕組み

DynamoDBのようなキーバリュー型のデータベースを理解するうえで、最も重要なのは「キー」の設計です。
はじめに「プライマリキー」の構成について説明します。

プライマリキーとパーティション

プライマリキーは「パーティションキー」と「ソートキー」で構成されており、これらを適切に設計することで、効率的なデータアクセスが可能になります。パーティションキーは必須、ソートキーは任意です。
パーティションキーの役割はその名の通り「パーティション(区画)を決定する」ことです。
DynamoDBでのパーティションは、SSDによってバックアップされ、AWSリージョン内の複数のアベイラビリティゾーン間で自動的にレプリケートされる、テーブル用のストレージの割り当てのことを指します。
公式ドキュメントには以下のように記載されています。

DynamoDBは、パーティションキーの値を内部ハッシュ関数への入力として使用します。
ハッシュ関数からの出力により、項目が保存されるパーティション (DynamoDB内部の物理ストレージ) が決まります。

出典: パーティションとデータ分散 - Amazon DynamoDB デベロッパーガイド

つまり、同じパーティションキーを持つデータは物理的に同じパーティションに格納され、異なるパーティションキーを持つデータは別のパーティションに格納されます。
以下の図は、同ドキュメントから引用したものです。

この図では、パーティションキー「AnimalType: Dog」を持つ項目が、ハッシュ関数を通過し、その出力値に基づいて特定のパーティションに格納される様子を示しています。
同様に「Fish」「Lizard」「Bird」「Cat」「Turtle」といった異なるパーティションキーを持つ項目は、それぞれ異なるパーティションに分散して格納されます。
これがパーティションキーのみを用いた場合の、DynamoDBにデータが格納される仕組みです。
この方法に加えて、パーティションキーとソートキーを組み合わせた複合プライマリキーと呼ばれるタイプも存在します。

この図では、パーティションキー「AnimalType」とソートキー「Name」を組み合わせています。同じパーティションキー「Dog」を持つ項目(Bowser、Fido、Rover)は、同じパーティションに格納されます。
その中でソートキー「Name」によって項目が一意に識別され、ソートされた状態で格納されます。複合プライマリキーを使用することで、「同じパーティションキーを持つ複数の項目」を1つのパーティションにまとめて格納し、ソートキーを使って範囲検索やソートを行うことが可能になります。
なので効率的なデータ探索をするためにも、シンプルなプライマリキーにするのか、複合プライマリキーを利用するべきなのかは、アプリケーションのユースケースによって異なるため、事前にどのようなアクセスパターンでデータ操作するのかを検討することが重要です。公式のベストプラクティスでも、アクセスパターンを事前に把握したうえでのキー設計が推奨されています。

NoSQL の設計の違い
対照的に、DynamoDB の場合は答えが必要な質問が分かるまで、スキーマの設計を開始すべきではありません。
ビジネス上の問題とアプリケーションのユースケースを理解することが不可欠です。

出典: NoSQL 設計のベストプラクティス - Amazon DynamoDB デベロッパーガイド

「答えが必要な質問が分かるまで」という表現は少々わかりづらいですが、言い換えると
「どのようなクエリパターンでデータを取得するのかが明確になるまで、スキーマ設計を始めるべきではない」
ということだと解釈しています。

ホットパーティションに注意する

ここまでのパーティションキーの説明を踏まえて、パーティションがキーごとに分散されるのなら、仮にユーザー数が増えたときにパーティションの上限が来るのではないかという疑問が浮かびました。
公式ドキュメントに以下のような記載がありました。

DynamoDB テーブルには、パーティションキーバリューごとに個別のソートキーバリューの数に上限はありません。何十億もの Dog 項目を Pets テーブルに保存する必要がある場合、DynamoDB はこの要件を自動的に処理するのに十分なストレージを割り当てます。

出典:パーティションとデータ分散 - Amazon DynamoDB デベロッパーガイド

つまり、パーティションが増えること自体は問題ではないということです。

むしろ注意すべきは、特定のパーティションにアクセスが集中することです。これを「ホットパーティション」と呼びます。
例えば、パーティションキーを「日付」にした場合を考えます。今日のデータへのアクセスが集中し、過去の日付のパーティションはほとんど使われません。せっかくパーティションが分かれていても、1つに集中してしまっては意味がありません。
今回のチャット機能では、パーティションキーをRoomIdにしています。1対1の個別チャットなので、全員が同じルームに集中することはなく、ルーム数が増えるにつれてアクセスは自然と分散されます。もしグループチャットや全社連絡用のルームを作る場合は、特定のルームにアクセスが集中する可能性があるため、別の設計を検討する必要があるかもしれません。
なので、ユーザーアクセスが特定のパーティションに集中することがないようにパーティションキーを設定する必要があります。

データアクセスパターン

DynamoDBのデータを取得する方法は主に2パターンあります。

方法 説明 速度・コスト
Query パーティションキーを指定して取得。ソートキーで範囲指定やソートも可能 高速・低コスト
Scan テーブル全体を走査して条件に合うものを取得 低速・高コスト

基本的にはQueryを使い、Scanは避けるべきとされています。Queryが高速なのは、たった今説明したように、パーティションキーやソートキーによってパーティションが明確に分けられているからです。この分散したデータ管理によって、DynamoDBは大規模なデータに対しても高速なアクセスを実現することができます。

具体的には、一つのテーブルに対して同時にアクセスする際にも、パーティションが分かれていることによって、同時実行性が向上し、スループットを効率的にスケールさせることが可能となります。
またScanはテーブル全体をスキャンするオペレーションであるため、テーブルが大きくなるにつれて、処理速度は遅くなります。そのため応答時間短縮のためにも、なるべくQueryを使用できるように、テーブル設計段階で適切なパーティションキーとソートキーを検討することが大切です。

RDBとの違い

ここまで理解したところで、ある疑問が浮かびました。
「RDBでもWHERE句を使えば、目的のデータを効率的に探索できるよね?DynamoDBのパーティションキーを指定するのと何が違うの?」
この時点では突出したDynamoDBの良さを感じることはできていませんでした。(初心者目線です...)
ただ、調べていくうちに、「1回のクエリの速度」ではなく「同時に大量のクエリが来たとき」 に違いが出ることがわかりました。

RDBの場合、1000人が同時にアクセスすると、すべてのリクエストが1台のDBサーバーに集中します。(リードレプリカやマルチAZ構成にした場合を除く)
その結果、CPUやコネクションが逼迫し、全体的にレスポンスが遅くなります。
一方、先ほども説明したとおり、DynamoDBの場合、各リクエストはパーティションキーに基づいて別々のパーティションに分散されます。
そのため、同時接続数が増えても負荷が1箇所に集中せず、レスポンス速度を維持できます。

公式ドキュメントにも、以下のような記載があります。

RDBMSでは、データは柔軟にクエリできますが、クエリは比較的コストが高く、トラフィックが多い状況では スケールがうまくいかない場合があります。

出典: リレーショナルデータ設計と NoSQL の相違点 - Amazon DynamoDB デベロッパーガイド

DynamoDBは、パーティションキーによるハッシュ分散があるからこそ、データ量やアクセス数が増えても性能を維持できます。これがRDBとの大きな違いの一つです。
なので大規模アプリケーションや人気イベント等などの、ユーザーからの大量なアクセスが予想される際は、こういったDynamoDBの分散管理の仕組みが輝くということがわかりました。

アクセスパターンを洗い出す

実際にここからテーブル設計の流れを共有します。
前述したNoSQLの設計のベストプラクティスの引用文の中でもあったように、DynamoDBでは「どのようなクエリパターンでデータを取得するのか」を明確にしてから設計を始める必要があります。
そこで、まずはチャット機能のアクセスパターンを洗い出しました。

チャット機能へのアクセスパターン

ケース パターン 使用例
1 特定ルームのメッセージ一覧を取得 チャットルームを開いたとき
2 最新N件のメッセージを取得 初期表示・スクロール時
3 自分が参加しているルーム一覧を取得 チャット画面を開いたとき
4 新しいメッセージを追加 メッセージ送信時
5 新しいチャットルームを作成 初めてのDMを送るとき
6 チャットルームを削除 ユーザーがルームを消したとき

このような形でアクセスパターンを洗い出しました。その後に、「何を起点にデータを探すか」を考えてみます。

  • ケース1, 2, 4:RoomIdを起点にメッセージを操作
  • ケース3:UserIdを起点にルームを検索
  • ケース5, 6:ルームの作成・削除

ここでケース3だけ他のケースとは起点となるデータが違ってしまうことに気づきました。
DynamoDBでは、パーティションキーとソートキーを指定することで効率的にクエリを実行できますが、パーティションキー以外の属性を検索条件にすることはできません。

テーブル設計

最初に考えた設計

メッセージを格納するテーブルとして、以下のような設計を考えました。

  • パーティションキー:RoomId(チャットルームのID)
  • ソートキー:Timestamp#UserId(送信時刻と送信者ID)
  • 属性:Message

アクセスパターンを踏まえて、一番起点となるデータをパーティションキーに持ってきました。また、 ソートキーにはチャットの履歴の取得などを行いたいので、タイムスタンプとして、同じ時間に送信した場合にどちらのユーザーが送ったのかを判別するためにUserIdと結合させて ソートキーに格納します。
パーティションキーにRoomIdを指定してQueryを実行すれば、そのルームのメッセージがソートキー(時刻順)でソートされて取得できるイメージです。

問題「自分の参加ルーム一覧」が取れない

しかし、「自分が参加しているルーム一覧を取得(ケース3)」を実現しようとした際、壁にぶつかりました。

現在の設計では、パーティションキーはRoomIdです。
DynamoDBのQueryはパーティションキーの完全一致が必須なので、「UserIdを起点にルームを検索する」ということはできません。
これを解決するのがGSI(グローバルセカンダリインデックス)という仕組みです。

GSIとは

GSIを一言で説明すると、「元のテーブルとは別のプライマリキーでQueryできるようにするコピーテーブル」です。
公式ドキュメントには以下のように記載されています。

グローバルセカンダリインデックスには、ベーステーブルとは異なるパーティションキーとソートキーがあります。

出典: グローバルセカンダリインデックス - Amazon DynamoDB デベロッパーガイド

GSIで何が解決できるのか

公式ドキュメントのシナリオがとてもわかりやすかったので、具体的な使用例はそちらを参考にしていただければと思います。(上記のGSIの出典先と同じリンク)
GSIを定義することで、元のテーブルに加えて、別の切り口で検索できるようになります。

【元のテーブル】
パーティションキー: RoomId
ソートキー: Timestamp#UserId
→ 「特定ルームのメッセージ」を取得できる

【GSI】
GSI-パーティションキー: UserId
GSI-ソートキー: RoomId
→ 「特定ユーザーの参加ルーム一覧」を取得できる

GSIで新たに選ばれたパーティションキーUserIdごとに、異なるパーティションにデータが元のテーブルからコピーされます。
これによって、ケース3の「自分が参加しているルーム一覧を取得」も実現できそうです。

GSIの特徴

GSIについて調べる中で、以下のような特徴があることがわかりました。

特徴 説明
元テーブルとは別のプライマリキーを設定可能 柔軟な検索が可能になる
データは自動で同期される 元テーブルに書き込むとGSIにも反映(結果整合性)
後から追加可能 設計を間違えても後からリカバリできる
追加コストがかかる 書き込み・ストレージ両方で課金

注意点として、結果整合性であるため、書き込み直後にGSIをクエリすると、最新のデータが反映されていない場合があります。
そのため、リアルタイム性が重要な場面では考慮が必要になります。今回はGSIを使うのが、自分が参加しているルーム一覧取得の場面のみなので、多少のデータ遅延は許容できると判断しました。(実際ルーム作成直後にそのままルームでチャットを始めると思うので...)
これが金融の残高照会等のシビアな要件が絡むと、この結果整合性に伴う遅延が大きな影響をもたらすので、要件と照らし合わせて適切なアーキテクチャを選択する必要があります。

射影(Projection)という概念

GSIを学ぶ中で、射影(Projection) という概念にも出会いました。
射影とは、「元テーブルのどの属性をGSIにコピーするか」を指定するものです。
今回はKEYS_ONLY(キーのみコピー)を選びました。
理由は、GSIで取得したいのは「どのルームに参加しているか」というRoomIdの一覧だけで、メッセージ本文などの属性は不要だからです。

GSIにコピーされていない属性が必要な場合、元テーブルに対して追加のクエリが必要になります。
そのため、アクセス頻度が高い属性は射影に含めておくことで、クエリ回数を削減できます。

公式ドキュメントにも、以下のような記載がありました。

一部のアプリケーションでは、テーブルに対して多くのクエリを発行する必要があり、その結果、プロビジョニングされたスループットの多くを消費することがあります。これを軽減するために、すべてまたは一部のテーブル属性をグローバルセカンダリインデックスに射影できます。

出典: グローバルセカンダリインデックス - Amazon DynamoDB デベロッパーガイド

ストレージ管理コストは増えますが、頻繁にアクセスする場合はクエリコストの削減で相殺されるという考え方です。

LSI(ローカルセカンダリインデックス)との違い

GSIを調べる中で、LSI(ローカルセカンダリインデックス) という似た概念があることも知りました。
LSIの「ローカル」とは、同じパーティション内に存在するという意味です。
GSIは元テーブルとは別のパーティションにデータがコピーされますが、LSIは同じパーティション内でソートキーだけを変えたインデックスです。
今回の設計ではGSIを使うのがよさそうだと判断しました。
理由は、LSIでは要件を満たせないからです。
今回のアクセスパターンの中で「ユーザーIDで、自分の参加ルーム一覧を検索する」部分があります。
メインテーブルのパーティションキーはRoomIdなので、UserIdで検索するにはパーティションキーを変更する必要があります。
LSIではパーティションキーを変更できないため、そもそも今回の要件を満たせません。
そのため、GSIを採用しました。

さて、これらのプロセスを経て、実際にテーブル設計が完了しました。
今回設計したテーブルは以下のとおりです。

テーブル設計(完成)

Messagesテーブル

項目 説明
パーティションキー RoomId チャットルームのID(例:suzuki_yamada
ソートキー Timestamp#UserId 送信時刻とユーザーID(例:2026-01-18T10:30:00Z#suzuki
属性 Message メッセージ本文

GSI(グローバルセカンダリインデックス)

項目 説明
GSI名 UserRoomsIndex
GSI-パーティションキー UserId ユーザーID
GSI-ソートキー RoomId チャットルームID
射影 KEYS_ONLY プライマリキーのみコピー

これにより、以下の検索が可能になります。

アクセスパターン 使用するインデックス クエリ方法
特定ルームのメッセージ一覧 メインテーブル パーティションキー=RoomIdでQuery
自分の参加ルーム一覧 GSI(UserRoomsIndex) GSI-パーティションキー=UserIdでQuery

ここまででテーブル設計が完了しました。

まとめ

今回、チャット機能のためのDynamoDBテーブル設計を行いました。
設計を通じて学んだポイントは以下の3つです。

  1. アクセスパターンを先に考える
    DynamoDBでは「どんなクエリが必要か」を明確にしてからキー設計を行う。RDBのように後からインデックスを追加して柔軟に対応する発想ではうまくいかない。
  2. パーティションキーの設計が最重要
    パーティションキーによってパーティションが決まり、検索効率とスケーラビリティが大きく変わる。ホットパーティションを避け、アクセスが分散されるようなPKを選ぶ。
  3. GSIで検索の柔軟性を確保
    メインテーブルのパーティションキーでは対応できないアクセスパターンがある場合、GSIを活用する。ただし結果整合性やコストには注意が必要。

今回はここまでで、実装はこれからです。
あくまでも机上の設計なので、実装段階で想定外のエラーや設計の見直しが発生するかもしれません。そのときはまた調べて共有できればと思います。
初心者なりにまとめてみましたが、同じようにDynamoDBを学び始めた方の参考になれば幸いです。

私たちは一緒に働いてくれる仲間を募集しています!

電通総研 キャリア採用サイト 電通総研 新卒採用サイト

執筆:@kawagishi.ibuki
Shodoで執筆されました