RDB脳の私が、DynamoDBの「読めないデータの羅列」を「構造」として理解できるまで
1. はじめに
この記事は、RDBしか使ったことがなかった自分が、実務で初めてNoSQLの代表のひとつであるDynamoDBのテーブルを目の当たりにし、「なぜ1テーブルなのか」「なぜこんなにスカスカなのか」と戸惑いながら、その理由を実装や設計を通して少しずつ理解していった体験をまとめたものです。
当時感じていた違和感や思考の変化を軸にしつつ、途中で腹落ちしたPKやSK、GSIといった設計の考え方については、具体的なテーブル例を使って整理しています。
想定読者
- RDBの経験はあるが、DynamoDBは初めて
- JOIN・正規化・インデックスの概念は理解している
- DynamoDBのキャッチアップを始めている
- PK・SK・GSI・LSIなどのキーワードがあることは知っているが、意味や使い方はよくわからない
この記事で分かること
この記事を読み終える頃には、最初は理解できなかった1テーブルの設計や、PK・SK・GSIといったDynamoDB特有のキー設計について、RDBとは違う前提で考える必要がある理由が感覚的に分かっている状態になることを目指しています。
注意事項
なお、この記事で扱うのはDynamoDBでよく使われる設計パターンのひとつ、シングルテーブル設計です。常に1テーブルが正解というわけではなく、システムの性質によっては複数テーブルを使うこともあります。
また、あらかじめ用語についてひとつだけ補足します。RDBでは行とカラムでデータを捉えますが、DynamoDBでは行に相当するものをアイテム、カラムに相当するものを属性と呼びます。本記事ではRDB視点での違和感を伝える文脈ではカラムという言葉を使いますが、実体としては属性のことを指しています。
2. DynamoDBの第一印象
以下のテーブルを見てどう感じますか。
私の最初の印象は、正直かなり気持ち悪いものでした。
(実際に運用されているテーブルはもっと大きくて複雑でした)
| PK | SK | name | settingKey | settingValue | status | loginCount | createdAt | updatedAt | |
|---|---|---|---|---|---|---|---|---|---|
| USER#123 | PROFILE | Taro | taro@example.com | active | 42 | 2023-12-01 | 2024-01-15 | ||
| USER#123 | SETTING#EMAIL | emailNotification | true | ||||||
| USER#123 | SETTING#THEME | theme | dark | ||||||
| USER#123 | HISTORY#20240101 | 2024-01-01 | |||||||
| USER#123 | HISTORY#20240115 | 2024-01-15 | |||||||
| USER#456 | PROFILE | Hanako | hanako@example.com | inactive | 5 | 2023-11-20 | 2024-01-10 | ||
| USER#456 | SETTING#EMAIL | emailNotification | false | ||||||
| USER#456 | SETTING#LANGUAGE | language | en | ||||||
| USER#456 | HISTORY#20231210 | 2023-12-10 | |||||||
| ORG#999 | PROFILE | Acme | contact@acme.com | active | 2020-04-01 | 2024-01-01 | |||
| ORG#999 | SETTING#PLAN | plan | enterprise | ||||||
| ORG#999 | MEMBER#USER#123 | role | admin | 2023-12-01 | |||||
| ORG#999 | MEMBER#USER#456 | role | member | 2023-12-05 |
テーブルは1つだけで、カラムはスカスカ。どこを見てもNULLっぽいデータばかりで、「なんやねんこれ」というのが率直な感想でした。
RDBの世界であれば、テーブルは役割ごとに分け、カラムにはなるべくNULLを入れずに意味のある値を持たせます。正規化されていないテーブルを見ると、そわそわして分割したくなりませんか。
ところがDynamoDBでは、ユーザー情報も設定情報も履歴データも、すべて同じテーブルに入っています。行ごとに持っている属性がまったく違い、RDB脳の自分からすると完全にカオスでした。
なぜ分割しないのか、まったく理解できませんでした。すべての情報が1つにまとまっているせいで、知りたい情報がどこにあるのかすら見当がつきません。
RDBであれば、ユーザー情報はユーザーテーブルを見て、そこからリレーションされたテーブルを辿れば目的の情報に行き着けます。しかし、このカオスなテーブルを前にすると、どこから探し始めればいいのかすらわかりませんでした。
ユーザーと設定は分けたいですし、履歴は外に出したい。そうしないと検索しづらいし、拡張もしづらい。当時はそう考えていました。
さらに追い打ちをかけたのが、PKとSKという謎の概念です。PKはプライマリキーっぽいものだとわかりますが、SKが何を意味しているのかがよくわかりません。PKはめちゃくちゃ重複しているし、なぜキーが2つあるのかが、その時点ではまったくピンと来ていませんでした。
3. SKを作ってみて見えた景色
転機になったのは、新機能の実装で新しくデータを保存する必要が生まれて、新しいSKのルールを自分で決める必要が出てきたときでした。
RDBなら新しいテーブルを複数作成してリレーションを考えて正規化して、という流れになると思いますが、今回はSKを切った方がいいねと先輩に言われ、頭の中がはてなマークでいっぱいでした。
しばらくテーブルを眺めてはやめ、を繰り返していたとき、ふと「SKって複合キーみたいなものなのかもしれない」と思いました。
すると不思議なことに、同じPKの下に、意味のあるデータのまとまりが見えてきました。これは単なる行の集合というより、目的ごとのデータセットだと感じた瞬間でした。
4. PKとSKは仮想的なテーブルを作るためのラベルだった
一番大きな気づきは、PKとSKごとにデータを切り出すと、RDBで見慣れた表と同じ形になるということでした。
テーブルは1つでも、PKとSKの値によって、仮想的なテーブルがいくつも存在しています。機能ごとに必要なデータが、最初から同じ場所にまとまっているのです。
RDBであれば、ユーザーを起点に複数のテーブルをJOINして、ようやく1画面分のデータが揃うことも珍しくありません。JOINが面倒だと感じていた自分にとって、1か所を見れば済むというのは、かなり楽に感じました。読み取り回数が多いシステムでは、最初からこの形を目指して設計されている理由も、少しずつ理解できるようになりました。
つまり、PKとSKは広大なデータの中から、今必要な仮想テーブルを切り出すためのラベルのようなものでした。RDBでは情報を細切れにしてJOINするのが当たり前でしたが、DynamoDBでは、ここさえ見れば必要なデータが全部揃っているという状態を作れます。理解してみると、想像以上に楽な体験でした。
DynamoDBのテーブル例
最初に例示したテーブルのPKを見て、まずUSER#123のものだけを抽出してみます。
| PK | SK | name | settingKey | settingValue | status | loginCount | createdAt | updatedAt | |
|---|---|---|---|---|---|---|---|---|---|
| USER#123 | PROFILE | Taro | taro@example.com | active | 42 | 2023-12-01 | 2024-01-15 | ||
| USER#123 | SETTING#EMAIL | emailNotification | true | ||||||
| USER#123 | SETTING#THEME | theme | dark | ||||||
| USER#123 | HISTORY#20240101 | 2024-01-01 | |||||||
| USER#123 | HISTORY#20240115 | 2024-01-15 |
USER#123に関係する情報だけになって多少スッキリした気がしますが、それでもごちゃごちゃしていてNULLだらけの行を見て気持ち悪くなりますよね。でも、DynamoDBではこの状態が普通です。
SKで見ると世界が変わる
ここでSKに注目します。SKがPROFILEの行だけを見る、SETTINGから始まる行だけを見る、HISTORYから始まる行だけを見る。こうして切り出すと、それぞれがRDBでいうところの1テーブルに相当する形になります。
| PK | SK | name | status | loginCount | createdAt | updatedAt | |
|---|---|---|---|---|---|---|---|
| USER#123 | PROFILE | Taro | taro@example.com | active | 42 | 2023-12-01 | 2024-01-15 |
| PK | SK | settingKey | settingValue |
|---|---|---|---|
| USER#123 | SETTING#EMAIL | emailNotification | true |
| USER#123 | SETTING#THEME | theme | dark |
| PK | SK | createdAt |
|---|---|---|
| USER#123 | HISTORY#20240101 | 2024-01-01 |
| USER#123 | HISTORY#20240115 | 2024-01-15 |
SKはソートキーとして文字列順に並ぶため、特定の文字列で始まるSKをまとめて取得できます。文字通り「ソート」されているのですね。この性質を使うと、SETTINGで始まるSKだけを一度に取得するといったことが可能になります。
つまり、PKはこのユーザーに関するデータの集合、SKはその中から意味のあるまとまりを切り出すためのラベルという関係です。
ここでいう仮想テーブルは、あくまで見え方の話です。RDBのように自由にJOINしたり、集計したりできるわけではありません。ただ、このSKを見るとこの用途のデータが全部揃っているという状態を作れる、という意味では、RDBのテーブルに近い体験が得られます。
さらにSETTING#EMAILとSETTING#THEMEに注目してください。どちらも設定情報ですが、画面描画用に必要な単位でSKを分けています。テーマ設定が欲しいときにメール設定まで一緒に取る必要はありません。
RDBでは設定情報は1つのsettingsテーブルにまとまっていることが多いですが、DynamoDBでは用途単位で分けて持つことがよくあります。
5. GSIも怖くなくなる
PKとSKの考え方が腹落ちすると、GSIやLSIといった言葉も、少しずつ理解できるようになりました。
DynamoDBは基本的に、PKとSKで定義された並び順でデータを効率よく取得します。
RDBのように、WHERE句で任意の属性を指定して自由に検索できるわけではありません。
ここで、先ほどのテーブルを使って具体的な例を考えてみます。
例: ログイン履歴の最新データを取りたい
USER#123には、複数のログイン履歴(HISTORY)が保存されています。
| PK | SK | createdAt |
|---|---|---|
| USER#123 | HISTORY#20240101 | 2024-01-01 |
| USER#123 | HISTORY#20240115 | 2024-01-15 |
この中から「最新の履歴だけ」を取得したいとします。
PKとSKだけで設計すると、
- PK = USER#123
- SKがHISTORYで始まるアイテムをすべて取得
- アプリケーション側でcreatedAtを比較して最新の1件を選ぶ
という流れになります。
履歴が少ないうちは問題ありませんが、履歴が増えてくると「最新の1件を取りたいだけなのに、不要な読み取りが増える」という状態になります。
既存の属性だけでGSIを作ってみる
そこで、GSIを使って並び替えられないかを考えます。
すでに createdAt という属性があるため、次のようなGSIを作れば良さそうに見えます。
- GSIのPK: PK
- GSIのSK: createdAt
このGSIに対して、
- PK = USER#123
- createdAtの降順
- LIMIT 1
とすれば、最新のデータが1件取得できます。
一見すると、これで解決できそうです。
実際に起きる問題
しかし、このGSIで取得できるのは、
- HISTORY
- PROFILE
- SETTING
を含めた、「USER#123に紐づくすべてのアイテムの中で最新の1件」です。
つまり、最新のログイン履歴を取りたいと思っていても直近で更新されたSETTINGが返ってくる、といったことが起きてしまいます。
既存の属性だけでは、「これは履歴用の並びである」という情報をGSIのキーとして表現できていませんでした。
GSIでも用途が分かるキー設計をする
そこで、GSIでも「これは履歴用の並びである」と分かるように、用途を表す属性を追加します。
履歴アイテムを追加するときに、次の属性を持たせるようにします。
- userHistoryPK = USER#123#HISTORY
- createdAt
GSIは次のように定義します。
- GSIのPK: userHistoryPK
- GSIのSK: createdAt
すると、履歴アイテムだけが次のようにGSIへ投影されます。
| GSI_PK | GSI_SK | 元のPK | 元のSK |
|---|---|---|---|
| USER#123#HISTORY | 2024-01-01 | USER#123 | HISTORY#20240101 |
| USER#123#HISTORY | 2024-01-15 | USER#123 | HISTORY#20240115 |
このGSIに対して、
- GSI_PK = USER#123#HISTORY
- createdAtの降順
- LIMIT 1
とするだけで、最新の履歴を確実に取得できます。
RDBでいうところの
「WHERE type = 'history' ORDER BY created_at DESC LIMIT 1」
を、あらかじめキー設計として持っておくイメージです。
GSIはもうひとつの見方を作るもの
この例から分かるように、GSIは後から自由に検索できる仕組みではありません。
- どのデータを
- どんな単位で
- どんな順序で取りたいか
を先に決め、その並び順をもうひとつ用意する仕組みです。
RDBのインデックスに近い存在ですが、検索条件を後付けできない点が大きく違います。
PKとSKで作った世界観を壊さずに、用途を限定した見方を追加できる。
そう考えると、GSIもそこまで怖いものではありませんでした。
6. まとめ
私がDynamoDBを使って面白いと感じたのは、実際にデータを入れたときの属性追加の気軽さでした。RDBでは、事前にカラムを定義しないとデータを入れられません。一方でDynamoDBでは、入れたいデータをそのままPutする感覚で、必要な属性が後から自然に増えていきます。
SKや属性は、目的に合わせて作っていい。その自由さに気づいたことが、DynamoDBを面白いと感じ始めた第一歩でした。
DynamoDBは、最初はよくわからないし、強敵のように感じます。でもそれは、RDBの感覚で無理に見ようとしているからかもしれません。
テーブルは箱ではなく、データの並び順のデザインです。SKを理解すれば、カオスの中に秩序を作れます。
もちろん、DynamoDB特有の検索の難しさなど、これから向き合うべき課題はあります。それでも、まずはこの自由さを楽しめるようになれば、キャッチアップは成功したと言っていいと思います。
RDBがわかっているあなたなら、きっとすぐに慣れるはずです。一緒にDynamoDBのキャッチアップを楽しんでいきましょう。
Discussion